注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

个人支付项目,已稳定收款 100+

对,没错,又趁着周末两天 + 几个工作日晚上熬夜开发了个支付项目出来,赞赏平台。我对这个项目的定位非常简单,就是一个买卖平台。平台内容由我来发布,免费内容大家只需注册即可观看,如需付费则支付相关费用方可查看。下面是项目运行首页下面是项目登录注册页下面是商品支付...
继续阅读 »

对,没错,又趁着周末两天 + 几个工作日晚上熬夜开发了个支付项目出来,赞赏平台。

我对这个项目的定位非常简单,就是一个买卖平台。平台内容由我来发布,免费内容大家只需注册即可观看,如需付费则支付相关费用方可查看。

下面是项目运行首页


下面是项目登录注册页


下面是商品支付页面


虽然项目整体规模较小但也算是五脏俱全,有认证相关、有支付相关、也有分布式问题相关。对于没有做过个人项目特别是没做过支付项目的小伙伴来说,用来练手或者写在简历上都是未尝不可的。


那下面我来具体项目中几个重要的业务点。


1、网关认证


以前我们开发项目要进行认证基本都是通过在服务中写个拦截器,然后配置拦截器拦截所有的请求,最终通过拦截器的逻辑进行认证。这中方法不是不可以,但我觉得不好,如果我们项目中有三个微服务以上,那么这个拦截器的认证逻辑就会存在于每个微服务中,这是我认为的不好的点。


那我是怎么做的呢!


对,在网关服务里做认证动作。将认证动作迁移,因为我的个人项目是通过网关进行请求转发,所以,所有的请求都会先进入网关,再进入各个具体的业务服务,那问题就好办了。我直接通过实现网关的 GlobalFilter 接口拦截所有的请求,通过实现该接口进行认证逻辑处理,完成本平台的认证、续约、限流等功能。


下面来看看请求流程图


2、支付逻辑


支付功能可以说是本项目的重中之重,需要有非常强的健壮性。因为我是一位个人开发者,所以不能对接需要有营业执照的支付功能,最终我选择了支付宝的当面付这一个功能。


当面付的好处很多,第一它不需要营业执照,第二对接也非常简单而且有支付宝封装的SDK,所以本人再对接的过程中没有费多少力气就把接口打通了。


主要就是对接了当面付的三个接口:

  1. 获取支付二维码接口

  2. 支付成功的回调接口

  3. 订单状态回查接口


当然,这三个接口的代码量也是很大的,所以本人为了通用就又对他做了一层封装,使得项目调用支付功能就更加简单了。如下就可以完成一个支付功能的完整逻辑:


是不是很简单,如果需要代码的可以看文章末尾哦!


下面我来介绍一下本项目中付费内容的整个业务流程。


1、用户获取付费商品详情


2、点击查看内容,这里就有两种结果了

  • 第一种结果:商品已支付,直接显示内容给用户观看

  • 第二种结果:商品未支付,提示用户付款查看


3、当显示第二种结果时,如果用户点击付款,则进入后续流程


4、服务器请求支付宝第三方,获取对应金额的支付二维码,并将返回的二维码和用户绑定生成一个未支付的订单,最终将这个待支付二维码返回给页面


5、页面显示二维码之后,用户就需要进行扫码付款(打开支付宝APP扫码付款)


6、用户付款成功之后,支付宝第三方会自动回调第四步我给支付宝的回调地址。回调接口的逻辑就是将订单状态改为已支付并做一些后续的流程操作。


7、为了防止回调接口出问题,还写了一个定时任务,定时回查订单表中未支付订单的状态,循环请求支付宝询问支付支付成功并执行支付成功的相应回调逻辑。


支付业务流程图


3、手写分布式锁


相信分布式锁大家都不陌生,无非就是向中间件中放入一个标志量,存在即表示已锁,反之则未锁执行相关逻辑。


说都会说,但要真正自己手写而且做到高可用确是一个非常困难的问题。其中非常关键的一点就是如何解锁,如何做到业务执行完成百分之百解锁,那我再项目中是如何考虑的呢!


我先来简单的说一下思路:


1、定义一个分布式锁注解,用来标注那些方法需要分布式锁加持


2、定义一个切面,逻辑就是给加上了分布式注解的方法进行增强


3、增强的逻辑为:加锁、生成续约任务、执行业务逻辑、解锁


4、另起一个延迟线程池,每隔一定时间遍历一次续约任务集合,判断任务是否需要进行续约(这个逻辑判断很多如:续约次数过多、业务已执行完毕、是否需要续约等等)


具体业务流程如图(我画的比较多)


当然,为了方便你们理解,我还出了相关视频,地址:



http://www.bilibili.com/video/BV1jP…



以上,就是赞赏平台项目中三个比较大的亮点,不论是写在简历上还是当作个人项目都是一个非常不错的选择,那我也把这个项目搭建起来了,地址如下:



admire.j3code.cn



需要项目代码 + 视频 + 详细文档的,我都放在这个平台上了,自取即可。


我是J3code(三哥),咱们下回见

作者:J3code
链接:https://juejin.cn/post/7199820362954588197
来源:稀土掘金
收起阅读 »

为了摸鱼,我开发了一个工具网站

  大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期...
继续阅读 »


  大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期所带的傲气也是被一点一点的慢慢的打磨掉,正是因为这些,带给我的成长是巨大的。好了,闲言少叙,下面让我们进入今天的主题。


创作背景


       因为”懒“得走路,所以发明了汽车飞机,因为”懒“得干苦力活,所以发明了机器帮助我们做,很早之前看到过这个梗,而我这次同样也是因为”懒“,所以才开发出了这个工具。这里先卖个关子,先不说这个工具的作用,容我向大家吐槽一下这一段苦逼的经历。在我实习刚入职不久,就迎来了自己第一个任务,由于自己对所参与的项目的业务并不太了解,所以只能先做一些类似测试的工作,比如就像这次,组长给了我一份Json 文件,当我打开文件后看到数据都是一些地区名称,但当我随手的将滚动条往下一拉,瞬间发现不对劲,因为这个小小的文件行数竟然达到了1w+❗❗❗❗


不禁让我脊背发凉,但是这时我的担心还没达到最坏的地步,毕竟我还对具体的任务不了解。但当组长介绍任务内容,主要是让我将这些数据添加到数据库对应的表中,由于没有sql脚本,只有这个json 文件,需要手动去操作,而且给我定的任务周期是两天。听到这个时间时内心的慌张瞬间消失了,因为在之前我就了解过Navicat支持Json格式的文件直接导入数据,一个这么简单的任务给我两天时间,这不是非要让我带薪学习。


当我接下任务自信打开Navicat的导入功能时发现了一个重要问题,虽然它支持字段映射,但是给的Json数据是省市区地址名称,里面包含着各种嵌套,说实话到想到这里我已经慌了,而且也测试了一下字段只能单个的批量导入,而且不支持嵌套的类型,突然就明白为什么 给我两天的时间。


这时候心里只能默默祈祷已经有大神开发出了能处理这种数据的工具网站,但是经过一个小时的艰苦奋斗,但最终依旧是没有结果,网上有很多JsonSQl的工具网站,但是很多都支持简单支持一下生成创建表结构的语句,当场心如死灰,跑路的心都有了。但最终还是咬着牙 手动初始化数据,其过程中的“趣味” 实属无法用语言表达……

 上述就是这个工具的开发背景,也是怕以后再给我分配这么“有趣” 的任务。那么下面就给大家分享一下我自制的 Json转译SQL 工具,而且它也是一个完全免费的工具网站,同时这次也是将项目进行了 开源分享,大家也可以自己用现成的代码完成本地部署测试,感兴趣的同学可以自行拉取代码!



开源地址:github.com/pdxjie/sql-…



项目简介


Sql-Translation (简称ST)是一个 Json转译SQL 工具,在同类工具的基础上增强了功能,为节省时间、提高工作效率而生。并且遵循 “轻页面、重逻辑” 的原则,由极简页面来处理复杂任务,且它不仅仅是一个项目,而是以“降低时间成本、提高效率”为目标的执行工具。


技术选型

前端:

Vue
AntDesignUI组件库
MonacoEditor 编辑器
sql-formatter SQL格式化

后端:

SpringBoot
FastJson

项目特点

1.内置主键:JSON块如果包含id字段,在选择建表操作模式时内部会自动为id设置primary key
2.支持JSON数据生成建表语句:按照内置语法编写JSON,支持生成创建表的SQL语句
3.支持JSON数据生成更新语句:按照内置语法编写JSON,支持生成创更新的SQL语句,可配置单条件、多条件更新操作
4.支持JSON数据生成插入语句:按照内置语法编写JSON,支持生成创插入的SQL语句,如果JSON中包含 多层 (children)子嵌套,可按照相关语法指定作为父级id的字段
5.内置操作语法:该工具在选取不同的操作模式时,内置特定的使用语法规范
6.支持字段替换:需转译的JSON中字段与对应的SQL字段不一致时可以选择字段替换
7.界面友好:支持在线编辑JSON代码,支持代码高亮、语法校验、代码格式化、查找和替换、代码块折叠等,体验良好

解决痛点

下面就让我来给大家介绍一下Sql-Translation 可以解决哪些痛点问题:

需要将大量JSON中的数据导入到数据库中,但是JSON中包含大量父子嵌套关系 ——> 可以使用本站

在进行JSON数据导入数据库时,遇到JSON字段与数据库字段不一致需要替换字段时 ——> 可以使用本站

根据Apifox工具来实现更新或新增接口(前提是对接口已经完成了设计工作),提供了Body体数据,而且不想手动编写SQL时 ——> 可以使用本站

对上述三点进行进行举例说明(按照顺序):

第一种情况:

{
"id": "320500000",
"text": "苏州工业园区",
"value": "320500000",
"children": [
{
"id": "320505006",
"text": "斜塘街道",
"value": "320505006",
"children": []
},
{
"id": "320505007",
"text": "娄葑街道",
"value": "320505007",
"children": []
},
....
]
}

第二种情况:


第三种情况


以上内容就是该工具的简单介绍,由于该工具内置了部分语法功能,想要了解本工具全部工具以及想要动手操作的的同学请点击前往操作文档 ,该操作文档中包含了具体的语法介绍以及每种转换的具体示例数据 提供测试使用。


地址传送门



如果感兴趣的同学还希望可以到源码仓库给作者点个star⭐ 作为支持,非常感谢!


作者:派同学
链接:https://juejin.cn/post/7168285867160076295
来源:稀土掘金
收起阅读 »

使用RecyclerView实现三种阅读器翻页样式

一、整体逻辑 为何直接对RecyclerView进行扩展而不使用ViewPager/ViewPager2?原因如下: Scroll Model(垂直滑动)需要自定义自动滑动(对指定页进行吸附) Flip Mode(仿真翻页)需要获取各种情况下的方向信息,以...
继续阅读 »

一、整体逻辑


image.png


为何直接对RecyclerView进行扩展而不使用ViewPager/ViewPager2?原因如下:



  1. Scroll Model(垂直滑动)需要自定义自动滑动(对指定页进行吸附)

  2. Flip Mode(仿真翻页)需要获取各种情况下的方向信息,以实现更好的控制

  3. RecyclerView方便拓展,同时三种模式同时使用RecyclerView实现,便于复用


实现逻辑:三种滑动模式都在RecyclerView地基础上更改其滑动行为,横向滑动需要修改子View层级,仿真翻页需要再覆盖一层仿真动画


二、横向覆盖滑动(Slide Mode)


横向.gif


Slide Mode 最适合直接使用 ViewPager,不过我们还是以 RecyclerView 为基础来实现,让三种模式统一实现方式。实现思路:先实现跨页吸附,再实现覆盖翻页效果


1、跨页吸附


实现跨页吸附,需要在手指离开屏幕时对 RecyclerView 进行复位吸附操作,有两种情况:


(1)Scroll Idle


拖拽发生后,RecyclerView 滑动状态变为 SCROLL_STATE_IDLE 时,需要进行复位吸附操作


// OrientationHelper为系统提供的辅助类,LayoutManager的包装类
// 可以让我们方便的计算出RecyclerView相关的各种宽高,计算结果和LayoutManager方向相关
open fun snapToTargetExistingView(helper: OrientationHelper): Pair<Int, Int>? {
val lm = mRecyclerView.layoutManager ?: return null
val childCount = lm.childCount // 可见数量
if (childCount < 1) return null

var closestChild: View? = null
var absClosest = Int.MAX_VALUE
var scrollDistance = 0
// RecyclerView中心点,LayoutManager为竖向则是Y轴坐标,为横向则是X轴坐标
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2

// 从可见Item中找到距RecyclerView离中心最近的View
for (i in 0 until childCount) {
val child = lm.getChildAt(i) ?: continue
if (consumeSnap(i, child)) return null // consumeSnap 默认返回false,竖直滑动模式才使用
val childCenter = (helper.getDecoratedStart(child)
+ helper.getDecoratedMeasurement(child) / 2)
val absDistance = abs(childCenter - containerCenter)
if (absDistance < absClosest) {
absClosest = absDistance
closestChild = child
scrollDistance = childCenter - containerCenter
}
}
closestChild ?: return null

// 滑动
when (orientation) {
VERTICAL -> mRecyclerView.smoothScrollBy(0, scrollDistance)
HORIZONTAL -> mRecyclerView.smoothScrollBy(scrollDistance, 0)
}
return Pair(scrollDistance, lm.getPosition(closestChild))
}


(2)Fling


可以通过 RecyclerView 提供的OnFlingListener消费掉Fling,将其转化为 SmoothScroll ,滑动到指定位置


①、找到吸附目标的位置(adapter position)


open fun findTargetSnapPosition(
lm: RecyclerView.LayoutManager,
velocity: Int,
helper: OrientationHelper
)
: Int {
val itemCount: Int = lm.itemCount
if (itemCount == 0) return RecyclerView.NO_POSITION

// 中心点以前距离最近的View
var closestChildBeforeCenter: View? = null
var distanceBefore = Int.MIN_VALUE // 中心点以前,距离为负数
// 中心点以后距离最近的View
var closestChildAfterCenter: View? = null
var distanceAfter = Int.MAX_VALUE // 中心点以后,距离为正数
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2

val childCount: Int = lm.childCount
for (i in 0 until childCount) {
val child = lm.getChildAt(i) ?: continue
if (consumeSnap(i, child)) return RecyclerView.NO_POSITION // consumeSnap 默认返回false,竖直滑动模式才使用

val childCenter = (helper.getDecoratedStart(child)
+ helper.getDecoratedMeasurement(child) / 2)
val distance = childCenter - containerCenter

// Fling需要考虑方向,先获取两个方向最近的View
if (distance in (distanceBefore + 1)..0) {
distanceBefore = distance
closestChildBeforeCenter = child
}
if (distance in 0 until distanceAfter) {
distanceAfter = distance
closestChildAfterCenter = child
}
}

// 根据方向选择Fling到哪个View
val forwardDirection = velocity > 0
if (forwardDirection && closestChildAfterCenter != null) {
return lm.getPosition(closestChildAfterCenter)
} else if (!forwardDirection && closestChildBeforeCenter != null) {
return lm.getPosition(closestChildBeforeCenter)
}

// 边界情况处理
val visibleView =
(if (forwardDirection) closestChildBeforeCenter else closestChildAfterCenter)
?: return RecyclerView.NO_POSITION
val visiblePosition: Int = lm.getPosition(visibleView)
val snapToPosition = (visiblePosition - 1)

return if (snapToPosition < 0 || snapToPosition >= itemCount) {
RecyclerView.NO_POSITION
} else snapToPosition
}

②、使用RecyclerView的「LinearSmoothScroller」完成吸附动画


private fun createScroller(
oh: OrientationHelper
)
: LinearSmoothScroller {
return object : LinearSmoothScroller(mRecyclerView.context) {
override fun onTargetFound(
targetView: View,
state: RecyclerView.State,
action: Action
)
{
val d = distanceToCenter(targetView, oh)
val time = calculateTimeForDeceleration(abs(d))
if (time > 0) {
when (orientation) {
VERTICAL -> action.update(0, d, time, mDecelerateInterpolator)
HORIZONTAL -> action.update(d, 0, time, mDecelerateInterpolator)
}
}
}

override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics) =
100f / displayMetrics.densityDpi

override fun calculateTimeForScrolling(dx: Int) =
100.coerceAtMost(super.calculateTimeForScrolling(dx))
}
}

protected fun distanceToCenter(targetView: View, helper: OrientationHelper): Int {
val childCenter = (helper.getDecoratedStart(targetView)
+ helper.getDecoratedMeasurement(targetView) / 2)
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2
return childCenter - containerCenter
}

完整操作:


protected fun snapFromFling(
lm: RecyclerView.LayoutManager,
velocity: Int,
helper: OrientationHelper
)
: Pair<Boolean, Int> {
val targetPosition = findTargetSnapPosition(lm, velocity, helper)
if (targetPosition == RecyclerView.NO_POSITION) return Pair(false, 0)
val smoothScroller = createScroller(helper)
smoothScroller.targetPosition = targetPosition
lm.startSmoothScroll(smoothScroller)
return Pair(true, targetPosition) // 消费fling
}

2、覆盖效果实现


(1)如果使用PageTransform实现


如果使用ViewPagerPageTransform,是可以实现覆盖动画的,实现思路:使可见View的第二个View跟随屏幕滑动


image.png


假设上图蓝色透明矩形为屏幕,其他为ItemView,图片上半部分正常滑动的状态,下半部分为 translate view 之后的状态。可以看到,在横向滑动过程中,最多可见2个View(蓝色透明方框最多覆盖2个View),此时将第二个View跟随屏幕,其他View保持跟随画布滑动,即可达到效果。在OnPageScroll回调中实现这个逻辑:


for (i in 0 until layoutManager.childCount) {
layoutManager.getChildAt(i)?.also { view ->
if (i == 1) {
// view.left是个负数,offsetPx(=-view.left)是个正数
view.translationX = offsetPx.toFloat() - view.width // 需要translate的距离(向前移需要负数)
} else {
// 恢复其余位置的translate
view.translationX = 0f
}
}
}

(2)扩展RecyclerView实现覆盖翻页


知道如何通过 PageTransfrom 实现后,我们来看看直接使用 RecyclerView 如何实现。观看ViewPager2源码可知PageTransfrom的实现方式


image.png


故我们直接copy代码,在OnScrollListener中自行实现onPageScrolled回调即可实现覆盖翻页效果。


但是此时还有一个问题,就是子View的层级问题,你会发现上面的滑动示意图中,绿色View会在黄色View之上,如何解决这个问题呢?我们需要控制View的绘制顺序,前面的View后绘制,保证前面地View在后面的View的绘制层级之上。


观看源码会发现,RecyclerView其实提供了一个回调ChildDrawingOrderCallback,可以很方便地实现这个效果:


override fun attach() {
super.attach()
mRecyclerView.setChildDrawingOrderCallback(this)
}

override fun onGetChildDrawingOrder(childCount: Int, i: Int) = childCount - i - 1 // 反向绘制

三、竖直滑动(Scroll Mode)


垂直.gif


竖直滑动需要滑动到跨章的位置时才吸附(自动回滚到指定位置),需要实现两个效果:跨章吸附、跨章Fling阻断。我们可以在横向覆盖滑动(Slide Mode)的基础上做一个减法,首先将LayoutManager改为竖向的,然后实现上述两个效果。


1、跨章吸附


实现跨章吸附,我们先在 RecyclerViewAdapter 中对每个View进行一个标记:


companion object {
const val TYPE_NONE = 100 // 其他
const val TYPE_FIRST_PAGE = 101 // 首页
const val TYPE_LAST_PAGE = 102 // 末页
}


fun bind() { // onBindViewHolder 时调用
itemView.tag = when {
textPage.isLastPage -> TYPE_LAST_PAGE
textPage.isFirstPage -> TYPE_FIRST_PAGE
else -> TYPE_NONE
}
......
}

其次我们实现横向覆盖滑动(Slide Mode)中的一段代码(做一个减法):


// 如果不是章节的最后一页,则消费Snap(不进行吸附操作)
override fun consumeSnap(index: Int, child: View) =
index == 0 && child.tag != ReadBookAdapter.TYPE_LAST_PAGE

这样就可以实现不是跨越章节的翻页不进行吸附,而跨越章节的滑动会自动吸附。


2、跨章Fling阻断


在滑动过程中,基于可见View只有两个的情况:



  • 如果向上滑动,判断第一个可见View是否「末页」,如果是,smoothScroll到第二个可见View

  • 如果向下滑动,判断第二个可见View是否「首页」,如果是,smoothScroll到第一个可见View


private var inFling = false     // 正在fling,在OnFlingListener中设置为true
private var inBlocking = false // 阻断fling


override val mScrollListener = object : RecyclerView.OnScrollListener() {
var mScrolled = false

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
inFling = false // 重置inFling
}
RecyclerView.SCROLL_STATE_IDLE -> {
inFling = false // 重置inFling
if (inBlocking) {
inBlocking = false // 忽略阻断造成的IDLE
} else if (mScrolled) {
mScrolled = false
snapToTargetExistingView(orientationHelper.value)
}
}
}
}

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy != 0) {
if (!mScrolled) {
this@VSnapHelper.mCallback.onScrollBegin()
mScrolled = true
}
val lm = mRecyclerView.layoutManager ?: return
// fling阻断
if (inFling && !inBlocking) {
val child: View?
val type: Int
if (dy > 0) { // 向上滑动
child = lm.getChildAt(0)
type = ReadBookAdapter.TYPE_LAST_PAGE
} else {
child = lm.getChildAt(lm.childCount - 1)
type = ReadBookAdapter.TYPE_FIRST_PAGE
}
child?.let {
if (it.tag == type) {
inBlocking = true
val d = distanceToCenter(it, orientationHelper.value)
mRecyclerView.smoothScrollBy(0, d)
}
}
}
}
}
}

四、仿真页(Flip Mode)


仿真.gif


仿真页在横向覆盖滑动(Slide Mode)基础之上实现,我们还需要实现:



  1. 确认手指滑动方向

  2. 所有可见View都跟随屏幕

  3. 绘制次序根据拖拽方向改变,保证目标页在当前页之上

  4. 绘制仿真页

  5. 手指抬起后的翻页动画(确认Fling、Scroll Idle产生的两种Snap的方向,因为手指会来回滑动导致方向判断错误)


1、确认手指滑动方向


滑动方向不能直接在 onTouchdispatchTouchEvent 这些方法中直接判断,
因为极微小的滑动都会决定方向,这样会造成轻微触碰就判定了方向,导致页面内容闪动、抖动等问题。
我们需要在滑动了一定距离后确定方向,最好的选择就是在 onPageScroll 中进行判断,系统为我们保证了ScrollState已变为DRAGGING,此时用户100%已经在滑动。可以看下源码真正触发「onPageScroll」的条件有哪些


image.png


我们实现的判断方向的代码:


// 在onScrolled中调用
// mCurrentItem:onPageSelected中赋值,代表当前Item
// position:第一个可见View的位置
// offsetPx:第一个可见View的left取负
// mForward:方向,true为画布向左滑动(向尾部滑动),false画布向右滑动(向头部滑动)
private fun dispatchScrolled(position: Int, offsetPx: Int) {
if (mScrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
mForward = mCurrentItem == position
}
mCallback.onPageScrolled(position, mCurrentItem, offsetPx, mForward)
}

image.png


不过这个规则在超快速滑动时会判断错误,即settling直接变dragging的时候,所以会对滑动做一点限制


override fun dispatchTouchEvent(e: MotionEvent): Boolean {
if (snapHelper.mScrollState == RecyclerView.SCROLL_STATE_SETTLING) {
return true // sellting过程中禁止滑动
}
delegate.onTouch(e)
return super.dispatchTouchEvent(e)
}

2、遮盖效果


所有可见View都跟随屏幕,横向覆盖滑动(Slide Mode)的增强版,因为给 RecyclerView设置了 offScreenLimit=1 的效果,所以 LayoutManagerchild 数量最多会有4个
(参照 ViewPager2 # LinearLayoutManagerImpl 实现,这里设置是为了滑动时可以第一时间生成目标页的截图)


// onPageScrolled中调用
private fun transform(offsetPx: Int, firstVisible: Int) {
val count = layoutManager.childCount
if (count == 2 || (count == 3 && offsetPx == 0)) {
// 可见View只有一个的时候,全部复位
for (i in 0 until count) {
layoutManager.getChildAt(i)?.also { view ->
view.translationX = 0f
}
}
} else {
var target = 1
if (count == 3 && firstVisible == 0) target-- // 首位适配,currentItem=0且存在滑动的时候
for (i in 0 until layoutManager.childCount) {
layoutManager.getChildAt(i)?.also { view ->
when (i) {
target -> view.translationX = offsetPx.toFloat()
target + 1 -> view.translationX = offsetPx.toFloat() - view.width
else -> view.translationX = 0f
}
}
}
}
}

3、绘制次序根据拖拽方向改变


保证目标页在当前页之上,防止绘制的仿真页消失时出现闪屏(瞬间显示了不正确的页)


// 画布左移则反向绘制,右移则正想绘制
override fun getDrawingOrder(childCount: Int, i: Int) =
if (snapHelper.mForward) childCount - i - 1 else i

4、绘制仿真页


我们在 RecyclerView 的父View上直接覆盖绘制一层仿真页Bitmap


(1)生成截图


如上面所说,实现了 offScreenLimit=1 的效果,我们在首次获取到方向时生成截图:


// 生成截图方法
fun View.screenshot(): Bitmap? {
return runCatching {
val screenshot = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
val c = Canvas(screenshot)
c.translate(-scrollX.toFloat(), -scrollY.toFloat())
draw(c)
screenshot
}.getOrNull()
}
private var isBeginDrag = false

override fun onPageStateChange(state: Int) {
when (state) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
isBeginDrag = true
}
}
}

override fun onPageScrolled(firstVisible: Int, current: Int, offsetPx: Int, forward: Boolean) {
if (isBeginDrag) {
isBeginDrag = false
delegate.apply {
if (forward) {
nextBitmap?.recycle()
nextBitmap = layoutManager.findViewByPosition(current + 1)?.screenshot()
curBitmap?.recycle()
curBitmap = layoutManager.findViewByPosition(current)?.screenshot()
} else {
prevBitmap?.recycle()
prevBitmap = layoutManager.findViewByPosition(current - 1)?.screenshot()
curBitmap?.recycle()
curBitmap = layoutManager.findViewByPosition(current)?.screenshot()
}
setDirection(if (forward) AnimDirection.NEXT else AnimDirection.PREV)
}
invalidate()
}
}

(2)绘制仿真页


绘制仿真页参考 gedoor/legadoSimulationPageDelegate



  • 基础知识:三角函数、Android的矩阵、贝塞尔曲线、canvas.clipPath的 XOR & INTERSECT 模式

  • 绘制方法:Android仿真翻页:cnblogs.com

  • 计算方法:使用手指触摸点和触摸点对应的角位置(比如触摸点靠近右下角,角位置就是右下角),这两个点可以算出所有参数


确认方向后,我们只用通过修改手指触碰点的参数即可控制整个动画(根据点击位置实时计算即可)


5、动画控制


手指抬起后的翻页动画通过 Scroller+invalidate实现


override fun computeScroll() {
if (scroller.computeScrollOffset()) {
setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
} else if (isStarted) {
stopScroll()
}
}

对于FlingScroll Idle产生的吸附效果,我们需要各自回调方向:


// 选中时开始动画,此时position改变
override fun onPageSelected(position: Int) {
val page = adapter.data[position]
ReadBook.onPageChange(page)
if (canDraw) {
delegate.onAnimStart(300, false)
}
}

// position未改变的情况
override fun onSnap(isFling: Boolean, forward: Boolean, changePosition: Boolean) {
if (!changePosition) {
delegate.onAnimStart(
300,
true,
// 未改变方向,向前则播放向后动画
if (forward) AnimDirection.PREV else AnimDirection.NEXT
)
}
}

Scroll Idle通过 SmoothScroll 所需要滑动的距离正负判断方向:


// Scroll
override fun snapToTargetExistingView(helper: OrientationHelper): Pair<Int, Int>? {
mSnapping = true
super.snapToTargetExistingView(helper)?.also {
// first为滑动距离,second为目标Item的position
mCallback.onSnap(false, it.first > 0, mCurrentItem != it.second)
return it
}
return null
}

// Fling
override val mFlingListener = object : RecyclerView.OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
val lm = mRecyclerView.layoutManager ?: return false
mRecyclerView.adapter ?: return false
val minFlingVelocity = mRecyclerView.minFlingVelocity
val result = snapFromFling(
lm,
velocityX,
orientationHelper.value
)
val consume = abs(velocityX) > minFlingVelocity && result.first
if (consume) {
mSnapping = true
// second为目标Item的position,这里直接通过速度正负来判断方向
mCallback.onSnap(true, velocityX > 0, result.second != mCurrentItem)
}
return consume
}
}

(以上为所有关键点,只截取了部分

作者:三尺丶
来源:juejin.cn/post/7244819106343829564
代码,提供一个思路)

收起阅读 »

为什么要招聘有经验的人?

周末带娃出去玩,回来的时候路过一家新开的水果店买东西,因为是新店,所以店里在搞促销活动。进去后发现一个很糟糕的问题。店员对收银系统不熟悉,排队的人要好几分钟才能结算一个,很多人等着不耐烦就走了。现在客户买东西,在哪里买都是买,体验不好,留不住客户的。 这让我想...
继续阅读 »

周末带娃出去玩,回来的时候路过一家新开的水果店买东西,因为是新店,所以店里在搞促销活动。进去后发现一个很糟糕的问题。店员对收银系统不熟悉,排队的人要好几分钟才能结算一个,很多人等着不耐烦就走了。现在客户买东西,在哪里买都是买,体验不好,留不住客户的。


这让我想到前年六月份,公司开第一家实体店的时候,也出现过类似情况。当初做的是让客户通过小程序线下扫码购买,优惠设计得很复杂,服务员虽然做过培训,但很多细节不清楚。当客户支付出现异常情况时,又来回沟通处理。这就让用户很不耐烦。最后虽然看起来店里人多热闹,但实际营收并不高。


这并不是特例,有经验的老板,在正式开店前会有一段时间的试营业,非常低调地开门。等员工都熟悉工作了,才会正式开业。


我们都知道招聘的时候,企业更喜欢招聘有经验的人。为啥?因为经验指的是你不仅知道一个东西,还做到过。你能解决某个问题,解决问题的方法才是经验。


如果你只是第一次做,就算培训过,那也不是经验,只能说你知道某件事。从知道到做到,中间还隔很远,越是复杂系统,越需要花更多时间在这个做到的过程上。就像我们做菜,你按照菜谱做,第一次也大概率做得也不会很好吃。


另外一个原因是,人往往容易高估自己的能力。我们在评估一个项目工作量时就很容易犯这个错误,你要是没经验,很容易把一个复杂需求看得很简单。导致工作量评估不足,这也是导致项目延期很重要的一个原因之一。


说到这里,相信你也知道招聘的背后是找一个能解决他们现有问题的人,这个解决问题的经验才是他们需要的。那如果我们想换工作,我想你应该也知道招聘的关键是”经验“,跟岗位相关的经验都是加分项。在进一步思考,人的时间都是一样的,把时间用哪里,把解决什么问题变成经验,这是我们可以思考的方向之一。比如,你想做一名项目经理。那项目管理经验就是你的加分项。十人的管理、百人的管理,管理的深度不一样,获得经验也是不一样的。


总结一下,今天主要想跟大家表达两个观点:


1、珍惜你的时间,用有限的时间去拥有更多跟工作相关的经验。


2、没有经验就不要太乐观地去做一件事情。反之,在做一件没有做过的事情前,应该找一个无利益关系且有经验的前辈咨询下。


推荐相关阅读: 初学者思维 - 找到解决

问题的新方法

收起阅读 »

Axios的封装思路与技巧

web
Axios的封装思路与技巧 提示:文中使用的为ts代码,对ts不熟悉的同学可以删除所有类型降级为js代码,不影响使用 前言 项目中或多或少会有一些需要接口发送请求的需求,与其复制粘贴别人在业务中对请求方法的使用,不如自己花点时间研究项目中请求方法的实现,这样在...
继续阅读 »

Axios的封装思路与技巧


提示:文中使用的为ts代码,对ts不熟悉的同学可以删除所有类型降级为js代码,不影响使用


前言


项目中或多或少会有一些需要接口发送请求的需求,与其复制粘贴别人在业务中对请求方法的使用,不如自己花点时间研究项目中请求方法的实现,这样在处理请求出现的问题时能够更好的定位问题原因。本文循序渐进的介绍了如何对axios进行封装实现自己项目中的请求方法,希望各位同学在阅读后能有一定的体会,如有问题还请大家在评论区指正。


1.创建axios实例


创建一个axios实例,在这里为实例进行一些配置(如超时时间)。但是要注意,不要在此处配置一些动态的属性,如headers中的token,具体的原因我们会在后面提起


import axios from 'axios'
const instance = axios.create({
 timeout: 1000 * 120, // 超时时间120s
})

为实例配置拦截器(请求拦截器,响应拦截器)


可能有些同学对axios不太熟悉,不了解请求拦截器和响应拦截器的作用,这里会简单介绍一下


请求拦截器:在请求发送前进行拦截,或者对请求错误进行拦截


instance.interceptors.request.use(
 config => {
   // config为AxiosRequestConfig的一个实例,它是包含请求配置参数的对象
   // 在这里可以在请求发送前做一些处理,如向config实例中添加属性,取消请求,设置loading等  
   return config
},
 // 这里是请求报错时的拦截方法,这里直接返回一个状态为reject的promise
 // 实际测试时,即使前端请求报错并且未到达后端,也没有触发这里的钩子函数
 error => Promise.reject(error),
)

响应拦截器:在响应被.then.catch处理前拦截


instance.interceptors.response.use(
 response => {
   // 响应成功的场景
   // 在这里可以关闭loading或者对响应的返参对象response进行处理
   return response
},
 error => {
   // 响应失败的场景
   // http状态码不为2xx时就会进入,根据项目要求处理接口401,404,500等情况
   // 返回的promise也可以根据项目要求进行修改
   return Promise.reject(error)
},
)

这样我们就创建了一个可用的axios实例,对于实例的一些其他配置可以参考axios官网


2.创建Abstract类进一步封装


在创建了一个axios实例之后,我们就可以使用它去发送请求了,但是出于减少重复代码的目的,我们不在业务代码中直接使用axios实例去发送各种请求,而是选择去做进一步的封装让整体的代码更加简洁


通常来说,我在项目中更喜欢用面向对象的方式去对axios做进一步的封装,使用这种方式的优点会在后面进行说明 创建一个类,起名可以按自己的喜好来,这里我写的是Abstract,因为它的主要作用是做为一个底层的类让其他类去继承,在这里我们提供一些属性的配置,以及一些基础的请求方法


import axios from './axios'
import type { AxiosRequest, CustomResponse } from './types/index'
class Abstract {
 // 配置接口的baseUrl,这里用的是vite环境变量,可以根据需求自行修改
 protected baseURL: string = import.meta.env.VITE_BASEURL
 // 配置接口的请求头,这里仅简单配置一下
 protected headers: object = {
   'Content-Type': 'application/json;charset=UTF-8',
}
 // 提供类的构造器,可以在这里修改一些基础参数如baseUrl
 constructor(baseURL?: string) {
   this.baseURL = baseURL ?? this.baseURL
}
 // 重点!发起请求的方法
 // 这里的T是ts中泛型的用法,主要用于控制接口返回的类型,不熟悉ts的同学可以略过
 private apiAxios<T = any>({
   baseURL = this.baseURL,
   headers = this.headers,
   method,
   url,
   data,
   params,
   responseType
}: AxiosRequest): Promise<CustomResponse<T>> {
 // 在这里加上请求头的好处在于,每次请求时都会动态读取存储的token值
 // 正如前面所说的,不要在创建axios实例时在header上配置token是因为,浏览器除非刷新,否则只会创建一次axios实例,它的header上的token的值不会发生变化,如果涉及到用户退出等清除token的操作,下次登录时获得的新token不会被使用
 Object.assign(headers, {
   // 根据情况使用localStorage或sessionStorage
   token: localStorage.getItem('token')        
})
 return new Promise((resolve, reject) => {
   axios({
     baseURL,
     headers,
     method,
     data,
     url,
     params,
     responseType,
  })
    .then(res => {
       // 在这里处理http2xx的接口,根据业务的需要进行一些处理,返回一个成功的promise
       // 这里仅为演示,直接返回了原始的res
       resolve(res)
    })
    .catch(err => {
       // 在这里处理http不成功的状态,并根据业务的需要进行一个处理,返回一个失败的promise
       reject(err)
    })
  })
}
 // 通常我还会在基础类上封装一些现成的请求方法,如Get Post等,可以根据自己的需要封装其他的请求方法
 protected getReq<T = any>({ baseURL, headers, url, data, params, responseType, messageType }: AxiosRequest) {
   return this.apiAxios<T>({
     baseURL,
     headers,
     method: 'GET',
     url,
     data,
     params,
     responseType,
     messageType,
  })
}
   
 protected postReq<T = any>({ baseURL, headers, url, data, params, responseType, messageType }: AxiosRequest) {
   return this.apiAxios<T>({
     baseURL,
     headers,
     method: 'POST',
     url,
     data,
     params,
     responseType,
     messageType,
  })
}

3.继承Abstract类实现业务相关的请求类


这样,我们就成功封装了一个Axios的基础类,接下来可以创建一个新的业务类去继承它并使用。这里我们创建一个User类,代表用户相关的请求


import Abstract from '@/api/abstract'

class User extends Abstract {
 constructor(baseUrl?: string) {
   super(baseUrl)
}

 // post请求
 login(data: unknown) {
   return this.postReq({
     data,
     url: 'back/v1/user/login',
  })
}
 
 // get请求
 getUser(param: { id: string })
   return this.getReq({
     param,
     url: 'back/v1/user/getUser'
  })
}
 
 // 需要修改请求头的Content-Type,如表单上传
 saveUser(data: any) {
   const formData = new FormData()
   Object.keys(data).forEach(key => {
     formData.append('file', data[key])
  })
   return this.postReq({
     data,
     headers: {
       'Content-Type': 'multipart/form-data',
    },
     url: 'back/v1/user/saveUser',
  })
}

export default User

文件创建好了之后我们就可以引用到具体项目中使用了


// 可以在这里传入baseUrl,这也是基于类封装的好处,我们可以实例化多个user并使用不同的baseUrl
const userInstance = new User()
const res = await userInstance.login({
 username: 'xxx',
 password: 'xxx'
})
作者:kamesan
来源:juejin.cn/post/7264749103125184527

收起阅读 »

我家等离子电视也能用的移动端适配方案

web
前几天我的领导“徐江”让我把一个移动端项目做一下适配,最好让他在家用等离子电视也能看看效果,做不出来就给我“埋了”,在这种情况下才诞生了这篇文章~ 什么是移动端适配 移动端适配是指在不同尺寸的移动端设备上,页面能相对达到合理的显示或者保持统一的等比缩放效果 ...
继续阅读 »

前几天我的领导“徐江”让我把一个移动端项目做一下适配,最好让他在家用等离子电视也能看看效果,做不出来就给我“埋了”,在这种情况下才诞生了这篇文章~



什么是移动端适配


移动端适配是指在不同尺寸的移动端设备上,页面能相对达到合理的显示或者保持统一的等比缩放效果


移动端适配的两个概念



  1. 自适应:根据不同的设备屏幕大小来自动调整尺寸、大小

  2. 响应式:会随着屏幕的实时变动而自动调整,是一种自适应


1.png

而在我们日常开发中自适应布局在pc端和移动端应用都极为普遍,一般是针对不同端分别做自适应布局,如果要想同时兼容移动端和pc端,尤其是等离子电视这样的大屏幕,那么最好还是使用响应式布局~


移动端适配-视口(viewport)


在一个浏览器中,我们可以看到的区域就是视口(viewport),我们在css中使用的fixed定位就是相对于视口进行定位的。

在pc端的页面中,我们不需要对视口进行区分,因为我们的布局视口和视觉视口都是同一个。而在移动端是不太一样的,因为我们的移动端的网页往往很小,有可能我们希望一个大的网页在移动端上也可以完整的显示,所以在默认情况下,布局视口是大于视觉视口的。


  <style>
.box {
width: 100px;
height: 100px;
background-color: orange;
}
</style>
<body>
<div class="box"></div>
</body>


pc端展示效果
3.png


移动端展示效果
4.png
从上图可以看出在移动端上同样是100px的盒子,但是却没有占到屏幕的1/3左右的宽度,这是因为在大部分浏览器上,移动端的布局视口宽度为980px,我们在把pc端的页面切换成移动端页面时,右上角也短暂的显示了一下我们的布局视口是980px x 1743px。

所以在移动端下,我们可以将视口划分为3种情况:



  1. 布局视口(layout viewport)

  2. 视觉视口(visual layout)

  3. 理想视口(ideal layout)


这些概念的提出也是来自于ppk,是一位世界级前端技术专家。
贴上大佬的文章链接quirksmode.org/mobile/view…


1.jpeg

所以我们相对于980px布局的这个视口,就称之为布局视口(layout viewport),而在手机端浏览器上,为了页面可以完整的显示出来,会对整个页面进行缩小,那么显示在可见区域的这个视口就是视觉视口(visual layout)。


3.png


4.png

但是我们希望设置的是100px就显示的是100px,而这就需要我们设置理想视口(ideal layout)。


// initial-scale:定义设备宽度与viewport大小之间的缩放比例
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

移动端适配方案



  1. 百分比设置

  2. rem单位+动态html的font-size

  3. flex的弹性布局

  4. vw单位


在我们移动端适配方案中百分比设置是极少使用的,因为相对的参照物可能是不同的,所以百分比往往很难统一,所以我们常常使用的都是后面3种方案。


rem单位+动态html的font-size


rem单位是相对于html元素的font-size来设置的,所以我们只需要考虑2个问题,第一是针对不同的屏幕,可以动态的设置html不同的font-size,第二是将原来的尺寸单位都转换为rem即可。



talk is cheap, show me the code



/*
* 方案1:媒体查询
* 缺点:需要针对不同的屏幕编写大量的媒体查询,且如果动态的修改尺寸,不会实时的进行更新
*/

<style>
@media screen and (min-width: 320px) {
html {
font-size: 20px;
}
}
@media screen and (min-width: 375px) {
html {
font-size: 24px;
}
}
@media screen and (min-width: 414px) {
html {
font-size: 28px;
}
}
.box {
width: 5rem;
height: 5rem;
background-color: orange;
}
</style>

<body>
<div class="box"></div>
</body>

/*
* 方案2:编写js动态设置font-size
*/

<script>
const htmlEl = document.documentElement;

function setRem() {
const htmlWidth = htmlEl.clientWidth;
const htmlFontSize = htmlWidth / 10;
htmlEl.style.fontSize = htmlFontSize + "px";
}
// 第一次不触发,需要主动调用
setRem();

window.addEventListener("resize", setRem);
</script>


<style>
.box {
width: 5rem;
height: 5rem;
background-color: orange;
}
</style>


<body>
<div class="box"></div>
</body>


但是写起来感觉还是好麻烦,如果可以的话我希望白嫖-0v0-


5.png

所以我选择 postcss-pxtorem,vscode中也可以下载到相关插件哦,一鱼多吃,😁


vw单位


/*
* 方案1:手动换算
*/

<style>
/** 设置给375的设计稿 */
/** 1vw = 3.75px */
.box {
width: 26.667vw;
height: 26.667vw;
background-color: orange;
}
</style>

/*
* 方案2:less/scss函数
*/

@vwUnit: 3.75;

.pxToVw(@px) {
result: (@px / @vwUnit) * 1vw;
}
.box {
width: .pxToVw(100) [result];
height: .pxToVw(100) [result];
background-color: orange;
}

当然白嫖党永不言输,我选择 postcss-px-to-viewport,贴一下我的配置文件~


module.exports = {
plugins: {
"postcss-px-to-viewport": {
unitToConvert: "px", //需要转换的单位,默认为"px"
viewportWidth: 750, // 视窗的宽度,对应设计稿的宽度
viewportUnit: "vw", // 指定需要转换成的视窗单位,建议使用 vw,兼容性现在已经比较好了
fontViewportUnit: "vw", // 字体使用的视口单位

// viewportWidth: 1599.96, // 视窗的宽度,对应设计稿的宽度
// viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用 vw
// fontViewportUnit: 'vw', // 字体使用的视口单位

unitPrecision: 8, // 指定`px`转换为视窗单位值的小数后 x位数
// viewportHeight: 1334, //视窗的高度,正常不需要配置
propList: ["*"], // 能转化为 rem的属性列表
selectorBlackList: [], //指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false, // 允许在媒体查询中转换
replace: true, //是否直接更换属性值,而不添加备用属性
// exclude: [], //忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
landscape: false, //是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
landscapeUnit: "rem", //横屏时使用的单位
landscapeWidth: 1134 //横屏时使用的视口宽度
}
}
};

rem单位和vw单位的区别


rem事实上是作为一个过渡方案,它利用的也是vw的思想,并且随着前端的发展,vw的兼容性已经越来越好了,可以说具备了rem的所有优势。

但是我们假想一个这样的场景,我们希望网页在达到800px的时候页面布局不需要继续扩大了,这个时候如果我们采用的是rem布局,我们可以使用媒体查询设置max-width,而vw则始终是以视口为单位,自然不容易处理这样的场景。

当然,vw相比于rem,存在以下优势:



  1. 不需要计算font-size大小

  2. 不存在font-size继承的问题

  3. 不存在因为某些原因篡改了font-size导致页面尺寸混乱的问题

  4. vw更加的语义化

  5. 具备rem的所有优点


所以在开发中也更推荐大家使用vw单位进行适配。


6.jpeg


作者:魔术师Grace
来源:juejin.cn/post/7197623702410248251
收起阅读 »

鹅厂七年半,写在晋升失败的不眠之夜

夜半惊醒 看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同...
继续阅读 »

夜半惊醒


看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同组小伙伴之后。技术人员,终究困于技术的窠臼。


工作经历


浑浑噩噩的四年


我毕业就进入鹅厂工作,15年,正是鹅厂大杀四方的一年,至今犹记得当时拿到鹅厂 offer 时的兴奋和同学眼中的艳羡。来了公司后,跟着项目组开始创新项目,一眼望到头的挣不来钱,浑浑噩噩的干了3年,和一起进公司的小伙伴们收入差距越来越大。


好在我的驱力不错,既然挣不到钱,那就好好积累技术能力,提升自己。那段时间,周围没有伙伴,没有对比,没有好的机会,只剩下我一个人慢慢悠悠的学习积累,我以为自己足够努力,没有荒废时间;后来才发现自己其实是井底之蛙,一起入职的小伙伴付出了百倍的努力和数不清的不眠之夜,1年多就已经能 cover 当时的业务了,没过2年就已经提干了。


2018年底,我满怀信心去答辩后台 3.1,没过。我当时还觉得评委装逼,挑刺,后来回头看写的都是s,一点含量都没有。


时来运转


2019年3月,答辩刚结束。部门业务调整,我们被整组划到了一个相对挣钱的业务上,leader 也调整成后台 leader。


新业务还是挣钱,凭着第一个项目积累的技术和项目经验,我得到了新领导的认可,第一年干的轻轻松松就挣到了远远超出之前的收入,和之前累死累活拿温饱线形成鲜明对比,可见大厂里跟对项目是多么的重要。


只是没想到第一年就是巅峰,后面随着贸易战、反垄断,鹅厂形势越来越差,我们业务收入也受到极大影响,人员也越来越冗余。挣的钱少了,分蛋糕的人还多了,可想而知,收入越来越差。


在这里,我成功从8级升到了10级工程师。说起来我们这一届实名惨,第一批毕业要等1年才能晋级,最后一批8升9需要公司范围内通道评审,第一批9升10走BG内评审,第一批10升11走部门内评审,全程试验品,一样没落下。


10升11


去年年底公司晋升改革,10升11评审下放部门,职级和待遇不再挂钩,不仅看重”武功“,还看中”战功“,同时传闻这次晋升名额极少。大领导找我谈话,要不就别去了?我想了想这两年的技术和项目积累,不甘心。


整个中心,就我和同组的小伙伴两个人一起去,我挑的是个一直在做的有技术挑战、持续优化的项目,小伙伴挑的是挣钱的项目。我准备了几个周末,小伙伴临时抱佛脚,结果小伙伴过了,我没过。评委说,我过度设计(优化过头)、没有实战演练容灾。


我和大领导说这个和我预期差太多了,我和小伙伴一起预答辩过,都知道讲的啥,咱是技术评审,我的项目技术含量、架构和挑战明明更好,为啥是我没过?大领导说你是认知偏差,人家讲的确实好。不忿,遂找评委 battle,咱要真按这个说法,能不能对参加评审的项目一致对待,不要双标?且不说我是不是过度设计,凭啥对我的项目就要求容灾演练,对别人的项目就视而不见?评委不语,你的项目对部门价值没产生什么收益和价值。


部门400+人,4个晋升11级名额,大概率是一个中心一个。一个技术评审,糅合了许多超过技术方面的考量,业务挣不挣钱和技术有啥关系?技术好的业务就能挣钱?业务挣钱多的是技术好的原因?


我以前也晋级失败过,但是我认输,因为相对而言,整个BG一起评审,你没过就是你技术差,其他因素影响没有这么大。这次明明是我更有技术深度、更投心思在优化上,却事与愿违。


反思与感悟


反思一下。千头万绪,毛主席说,那是没有抓住主要矛盾。


大的方面上讲:这两年大环境差,公司收入减少,需要降本增效。我要是管理层,也会对内部薪资成本、晋升等要求控制增速;政策制定上,也会将资源更倾斜于现金流业务,控制亏损业务的支出。在这个大的背景下,公司不愿意养“能力强,战功少”的技术骨干了;更愿意养“能力强,战功多”的人员。希望员工把精力都聚焦在业务挣钱上。


部门层面上讲:和公司政策一脉相承。晋升名额少,那紧着挣钱的中心、挣钱的小组、挣钱的个人给。这个意义上讲,职级体系已经不是衡量技术能力的标尺了。你技术能力强,没用。你得是核心,得是领导认可的人。


中心层面上讲:要争取名额,否则怎么团结手底下人干活。而且名额要给业务干活干的最出色的那个人,其他人就往后稍稍。我要是我领导,年前也会劝退我的。毕竟我技术能力虽然强,更多的是问题专家的角色,而不是业务核心。


且预测一下,中心之间肯定进行了各种资源置换。评审估计流于形式。慢说我被挑出来了问题,就是挑不出来问题也得给你薅下来。我就是和小伙伴互换项目去讲,估计还是小伙伴过,到时候评委就该说我“你做这东西有啥难度,我叫个毕业生都能来做”了。此处我想起了一副对联,不太合适,就是很搞笑。“说你行你就行不行也行,说你不行你就不行行也不行”,横批是“不服不行”。



从个人角度讲,付出和收获不成正比,难受吗?那肯定的。但谁让方向一开始就错了呢?这世界不公平的事情多了,不差这一个。



重新出发


综上来说,短期内,不改变发力方向到业务上,后面可以说晋升无望。以前老是觉得自己技术能力强,心高气傲,心思没在业务上,勤于术而懒于道,实在是太过幼稚。做了几年都对很多东西不甚了了。


今后,需要趁着还能拼的时候拼一下,潜心业务。编码和开发只是很小一部分,更要从:



  1. 业务大局出发,实时数据驱动,监控、统计一定要完善,要有看板,否则无法衡量。

  2. 主动探索业务,给产品经理出主意,一起合力把产品做上去做大,提升对业务的贡献,探索推荐、探索推广渠道等等。产品做不上去,个人也没机会发展。

  3. 多做向上管理,和领导、大领导多沟通。做好安排的每一件小事,同时主动汇报,争取重获信任。

  4. 主动承担,做一个领导眼里靠谱放心的人。

  5. 多思考总结,多拔高提升,不要做表面的勤奋工作,看似没有浪费时间,实则每分每秒都在浪费时间。

  6. 多社交,多沟通,多交流,打破技术人员的牢笼。


凡事都有两面性,福兮祸所依,祸兮福所倚。多受挫折、早受挫折不是坏事。


2023,就立个

作者:醉梦星河
来源:juejin.cn/post/7208907027840630840
flag 在这里吧。

收起阅读 »

逆流而上、拒绝躺平

前言 这是一篇非技术的文章,有感于近期和团队小伙伴们的交流、以及观察到的最近这一两年来大家对生活和工作态度的明显变化,一直在思考如何在这种逆风的局面下让自己成为受益者、而不是单纯的悲观主义者。算是一篇心灵鸡汤吧,写给自己、写给自己团队小伙伴们、也写给所有的同行...
继续阅读 »

前言


这是一篇非技术的文章,有感于近期和团队小伙伴们的交流、以及观察到的最近这一两年来大家对生活和工作态度的明显变化,一直在思考如何在这种逆风的局面下让自己成为受益者、而不是单纯的悲观主义者。算是一篇心灵鸡汤吧,写给自己、写给自己团队小伙伴们、也写给所有的同行(特别是几年才入行、或者即将入行的)。


现实总是那么艰难


一种整体的社会现象


“躺平”是一种来自中国的社会现象,源于一些年轻人对当前社会竞争压力和生活压力的反思。这个词汇在近年来逐渐流行起来,它代表了一种消极的、放弃抵抗的生活态度。躺平者认为,面对高房价、高消费、高竞争压力等现实问题,他们选择不再过度努力,而是保持低消费、低欲望的生活方式,以求在现实压力下保持心理平衡。躺平主义者通常会减少工作时间,避免加班,降低生活消费水平,不再过分追求物质和金钱。这种现象在一定程度上反映了年轻人对现实生活的无奈和对未来的担忧。


一个无可逃避的经济周期


【经济不可能永远高速增长】


经过改革开放这40多年以来的高速发展,经济的绝对值已经很大,这注定了速度会越来越慢;而且经济是有周期,不可能一直线性增长。


经济发展就像人的一生一样,总是在不断的波折中向前发展,只是当前我们正好赶上了前进道路上的逆境。


【社会的长期稳定性和阶层之间的流动性成反比】


如果一个社会长期稳定,那么阶层固化就会越来越严重,这是历史发展的必然,不是我们任何一个个体可以抗衡的。


一种心存美好但又无比失望的纠结心态


【心存美好是因为现实确实很美好】


能看到这篇文章的读者,一定是这个时代的受益者,我们一步一步见证了生活在不断地变好;尤其是我们这个行业的从业者,正享受着比大多数行业都要优厚的福利。


【失望是因为我们一直心存希望】


之所以现在大家感受到失望、甚至绝望,是因为还有希望,只是今非昔比,希望比较渺茫而已。


幸与不幸,完全来自于内心


面对当前的逆风局,无外乎以下几种选择:


1、消极应对:就地躺下,放弃抵抗,不做任何努力


能不干的就不干,降低欲望,放弃自我提升,放弃一切竞争


2、积极应对:保持拼搏和竞争意识,不放弃任何机会


保持顺风局一样的心态,不断学习进度、调整自我心态,努力拼搏,抓住任何能抓住的机会


更多的失望来源于应对方式和期待收获之间的错位


> 消极应对、降低甚至放弃期待
> 积极应对、保持或降低期待
> 消极应对、保持期待

从现实中的观察来看,第一类和第二类人所占的比例并不高,更多的是属于第三类,这类人一边吐槽和抱怨现实的不公、一边用实际行动来表明自己是在躺平,这两种行为螺旋交织在一起让这类人充满了负能量,随着时间的推移不断带来负向的反馈,结果就是内心的不平衡不断加剧,最终影响到生活以及身边亲近的人。


心态好,眼里才会有光


逆风局是弯道超车的机会


在当前逆风的情况下,大部分人都放弃了竞争和自我进步,这意味着相比之前我们可以用更少的投入,就能在竞争格局中保持一个相对有利的位置。


如果你能继续保持之前的投入的话,那么你将与其他竞争者拉开更大的差距。


现在是在为未来做投资


选择长期利益还是短期利益,这会决定你的心态和实际行动:


选择短期利益:这个更符合人性,因为让人会更加的舒适,顺应内心的想法,过好眼前的每一天


选择长期利益:就像投资一样,这是反人性的,毕竟短期看投入和收益是不成正比的;要想收获更多的长远利益,就得不断地调整自己的心态,不断对抗人性,这对大多人来说是非常痛苦的。


这两种选择都没有对错之分,只是一种生活态度罢了,只要你能接受因你的选择而带来的结果就行,不必刻意纠结


我的人生态度


用最悲观的眼光看待现实,用最积极的态度应对未来


现实不是我们任何一个个体能够干预的,我们唯一能做的就是时刻保持一个好的心态,降低短期的收获期待,时刻保持竞争意识,这样才能在逆风局中用相对较小的代价超越更多的竞争者,让自己保持在一个相对有利的位置。


结束语


以上是我最近一年的一些感想以及对未来的应对

作者:先秦剑仙
来源:juejin.cn/post/7264921418810966031
态度,与君共勉!!!

收起阅读 »

又干倒一家公司,我悟了

目前这家公司由于是政府项目,好多项目周期过长,未能及时回款,导致资金链断裂,摇摇欲坠,开始裁员。虽然由于项目资源稀缺,需要我,不至于裁到我头上,但是生存压力迫使我看向了外边,很快决定了入职新公司,同事请领导们吃散伙饭,有被裁的大佬,有留下的,有主动辞职的,各自...
继续阅读 »

目前这家公司由于是政府项目,好多项目周期过长,未能及时回款,导致资金链断裂,摇摇欲坠,开始裁员。虽然由于项目资源稀缺,需要我,不至于裁到我头上,但是生存压力迫使我看向了外边,很快决定了入职新公司,同事请领导们吃散伙饭,有被裁的大佬,有留下的,有主动辞职的,各自怀着复杂的心情,聊了几个小时,现总结一下,作为教训:


1. 技术方面

  • 谨慎对待新技术,技术路线要有延续性,做大版本更新,而不是每个项目都尝试新的路线方案。
  • 小项目,以高复用和快速实现为首要目的,不频繁更换框架。


2. 需求方面

  • 以产品或项目的需求导向而不是以技术为纲。

  • 需求方也许不懂要什么,产品去引导,简单到复杂,别把自己带进技术化和复杂化的迷宫。

  • 有些急的项目前期可以快速简单实现,先验证,而不是等原型、设计,错失良机。


3. 人员方面

  • 主要技术领导别追求完美,要控主方向和整体,而不是较真细节。

  • 十个项目一个成,架构师们歇歇吧,杀鸡不兴用牛刀,尽可能简单实现,别整太复杂的框架和路线。

  • 团队内聚,可以将产品、测试长期配置到各开发团队,减少成本矛盾和沟通问题。

  • boss需要及时频繁的与主要领导沟通,定方向,放大权,用人不疑。


4. 企业管理方面

  • 切忌盲目自信,疯狂扩张团队,利润和成本须同步,不能因短期项目招兵买马。

  • 适时而果断拿起手术刀,坚决断臂求生,及时且合规地删减人员,要有人情但不能妇人之仁、拖泥带水,对个体和公司都好。

最后,警惕以上问题,期待自己尽快找到更明确的方向,去奋斗!


作者:Adam289
链接:https://juejin.cn/post/7208534700223184951
来源:稀土掘金
收起阅读 »

Xcode快捷Behavior

前言在Xcode开发环境中,有一些可以自定义的快捷Behavior,可以大大提高开发效率。如何配置Behavior以下是在Xcode中配置Behavior的通用步骤:1.打开Xcode的偏好设置。2.点击“Behaviors”选项卡。3.点击左下角的"+"号创...
继续阅读 »

前言

在Xcode开发环境中,有一些可以自定义的快捷Behavior,可以大大提高开发效率。


如何配置Behavior

以下是在Xcode中配置Behavior的通用步骤:

1.打开Xcode的偏好设置。

2.点击“Behaviors”选项卡。

3.点击左下角的"+"号创建一个新的Behavior。

4.为Behavior命名,例如你希望调用的脚本名。

5.在“Run”下选择“Script”,然后粘贴你的脚本。

6.按需配置快捷键,并保存。

现在,每当你使用配置的快捷键时,它就会运行你的脚本。

Behavior1:打开终端并cd到当前工作目录
创建open_terminal.sh,写入以下内容

#!/bin/bash
open -a terminal "`pwd`"

并添加执行权限

sudo chmod +x open_terminal.sh

添加Behavior并在Run处选中该脚本路径,配置好快捷键。 当你使用配置的快捷键时,就会打开终端并cd到当前工作目录。

Behavior2:打开项目文件夹
创建open_project_folder.sh,写入以下内容

#!/bin/bash

# Path to your project
project_path="$1"

# Open the project folder in Finder
open "$project_path"

并添加执行权限

sudo chmod +x open_project_folder.sh

添加Behavior并在Run处选中该脚本路径,配置好快捷键。
当你使用配置的快捷键时,就会在Finder中打开你的项目文件夹。

总结

通过配置Behavior,我们可以更快速地访问项目文件夹和命令行等,从而提高开发效率。自定义Behavior是Xcode强大功能的一个体现,它允许我们根据自己的需求调整开发环境。

作者:冰淇淋真好吃
链接:https://juejin.cn/post/7262634764301844536
来源:稀土掘金

收起阅读 »

60分前端之canvas

web
前言 最近在等前端对接,没事干就折腾一下前端。写了这么多年服务端,一直都是写api接口,还没好好学过前端呢。前端框架那么多,什么Vue、React,还分移动端、PC端,还有什么响应式、自适应,各种花里胡哨的东西,也没那么多精力去折腾,直接上手小程序吧。 小程序...
继续阅读 »

前言


最近在等前端对接,没事干就折腾一下前端。写了这么多年服务端,一直都是写api接口,还没好好学过前端呢。前端框架那么多,什么Vue、React,还分移动端、PC端,还有什么响应式、自适应,各种花里胡哨的东西,也没那么多精力去折腾,直接上手小程序吧。


小程序开发虽然简单,但基本的CSS样式还是得学一下吧:

1. MDN官方CSS教程
2. 谷歌出的CSS教程
3. 谷歌HTML教程


在国内,微信小程序开发,应该是每个前端程序员必会的技术了吧。


话不多说,直接开干。


写了这么多年的hello world了,学一门“新”的技术还是很简单的。而且咱要求也不高,毕竟不是专门写前端的,所以对于前端的技术,只要做到60分就够了。


所以,下面很多东西,我们都是能忽略就忽略,主打一个囫囵吞枣、不求甚解,以解决问题为主。


微信小程序类目的坑


微信小程序文档


学习任何技术,官方文档都是最好的入门方式之一。


查看微信小程序官方文档,照着文档一步一步往下走,有个两年开发经验的人,基本都能搞定,没什么难度。


但是问题来了,在配置小程序时,需要设置小程序类目。这里提前说一下,因为是练手的项目,所以我写的这个小程序是个大杂烩,既有数独小游戏,又有背单词。由于微信小程序类目可以选择多个,所以我先选择了小游戏......


问题来了!


微信小程序类目,选择了游戏之后,就不能选择其他的了!


而且还改不了!!


改不了就算了,更坑的是,小游戏的代码结构和小程序还不一样,没办法通用。选择小游戏,就必须用小游戏的代码结构,没办法和小程序混用。


练手的几个小游戏,我都是用小程序实现的,没办法在小游戏模式下运行。


网上搜了一下,发现很多人都遇到过这样的问题,也都向官方反映过了,但官方都没有给回复。顺带一提,好像社区里很多问题,官方基本都不回复的。


没办法,只能重新注册一个号了。或者注销之前的号,然后重新注册也行。嫌麻烦,干脆重新注册一个号吧。


数独81宫格实现过程

数独游戏估计很多人都只听过,但没实际玩过,这里就不详细介绍数独游戏了,我们只需要知道一点,数独游戏需要有一个81宫格。


可以看到,数独的81宫格最外层边框加粗,里面每三列的右边框、每三行的下边框也加粗,相当于是9个9宫格合并在一起。

作为前端的初学者,想要实现一个样式,第一反应肯定是用CSS来实现。

CSS选择器

先来把基本的框给实现了。

.sudoku {
box-sizing: border-box;
width: 700rpx;
min-height: 730rpx;
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(9, 1fr);
gap: 0;
border: 2px solid #000000;
}
.cell {
background-color: #FFFFFF;
border: 1px solid #000000;
border-right-width: 0;
border-bottom-width: 0;
}

实现效果是这样的:


不知道大家有没有注意到这段样式min-height: 730rpx;,设置最小高度。


一开始设置height: 700rpx;,宽高都是700rpx,在iPhone12/13 Pro Max机型下没问题,可是在iPhone12/13 Pro机型下,最下面一行会少一截,高度不够了!


前端的同学都知道CSS的一个概念,叫盒模型。这里高度不够是因为每个小格子的边框也有个宽度,实际总高度应该是每个格子的高度加上格子边框的宽度之和。宽度也是一样的。


这里有一个问题,就算宽高要加上每个小格子边框的宽度,最终的宽高也应该一样的才对啊?!为什么高度反而比宽度还大了呢?!


不理解!算了,毕竟是写服务端的,前端只要做到60分就够了。解决问题就行。高度不够的话,给它加高不就行了。/doge


加多少呢?试呗。😁


好了,在iPhone12/13 Pro机型下问题是解决了,但其他机型下呢?


这个好办,既然知道高度可能不够,继续加高的话,怕影响其他机型下的效果,那干脆给一个最低高度,超过这个高度就让它自己加高,简单粗暴。


它已经是个成熟的高度了,已经学会了自己长高。


还有一种更直接的办法,就是不设置高度,让它自己撑开。但是这样有一个不好的点,高度在撑开的过程中,会很明显看到页面抖动。设置一个最低高度的话,抖动就不明显了。


只要我看不到它抖动,它就不存在。


基本的形状已经有了,下面开始对内部指定的边框进行加粗。先来对每三列的右边框加粗,看过上面的CSS教程的话,稍微学点CSS知识的人都知道,CSS有个伪类选择器,我们用选择器nth-child来实现:

.cell:nth-child(3n+1) {
border-left-width: 2px;
}

:nth-child(n) 选择器匹配父元素中的第 n 个子元素,元素类型没有限制。


n 可以是一个数字,一个关键字,或者一个公式。



这里,我们用的是公式的方式。



使用公式(an+ b)a代表一个循环的大小,n是一个计数器(从0开始),以及b是偏移量。ab都必须是整数,an 必须写在 b 的前面,不能写成 b+an 的形式。


第一列左边框看起来太粗了,虽然样式上看起来边框的宽度是2px,但视觉上却像是4px也不知道是什么原因?


不管它什么原因了,毕竟我们不是专业的前端,就像上面说的那样,我们只需要做到60分就够了。


既然视觉上看起来像是4px,那就让它在视觉上看起来像2px

.cell:nth-child(9n+1) {
border-left-width: 1px;
}


现在看起来顺眼了很多。


宽度明明设置的是1px,为什么视觉上看起来像2px,不知道什么原因?老规矩,不管!😁


每三列的左边框加粗,这个已经实现了,下面开始实现每三行的下边框加粗


可是问题来了,使用:nth-child选择器貌似不能选中连续的子元素,网上搜了一堆,都没有解决方案,后来问了一下ChatGPT,给的回复是这样的:

/* 设置第3行和第6行的下边框宽度为2px */
.cell:nth-child(3n+1):nth-child(n+7),
.cell:nth-child(3n+2):nth-child(n+8),
.cell:nth-child(3n+3):nth-child(n+9) {
border-bottom-width: 2px;
}

还亲切地给了注释。可惜根本用不了。


除了第一行前6个的下边框没加粗外,其他元素的下边框全加粗了!


题外话:ChatGPT对CSS的问题,一本正经地胡说八道,给的答案基本都用不了。


折腾了好久,都没折腾出怎么用CSS选择器实现每三行下边框加粗这个效果,算了,换种方式吧。


对象模型


数独游戏不但要有这个宫格,相应的宫格里还得要有数字的。最终的效果应该像这样:


既然没办法自动计算出每三列和每三行的元素,那我们就用笨办法,手动给它们全标记出来。

定义对象模型:

interface SudokuItem {
numerical: number, // 数值
tower: boolean, // 左边框是否需要加粗
floor: boolean // 下边框是否需要加粗
}

初始化对象模型:

Page({
data: {
sudoku: [
{numerical: 0, tower: false, floor: false},
{numerical: 0, tower: false, floor: false},
{numerical: 0, tower: true, floor: false},
...
]
}
})

实际初始化的时候,通过JS代码初始化sudoku,并计算哪个元素的tower为true,哪个floor为true。

页面渲染的时候:


class="cell-item {{item.tower ? 'tower' : ''}} {{item.floor ? 'floor' : ''}}"
bindtap="focusTheCell"
data-index="{{idx}}"
>
{{item.numerical == 0 ? undefined : item.numerical}}

.sudoku .floor {
border-top-width: 2px;
}

.sudoku .tower {
border-right-width: 2px;
}

好了,基本的样式是实现了,虽然过程很简单粗暴,但咱毕竟是写服务端的,对前端的要求也不高,能做到60分就够了。


继续折腾,上面是用CSS样式实现的81宫格,还有其他办法吗?


必须有啊,前端的Canvas技术也很火啊,这个必须得折腾一下啊。


而且貌似还有很多前端不会Canvas的哦,等我们学会了,就去找前端嘚瑟一下。


Canvas


MDN官方教程


先看Canvas官方教程,嗯......,东西挺多的,好像有点复杂,几十个API,看得人眼花。没关系,毕竟我们是带着问题来学习的。这里,我们只需要几个API就行了。


什么问题?使用Canvas画出一个数独的81宫格。


我在学习一个新技术的时候,都是先上手再深入,不懂的地方暂时先跳过去,前面说过的,主打一个囫囵吞枣、不求甚解。先把问题解决了再说。


我们先暂时抛开小程序,只专注于Canvas。


直接开干。


首页,创建一个Canvas画布。

const canvas = document.getElementById("sudoku");
const ctx = canvas.getContext("2d");

好了,一个画布就创建好了。是不是很简单。而且是固定套路,可以不理解,记住就行了。下次用的时候,直接复制粘贴,再简单改改就行了。


id:和HTML标签的id属性一样。


widthheight:画布的宽高,可以通过CSS属性调整。这里的宽高类似于图片的原始宽高,CSS调整后的宽高就和修改页面图片的宽高一样,实际是对图片进行缩放。


canvas.getContext("2d"):接受一个参数,即上下文的类型。可以传入2dwebglwebgl2bitmaprenderer,用于创建不同类型的上下文。


这里我们传入2d,创建一个二维渲染上下文。


画布创建好了,下面该画画了。


想想看,如果让你在现实中用笔画一个81宫格,你应该怎么做?


首先,得有张白纸,然后提笔,之后开始画。


在Canvas中也一样,白纸我们已经有了,上面创建的画布就是白纸。


下面,该考虑怎么画了。初中数学老师告诉我们,两点确定一条直线,从A点到B点,画一条直线。


Canvas通过路径和填充的方式来绘制图画的,填充我们暂时不管,路径可以简单理解为线段,直线、曲线这些。


每条线段就是一条路径。



生成路径的第一步叫做 beginPath()。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形、等等)构成图形。而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形、等等)构成图形。而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形。



不理解?记住就行了,都是固定套路。先记住怎么用,等bug写多了,自然就理解了。别管代码是不是写的垃圾,先敲出来再说,别只光在脑子里想。


ctx.beginPath():清空子路径列表开始一个新路径。可以简单理解成“提笔”。


ctx.moveTo(x, y):将一个新的子路径的起始点移动到 (x,y) 坐标。可以理解成上面的“从A点......”


ctx.lineTo(x, y):使用直线连接子路径的终点到(x,y)坐标。可以理解成上面的“到B点,画一条直线”


上面的(x, y)为坐标,页面坐标这个东西,相信很多人应该都已经知道了。


这里的坐标为画布中的坐标,坐标原点(0, 0)为画布的左上角。

const canvas = document.getElementById("sudoku");
// 创建二维上下文
const ctx = canvas.getContext("2d");
// 从A点
ctx.moveTo(100, 100);
// 到B点
ctx.lineTo(200, 100);

刷新一下页面,什么都没有!


别急,这就和我们现实中画画一样,先要在脑中构思,构思完了,才开始落笔绘画。


同样的道理,上面的那些相当于是在构思,构思完了后,我们得要落笔绘制。


ctx.stroke():根据当前的画线样式,绘制当前或已经存在的路径。


所以,完整的代码应该是这样的:

const canvas = document.getElementById("sudoku");
// 创建二维上下文
const ctx = canvas.getContext("2d");
ctx.beginPath();
// 从A点
ctx.moveTo(100, 100);
// 到B点
ctx.lineTo(200, 100);
// 画线
ctx.stroke();


具体可以看一下MDN的示例,使用canvas来绘制图形


好了,一条直线就绘制出来了。


可以了,能画出一条直线,我们可以开始画81宫格了。81宫格就是10条横线、10条竖线组成的。只要计算好每个点的坐标,然后画线就行了。


计算点的坐标,这是数学的范畴,就不过多赘述了,而且点的坐标计算也不难。

const canvas = document.getElementById("sudoku");
const ctx = canvas.getContext("2d");
// 单元格的宽高
const cellSize = 50;

for (let i = 0; i < 10; i++) {
// 画横线,留5px的间距
ctx.beginPath();
ctx.moveTo(5, i * cellSize + 5);
ctx.lineTo(455, i * cellSize + 5);
ctx.stroke();

// 画竖线
ctx.beginPath();
ctx.moveTo(i * cellSize + 5, 5);
ctx.lineTo(i * cellSize + 5, 455);
ctx.stroke();
}


81宫格是画出来了,可还没法用,因为数独中的81宫格,要求最外围边框加粗,里面每三列左边框加粗、每三行下边框加粗。


ctx.lineWidth:设置线段的宽度


还记得上面我们通过CSS选择器给边框加粗时遇到的痛苦吗,现在就简单多了,只需要简单的数学计算就可以了。

// 设置画笔宽度
if (i % 3 == 0) {
ctx.lineWidth = 3;
} else {
ctx.lineWidth = 1;
}

设置完后我们,看一下效果


下面是完整的代码:






<span class="hljs-string">Canvas</span> <span class="hljs-string">练习</span>





之后只要根据小程序的规则,将代码移植到小程序里就行了。至于怎么往数独里填充数字,这个之后再说吧,我们一点一点的来。这里我们只介绍这么画直线。


学一门新技术,一定要多练。数独的81宫格画完了,还可以画点其他的啊。比如画一个象棋的棋盘,或者画一个围棋的棋盘。


多练手,遇到的问题多了,自然就会了。


最后


虽然是一个练手的项目,但是既然已经做出来了,干脆就上线算了。

作者:W_懒虫
链接:https://juejin.cn/post/7260820017758093349
来源:稀土掘金
收起阅读 »

历时一个月,终于找到自己满意的工作了

由于公司经营遇到了巨大问题,出现严重亏损。 不得不忍痛告诉全体员工团队解散一事 衷心感谢全体小伙伴们在公司付出努力与汗水 目前待定截止今日,后续事宜人事小姐姐会逐步跟进安排。 再次感谢各位伙伴们 希望大家都能有一个好的前程,在未来的时候一定要努力奋斗,前程似锦...
继续阅读 »

由于公司经营遇到了巨大问题,出现严重亏损。
不得不忍痛告诉全体员工团队解散一事
衷心感谢全体小伙伴们在公司付出努力与汗水
目前待定截止今日,后续事宜人事小姐姐会逐步跟进安排。
再次感谢各位伙伴们
希望大家都能有一个好的前程,在未来的时候一定要努力奋斗,前程似锦



2023年6月16日我正在开开心心的写着代码,突然来了这么一条消息;我直接原地呆住了,沉思良久我才发现办公室内寂静的可怕,没有了键盘的敲击声;有的只有同事们的呼吸声,大家都依靠在椅子上,像是在思考什么。很快大家也都接受了这个不愿看到的实事。过了一两天就在开始办理离职一类的事情了。


投递简历


在离职以后,我们同事之间约好一起打了2个小时的球,吃了一个饭;在我们经常唱歌的地方,唱了几个小时歌,我们玩的很开心、吃的很开心,就像从来没有经历过这件事情一样。


在过了一段时间以后,我开始修改简历投递简历了。沟通的第一天就给我狠狠的打了一次脸,我在沟通了20/30个公司的时候,恢复我消息的不足5个,已读的大约有一半左右;后面我就开始加大了沟通的力度;大约在沟通了140家以后,收到了第一个面试机会。


然后第二个面试机会是在人才市场去找的。
第三个面试机会是在沟通了60多家以后得到的机会也是我满意的一家。


第一家


该公司是一家外包公司在成都的分公司,我进去面试的时候没有笔试题,直接是主管来面试的,主要的问题还是围绕业务层面以及上家的一些工作经历,然后就是一些关于vue的一些原理以及简单的算法问题。当时在面试完以后我自己感觉很好;觉得肯定能面试上,结果真的面试上了;下午的时候这家公司就给我打电话了,给出的工资是11k;但是我觉得外包不是很喜欢,而且是单双休,后面就给拒绝了,没想到周一的时候人事又给我打电话了,说主管这边商量了以后决定给你涨一千;请问你愿意来吗?说实话,当时是真的心动了;我考虑了一天以后还是拒绝了,因为我实在是不喜欢外包。


第二家


该公司属于半外包性质的,经历了两面,第一面是技术面(超级简单),第二面是主管过来的(主管是后端)面试的,第二面主要就是业务方面的,当时说的是智慧数字一类的产品,我当时确实被该概念吸引了,后面再谈了以后,发现并没有给我offer,我就发了一个消息过去问,然后人事告诉我说是:工资要的太高了,公司给不了,然后我说可以调薪;在多次沟通后给到了9.5k;然后半年有一次涨薪的机会、年底双薪以及试用期交社保一类的。我进去待了几天发现他们并不是什么智慧数字,而且技术用的不是很好的,所以我就放弃了,我个人觉得对我的技术提升没有太多的帮助


第三家


该公司是一家完全自研的公司,并且产品已经上线,用户量达到了千万,日活也有10多20万的样子;让我觉得很不错,所以在知晓了之后就对自己说一定要好好面一定要进去,哈哈哈


在该公司经历了三次面试吧!


第一次是技术面,问的问题也是一些业务问题;然后会涉及到一些js的基础原理以及vue中的一些实现原理等等问题(主要是大部分都忘了,哈哈哈);


然后第二次就是人事小姐姐问了一些问题,问了问题以后,人事小姐姐叫来了一个领导然后跟我谈,主要谈的话就是一些收获啊、自豪感啊、研究等等问题。


在面试完以后都过去了4个小时了,又遇到了下大雨;我骑上我的小电驴穿梭在城市的街头,却充满了一点小小的期待。


过了几个小时以后,通知我说面试通过了,试用期也是有社保、有年终、也有涨薪制度等等


补充:在这家公司中可能工资不高,但是这家公司的技术能力比较强而且用的很多技术我都不会,并且我也很想去学习此类的技术,刚好有这个机会,所以我是很开心的。


总结


以上是我这段时间面试的一些经历;但是工作确实并不好找。


主要原因还是面试机会少,很多公司都要求本科以及本专业等等,其实面试的话都还好基本上跟原来差别不大,还有就是对刚出来的这些小伙伴可能不是很友好。所以希望大家如果有工作的话,就先好好上班吧!目前大环境都是这样的,加油哦!


作者:雾恋
链接:https://juejin.cn/post/7263274550074769465
来源:稀土掘金
收起阅读 »

作为一个老程序员,想对新人说什么?

前言最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。这篇文章根据我多年的工作经验,给新人总结了26条建议,希望对你会有所帮...
继续阅读 »

前言

最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。


在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。

这篇文章根据我多年的工作经验,给新人总结了26条建议,希望对你会有所帮助。

1.写好注释


很多小伙伴不愿意给代码写注释,主要有以下两个原因:

1. 开发时间太短了,没时间写注释。
2.《重构》那本书说代码即注释。


我在开发的前面几年也不喜欢写注释,觉得这是一件很酷的事情。


但后来发现,有些两年之前的代码,业务逻辑都忘了,有些代码自己都看不懂。特别是有部分非常复杂的逻辑和算法,需要重新花很多时间才能看明白,可以说自己把自己坑了。


没有注释的代码,不便于维护。


因此强烈建议大家给代码写注释。


但注释也不是越多越好,注释多了增加了代码的复杂度,增加了维护成本,给自己增加工作量。


我们要写好注释,但不能太啰嗦,要给关键或者核心的代码增加注释。我们可以写某个方法是做什么的,主要步骤是什么,给算法写个demo示例等。


这样以后过了很长时间,再去看这段代码的时候,也会比较容易上手。


2.多写单元测试


我看过身边很多大佬写代码有个好习惯,比如新写了某个Util工具类,他们会同时在test目录下,给该工具类编写一些单元测试代码。


很多小伙伴觉得写单元测试是浪费时间,没有这个必要。


假如你想重构某个工具类,但由于这个工具类有很多逻辑,要把这些逻辑重新测试一遍,要花费不少时间。


于是,你产生了放弃重构的想法。


但如果你之前给该工具类编写了完整的单元测试,重构完成之后,重新执行一下之前的单元测试,就知道重构的结果是否满足预期,这样能够减少很多的测试时间。


多写单元测试对开发来说,是一个非常好的习惯,有助于提升代码质量。


即使因为当初开发时间比较紧,没时间写单元测试,也建议在后面空闲的时间内,把单元测试补上。


3.主动重构自己的烂代码


好的代码不是一下子就能写成的,需要不断地重构,修复发现的bug。


不知道你有没有这种体会,看自己1年之前写的代码,简直不忍直视。


这说明你对业务或者技术的理解,比之前更深入了,认知水平有一定的提升。


如果有机会,建议你主动重构一下自己的烂代码。把重复的代码,抽取成公共方法。有些参数名称,或者方法名称当时没有取好的,可以及时修改一下。对于逻辑不清晰的代码,重新梳理一下业务逻辑。看看代码中能不能引入一些设计模式,让代码变得更优雅等等。


通过代码重构的过程,以自我为驱动,能够不断提升我们编写代码的水平。


4.代码review很重要


有些公司在系统上线之前,会组织一次代码评审,一起review一下这个迭代要上线的一些代码。


通过相互的代码review,可以发现一些代码的漏洞,不好的写法,发现自己写代码的坏毛病,让自己能够快速提升。


当然如果你们公司没有建立代码的相互review机制,也没关系。


可以后面可以多自己review自己的代码。


5.多用explain查看执行计划


我们在写完查询SQL语句之后,有个好习惯是用explain关键字查看一下该SQL语句有没有走索引。


对于数据量比较大的表,走了索引和没有走索引,SQL语句的执行时间可能会相差上百倍。


我之前亲身经历过这种差距。


因此建议大家多用explain查看SQL语句的执行计划。

6.多看看优秀的工具


太空电梯、MOSS、ChatGPT等,都预兆着2023年注定不会是平凡的一年。任何新的技术都值得推敲,我们应要有这种敏感性。


这几年隐约碰过低代码,目前比较热门,很多大厂都相继加入。



低代码平台概念:通过自动代码生成和可视化编程,只需要少量代码,即可快速搭建各种应用。



到底啥是低代码,在我看来就是拖拉拽,呼呼呼,一通操作,搞出一套能跑的系统,前端,后端,数据库,一把完成。当然这可能是最终目标。


链接:http://www.jnpfsoft.com/?juejin,如果你感兴趣,也体验一下。


JNPF的优势就在于它能生成前后台代码,提供了极大的灵活性,能够创建更复杂、定制化的应用。它的架构设计也让开发者无需担心底层技术细节,能够专注于应用逻辑和用户体验的开发。


7.上线前整理checklist


在系统上线之前,一定要整理上线的清单,即我们说的:checklist。


系统上线有可能是一件很复杂的事情,涉及的东西可能会比较多。


假如服务A依赖服务B,服务B又依赖服务C。这样的话,服务发版的顺序是:CBA,如果顺序不对,可能会出现问题。


有时候新功能上线时,需要提前执行sql脚本初始化数据,否则新功能有问题。


要先配置定时任务。


上线之前,要在apollo中增加一些配置。


上线完成之后,需要增加相应的菜单,给指定用户或者角色分配权限。


等等。


系统上线,整个过程中,可能会涉及多方面的事情,我们需要将这些事情记录到checklist当中,避免踩坑。


8.写好接口文档


接口文档对接口提供者,和接口调用者来说,都非常重要。


如果你没有接口文档,别人咋知道你接口的地址是什么,接口参数是什么,请求方式时什么,接口多个参数分别代码什么含义,返回值有哪些字段等等。


他们不知道,必定会多次问你,无形当中,增加了很多沟通的成本。


如果你的接口文档写的不好,写得别人看不懂,接口文档有很多错误,比如:输入参数的枚举值,跟实际情况不一样。


这样不光把自己坑了,也会把别人坑惨。


因此,写接口文档一定要写好,尽量不要马马虎虎应付差事。


如果对写接口文档比较感兴趣,可以看看我的另一篇文章《瞧瞧别人家的API接口,那叫一个优雅》,里面有详细的介绍。


9.接口要提前评估请求量


我们在设计接口的时候,要跟业务方或者产品经理确认一下请求量。


假如你的接口只能承受100qps,但实际上产生了1000qps。


这样你的接口,很有可能会承受不住这么大的压力,而直接挂掉。


我们需要对接口做压力测试,预估接口的请求量,需要部署多少个服务器节点。


压力测试的话,可以用jmeter、loadRunner等工具。


此外,还需要对接口做限流,防止别人恶意调用你的接口,导致服务器压力过大。


限流的话,可以基于用户id、ip地址、接口地址等多个维度同时做限制。


可以在nginx层,或者网关层做限流。


10.接口要做幂等性设计


我们在设计接口时,一定要考虑并发调用的情况。


比如:用户在前端页面,非常快的点击了两次保存按钮,这样就会在极短的时间内调用你两次接口。


如果不做幂等性设计,在数据库中可能会产生两条重复的数据。


还有一种情况时,业务方调用你这边的接口,该接口发生了超时,它有自动重试机制,也可能会让你这边产生重复的数据。


因此,在做接口设计时,要做幂等设计。


当然幂等设计的方案有很多,感兴趣的小伙伴可以看看我的另一篇文章《高并发下如何保证接口的幂等性?》。


如果接口并发量不太大,推荐大家使用在表中加唯一索引的方案,更加简单。


11.接口参数有调整一定要慎重


有时候我们提供的接口,需要调整参数。


比如:新增加了一个参数,或者参数类型从int改成String,或者参数名称有status改成auditStatus,参数由单个id改成批量的idList等等。


建议涉及到接口参数修改一定要慎重。


修改接口参数之前,一定要先评估调用端和影响范围,不要自己偷偷修改。如果出问题了,调用方后面肯定要骂娘。


我们在做接口参数调整时,要做一些兼容性的考虑。


其实删除参数和修改参数名称是一个问题,都会导致那个参数接收不到数据。


因此,尽量避免删除参数和修改参数名。


对于修改参数名称的情况,我们可以增加一个新参数,来接收数据,老的数据还是保留,代码中做兼容处理。


12.调用第三方接口要加失败重试


我们在调用第三方接口时,由于存在远程调用,可能会出现接口超时的问题。


如果接口超时了,你不知道是执行成功,还是执行失败了。


这时你可以增加自动重试机制。


接口超时会抛一个connection_timeout或者read_timeout的异常,你可以捕获这个异常,用一个while循环自动重试3次。


这样就能尽可能减少调用第三方接口失败的情况。


当然调用第三方接口还有很多其他的坑,感兴趣的小伙伴可以看看我的另一篇文章《我调用第三方接口遇到的13大坑》,里面有详细的介绍。


13.处理线上数据前,要先备份数据


有时候,线上数据出现了问题,我们需要修复数据,但涉及的数据有点多。


这时建议在处理线上数据前,一定要先备份数据。


备份数据非常简单,可以执行以下sql:

create table order_2022121819 like `order`;
insert into order_2022121819 select * from `order`;

数据备份之后,万一后面哪天数据处理错了,我们可以直接从备份表中还原数据,防止悲剧的产生。


14.不要轻易删除线上字段


不要轻易删除线上字段,至少我们公司是这样规定的。


如果你删除了某个线上字段,但是该字段引用的代码没有删除干净,可能会导致代码出现异常。


假设开发人员已经把程序改成不使用删除字段了,接下来如何部署呢?


如果先把程序部署好了,还没来得及删除数据库相关表字段。


当有insert请求时,由于数据库中该字段是必填的,会报必填字段不能为空的异常。


如果先把数据库中相关表字段删了,程序还没来得及发。这时所有涉及该删除字段的增删改查,都会报字段不存在的异常。


所以,线上环境字段不要轻易删除。


15.要合理设置字段类型和长度


我们在设计表的时候,要给相关字段设置合理的字段类型和长度。


如果字段类型和长度不够,有些数据可能会保存失败。


如果字段类型和长度太大了,又会浪费存储空间。


我们在工作中,要根据实际情况而定。


以下原则可以参考一下:

1.尽可能选择占用存储空间小的字段类型,在满足正常业务需求的情况下,从小到大,往上选。

2.如果字符串长度固定,或者差别不大,可以选择char类型。如果字符串长度差别较大,可以选择varchar类型。

3.是否字段,可以选择bit类型。

4.枚举字段,可以选择tinyint类型。

5.主键字段,可以选择bigint类型。

6.金额字段,可以选择decimal类型。

7.时间字段,可以选择timestamp或datetime类型。


16.避免一次性查询太多数据


我们在设计接口,或者调用别人接口的时候,都要避免一次性查询太多数据。


一次性查询太多的数据,可能会导致查询耗时很长,更加严重的情况会导致系统出现OOM的问题。


我们之前调用第三方,查询一天的指标数据,该接口经常出现超时问题。


在做excel导出时,如果一次性查询出所有的数据,导出到excel文件中,可能会导致系统出现OOM问题。


因此我们的接口要做分页设计。


如果是调用第三方的接口批量查询接口,尽量分批调用,不要一次性根据id集合查询所有数据。


如果调用第三方批量查询接口,对性能有一定的要求,我们可以分批之后,用多线程调用接口,最后汇总返回数据。


17.多线程不一定比单线程快


很多小伙伴有一个误解,认为使用了多线程一定比使用单线程快。


其实要看使用场景。


如果你的业务逻辑是一个耗时的操作,比如:远程调用接口,或者磁盘IO操作,这种使用多线程比单线程要快一些。


但如果你的业务逻辑非常简单,在一个循环中打印数据,这时候,使用单线程可能会更快一些。


因为使用多线程,会引入额外的消耗,比如:创建新线程的耗时,抢占CPU资源时线程上下文需要不断切换,这个切换过程是有一定的时间损耗的。


因此,多线程不一定比单线程快。我们要根据实际业务场景,决定是使用单线程,还是使用多线程。


18.注意事务问题


很多时候,我们的代码为了保证数据库多张表保存数据的完整性和一致性,需要使用@Transactional注解的声明式事务,或者使用TransactionTemplate的编程式事务。


加入事务之后,如果A,B,C三张表同时保存数据,要么一起成功,要么一起失败。


不会出现数据保存一半的情况,比如:表A保存成功了,但表B和C保存失败了。


这种情况数据会直接回滚,A,B,C三张表的数据都会同时保存失败。


如果使用@Transactional注解的声明式事务,可能会出现事务失效的问题,感兴趣的小伙伴可以看看我的另一篇文章《聊聊spring事务失效的12种场景,太坑了》。


建议优先使用TransactionTemplate的编程式事务的方式创建事务。


此外,引入事务还会带来大事务问题,可能会导致接口超时,或者出现数据库死锁的问题。


因此,我们需要优化代码,尽量避免大事务的问题,因为它有许多危害。关于大事务问题,感兴趣的小伙伴,可以看看我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,里面有详情介绍。


19.小数容易丢失精度


不知道你在使用小数时,有没有踩过坑,一些运算导致小数丢失了精度。


如果你在项目中使用了float或者double类型的数据,用他们参与计算,极可能会出现精度丢失问题。


使用Double时可能会有这种场景:

double amount1 = 0.02;
double amount2 = 0.03;
System.out.println(amount2 - amount1);

正常情况下预计amount2 - amount1应该等于0.01

但是执行结果,却为:

0.009999999999999998

实际结果小于预计结果。


Double类型的两个参数相减会转换成二进制,因为Double有效位数为16位这就会出现存储小数位数不够的情况,这种情况下就会出现误差。


因此,在做小数运算时,更推荐大家使用BigDecimal,避免精度的丢失。


但如果在使用BigDecimal时,使用不当,也会丢失精度。

BigDecimal amount1 = new BigDecimal(0.02);
BigDecimal amount2 = new BigDecimal(0.03);
System.out.println(amount2.subtract(amount1));

这个例子中定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。

结果:

0.0099999999999999984734433411404097569175064563751220703125

使用BigDecimal的构造函数创建BigDecimal,也会导致精度丢失。

如果如何避免精度丢失呢?

BigDecimal amount1 = BigDecimal.valueOf(0.02);
BigDecimal amount2 = BigDecimal.valueOf(0.03);
System.out.println(amount2.subtract(amount1));

使用BigDecimal.valueOf方法初始化BigDecimal类型参数,能保证精度不丢失。


20.优先使用批量操作


有些小伙伴可能写过这样的代码,在一个for循环中,一个个调用远程接口,或者执行数据库的update操作。


其实,这样是比较消耗性能的。


我们尽可能将在一个循环中多次的单个操作,改成一次的批量操作,这样会将代码的性能提升不少。

例如:

for(User user : userList) {
userMapper.update(user);
}

改成:

userMapper.updateForBatch(userList);

21.synchronized其实用的不多


我们在面试中当中,经常会被面试官问到synchronized加锁的考题。


说实话,synchronized的锁升级过程,还是有点复杂的。


但在实际工作中,使用synchronized加锁的机会不多。


synchronized更适合于单机环境,可以保证一个服务器节点上,多个线程访问公共资源时,只有一个线程能够拿到那把锁,其他的线程都需要等待。


但实际上我们的系统,大部分是处于分布式环境当中的。


为了保证服务的稳定性,我们一般会把系统部署到两个以上的服务器节点上。


后面哪一天有个服务器节点挂了,系统也能在另外一个服务器节点上正常运行。


当然也能会出现,一个服务器节点扛不住用户请求压力,也挂掉的情况。


这种情况,应该提前部署3个服务节点。


此外,即使只有一个服务器节点,但如果你有api和job两个服务,都会修改某张表的数据。


这时使用synchronized加锁也会有问题。


因此,在工作中更多的是使用分布式锁。


目前比较主流的分布式锁有:

1.数据库悲观锁。

2.基于时间戳或者版本号的乐观锁。

3.使用redis的分布式锁。

4.使用zookeeper的分布式锁。


其实这些方案都有一些使用场景。


目前使用更多的是redis分布式锁。


当然使用redis分布式锁也很容易踩坑,感兴趣的小伙伴可以看看我的另一篇文章《聊聊redis分布式锁的8大坑》,里面有详细介绍。


22.异步思想很重要


不知道你有没有做过接口的性能优化,其中有一个非常重要的优化手段是:异步。


如果我们的某个保存数据的API接口中的业务逻辑非常复杂,经常出现超时问题。


现在让你优化该怎么优化呢?


先从索引,sql语句优化。


这些优化之后,效果不太明显。


这时该怎么办呢?


这就可以使用异步思想来优化了。


如果该接口的实时性要求不高,我们可以用一张表保存用户数据,然后使用job或者mq,这种异步的方式,读取该表的数据,做业务逻辑处理。


如果该接口对实效性要求有点高,我们可以梳理一下接口的业务逻辑,看看哪些是核心逻辑,哪些是非核心逻辑。


对于核心逻辑,可以在接口中同步执行。


对于非核心逻辑,可以使用job或者mq这种异步的方式处理。


23.Git提交代码要有好习惯


有些小伙伴,不太习惯在Git上提交代码。


非常勤劳的使用idea,写了一天的代码,最后下班前,准备提交代码的时候,电脑突然死机了。


会让你欲哭无泪。


用Git提交代码有个好习惯是:多次提交。


避免一次性提交太多代码的情况。


这样可以减少代码丢失的风险。


更重要的是,如果多个人协同开发,别人能够尽早获取你最新的代码,可以尽可能减少代码的冲突。


假如你开发一天的代码准备去提交的时候,发现你的部分代码,别人也改过了,产生了大量的冲突。


解决冲突这个过程是很痛苦的。


如果你能够多次提交代码,可能会及时获取别人最新的代码,减少代码冲突的发生。因为每次push代码之前,Git会先检查一下,代码有没有更新,如果有更新,需要你先pull一下最新的代码。


此外,使用Git提交代码的时候,一定要写好注释,提交的代码实现了什么功能,或者修复了什么bug。


如果有条件的话,每次提交时在注释中可以带上jira任务的id,这样后面方便统计工作量。


24.善用开源的工具类


我们一定要多熟悉一下开源的工具类,真的可以帮我们提升开发效率,避免在工作中重复造轮子。


目前业界使用比较多的工具包有:apache的common,google的guava和国内几个大佬些hutool。


比如将一个大集合的数据,按每500条数据,分成多个小集合。


这个需求如果要你自己实现,需要巴拉巴拉写一堆代码。


但如果使用google的guava包,可以非常轻松的使用:

List list = Lists.newArrayList(1, 2, 3, 4, 5);
List> partitionList = Lists.partition(list, 2);
System.out.println(partitionList);

如果你对更多的第三方工具类比较感兴趣,可以看看我的另一篇文章《吐血推荐17个提升开发效率的“轮子”》。


25.培养写技术博客的好习惯


我们在学习新知识点的时候,学完了之后,非常容易忘记。


往往学到后面,把前面的忘记了。


回头温习前面的,又把后面的忘记了。


因此,建议大家培养做笔记的习惯。


我们可以通过写技术博客的方式,来记笔记,不仅可以给学到的知识点加深印象,还能锻炼自己的表达能力。


此外,工作中遇到的一些问题,以及解决方案,都可以沉淀到技术博客中。


一方面是为了避免下次犯相同的错误。


另一方面也可以帮助别人少走弯路。


而且,在面试中如果你的简历中写了技术博客地址,是有一定的加分的。


因此建议大家培养些技术博客的习惯。


26.多阅读优秀源码


建议大家利用空闲时间,多阅读JDK、Spring、Mybatis的源码。


通过阅读源码,可以真正的了解某个技术的底层原理是什么,这些开源项目有哪些好的设计思想,有哪些巧妙的编码技巧,使用了哪些优秀的设计模式,可能会出现什么问题等等。


当然阅读源码是一个很枯燥的过程。


有时候我们会发现,有些源码代码量很多,继承关系很复杂,使用了很多设计模式,一眼根本看不明白。


对于这类不太容易读懂的源码,我们不要一口吃一个胖子。


要先找一个切入点,不断深入,由点及面的阅读。


我们可以通过debug的方式阅读源码。


在阅读的过程中,可以通过idea工具,自动生成类的继承关系,辅助我们更好的理解代码逻辑。


我们可以一边读源码,一边画流程图,可以更好的加深印象。

作者:高端章鱼哥
链接:https://juejin.cn/post/7257735219435765820
来源:稀土掘金
收起阅读 »

iOS热修复,看这里就够了(手把手教你玩热修)

背景 对于app store的审核周期不确定性,可长到2星期,短到1天。假如线上的应用出现了一些bug,甚至是致命的崩溃,这时候假如按照苹果的套路乖乖重新发布一个版本,然后静静等待看似漫无期限的审核周期,最终结果就是:用户大量流失。因此,对于一些线上...
继续阅读 »

背景


对于app store的审核周期不确定性,可长到2星期,短到1天。假如线上的应用出现了一些bug,甚至是致命的崩溃,这时候假如按照苹果的套路乖乖重新发布一个版本,然后静静等待看似漫无期限的审核周期,最终结果就是:用户大量流失。因此,对于一些线上的bug,需要有及时修复的能力,这就是所谓的热修复(hotfix)。


随着迭代频繁或者次数的增多,应用出现功能异常或不可用的情况也会随之增多。这时候又有什么方法可以快速解决线上的问题呢?第一、在一开始功能设计的时候就设计降级方案,但随之开发成本和测试成本都会双倍增加;第二、每个功能加上开关配置,这样治标不治本,当开关关掉的时候,就意味着用户无法使用该功能。这时候热修复就是解决这种问题的最佳之选,既能修复问题,又能让用户无感知,两全其美。iOS热修复技术从最初的webView到最近的SOT,技术的发展越来越快,新技术已经到来:


一. 首先是原理篇MangoFix:(知道原理才能更好的干活)


热修复的核心原理:


1.  拦截目标方法调用,让其调用转发到预先埋好的特定方法中

1.  获取目标方法的调用参数


只要完成了上面两步,你就可以随心所欲了。在肆意发挥前,你需要掌握一些 Runtime 的基础理论,

Runtime 可以在运行时去动态的创建类和方法,因此你可以通过字符串反射的方式去动态调用OC方法、动态的替换方法、动态新增方法等等。下面简单介绍下热修复所需要用到的 Runtime 知识点。


OC消息转发机制



由上图消息转发流程图可以看出,系统给了3次机会让我们来拯救。


第一步,在resolveInstanceMethod方法里通过class_addMethod方法来动态添加未实现的方法;


第二步,在forwardingTargetForSelector方法里返回备用的接受者,通过备用接受者里的实现方法来完成调用;


第三步,系统会将方法信息打包进行最终的处理,在methodSignatureForSelector方法里可以对自己实现的方法进行方法签名,通过获取的方法签名来创建转发的NSInvocation对象,然后再到forwardInvocation方法里进行转发。


方法替换就利用第三步的转发进行替换。


当然现在有现成的,初级及以上iOS开发工程师很快就可以理解的语法分析,大概了解一下mangofix是可以转化oc和swift代码的:具体详情请看

http://www.jianshu.com/p/7ae91a2da…


那么为什么它可以执行转化呢,转化逻辑是什么?

MangoFix项目主页上中已经讲到,MangoFix既是一个iOS热修复SDK,但同时也是一门DSL(领域专用语言),即iOS热修复领域专用语言。既然是一门语言,那肯定要有相应的编译器或者解析器。相对于编译器,使用解析器实现语言的执行,虽然效率低了点,但显然更加简单和灵活,所以MangoFix选择了后者。下面我们先用一张简单流程图,看一下MangoFix的运行原理,然后逐一解释。




1、MangoFix脚本


首先热修复之前,我们先要准备好热修复脚本文件,以确定我们的修复目标和执行逻辑,这个热修复脚本文件便是我们这里要介绍的MangoFix脚本,正常是放在我们的服务端,然后由App在启动时或者适当的运行期间进行下载,利用MangoFix提供的MFContext对象进行解析执行。对于MangoFix脚本的语法规则,这点可以参考MangoFix Quick Start,和OC的语法非常类似,你如果有OC开发经验,相信你花10分钟便可以学会。当然,在后续的文章中我可能也会介绍这一块。


2、词法分析器


几乎所有的语言都有词法分析器,主要是将我们的输入文件内容分割成一个个token,MangoFix也不例外,MangoFix词法分析器使用Lex所编写,如果你想了解MangoFix词法分析器的代码,可以点击这里


3、语法分析器


和词法分析器类似,几乎所有语言也都有自己的语法分析器,其主要目的是将词法分析器输出的一个个token构建成一棵抽象语法树,而且这颗抽象语法树是符合我们预先设计好的上下文无关文法规则的,如果你想了解MangoFix语法分析器的代码,可以点击这里


4、语义检查


由于语法分析器输出的抽象语法树,只是符合上下文无关文法规则,没有上下文语义关联,所以MangoFix还会进一步做语义检查。比如我们看下面代码:

less  
复制代码
@interface MyViewController : UIViewController

@end
angelscript  
复制代码
class MyViewController : BaseViewController{

- (void)viewDidLoad{
    //TODO
}

}

上面部分是OC代码,下面部分是MangoFix代码,从文法角度MangoFix这个代码是没有问题的,但是在逻辑上却有问题, MyViewController在原来OC中和MangoFix中继承的父类不一致,这是OC runtime所不允许的。


5、创建内置对象


MangoFix脚本中很多功能都是通过预先创建内置对象的方式支持的,比如常用结构体的声明、变量、宏、C函数和GCD相关的操作等,如果想详细了解MangoFix中有哪些内置对象,可以点击这里。当然MangoFix也开放了相关接口,你也可以向MangoFix执行上下文中注入你需要的对象。


6、执行顶层语句


在做完上面的操作后,MangoFix解析器就开 始真正执行MangoFix脚本了,比如顶层语句的执行、结构体的声明、类的定义等。


7、利用runtime热修复


现在就到了最关键一步了,就是利用runtime替换掉原来Method的IMP指针,MangoFix利用libffi库动态创建C函数,在创建的C函数中调用MangoFix脚本中方法,然后用刚刚创建的C函数替换原来Method的IMP指针,当然MangoFix也会保留原有的IMP指针,只不过这时候该IMP指针对应的selector要在原有的基础上在前面拼接上ORG,这一点和JSPatch一致。当然,MangoFix也支持对属性的添加。


8、MangoFix方法执行


最后当被修复的OC方法在被调用的时候,程序会走到我们动态创建的C函数中,在该函数中我们通过查找一个全局的方法替换表,找到对应的MangoFix方法实现,然后利用MangoFix解析器执行该MangoFix的方法。


二. 具体执行(OC修复OC)。


1.后台分发补丁平台:


补丁平台:patchhub.top/mangofix/lo…


github地址:github.com/yanshuimu/M…

1.首先你要明白:必须得有个后台去上传,分发bug的文件,安全起见,脚本已经通过AES128加密,终端收到加密的脚本再去解密,防止被劫持和篡改,造成代码出现问题。
登录这个补丁平台,可以快速创建appid。
github地址下载并配合使用:
以下是MangoFixUtil的说明:
MangoFixUtil是对MangoFix进行了简单的封装,该库在OC项目中实战已经近2年多,经过多次迭代,比较成熟。但需要搭配补丁管理后台一起使用,后台由作者开发维护,目前有50+个已上架AppStore的应用在使用,欢迎小伙伴们使用。

2.举个实战中的例子:

我们快速迭代中遇到的一些问题:



有一次我们解析到后台数据从中间截取字符串,然而忘了做判空操作,后台数据一旦不给返回,那么项目立马崩溃,所以做了热修复demo.mg文件放到Patch管理平台,具体具体代码如OC基本一致:

class JRMineLoginHeaderView:JRTableViewHeaderView {  

- (NSString *)getNetStringNnm:(NSString *)str{
    NSError *error = nil;
    if(str.length<=0) {
        return @"";
    }
    
    NSRegularExpression *regex = NSRegularExpression.regularExpressionWithPattern:options:error:(@"\d+",0,&error);

    if (error) {
        return @"";
    } else {
    
    if (str.length == 0) {
        return @"";
    }
        
        NSArray *matches = regex.matchesInString:options:range:(str,0,NSMakeRange(0, str.length));
        for (NSTextCheckingResult *match in matches) {
            NSString *matchString = str.substringWithRange:(match.range);
            return matchString;
        }
    }
    return @"";
}

}

以上代码中,新增了对象长度判空操作: if(str.length<=0) {
return @"";
}
完美的解决了崩溃的问题。


2.oc转换成DSL语言。


一切准备就绪,oc转换成DSL语言浪费人力,而且准确率又低怎么办?怎么可以快速的用oc转换成mangofix语言呢?

这是macOS系统上的可视化辅助工具,将OC语言转成mangofix脚本。


做iOS热修复时,大量时间浪费在OC代码翻译成脚本上,提供这个辅助工具,希望能给iOSer提供便利,
本人写了一个mac应用,完美的解决了不同语法障碍,转换问题。

mac版本最低(macos10.11)支持内容:


(1)OC代码 一键 批量转换成脚本


(2)支持复制.m内容粘贴,转换


(3)支持单个OC API转换,自动补全


(4)报错提示:根据行号定位到OC代码行



自动转化文件QQ群获取。



3.打不开“OC2PatchTool.app”,因为它来自身份不明的开发者


方案1.系统偏好设置>>安全与隐私>>允许安装未知来源


方案2.打开 Terminal 终端后 ,在命令提示后输入

sudo spctl --master-disable

OC转换成 脚本 支持两种方式

方式1.拷贝.m文件所有内容,粘贴到OC输入框内。 示例代码:AFHTTPSessionManager.m


方式2. 拷贝某个方法粘贴到OC输入框内,转换时会自动补全



三.App 审核分析


其实能不能成功上线是热修复的首要前提,我们辛辛苦苦开的框架如果上不了线,那一切都是徒劳无功。下面就来分析下其审核风险。


-   首先这个是通过大量C语言混编转换的,所以苹果审核无法通过静态代码识别,这一点是没有问题的。

-   其次系统库内部也大量使用了消息转发机制。这一点可以通过符号断点验证_objc_msgForwardforwardInvocation:。所以不存在风险。此外,你还可以通过一些字符串拼接和base64编码方式进行混淆,这样就更加安全了。

-   除非苹果采用动态检验消息转发,非系统调用都不能使用,但这个成本太大了,几乎不可能。

-   Mangofix 库目前线上有大量使用,为此不用担心。就算 Mangofix 被禁用,参考 Mangofix 自己开发也不难。


综上所述:超低审核风险。


热修复框架只是为了更好的控制线上bug影响范围和给用户更好的体验。

建议:

Hotfix会在应用程序运行时动态地加载代码,因此需要进行充分的测试,以确保修复的bug或添加的新功能不会导致应用程序崩溃或出现其他问题。


有兴趣的一起来研究,QQ群:770600683


作者:洞窝技术
链接:https://juejin.cn/post/7257333598469783610
来源:稀土掘金
收起阅读 »

如何使用 Xcode 15 新组件 TipKit

iOS
TipKit 介绍今年的 WWDC 发布了一个新的 UI 组件库 TipKit,使用 TipKit 可以很方便的在 iOS/macOS/watchOS 等平台的 App 上展示一个提示框,并且内置了 UI 布局,并且支持配置展示频率、规则等功能。今天 Xcod...
继续阅读 »

TipKit 介绍

今年的 WWDC 发布了一个新的 UI 组件库 TipKit,使用 TipKit 可以很方便的在 iOS/macOS/watchOS 等平台的 App 上展示一个提示框,并且内置了 UI 布局,并且支持配置展示频率、规则等功能。

今天 Xcode 15 Beta 5 发布了,TipKit 也终于带了进来,我大概尝试了一下这套 API,和一个月前 WWDC 的视频教程上有些不一样的地方,今天就来讲讲怎么使用。

今天的代码使用 SwiftUI 来演示。

启动配置

想要正常展示 Tip 组件,需要在 App 启动入口加载和配置应用程序中所有 Tip 的统一状态:

import SwiftUI
import TipKit

@main
struct TipKitDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
try? await Tips.configure()
}
}
}
}

这里的 Tips.configure() 函数支持设置一系列用于自定义 Tip 的选项,我这里没有传参数,它会自动帮我配置默认值。

自定义 Tip

首先导入 TipKit 框架:

然后声明一个 struct 继承 Tip:

struct MyTip: Tip {
var title: Text {
Text("Tip Title")
}
}

Tip 是一个协议,title 是必须实现的,其他值都可选。

展示 Tip

Tip 有两种展示方式,popover 和 Inline,popover 需要在指定的元素上使用 popoverTip 方法挂载这个 Tip,Tip 展示出来后会有个箭头指向这个元素,比如我在收藏按钮下展示这个 Tip:

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "star")
.imageScale(.large)
.foregroundStyle(.tint)
.popoverTip(MyTip(), arrowEdge: Edge.top) { action in
print(action)
}
}
.padding()
}
}

看下效果:


Inline 的方式是作为一个独立的 View 展示在视图上的,需要用到 TipView 组件:


Tip 的 UI 配置

刚刚提到 Tip 是一个协议,可以配置一些其他 UI,比如,在标题下方增加一行描述 (下边的效果截图均以 popover 的方式展示):

struct MyTip: Tip {
var title: Text {
Text("Save as a Favorite")
}

var message: Text? {
Text("Your favorite backyards always appear at the top of the list.")
}
}


添加图标:

struct MyTip: Tip {
// 其他代码...
var asset: Image? {
Image(systemName: "star")
}
}


添加按钮

struct MyTip: Tip {
// 其他代码...
var actions: [Action] {
[
Action(id: "1", title: "Learn More", perform: {
print("点击了第一个按钮")
}),
Action(id: "2", title: "OK", perform: {
print("点击了第二个按钮")
})
]
}
}


配置规则

除此之外,还可以配置一系列显示的规则,比如我定义一个 Bool 来控制这个 Tip 的展示与隐藏:

struct MyTip: Tip {
@Parameter
static var isShowing: Bool = false

// ...其他代码...

var rules: [Rule] {
[
#Rule(MyTip.$isShowing) { $0 == true }
]
}
}

然后我们再稍微改一下 ContentView 的代码,每次点击按钮的时候反转 isShowing 这个参数,来控制 Tip 的出现和消失:

struct ContentView: View {
var body: some View {
VStack {
Button(action: {
// 控制隐藏和出现
MyTip.isShowing.toggle()
}, label: {
Image(systemName: "star.fill")
})
.popoverTip(MyTip(), arrowEdge: Edge.top) { action in
print(action)
}
}
.padding()
}
}

这样我们就可以通过点击按钮来展示和隐藏这个提示框了:



这里需要注意,目前 Xcode Beta 5 有个已知的问题是不能正常访问 @Parameter 这个宏,解决办法是在项目的 Build Settings 的 Other Swift Flags 中手动添加 -external-plugin-path (SYSTEM\_DEVELOPER\_DIR)/Platforms/iPhoneOS.platform/Developer/usr/lib/swift/host/plugins#(SYSTEM_DEVELOPER_DIR)/Platforms/iPhoneOS.platform/Developer/usr/bin/swift-plugin-server,否则无法编译通过


配置显示选项

通过 TipOption 可以配置一些额外的展示选项,比如我这里配置这个 Tip 最大显示 5 次:

struct MyTip: Tip {

// ...其他代码...

var options: [TipOption] {
[ Tips.MaxDisplayCount(5) ]
}
}

更多的配置大家可以自行探索。


作者:杂雾无尘
链接:https://juejin.cn/post/7262162940971139109
来源:稀土掘金

收起阅读 »

程序员必备摸鱼神器:Genact,让你的电脑看起来很忙

先看图,领导看到是不是觉得你正忙着编译软件,本想告诉你客户需求又有变,最后还是自己承担了所有。genact 是 Rust 编写的一个开源项目,可以随机生成无意义的活动,让你看起来正在忙着干活。支持 Windows、macOS、Linux,它会一直不停的模拟终端...
继续阅读 »

先看图,领导看到是不是觉得你正忙着编译软件,本想告诉你客户需求又有变,最后还是自己承担了所有。


genact 是 Rust 编写的一个开源项目,可以随机生成无意义的活动,让你看起来正在忙着干活。

支持 Windows、macOS、Linux,它会一直不停的模拟终端运行,显示很多内容,假装你的电脑很忙,让你摸鱼!用你疯狂的多任务处理能力给人留下深刻印象。只要打开几个 genact 实例就可以观看了。Genact 有多个场景,假装正在做一些令人兴奋或有用的事情,而实际上什么也没有发生。


摸鱼的最高境界,就是连我自己都信了!

天天水群,总不能让电脑闲着吧,快去试试:https://svenstaro.github.io/genact/

收起阅读 »

宽表为什么横行?

宽表在BI业务中比比皆是,每次建设BI系统时首先要做的就是准备宽表。有时系统中的宽表可能会有上千个字段,经常因为“过宽”超过了数据库表字段数量限制还要再拆分。 为什么大家乐此不疲地造宽表呢?主要原因有两个。 一是为了提高查询性能。现代BI通常使用关系数据库作为...
继续阅读 »

宽表在BI业务中比比皆是,每次建设BI系统时首先要做的就是准备宽表。有时系统中的宽表可能会有上千个字段,经常因为“过宽”超过了数据库表字段数量限制还要再拆分。


为什么大家乐此不疲地造宽表呢?主要原因有两个。


一是为了提高查询性能。现代BI通常使用关系数据库作为后台,而SQL通常使用的HASH JOIN算法,在关联表数量和关联层级变多的时候,计算性能会急剧下降,有七八个表三四层级关联时就能观察到这个现象,而BI业务中的关联复杂度远远超过这个规模,直接使用SQL的JOIN就无法达到前端立等可取的查询需要了。为了避免关联带来的性能问题,就要先将关联消除,即将多表事先关联好采用单表存储(也就是宽表),再查询的时候就可以不用再关联,从而达到提升查询性能的目的。


二是为了降低业务难度。因为多表关联尤其是复杂关联在BI前端很难表达和使用。如果采用自动关联(根据字段类型等信息匹配)当遇到同维字段(如一个表有2个以上地区字段)时会“晕掉”不知道该关联哪个,表间循环关联或自关联的情况也无法处理;如果将众多表开放给用户来自行选择关联,由于业务用户无法理解表间关系而几乎没有可用性;分步关联可以描述复杂的关联需求,但一旦前一步出错就要推倒重来。所以,无论采用何种方式,工程实现和用户使用都很麻烦。但是基于单表来做就会简单很多,业务用户使用时没有什么障碍,因此将多表组织成宽表就成了“自然而然”的事情。


不过,凡事都有两面性,我们看到宽表好处而大量应用的同时,其缺点也不容忽视,有些缺点会对应用产生极大影响。下面来看一下。


宽表的缺点


数据冗余容量大


宽表不符合范式要求,将多个表合并成一个表会存在大量冗余数据,冗余程度跟原表数据量和表间关系有关,通常如果存在多层外键表,其冗余程度会呈指数级上升。大量数据冗余不仅会带来存储上的压力(多个表组合出来的宽表数量可能非常多)造成数据库容量问题,在查询计算时由于大量冗余数据参与运算还会影响计算性能,导致虽然用了宽表但仍然查询很慢。


数据错误


由于宽表不符合三范式要求,数据存储时可能出现一致性错误(脏写)。比如同一个销售员在不同记录中可能存储了不同的性别,同一个供应商在不同记录中的所在地可能出现矛盾。基于这样的数据做分析结果显然不对,而这种错误非常隐蔽很难被发现。


另外,如果构建的宽表不合理还会出现汇总错误。比如基于一对多的A表和B表构建宽表,如果A中有计算指标(如金额),在宽表中就会重复,基于重复的指标再汇总就会出现错误。


灵活性差


宽表本质上是一种按需建模的手段,根据业务需求来构建宽表(虽然理论上可以把所有表的组合都形成宽表,但这只存在于理论上,如果要实际操作会发现需要的存储空间大到完全无法接受的程度),这就出现了一个矛盾:BI系统建设的初衷主要是为了满足业务灵活查询的需要,即事先并不知道业务需求,有些查询是在业务开展过程中逐渐催生出来的,有些是业务用户临时起意的查询,这种灵活多变的需求采用宽表这种要事先加工的解决办法极为矛盾,想要获得宽表的好就得牺牲灵活性,可谓鱼与熊掌不可兼得。


可用性问题


除了以上问题,宽表由于字段过多还会引起可用性低的问题。一个事实表会对应多个维表,维表又有维表,而且表之间还可能存在自关联/循环关联的情况,这种结构在数据库系统中很常见,基于这些结构的表构建宽表,尤其要表达多个层级的时候,宽表字段数量会急剧增加,经常可能达到成百上千个(有的数据库表有字段数量限制,这时又要横向分表),试想一下,在用户接入界面如果出现上千个字段要怎么用?这就是宽表带来的可用性差的问题。


总体来看,宽表的坏处在很多场景中经常要大于好处,那为什么宽表还大量横行呢?


因为没办法。一直没有比宽表更好的方案来解决前面提到的查询性能和业务难度的问题。其实只要解决这两个问题,宽表就可以不用,由宽表产生的各类问题也就解决了。


SPL+DQL消灭宽表


借助开源集算器SPL可以完成这个目标。


SPL(Structured Process Language)是一个开源结构化数据计算引擎,本身提供了不依赖数据库的强大计算能力,SPL内置了很多高性能算法,尤其是对关联运算做了优化,对不同的关联场景采用不同的手段,可以大幅提升关联性能,从而不用宽表也能实时关联以满足多维分析时效性的需要。同时,SPL还提供了高性能存储,配合高效算法可以进一步发挥性能优势。


只有高性能还不够,SPL原生的计算语法不适合多维分析应用接入(生成SPL语句对BI系统改造较大)。目前大部分多维分析前端都是基于SQL开发的,但SQL体系(不用宽表时)在描述复杂关联计算上又很困难,基于这样的原因,SPL设计了专门的类SQL查询语法DQL(Dimensional Query Language)用于构建语义层。前端生成DQL语句,DQL Server将其转换成SPL语句,再基于SPL计算引擎和存储引擎完成查询返回给前端,实现全链路BI查询。需要注意的是,SPL只作为计算引擎存在,前端界面仍要由用户自行实现(或选用相应产品)。



SPL:关联实现技术


SPL如何不用宽表也能实现实时关联以满足性能要求的目标?


在BI业务中绝大部分的JOIN都是等值JOIN,也就是关联条件为等式的 JOIN。SPL把等值关联分为外键关联和主键关联。外键关联是指用一个表的非主键字段,去关联另一个表的主键,前者称为事实表,后者称为维表,两个表是多对一的关系,比如订单表和客户表。主键关联是指用一个表的主键关联另一个表的主键或部分主键,比如客户表和 VIP 客户表(一对一)、订单表和订单明细表(一对多)。


这两类 JOIN 都涉及到主键,如果充分利用这个特征采用不同的算法,就可以实现高性能的实时关联了。


不过很遗憾,SQL 对 JOIN 的定义并不涉及主键,只是两个表做笛卡尔积后再按某种条件过滤。这个定义很简单也很宽泛,几乎可以描述一切。但是,如果严格按这个定义去实现 JOIN,理论上没办法在计算时利用主键的特征来提高性能,只能是工程上做些有限的优化,在情况较复杂时(表多且层次多)经常无效。


SPL 改变了 JOIN 的定义,针对这两类 JOIN 分别处理,就可以利用主键的特征来减少运算量,从而提高计算性能。


外键关联


和SQL不同,SPL中明确地区分了维表和事实表。BI系统中的维表都通常不大,可以事先读入内存建立索引,这样在关联时可以少计算一半的HASH值。


对于多层维表(维表还有维表的情况)还可以用外键地址化的技术做好预关联。即将维表(本表)的外键字段值转换成对应维表(外键表)记录的地址。这样被关联的维表数据可以直接用地址取出而不必再进行HASH值计算和比对,多层维表仅仅是多个按地址取值的时间,和单层维表时的关联性能基本相当。


类似的,如果事实表也不大可以全部读入内存时,也可以通过预关联的方式解决事实表与维表的关联问题,提升关联效率。


预关联可以在系统启动时一次性读入并做好,以后直接使用即可。


当事实表较大无法全内存时,SPL 提供了外键序号化方法:将事实表中的外键字段值转换为维表对应记录的序号。关联计算时,用序号取出对应维表记录,这样可以获得和外键地址化类似的效果,同样能避免HASH值的计算和比对,大幅提升关联性能。


主键关联


有的事实表还有明细表,比如订单和订单明细,二者通过主键和部分主键进行关联,前者作为主表后者作为子表(还有通过全部主键关联的称为同维表,可以看做主子表的特例)。主子表都是事实表,涉及的数据量都比较大。


SPL为此采用了有序归并方法:预先将外存表按照主键有序存储,关联时顺序取出数据做归并,不需要产生临时缓存,只用很小的内存就可以完成计算。而SQL采用的HASH分堆算法复杂度较高,不仅要计算HASH值进行对比,还会产生临时缓存的读写动作,运算性能很差。


HASH 分堆技术实现并行困难,多线程要同时向某个分堆缓存数据,造成共享资源冲突;某个分堆关联时又会消费大量内存,无法实施较大的并行数量。而有序归则易于分段并行。数据有序时,子表就可以根据主表键值进行同步对齐分段以保证正确性,无需缓存,且因为占用内存很少可以采用较大的并行数,从而获得更高性能。


预先排序的成本虽高,但是一次性做好即可,以后就总能使用归并算法实现 JOIN,性能可以提高很多。同时,SPL 也提供了在有追加数据时仍然保持数据整体有序的方案。


对于主子表关联SPL还可以采用更有效的存储形式将主子表一体化存储,子表作为主表的集合字段,其取值是由与该主表数据相关的多条子表记录构成。这相当于预先实现了关联,再计算时直接取数计算即可,不需要比对,存储量也更少,性能更高。


存储机制


高性能离不开有效的存储。SPL也提供了列式存储,在BI计算中可以大幅降低数据读取量以提升读取效率。SPL列存采用了独有的倍增分段技术,相对传统列存分块并行方案要在很大数据量时(否则并行会受到限制)才会发挥优势不同,这个技术可以使SPL列存在数据量不很大时也能获得良好的并行分段效果,充分发挥并行优势。


SPL还提供了针对数据类型的优化机制,可以显著提升多维分析中的切片运算性能。比如将枚举型维度转换成整数,在查询时将切片条件转换成布尔值构成的对位序列,在比较时就可以直接从序列指定位置取出切片判断结果。还有将多个标签维度(取值是或否的维度,这种维度在多维分析中大量存在)存储在一个整数字段中的标签位维度技术(一个整数字段可以存储16个标签),不仅大幅减少存储量,在计算时还可以针对多个标签同时做按位计算从而大幅提升计算性能。


有了这些高效机制以后,我们就可以在BI分析中不再使用宽表,转而基于SPL存储和算法做实时关联,性能比宽表还更高(没有冗余数据读取量更小,更快)。


不过,只有这些还不够,SPL原生语法还不适合BI前端直接访问,这就需要适合的语义转换技术,通过适合的方式将用户操作转换成SPL语法进行查询。


这就需要DQL了。


DQL:关联描述技术


DQL是SPL之上的语义层构建工具,在这一层完成对于SPL数据关联关系的描述(建模)再为上层应用服务。即将SPL存储映射成DQL表,再基于表来描述数据关联关系。



通过对数据表关系描述以后形成了一种以维度为中心的总线式结构(不同于E-R图中的网状结构),中间是维度,表与表之间不直接相关都通过维度过渡。



基于这种结构下的关联查询(DQL语句)会很好表达。比如要根据订单表(orders)、客户表(customer)、销售员表(employee)以及城市表(city)查询:本年度华东的销售人员,在全国各销售区的销售额


用SQL写起来是这样的:


SELECT
ct1.area,o.emp_id,sum(o.amount) somt
FROM
orders o
JOIN customer c ON o.cus_id = c.cus_id
JOIN city ct1 ON c.city_id = ct1.city_id
JOIN employee e ON o.emp_id = e.emp_id
JOIN city ct2 ON e.city_id = ct2.city_id
WHERE
ct2.area = 'east' AND year(o.order_date)= 2022
GROUP BY
ct1.area, o.emp_id

多个表关联要JOIN多次,同一个地区表要反复关联两次才能查到销售员和客户的所在区域,对于这种情况BI前端表达起来会很吃力,如果将关联开放出来,用户又很难理解。


那么DQL是怎么处理的呢?


DQL写法:


SELECT
cus_id.city_id.area,emp_id,sum(amount) somt
FROM
orders
WHERE
emp_id.city_id.area == "east" AND year(order_date)== 2022
BY
cus_id.city_id.area,emp_id

DQL不需要JOIN多个表,只基于orders单表查询就可以了,外键指向表的字段当成属性直接使用,有多少层都可以引用下去,很好表达。像查询客户所在地区通过cus_id.city_id.area一直写下去就可以了,这样就消除了关联,将多表关联查询转化成单表查询。


更进一步,我们再基于DQL开发BI前端界面就很容易,比如可以做成这样:



用树结构分多级表达多层维表关联,这样的多维分析页面不仅容易开发,普通业务用户使用时也很容易理解,这就是DQL的效力。


总结一下,宽表的目的是为了解决BI查询性能和前端工程实现问题,而宽表会带来数据冗余和灵活性差等问题。通过SPL的实时关联技术与高效存储可以解决性能问题,而且性能比宽表更高,同时不存在数据冗余,存储空间也更小(压缩);DQL构建的语义层解决了多维分析前端工程的实现问题,让实时关联成为可能,,灵活性更高(不再局限于宽表的按需建模),界面也更容易实现,应用范围更广。


SPL+DQL继承(超越)宽表的优点同时改善其缺点,这才是BI该有的样子。


SPL资料


收起阅读 »

IDEA建议:不要在字段上使用@Autowire了!

在使用IDEA写Spring相关的项目的时候,在字段上使用@Autowired注解时,总是会有一个波浪线提示:Field injection is not recommended. 纳尼?我天天用,咋就不建议了,今天就来一探究竟。 众所周知,在Spring里...
继续阅读 »

在使用IDEA写Spring相关的项目的时候,在字段上使用@Autowired注解时,总是会有一个波浪线提示:Field injection is not recommended. 纳尼?我天天用,咋就不建议了,今天就来一探究竟。



众所周知,在Spring里面有三种可选的注入方式:构造器注入、Setter方法注入、Field注入,我们先来看下这三种注入方式的使用场景。


构造器注入


如下所示,使用构造器注入,可以将属性字段设置为final,在Aservice进行实例化时,BService对象必须得提前初始化完成,所以使用构造器注入,能够保证被注入的对象一定不为null。构造器注入适用于对象之间强依赖的场景,但是无法解决的循环依赖问题(因为必须互相依赖对方初始化完成,当然会产生冲突无法解决)。关于循环依赖,推荐阿里的一篇文章 一文详解Spring Bean循环依赖


@Service
public class AService {
   private final BService bService;

   @Autowired  //spring framework 4.3之后可以不用在构造方法上标注@Autowired
   public AService(BService bService) {
       this.bService = bService;
  }
}

Setter 方法注入


使用Setter方法进行注入时,Spring会在执行默认的无参构造函数实例化Bean对象之后,调用Setter方法来注入依赖。使用Setter方法注入可以将 required 属性设置为 false,表示若注入的Bean对象不存在,直接跳过注入,不会报错。


@Service
public class AService {
   private  BService bService;

   @Autowired(required = false)
   public void setbService(BService bService) {
       this.bService = bService;
  }
}

Field注入


一眼看去,Field注入简洁美观,被大家普遍大量使用。Spring容器会在对象实例化完成之后,通过反射设置需要注入的的字段。


@Service
public class AService {
   @Autowired
   private  BService bService;
}

为什么IDEA不推荐使用Field注入


经查阅各方资料,我找到了如下几个比较重要的原因:



  • 可能导致空指针异常:如果创建对象不使用Spring容器,而是直接使用无参构造方法new一个对象,此时使用注入的对象会导致空指针。

  • 不能使用final修饰字段:不使用final修饰,会导致类的依赖可变,进而可能会导致一些不可预料的异常。通常情况下,此时可以使用构造方法注入来声明强制依赖的Bean,使用Setter方法注入来声明可选依赖的Bean。

  • 可能更容易违反单一职责原则:个人认为这点是一个很重要的原因。因为使用字段注入可以很轻松的在类上加入各种依赖,进而导致类的职责过多,但是往往开发者对此不能轻易察觉。而如果使用构造方法注入,当构造方法的参数过多时,就会提醒你,你该重构这个类了。

  • 不利于写单元测试:在单元测试中,使用Field注入,必须使用反射的方式来Mock依赖对象。


那么替代方案是什么呢?其实上面已经提到了,当我们的类有强依赖于其他Bean时,使用构造方法注入;可选依赖时,使用Setter方法注入(需要自己处理可能出现的引用对象不存在的情况)。


Spring官方的态度


Spring 官方文档在依赖注入这一节其实没有讨论字段注入这种方式,重点比较了构造方法注入和Setter注入。可以看到Spring团队强推的还是构造方法注入


构造方法注入还是Setter注入


总结


在Spring中使用依赖注入时,首选构造方法注入,虽然其无法解决循环依赖问题,但是当出现循环依赖时,首选应该考虑的是是否代码结构设计出现问题了,当然,也不排除必须要循环依赖的场景,此时字段注入也有用武之地。


最后想说的是,平时在使用IDEA的过程中,可能会有一些下划线或飘黄提醒,如果多细心观察,可以学习到很多他人已经总结好的最佳实践经验,有助于自己代码功底的提升,共勉!


参考文献:


Spring 官方文档关于依赖注入: docs.spring.io/spring-fram…


StackOverFlow关于避免使用字段注入的讨论:stackoverflow.com/questi

ons/3…

收起阅读 »

接受外包Offer前一定要清楚的4件事

Hello 我是光影少年。 最近有一些刚毕业的小朋友私信我,说工作贼难找,能不能先去一个软件外包公司先苟着,之后的事情等行情好些了再说。 去外包公司当然没什么不可以,成年人能基于实际做出判断和选择,并承受相应的结果就行。 只是我想补充一些细节,让可能有这个想法...
继续阅读 »

Hello 我是光影少年。


最近有一些刚毕业的小朋友私信我,说工作贼难找,能不能先去一个软件外包公司先苟着,之后的事情等行情好些了再说。


去外包公司当然没什么不可以,成年人能基于实际做出判断和选择,并承受相应的结果就行。


只是我想补充一些细节,让可能有这个想法的小朋友们对这种公司和岗位有一些大概的了解。


在这里先给部分不太清楚的观众扫个盲,软件开发行业有两种基本的用工方式:



  • 作为所服务公司的正式员工,和该公司主体签订劳动合同。

  • 作为所服务公司的劳务派遣员工,和所服务公司委托的第三方公司签订劳动合同。简单来说,你和所服务的公司实际上没有什么关系,是那家签合同的第三方公司的员工,只是替这家公司干活而已。这也是俗称的「外包员工」或者「顾问」


劳务派遣这种用工方式已经广泛地存在于许多的行业。事实上,有些公司中,围绕着派遣员工和正式员工之间的差异,存在着一整套完整的「潜规则」。


围绕着这两种不同类型的员工,在日常工作中往往会衍生出的所谓「戴蓝色工牌」和「戴绿色工牌」的区别对待的概念。


「戴绿色工牌」的他们的工资通常较低,没有其他福利待遇,在某些公司甚至被告知他们不能使用专门的员工通道,也不能参与公司组织的一些诸如团建,抽奖等活动。


「戴蓝色工牌」的是正式员工,有权享受所有的福利待遇。同时要想戴上蓝色工牌是很困难的。


在劳务派遣公司招募「戴绿色工牌」的员工时,往往会拿「表现良好者可以戴上蓝色工牌」这种承诺来作为吸引人的筹码,但是绝少有人能够如愿以偿。


表面上看上去,似乎完全不用选,正式员工就是比派遣员工要好。在大多数情况下这没错。不过我还是想介绍一下派遣员工的现状,和你在考虑是否接受这个岗位时可能会忽略的一些实际情况。


在这里,我只介绍狭义上「劳务派遣」所指的群体,


这种类型的岗位员工和一家代理机构(比如最常见的某软,某辉)签订劳动合同,然后这家机构会把他们派遣到客户公司的现场,和客户公司的雇员一起工作。


相对于客户公司,他们就是「劳务派遣」员工。代理机构会定期向客户收钱,然后从中抽成之后,再把「工资」发给员工。比如客户公司为一个岗位支付的成本是2w/月,很可能发到员工手里只有1w/月(随便举例,不代表实际情况)。


作为这种类型的员工,通常会要求定期填写一份工时记录表,来证明为客户工作的时间,然后根据这张表从所在的机构获得报酬。


代理机构的最根本的盈利模式就是赚差价,所以通常客户向机构支付的费用要比机构实际支付给员工的工资高出 50%~100%的情况也并不算少见。


可能有些小伙伴看到这里已经开始生气了。「存在即合理」,在国内,代理机构起码还得负责员工的五险一金等一些其他的基本保障。往往通过代理机构给大型客户工作,比直接去一家朝不保夕的初创公司还是稳定轻松许多,甚至收入也会高上不少——哪怕已经被拿走了一大部分。


当然,如果咱们已经选择接受一个派遣的岗位,你可以通过一些方式了解到客户给你付出的成本,之后不论是和机构谈判,还是更新对自己的定位都有好处。


同工不同酬


首先就要聊最重要的:

关于薪资。


在大多数情况下,在同等工作岗位上,作为一个派遣员工,只看固定薪资的话,单价是更高的。


在我之前的工作岗位上,我的固定时薪是比不上和我同样职级的顾问同学的。


但是,但是,通常能大量使用派遣员工的大型企业,他们的薪资构成不光是固定薪资。往往是:


薪资总包 = 薪资+绩效+其他(各种福利、股票期权等等)


派遣员工往往只有固定薪资,或者一些超时工作的加班补偿。


稳定性


为什么公司宁可支付更高的单价,也要大量使用派遣员工呢?其中一个很重要的因素就是控制固定成本支出


雇佣一个正式员工,除了薪资总包以外,还需要支付和固定工资匹配的五险一金,其他的各种福利,雇佣一个人付出的成本大概是纸面薪资的1.5倍甚至更高。而且如果公司遇到业务收缩,需要裁撤员工,正式员工也需要支付大量的赔偿金。


如果是派遣员工,那么大可以在项目建设期需要大量人力投入时短期购入大量的人手,在项目上线稳定进入维护期后终止合作,而遣散派遣员工所付出的成本低的可以忽略不计。


所以,如果你对于频繁地更换工作场所有所抵触的话,派遣员工可能对你来说是个很大的挑战。尤其是在有些时段没有什么客户需要,很可能要在代理机构坐冷板凳,甚至被裁撤掉。


工作环境


就像本文开头提到的,有人的地方就有江湖。在有些公司,派遣员工和正式员工之间存在巨大的鸿沟;有些公司中又不是那么明显,但就像一道空气墙,不知道在什么时间就会拦住你的去路。


为了控制成本,在扩张员工规模时的大型企业是十分谨慎的,对待正式员工的招募流程会比派遣员工的进场流程严格的多。高标准,严要求下,如果正式员工的数量还比派遣员工的数量少,那么难免的一些「精英思维」就会开始弥漫。


这是难以避免的,这也是许多在派遣员工岗位的小伙伴反复和我提及的:


一定要端正心态。


在很多你不经意的时候,甚至会有「被抛弃」的感觉。并不是每个客户公司的氛围都那么让人舒服,但这就是正常的,不要太过于纠结这个。


而且派遣员工有一个小小的优势,正式员工通常会因为卷而主动加班,而这种加班在不少地方是无偿的,派遣员工由于是代理机构提供的劳动力,所以通常加班都是有补偿的——毕竟你们的工作就是机构的收益,机构也会争取他们的利益。


长期发展


作为派遣员工,短期收益甚至可以说还不错。进入大型公司的劳务派遣基本不会比去初创企业工作难多少。而且也没有传统大企业对于正式员工所提出的高要求,压死人的KPI之类的影响。


但如果你想从工作中获得的不仅仅只有报酬,更想有一些为日后的履历增光添彩的项目经历的话,派遣员工可能就不是很合适。通常情况下,客户公司总倾向于让派遣员工去做一些相对简单的外围部分工作,核心的部分很难接触到,这没得说。


除非你愿意非常频繁地跳槽来换工作,否则涨薪在派遣员工中也是很困难的,而作为规模公司的正式员工,往往有很成熟的薪资上升机制,也有晋升的机会。


而且业内有一些传言,把进代理机构的经历称之为「弄花简历」,这段经历有可能会对后期想要进入大厂时的筛选过程有一定的影响。


至于说HR招人时所提到的「有转为正式员工的机会」,就像真爱,听过的人多,见过的人少,

作者:幸福少年
来源:juejin.cn/post/7217360688264134713
我也不过多发表意见。

收起阅读 »

背包算法——双条件背包

web
通过8.6日的小红书笔试第二题,我彻底搞懂了01背包与完全背包。 这篇文章不是手把手的基础教学,简单从我自己的对背包问题掌握的基础上分享一下新的心得,随缘帮助陌生人。 题目: 小红很喜欢前往小红书分享她的日常生活。已知她生活中有n个事件,分享第i个事件需要她花...
继续阅读 »

通过8.6日的小红书笔试第二题,我彻底搞懂了01背包与完全背包。


这篇文章不是手把手的基础教学,简单从我自己的对背包问题掌握的基础上分享一下新的心得,随缘帮助陌生人。


题目:


小红很喜欢前往小红书分享她的日常生活。已知她生活中有n个事件,分享第i个事件需要她花费ti的时间和hi的精力来编辑文章,并能获得ai的快乐值。
小红想知道,在总花费时间不超过T且总花费精力不超过H的前提下,小红最多可以获得多少快乐值?

第一行输入一个正整数n,代表事件的数量。


第二行输入两个正整数T和H,代表时间限制和精力限制。


接下来的n行,每行输入三个正整数ti,hi,ai,代表分享第i个事件需要花费ti的时间、hi的精力,收获ai的快乐值。


输入示例:


3
5 4
1 2 2
2 1 3
4 1 5

输出示例:


7

考场上的做法——暴力递归(超时,通过36%)


下面的一段没啥好说的,输入数据的处理:


const n = parseInt(read_line());
const tArr = new Array(n).fill();
const hArr = new Array(n).fill();
const aArr = new Array(n).fill();
let line2 = read_line();
const T = parseInt(line2.split(" ")[0]);
const H = parseInt(line2.split(" ")[1]);
// 构造t\h\a数组
for(let i = 0; i < n; i ++) {
 let line = read_line().split(" ");
 line = line.map((str) => {
   return parseInt((str));
})
 tArr[i] = line[0];
 hArr[i] = line[1];
 aArr[i] = line[2];
}

最终处理的目标是如下的数据结构:


// 输入:
// 3
// 5 4
// 1 2 2
// 2 1 3
// 4 1 5
let n = 3;
const T = 5;
const H = 4;
const tArr = [1, 2, 4];
const hArr = [2, 1, 1];
const aArr = [2, 3, 5];

递归算法部分,没啥好说的,下面是笔试时的源代码:


let maxA = 0; // 最大快乐值
// 考察第i件事情,并且考察前的t, h, a值分别为t, h, a
function backTrace(i, t, h, a) {
 if(a > maxA) {
   maxA = a;
}
 if(i === n) return;
 backTrace(i + 1, t, h, a); // 不做第i件事
 if(t + tArr[i] <= T && h + hArr[i] <= H) { // 做第i件事
   backTrace(i + 1, t + tArr[i], h + hArr[i], a + aArr[i]);
}
}
backTrace(0, 0, 0, 0);
console.log(maxA);

dp动态规划


数据读取同上,dp算法部分:


let dp = new Array(T + 1).fill(); // 定义dp[i][j]表示i的精力与j的体力能获得的最大快乐值
dp = dp.map(() => {
 return new Array(H + 1).fill(0);
})

for(let k = 0; k < n; k ++) { // k表示下面计算dp[i][j]时事件的集合是前k件事情
 for(let i = 1; i <= T; i ++) { // i, j 遍历各种时间和精力组合,计算dp[i][j],注意必须是倒序遍历i\j!!!
   for(let j = 1; j <= H; j ++) {
     if(i >= tArr[k] && j >= hArr[k]) {
       dp[i][j] = Math.max(dp[i - tArr[k]][j - hArr[k]] + aArr[k], dp[i][j]);
    }
  }
}
}

// 打印dp数组
for(let i = 0; i <= T; i ++) {
 let str = '';
 for(let j = 0; j <= H; j ++) {
   str += dp[i][j];
   str += " ";
}
 console.log(str);
}

console.log(dp[T][H]);

感悟:



  1. 这里的k,也就是事件,等价于背包问题中的物品;对于背包问题来说,只对物品的体积(重量)这一个指标进行限制,这里的精力与体力就相当于背包问题中的体积,相当于两个限制。

  2. 之所以不管01背包还是完全背包,都可以把dp数组初始化为一维而不是二维,就是因为省略了第一维的物品数量;这里我们dp数组初始化为二维,但是最外层进行k次迭代,也就相当于省略了事件数量。

  3. 基于上面的理解,为什么i和j都要倒序遍历就显而易见了,因为本题显然对标01背包问题,也就是说每件事情最多分享一次,所以说如果不省略第一维度k的递推公式应该是:dp[k][i][j] = max(dp[k - 1][i][j], dp[k - 1][i - tArr[k]][j - hArr[k]]) + aArr[k]。说白了就是**dp[k] = f(dp[k - 1])** ,所以因为我们省略了第一维k,完全等价于背包问题省略第一维,所以需要倒序计算,可以类比01背包问题的一维dp数组,倒序的目的就是保证计算dp[j]时所使用的dp数据的正确性。

  4. plus,如果修改题目,没件事情可以重复分享,那么就正序计算dp[i][j],按照上面的测例算出来结果是8,完全没问题。


如果以前没接触过背包问题,这篇文章确实p用没有,但是如果正对背包问题处于似懂非懂的状态,或许看下

作者:荣达
来源:juejin.cn/post/7264792741951062068
对你有所帮助,加油。

收起阅读 »

Flutter 仿 Hero 的动画

Flutter 模仿 Hero 动画的效果,实现逻辑比较简单,就是用 Stack 结合 AnimatedBuilder 组件实现类似 Hero 的转场的动画效果。 效果 代码 DEMO class TWAnimationHeroApp extends Sta...
继续阅读 »

Flutter 模仿 Hero 动画的效果,实现逻辑比较简单,就是用 Stack 结合 AnimatedBuilder 组件实现类似 Hero 的转场的动画效果。


效果


Simulator Screen Recording - iPhone 14 Pro - 2023-08-08 at 22.32.59.gif


代码


DEMO


class TWAnimationHeroApp extends StatelessWidget {
final controller = TWAnimationHeroController();
TWAnimationHeroApp({super.key});

@override
Widget build(BuildContext context) {
Widget heroChild = GestureDetector(
onTap: () => controller.executeAnimation(),
child: Image.asset(
Assets.beauty.path,
fit: BoxFit.fitHeight,
),
);

return MaterialApp(
theme: ThemeData(primarySwatch: Colors.grey),
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(),
body: TWAnimationHero(
controller: controller,
heroChild: heroChild,
child: Stack(
children: [
ListView(
children: [
Container(
height: 100,
alignment: Alignment.center,
color: Colors.orange,
child: GestureDetector(
onTap: () => controller.reverseAnimation(),
child: SizedBox(
width: 50,
height: 50,
key: controller.targetKey,
child: Image.asset(
Assets.beauty.path,
),
),
),
),
Container(
height: 100,
color: Colors.black,
),
Container(
height: 100,
color: Colors.green,
),
Container(
height: 100,
color: Colors.red,
),
Container(
height: 100,
color: Colors.lime,
),
Container(
height: 100,
color: Colors.green,
),
Container(
height: 100,
color: Colors.yellow,
),
Container(
height: 100,
color: Colors.blueAccent,
),
],
),
],
),
),
),
);
}
}

TWAnimationHeroController


class TWAnimationHeroController extends ChangeNotifier {
GlobalKey targetKey = GlobalKey();
GlobalKey heroKey = GlobalKey();

/// 是否可见
bool get isHeroVisible => _isHeroVisible;

bool _isHeroVisible = true;

set heroVisible(bool value) {
_isHeroVisible = value;
notifyListeners();
}

/// 是否方向状态
bool isReverse = false;
AnimationController? controller;
Animation? animation;

double offTop = 0;
double offBottom = 0;
double offLeft = 0;
double offRight = 0;
TWAnimationHeroController();

/// 执行正向动画
executeAnimation() {
if (isReverse) return;
isReverse = true;
final child1Rect = fetchChildRect(targetKey);
final child2Rect = fetchChildRect(heroKey);
if (child1Rect == null || child2Rect == null) return;
offTop = child1Rect.top - child2Rect.top;
offBottom = child2Rect.bottom - child1Rect.bottom;
offLeft = child1Rect.left - child2Rect.left;
offRight = child2Rect.right - child1Rect.right;
controller?.forward();
}

/// 执行反向动画
reverseAnimation() {
if (!isReverse) return;
heroVisible = true;
isReverse = false;
controller?.reverse();
}

Rect? fetchChildRect(GlobalKey key) {
RenderBox? renderBox = key.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return null;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
final childRect = offset & size;
return childRect;
}
}

TWAnimationHero 组件


class TWAnimationHero extends StatefulWidget {
final Widget child;
final Widget? heroChild;

final TWAnimationHeroController controller;
const TWAnimationHero({
super.key,
required this.controller,
required this.child,
this.heroChild,
});

@override
State<TWAnimationHero> createState() => _TWAnimationHeroState();
}

class _TWAnimationHeroState extends State<TWAnimationHero>
with TickerProviderStateMixin
{
@override
void initState() {
super.initState();
createController();
}

/// 创建控制器
createController() {
final controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);

//应用curve
widget.controller.animation = CurvedAnimation(
parent: controller,
curve: Curves.easeIn,
);

controller.addListener(() {
// 注意正向动画才会监听到 isCompleted
if (controller.isCompleted) {
widget.controller.heroVisible = false;
}
});

widget.controller.controller = controller;
}

@override
void didUpdateWidget(covariant TWAnimationHero oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller.controller == null) {
widget.controller.controller?.dispose();
createController();
}
}

@override
void dispose() {
widget.controller.controller?.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
if (widget.heroChild != null &&
widget.controller.controller != null &&
widget.controller.animation != null)
AnimatedBuilder(
animation: widget.controller.controller!,
builder: (BuildContext context, Widget? child) {
return Positioned(
top: widget.controller.animation!.value *
widget.controller.offTop,
bottom: widget.controller.animation!.value *
widget.controller.offBottom,
left: widget.controller.animation!.value *
widget.controller.offLeft,
right: widget.controller.animation!.value *
widget.controller.offRight,
child: child!,
);
},
child: AnimatedBuilder(
animation: widget.controller,
builder: (BuildContext context, Widget? child) {
return Visibility(
visible: widget.controller.isHeroVisible,
child: Container(
color: Colors.transparent,
key: widget.controller.heroKey,
child: widget.heroChild,
),
);
},
),
),
],
);
}
}


作者:zeqinjie
来源:juejin.cn/post/7264921108398604343
收起阅读 »

怎么做登录(单点登录)功能?

登陆是系统最基础的功能之一。这么长时间了,一直在写业务,这个基础功能反而没怎么好好研究,都忘差不多了。今天没事儿就来撸一下。 以目前在接触和学习的一个开源系统为例,来分析一下登陆该怎么做。代码的话我就直接从里面CV了。 简单上个图(有水印。因为穷所以没开会员...
继续阅读 »

登陆是系统最基础的功能之一。这么长时间了,一直在写业务,这个基础功能反而没怎么好好研究,都忘差不多了。今天没事儿就来撸一下。


以目前在接触和学习的一个开源系统为例,来分析一下登陆该怎么做。代码的话我就直接从里面CV了。



简单上个图(有水印。因为穷所以没开会员)


怎么做登陆(单点登陆)?.png


先分析下登陆要做啥



首先,搞清楚要做什么。


登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。



怎么落实?



怎么实现它?用什么实现?




我们的项目是Springboot + Vue前后端分离类型的。


选择用token + redis 实现,权限的话用SpringSecurity来做。




前后端分离避不开的一个问题就是单点登陆,单点登陆咱们有很多实现方式:CAS中央认证、JWT、token等,咱们这种方式其实本身就是基于token的一个单点登陆的实现方案。


单点登陆我们改天整理一篇OAuth2.0的实现方式,今天不搞这个。



上代码



概念这个东西越说越玄。咱们直接上代码吧。



接口:

@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
   AjaxResult ajax = AjaxResult.success();
   // 生成令牌
   //用户名、密码、验证码、uuid
   String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                                     loginBody.getUuid());
   ajax.put(Constants.TOKEN, token);
   return ajax;
}


用户信息验证交给SpringSecurity



/**
* 登录验证
*/

public String login(String username, String password, String code, String uuid)
{
   // 验证码开关,顺便说一下,系统配置相关的开关之类都缓存在redis里,系统启动的时候加载进来的。这一块儿的代码就不贴出来了
   boolean captchaEnabled = configService.selectCaptchaEnabled();
   if (captchaEnabled)
  {
       //uuid是验证码的redis key,登陆页加载的时候验证码生成接口返回的
       validateCaptcha(username, code, uuid);
  }
   // 用户验证 -- SpringSecurity
   Authentication authentication = null;
   try
  {
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
       AuthenticationContextHolder.setContext(authenticationToken);
       // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername。
       //
       authentication = authenticationManager.authenticate(authenticationToken);
  }
   catch (Exception e)
  {
       if (e instanceof BadCredentialsException)
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
           throw new UserPasswordNotMatchException();
      }
       else
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
           throw new ServiceException(e.getMessage());
      }
  }
   finally
  {
       AuthenticationContextHolder.clearContext();
  }
   AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
   LoginUser loginUser = (LoginUser) authentication.getPrincipal();
   recordLoginInfo(loginUser.getUserId());
   // 生成token
   return tokenService.createToken(loginUser);
}

把校验验证码的部分贴出来,看看大概的逻辑(这个代码封装得太碎了。。。没全整出来)

/**
* 校验验证码
*/

public void validateCaptcha(String username, String code, String uuid)
{
   //uuid是验证码的redis key
   String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); //String CAPTCHA_CODE_KEY = "captcha_codes:";
   String captcha = redisCache.getCacheObject(verifyKey);
   redisCache.deleteObject(verifyKey);
   if (captcha == null)
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
       throw new CaptchaExpireException();
  }
   if (!code.equalsIgnoreCase(captcha))
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
       throw new CaptchaException();
  }
}

token生成部分


这里,token



/**
* 创建令牌
*/

public String createToken(LoginUser loginUser)
{
   String token = IdUtils.fastUUID();
   loginUser.setToken(token);
   setUserAgent(loginUser);
   refreshToken(loginUser);

   Map<String, Object> claims = new HashMap<>();
   claims.put(Constants.LOGIN_USER_KEY, token);
   return createToken(claims);
}

刷新token

/**
* 刷新令牌
*/

public void refreshToken(LoginUser loginUser)
{
   loginUser.setLoginTime(System.currentTimeMillis());
   loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
   // 根据uuid将loginUser缓存
   String userKey = getTokenKey(loginUser.getToken());
   redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}

验证token

/**
* 验证令牌
*/

public void verifyToken(LoginUser loginUser)
{
   long expireTime = loginUser.getExpireTime();
   long currentTime = System.currentTimeMillis();
   if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
  {
       refreshToken(loginUser);
  }
}


注意这里返回给前端的token其实用JWT加密了一下,SpringSecurity的过滤器里有进行解析。


另外,鉴权时会刷新token有效期,看下面第二个代码块的注释。



@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
   //...无关的代码删了
   httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
}

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
   @Autowired
   private TokenService tokenService;

   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
       throws ServletException, IOException
  {
       LoginUser loginUser = tokenService.getLoginUser(request);
       if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
      {
           //刷新token有效期
           tokenService.verifyToken(loginUser);
           UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
           authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
           SecurityContextHolder.getContext().setAuthentication(authenticationToken);
      }
       chain.doFilter(request, response);
  }
}


这个登陆方案里用了token + redis,还有JWT,其实用哪一种方案都可以独立实现,并且两种方案都可以用来做单点登陆。


这里JWT只是起到个加密的作用,无它。


作者:harhar
来源:juejin.cn/post/7184266088652210231

收起阅读 »

iPhone 14 被用户吐槽电池老化

国内要闻 香港高校陆续拥抱 ChatGPT,暑期忙于筹备新学期变革 香港众多高校拥抱了 OpenAI 的聊天机器人 ChatGPT。香港科技大学率先引入ChatGPT。6 月 1 日它正式为学生与教职人员提供港科大版 ChatGPT,是香港首所为学生与教职工提...
继续阅读 »

dm1.webp


国内要闻


香港高校陆续拥抱 ChatGPT,暑期忙于筹备新学期变革


香港众多高校拥抱了 OpenAI 的聊天机器人 ChatGPT。香港科技大学率先引入ChatGPT。6 月 1 日它正式为学生与教职人员提供港科大版 ChatGPT,是香港首所为学生与教职工提供 ChatGPT 的大学。香港中文大学、香港理工大学、香港浸会大学等高校也陆续推出使用 AI 工具的指引,共同希望师生批判性探索和谨慎使用 AI。除了在高等教育掀起热潮,AI 也将进入香港的初中课堂。香港教育局指出,ChatGPT 可以成为有价值的教育工具,但要留意抄袭的伦理问题,并期望所有公立中学尽快规划,于 2023/24 学年在“资讯和通讯科技课程”中安排 10 至 14 小时的 AI 课程教授。(奇客Solidot)


小鹏智驾灵魂人物吴新宙确认离职


小鹏汽车董事长何小鹏发文称,因家庭和多方面的原因,小鹏汽车自动驾驶中心副总裁吴新宙在 2022 年下半年表示要回到美国。在此后 10 个月时间里,小鹏汽车确立全新的工作模式,并在架构和组织上进行了提前优化和迭代。负责 XNGP 项目的李力耘博士将接手自动驾驶团队。


据业内人士透露,吴新宙或将担任英伟达“全球副总裁”这一级别的职位,直接向黄仁勋汇报,“是黄仁勋本人亲自出马,将吴新宙招至麾下。”届时,吴新宙将成为全球知名公司的最高等级华人高管,并继续在芯片等多个方面和小鹏汽车深度合作。(雷锋网)


微信要做“小绿书”?知情人士:小范围内测,优化视频号图文发布及呈现


据网传消息,微信正在灰度测试“小绿书”。从知情人士处了解到,这是一次非常小范围的内测,不是新功能,初衷就是为了更方便视频号创作者发布图文短内容,以及提高用户获得信息的效率。(36氪)


OPPO IoT 事业群负责人李开新离职,电视业务几近裁撤


OPPO IoT 事业群负责人李开新离职,可能导致其电视业务几近裁撤。OPPO IoT 部门最近两年变动不断,一直在探索新的产品线。虽然 OPPO 在 IoT 方面也尝试过其他小品类,但较为稳定的业务还是耳机和可穿戴设备。近期有报道称 OPPO 将裁撤电视业务,但 OPPO 方面表示电视业务目前运营正常。


百度千帆接入 LLaMA2 等 33 个大模型


8 月 2 日,百度智能云宣布千帆大模型平台完成新一轮升级,全面接入LLaMA2全系列、ChatGLM2、RWKV、MPT 等 33 个大模型,成为国内拥有大模型最多的平台,接入的模型经过千帆平台二次性能增强,模型推理成本可降低50%。同时,上线 103 个预置 Prompt 模板,覆盖对话、游戏、编程、写作十余个场景,并发布多款全新插件。


国际要闻


iPhone 14 被用户吐槽电池老化


据报道,不少 iPhone 14 系列机主在社交媒体吐槽,该系列出现了严重的电池老化问题。iPhone 14 系列于 2022 年 9 月上市发售,首批用户持有时间还不到一年。社交网站上不少用户留言反馈称手机电池健康已经低于 90%,最多的跌到 87%。苹果官方对“电池健康”的描述为:包含最大电池容量和峰值性能容量。一般在手机电池正常使用的情况下,完整充电次数达到 500 次,电池健康的最大容量低于 80% 则会影响手机峰值性能,保修期内的 iPhone 可以得到官方保修甚至更换。(IT之家)


消息称 OpenAI 正测试第三代图片生成模型


OpenAI 在去年 4 月推出了第二代 DALL-E“文生图”模型,该模型凭借过硬的实力吸引了业界广泛注意,据外媒表示,OpenAI 日前正在准备下一代 DALL-E AI 模型(DALL-E 3),目前该公司正在进行一系列 Alpha 测试,而部分用户已经提早接触到了该 AI 模型。(财联社)


韩国室温超导团队称论文存在缺陷


韩国一研究团队近日发布论文称实现了室温超导,在引起全球广泛关注的同时,也遭到了质疑。而该研究团队的成员表示,论文存在缺陷,系团队中的一名成员擅自发布,目前团队已要求下架论文。分析师郭明錤认为,常温常压超导体商业化的时程并没有任何能见度,但未来若能够顺利商业化,将对计算器与消费电子领域的产品设计有颠覆性的影响。即便是小如iPhone的行动装置,都能拥有与量子计算机匹敌的运算能力。(财联社)


消息称苹果 Vision Pro 开发者实验室冷清,开发者兴趣不大


苹果公司在 7 月份开始邀请开发者去 Vision Pro 的开发者实验室,这些实验室分布在库比蒂诺、伦敦、慕尼黑、上海、新加坡和东京等城市,但是目前看来,开发者对这些实验室并没有表现出很大的兴趣。据彭博社的 Mark Gurman 报道,这些开发者实验室“参与人数不多,只有少量的开发者”。


AI 打败 AI:谷歌研究团队利用 GPT-4 击败 AI-Guardian 审核系统


8 月 2 日消息,谷歌研究团队正在进行一项实验,他们使用 OpenAI 的 GPT-4 来攻破其他 AI 模型的安全防护措施,该团队目前已经攻破 AI-Guardian 审核系统,并分享了相关技术细节。谷歌 Deep Mind 的研究人员 Nicholas Carlini 在一篇题为“AI-Guardian 的 LLM 辅助开发”的论文中,探讨了使用 GPT-4“设计攻击方法、撰写攻击原理”的方案。据悉,GPT-4 会发出一系列错误的脚本和解释来欺骗 AI-Guardian ,论文中提到,GPT-4 可以让 AI-Guardian 认为“某人拿着枪的照片”是“某人拿着无害苹果的照片”,从而让 AI-Guardian 直接放行相关图片输入源。谷歌研究团队表示,通过 GPT-4 的帮助,他们成功地“破解”了 AI-Guardian 的防御,使该模型的精确值从 98% 的降低到仅 8%。(IT之家)


程序员专区


KubeSphere 3.4.0 发布


致力于打造以 Kubernetes 为内核的云原生分布式操作系统 KubeSphere 3.4.0 发布,该版本带来了值得大家关注的新功能以及增强:扩大对 Kubernetes 的支持范围,最新稳定性支持 1.26;重构告警策略架构,解耦为告警规则与规则组;提升集群别名展示权重,减少原集群名称不可修改导致的管理问题;升级 KubeEdge 组件到 v1.13 等。同时,还进行了多项修复、优化和增强,更进一步完善交互设计,并全面提升了用户体验。


Firefox 116 发布


浏览器 Firefox 116 正式发布,该版本新增加了编辑现有文本注释的可能性、用户可以从操作系统复制任何文件并将其粘贴到 Firefox 中,开发方面,Firefox 现在支持 CSP3 external hashes,添加了对 dirname 属性的支持。具体可查看发布说明:http://www.mozilla.org/en-US/

作者:小莫欢
来源:juejin.cn/post/7262900590936604730
firef…

收起阅读 »

iOS 灵动岛上岛指南

零、关于灵动岛的认识灵动岛,即实时活动(Live Activity)它允许人们以瞥见的形式来观察事件或任务的状态.我的理解是"我不需要一直盯着看,但是我偶尔想看的时候能很方便的看到".这就需要再设计的时候尽可能扔掉没用的信息,保持信息的简洁.实时活动的事件构成...
继续阅读 »

零、关于灵动岛的认识

灵动岛,即实时活动(Live Activity)

它允许人们以瞥见的形式来观察事件或任务的状态.我的理解是"我不需要一直盯着看,但是我偶尔想看的时候能很方便的看到".这就需要再设计的时候尽可能扔掉没用的信息,保持信息的简洁.

实时活动的事件构成最好是包含明确开始 + 结束的事件.例如:外卖、球赛等.

实时活动在结束前最多存活8小时,结束后在锁屏界面最多再保留4小时.

关于更多灵动岛(实时活动)的最佳实践及设计思路可以参考一下:
知乎-苹果开放第三方App登岛,灵动岛设计指南来了!

一、灵动岛的UI布局

接入灵动岛后,有的设备支持(iPhone14Pro / iPhone14ProMax)展示灵动岛+通知中心,有的设备不支持灵动岛则只在通知中心展示一条实时活动的通知.
所以以下四种UI都需要实现:

1.紧凑型


2. 最小型


3. 扩展型


4. 通知


二、代码实现

1.在主工程中创建灵动岛Widget工程

Xcode -> Editor -> Add Target


如图勾选即可


2.在主工程的info.plist中添加key

Supports Live Activities = YES (允许实时活动)

Supports Live Activities Frequent Updates = YES(实时活动支持频繁更新) 这个看项目的需求,不是强制的


3.添加主工程与widget数据交互模型

在主工程中,新建Swift File,作为交互模型的文件.这里将数据管理与模型都放到这一个文件里了.


创建文件后的目录结构


import Foundation
import ActivityKit

//整个数据交互的模型
struct TestWidgetAttributes: ActivityAttributes {
    public typealias TestWidgetState = ContentState
    //可变参数(动态参数)
    public struct ContentState: Codable, Hashable {
        var data: String
    }
    //不可变参数 (整个实时活动都不会改变的参数)

    var id: String

}

如果参数过多.或者与OC混编,默认给出的这种结构体可能无法满足要求.此时可以使用单独的模型对象,这样OC中也可直接构造与赋值.注意,此处的模型需要遵循Codable协议

import Foundation
import ActivityKit

struct TestWidgetAttributes: ActivityAttributes {
    public typealias TestWidgetState = ContentState
    //可变参数(动态参数)
    public struct ContentState: Codable, Hashable {
        var dataModel: TestLADataModel
    }
    //不可变参数 (整个实时活动都不会改变的参数)
    //var name: String
}

@objc public class TestLADataModel: NSObject, Codable {
    @objc var idString : String = ""
    @objc var nameDes : String = ""
    @objc var contentDes : String = ""
    @objc var completedNum : Int//已完成人数
    @objc var notCompletedNum : Int//未完成人数
    var allPeopleNum : Int {
        get {
            return completedNum + notCompletedNum
        }
    }

    public override init() {
        self.nameDes = ""
        self.contentDes = ""
        self.completedNum = 0
        self.notCompletedNum = 0
        super.init()
    }

    /// 便利构造
    @objc convenience init(nameDes: String, contentDes: String, completedNum: Int, notCompletedNum: Int) {
        self.init()
        self.nameDes = nameDes
        self.contentDes = contentDes
        self.completedNum = completedNum
        self.notCompletedNum = notCompletedNum
    }
}

4.Liveactivity widget的UI

打开前文创建的widget,我的叫demoWLiveActivity.swift

这里给出了默认代码的注释,具体的布局代码就不再此处赘述了.

import ActivityKit
import WidgetKit
import SwiftUI

struct demoWLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: demoWAttributes.self) { context in
            // 锁屏之后,显示的桌面通知栏位置,这里可以做相对复杂的布局
            VStack {
                Text("Hello")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)
        } dynamicIsland: { context in
            DynamicIsland {
                /*
                 这里是长按灵动岛[扩展型]的UI
                 有四个区域限制了布局,分别是左、右、中间(硬件下方)、底部区域
                 */
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.center) {
                    Text("Center")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom")
                    // more content
                }
            } compactLeading: {
                // 这里是灵动岛[紧凑型]左边的布局
                Text("L")
            } compactTrailing: {
                // 这里是灵动岛[紧凑型]右边的布局
                Text("T")
            } minimal: {
                // 这里是灵动岛[最小型]的布局(有多个任务的情况下,展示优先级高的任务,位置在右边的一个圆圈区域)
                Text("Min")
            }
            .widgetURL(URL(string: "http://www.apple.com"))
            .keylineTint(Color.red)
        }
    }
}

5.Liveactivity 的启动 / 更新(主工程) / 停止

启动

let attributes = TestWidgetAttributes()
let initialConetntState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
do {
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialConetntState, staleDate: nil),
pushType: nil
//              pushType: .token
)
print("请求开启实时活动: \(activity.id)")
} catch (let error) {
print("请求开启实时出错: \(error.localizedDescription)")
}

更新

let updateState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
let alertConfig = AlertConfiguration(
title: "\(dataModel.nameDes) has taken a critical hit!",
body: "Open the app and use a potion to heal \(dataModel.nameDes)",
sound: .default
)
await activity.update(
ActivityContent<TestWidgetAttributes.ContentState>(
state: updateState,
staleDate: nil
),
alertConfiguration: alertConfig
)
print("更新实时活动: \(activity.id)")

结束

let finalContent = TestWidgetAttributes.ContentState(
dataModel: TestLADataModel()
)
let dismissalPolicy: ActivityUIDismissalPolicy = .default
await activity.end(
ActivityContent(state: finalContent, staleDate: nil),
dismissalPolicy: dismissalPolicy)
removeActivityState(id: idString);
print("结束实时活动: \(activity)")

三、更新数据

数据的更新主要通过两种方式:

1.服务端推送

2.主工程更新

其中主工程的更新参见(2.5.Liveactivity 的启动 / 更新(主工程) / 停止)

这里主要讲通过推送方式的更新

首先为主工程开启推送功能,但不要使用registerNotifications()为ActivityKit推送通知注册您的实时活动,具体的注册方法见下.

1. APNs 认证方式选择

APNs认证方式分为两种:

1.cer证书认证

2.Token-Based认证方式

此处只能选择Token-Based认证方式,选择cer证书认证发送LiveActivity推送时,会报TopicDisallowed错误.

Token-Based认证方式的key生产方法 参见:Apple Documentation - Establishing a token-based connection to APNs

2. Liveactivity 的启动

let attributes = TestWidgetAttributes()
let initialConetntState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
do {
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialConetntState, staleDate: nil),
pushType: .token//须使用此值,声明启动需要获取token
)
//判断启动成功后,获取推送令牌 ,发送给服务器,用于远程推送Live Activities更新
//不是每次启动都会成功,当已经存在多个Live activity时会出现启动失败的情况
print("请求开启实时活动: \(activity.id)")
Task {
for await pushToken in activity.pushTokenUpdates {
let pushTokenString = pushToken.reduce("") { $0 + String(format: "x", $1) }
//这里拿到用于推送的token,将该值传给后端
pushTokenDidUpdate(pushTokenString, pushToken);
}
}
} catch (let error) {
print("请求开启实时出错: \(error.localizedDescription)")
}

3. 模拟推送

1.可以使用terminal简单的构建推送,这里随便放上一个栗子

2.也可以使用这个工具 - SmartPushP8

此处使用SmartPushP8


发出推送后,在设备上查看推送结果即可.

注意:模拟器也是可以收到liveactivity的推送的但是需要满足:使用T2安全芯片 or 运行macOS13以上的 M1 芯片设备

四、问题排查

推送失败:

1.TooManyProviderTokenUpdates

测试环境对推送次数有一定的限制.尝试切换线路(sandbox / development)可以获得更多推送次数.

如果切换线路无法解决问题,建议重新run一遍工程,这样可以获得新的deviceToken,完成推送测试.

2.InvalidProviderToken / InternalServerError

尝试重新选择证书,重新run工程吧...暂时无解.

推送成功,但设备并未收到更新

这种情况需要打开控制台来观察日志

1.选择对应设备

2.点击错误和故障

3.过滤条件中添加这三项进程

liveactivitiesd
apsd
chronod

4.点击开始

在下方可以看到错误日志.

demo参考:demo


作者:大功率拖拉机
链接:https://juejin.cn/post/7254101170951192613
来源:稀土掘金

收起阅读 »

因为美工小妹妹天还没亮就下班,我做了一版可以把svg文件转成web组态组件的svg编辑器

web
自我介绍 本猫生活在东北的一个四线小城市,目前在一家小单位任职前端工程师的职位。早八晚五,生活充实,好不快活! 噩耗 Ctrl+c,Ctrl+v。Ctrl+c,Ctrl+v。新的一天开始了,本猫正在努力的工作着。看着旁边的美工小妹妹,我的口水止不住的往下流,别...
继续阅读 »

自我介绍


本猫生活在东北的一个四线小城市,目前在一家小单位任职前端工程师的职位。早八晚五,生活充实,好不快活!


噩耗


Ctrl+c,Ctrl+vCtrl+c,Ctrl+v。新的一天开始了,本猫正在努力的工作着。看着旁边的美工小妹妹,我的口水止不住的往下流,别误会,是因为公司没有几个人,所以我们每天中午都在一起吃饭,而且我本来身为一个吃货,想到午饭就会流口水,很合理吧。


正当我沉迷在幻想之际,手机的一声震动把我拽回到现实。


“来我办公室一趟!”

竟然是老板给我发的消息!!!

难不成是我每天辛苦的工作被他发现了?要给我涨工资?

我一溜烟的从工位冲了出去,直奔老板的办公室。

这一路上我想了很多:想我这么多年的辛劳付出终于达到了回报、想我迎娶白富美的现场何其壮观、想我出任ceo之后回村又是多么的风光......

终于在三秒之后,我来到了老板的办公室,见到了我最敬爱的老板。


“猫啊,听说你最近表现不错”

还没等我说话,老板又接着说到:

“咱们公司最近新接了一个项目要交给你完成,你有没有信心啊?”

我拍了拍胸脯:

“必须的!老板您说让我做什么我就做什么”

然后老板对我展开了需求攻势:

“巴拉巴拉,如此如此,这般这般,巴巴拉拉加班拉拉巴巴”

“什么?加班!”

其实老板说了那么多我是左耳听,右耳冒,但是加班这两个重重的砸在了我心上。我突然感觉一股热流冲到了头顶,双腿也止不住的颤抖,随即我双手用力的支撑在了老板的办公桌上,才勉强地站稳脚跟。

老板似乎也看出了我的不适,继续说道:

“小猫啊!你年龄这么小,正是应该努力奋斗的时候,我像你这么大的时候每天只睡十分钟,才有了今天的成就。巴拉巴拉。。。。”

听着老板的经历,我不由得留下了感动的泪水。原来老板是如此的器重我,耐心的劝导我只为了让我成长,想到我刚才还想向老板提涨工资的事,我的脸唰的一下就红了。我赶紧向老板鞠了一躬,猫着腰往办公室外面跑。

“谢谢老板关心,我一定完美完成任务!”

“公司就你一个前端,一定得好好做啊!”

随着老板的声音越来越小,我知道我已经离开了老板的办公室,我挺起胸膛,这一刻我仿佛重生了一般,浑身充满了干劲。迈着自信的步伐,我回到了我的工位。


打开电脑,老板已经把项目需求文档发给了我,打开1个Gdoc文件,首页赫然印着几个大字:项目工期一个月


我眼前一黑,晕了过去


曙光


“醒醒!醒醒!你怎么睡着了呢?”

我闻到一股淡淡的清香味,原来是我旁边的美工小妹妹在叫我。

“老板发你的新需求的项目文档你看了吧?老板让我们一起做这个项目”

“看到了看到了”

我惊喜着大叫一声,丝毫不顾忌周围同事的目光。我把头发往后捋了捋,望向我旁边的美工小妹妹:

“放心吧,包在我身上!”

“好的吧,那我先回去了,需要我做什么跟我说吖!”

我比了一个ok的手势,目送美工小妹妹转身回到了她的工位上。


需求分析


于是乎,我赶紧打开了文档,细心的分析了这个项目的需求。

原来这个项目是希望完成一个web端的拓扑图,要求使用svg技术实现,布局大概是长这样:


布局.png


功能呢,也不复杂,可以从左侧的节点区把节点拖到画布,在画布上选中绘制的节点可以缩放旋转,在右侧属性区可以快捷的更改节点的一些属性


美工小妹妹就负责配合我去设计svg的图形


我冷笑一声,这还不简单?真是小看了我这个代码吸血鬼。我打开搜索引擎,用出了我的绝招



“无敌暴龙cv大法”



不出半天,上面的布局就被我给做好了。


根据项目需求还需要去实现拖动的功能,凭借我多年的工作经验,我很快便找到了如下可以进行参考的文章:


一个低代码(可视化拖拽)教学项目


详细的拖拽需求以及缩放旋转的操作这篇文章里都有讲,我这里就不重复赘述了,经过了几天没日没夜的开发,主要说下我遇到的问题吧!


上面文章包含的项目实际上是采用定位来实现的,本身也是支持集成svg文件的,我们先看一下svg文件如何集成:


svg节点定义:


<template>
   <div class="svg-star-container">
       <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
           <circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" :fill="fill" />
       </svg>
   </div>
</template>

<script>
export default {
   props: {
       fill: {
           type: String,
           require: true,
           default: '#ff0000',
      },
  },
}
</script>

右侧属性面板定义:


<template>
   <div>
       <el-form>
           <el-form-item label="填充色">
               <el-color-picker v-model="curComponent.fill" />
           </el-form-item>
       </el-form>
   </div>
</template>

上面这是我新集成的一个svg图形,里面只有一个圆形,我这里希望可以在右侧改变圆形的填充(fill)颜色,集成之后的效果就是这样的:


集成效果.png


其实本质上是可以满足我们的需求的,因为我们也就是想改改svgfill或者是stroke这些基本属性的,但是我们的项目大概有300多个组件,这样一来我需要写600多vue文件。上面的示例我只是演示了如何更改svgfill属性,没做缩放适配效果是这样的:


未适配.png


所以我还需要手动调整每个文件去适配缩放,这个工作量堪比吨级,我的脑中顿时思绪万千:


想起来这几日美工小妹妹准时下班向我发我来的甜蜜问候


美工下班.png


想起来昨天向公司申请换键盘的尴尬经历


申请键盘.png


望着手机屏幕反射出连续高强度加班憔悴的我,看着我新换回来CV按键清晰可见的键盘,看着美工小妹妹键盘上那纤细的手指,我不由得感叹道:这么好看的手,不多做几个图可惜了。于是乎我的脑海里萌生了一个大胆的想法:



我要写个逻辑自动把svg文件转成组件,每个svg文件写个配置文件就能在右侧属性区动态的设置svgfillstroke等属性!!!



理想很丰满,现实很骨感


开始我是打算用viteraw以字符串的方式加载svg文件,再用v-html指令将字符串渲染成html


<script setup lang="ts">
import testSvgString from '../assets/vue.svg?raw'
</script>

<template>
 <div>
   <div v-html="testSvgString"></div>
   <div v-html="testSvgString"></div>
 </div>
</template>



效果是这样的:


raw效果.png


这个方案确实行得通,不过如果真正绘制图像的话,会导致dom节点变的很多很多。


dom节点.png


而且现在还没有考虑怎么适应svg节点的实际大小,怎么动态的改变节点fill等基本属性。。。


越想头越大,迷迷糊糊中我仿佛身处在一片森林中,映入我眼帘的是一个正在从我面前河里飘出来的白发老人


“少年哟,你掉的是这个<symbol>标签呢?还是这个<use>标签呢?”


我猛然惊醒


皇天不负有心人


对啊,之前我们用过的一个项目里面的icon图标就是一个个文件,当时是用的这个插件vite-plugin-svg-icons


它可以把svg文件加载成symbol标签,然后在需要的地方用use标签引用就可以了


说干就干,于是我赶紧熟练的打开了搜索引擎,开始了我的求知之路


终于


这个基于 vue3.2+ts 实现的 svg 可视化 web 组态编辑器项目诞生了!


项目介绍


svg文件即组件,引入并编写好配置文件后之后无需进行额外配置,编辑器会自适应解析加载组件。
同时支持自定义svg组件和传统的vue组件


开源地址


github


gitee


在线预览


绘画


选中左侧的组件库,按住鼠标左键即可把组件拖动到画布中


绘画.gif


操作


选中绘制好的节点后会出现锚点,可以直接进行移动、缩放、旋转等功能,右侧属性面板可以设置配置好的节点的属性,鼠标右键可以进行一些快捷操作


操作.gif


连线


鼠标移动到组件上时会出现连线锚点,左键点击锚点创建线段,继续左键点击画布会连续创建线段,右键停止创建线段,鼠标放在线段上会出现线段端点提示,拖动即可重新设置连线,选中线段后还可以在右侧的动画面板设置线段的动画效果


连线.gif


支持集成到已有项目


脚手架项目


# 创建项目(已有项目跳过此步骤)
npm init vite@latest

# 进入项目目录
cd projectname

# 安装插件
pnpm i webtopo-svg-edit

# 安装pinia
pnpm i pinia

# 修改main.ts 注册pinia
import { createPinia } from 'pinia';
const app = createApp(App);
app.use(createPinia());
app.mount('#app')

#在需要的页面引入插件
import { WebtopoSvgEdit,WebtopoSvgPreview } from 'webtopo-svg-edit';
import 'webtopo-svg-edit/dist/style.css'

umd方式集成


<!DOCTYPE html>
<html>
 <head>
   <title>webtopo-svg-edit Example</title>
   <link href="https://unpkg.com/webtopo-svg-edit@0.0.8/dist/style.css" rel="stylesheet" />
   <script src="https://unpkg.com/vue@3.2.6/dist/vue.global.prod.js"></script>
   <script src="https://unpkg.com/vue-demi@0.13.11/lib/index.iife.js"></script>
   <script src="https://unpkg.com/pinia@2.0.33/dist/pinia.iife.prod.js"></script>
   <script src="https://unpkg.com/webtopo-svg-edit@0.0.8/dist/webtopo-svg-edit.umd.js"></script>
 </head>
 <body>
   <div id="app"></div>
   <script>
     const pinia = Pinia.createPinia()
     const app = Vue.createApp(WebtopoYLM.WebtopoSvgEdit)
     app.use(pinia)
     app.mount('#app')
   
</script>
 </body>
</html>


es module方式集成


<!DOCTYPE html>
<html>
 <head>
   <title>webtopo-svg-edit Example</title>
   <link href="https://unpkg.com/webtopo-svg-edit@0.0.8/dist/style.css" rel="stylesheet" />
 </head>
 <body>
   <div id="app"></div>
 </body>
</html>
<script type="importmap">
{
  "imports": {
    "vue": "https://unpkg.com/vue@3.2.47/dist/vue.esm-browser.prod.js",
    "@vue/devtools-api": "https://cdn.jsdelivr.net/npm/@vue/devtools-api/lib/esm/index.min.js",
    "vue-demi": "https://unpkg.com/vue-demi@0.13.11/lib/index.mjs",
    "pinia": "https://unpkg.com/pinia@2.0.29/dist/pinia.esm-browser.js",
    "WebtopoYLM": "https://unpkg.com/webtopo-svg-edit@0.0.8/dist/webtopo-svg-edit.es.js"
  }
}
</script>
<script type="module">
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 import { WebtopoSvgEdit } from 'WebtopoYLM'
 const app = createApp(WebtopoSvgEdit)
 app.use(createPinia())
 app.mount('#app')
</script>


后记


“报告老板,以前一个月的工作,现在7天就能做完!”


“小伙子你很有前途,等公司赚钱了一定不会忘了你的!”


“老板这是哪里话,牛马的命也是命,当牛做马是我的荣幸!”


“行了,没什么事就去忙吧,7天之后等你们的好消息!”


凌晨三点,我看着美工小妹妹忙碌的身影,不由得嘴角上扬


嘿嘿!天

作者:咬轮猫
来源:juejin.cn/post/7260126013111812153
还没亮,谁也别想走!

收起阅读 »

北漂五年,我回家了。后悔吗?

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租...
继续阅读 »

2017年毕业后,我来到了北京,成为了北漂一族。五年后,我决定回家了。也许是上下班一个多小时的通勤,拥挤的地铁压得我喘不过气;也许是北漂五年依然不能适应干燥得让人难受的气候。北京很大,大得和朋友机会要提前两个小时出门;北京很小,我每天的活动范围就只有公司、出租屋两点一线。今年我觉得是时候该回家乡了。


1280X1280 (1).JPEG


(在北京大兴机场,天微微亮)


有些工作你一面试就知道是坑


决定回家乡后,我开始更新自己的简历。我想过肯定会被降薪,但是没想到降薪幅度会这么大,成都前端岗位大多都是1w左右,想要双休那就更少了。最开始面试的一些岗位是单休或者大小周,后面考虑了一下最后都放弃了。那时候考虑得很简单,一是我没开始认真找工作,只是海投了几个公司,二是我觉得我找工作这儿时间还比较短,暂时找不到满意的很正常。


辞职后,我的工作还没有着落,于是决定先不找了,出去玩一个月再说。工作了这么久,休息一下不为过吧,于是在短暂休息了一个月后,我又开始认真找工作。


但是,但是没想到成都的就业环境还蛮差的,找工作的第二个月还是没有合适的,当时甚至有点怀疑人生了,难道我做的这个决定是错误的?记得我面试过一家公司,那家公司应该是刚刚成立的,boss上写的员工数是15个,当时我想着,刚成立的公司嘛,工资最开始低点也行,等公司后续发展起来了,升职加薪岂不美滋滋。


面试时,我等了老板快半小时,当时我对这家公司的观感就不太好了。但想着来都来了,总不能浪费走的这一趟。结果,在面试的时候老板开始疯狂diss我的技术不行,会的技能太少,企图用这种话来让我降薪。我是怎么知道他想通过这种方式让我降薪呢,因为最后那老板说“虽然你技术不行,但是我很看好你的学习能力,给你开xxx工资你愿意来吗?”


也是因为这次面试,我在招聘软件上看到那种小公司都不轻易去面试了,简直浪费我时间。


1280X1280.JPEG


(回家路上骑自行车等红绿灯,我的地铁卡被我甩出去了,好险,但是这张地铁卡最后还是掉了,还是在我刚充值完100后,微笑)


终于,找了大概3个月,终于找到一家还算不错的公司,在一家教育行业的公司做前端。双休,工资虽然有打折,但是在我能接受的范围内。


有些人你一见面就知道是正确的


其实我打算回家乡还有一个重要原因是通过大厂相亲角网恋了一个女孩子,她和我是一个家乡的。我们刚认识的时候几乎每天都在煲电话粥,基本上就是陪伴入眠,哈哈哈哈哈。语言的时候她还会唱歌给我听,偏爱、有可能的夜晚......都好好听,声音软绵绵的。认识一个月后,我们回了一趟成都和她面基。一路上很紧张,面基的时候也很害怕自己有哪里做得不好的地方,害怕给她留下不好的印象。我们面基之后一个月左右就在一起啦。有些人真的是你一见面就知道她是正确的那个人,一见面心里有一个声音告诉你“嗯,就是她了!”。万幸,我遇到了。


58895b3fc4db3554881bdbcaa35384f.jpg


1280X1280 (2).JPEG


说一些我们在一起后的甜蜜瞬间吧


打语言电话的时候,听着对方的呼吸声入睡;


走在路上的时候,我牵她的手,她会很顺其自然地与我十指相扣;


在一起吃饭的时候,她会把自己最好吃的一半分享给我;



总结


回到正题,北漂五年。我回家了,后悔吗?不后悔。离开北京快一年了,有时候还是会想念自己还呆在北京的不足10平米的小出租屋里的生活,又恍惚“噢,我已经回四川了啊”。北漂五年,我还是很感激那段时间,让刚毕业的我迅速成长成可以在工作上独当一面的合格的程序员,让我能有拿着不菲的收入,有一定的积蓄,有底气重新选择;感谢大厂相亲角,让我遇见我的女朋友,让我不再是单身狗。

作者:川柯南
来源:juejin.cn/post/7152045204311113736

收起阅读 »

被约谈,两天走人,一些思考

五档尼卡引爆全场 前言 个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流 来龙去脉 上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎...
继续阅读 »

五档尼卡引爆全场



前言


个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流


来龙去脉


上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎有什么事发生


微信上问了一句:什么情况?


前端同事:裁员,最好准备



公司现状


从我去年入职公司后,就在喊降本增效了,周一晨会时不时也会提一下降本增效,毕竟大环境不好,公司的业务这两年也受到不小的影响


今年好几个项目组人手不够,两三月前还在疯狂面试前后端测试产品,我们这边的业务线前端都面试都超过五十个人了,最后招了一个前端一个后端一个测试


想着这种情况,公司薪资给的也不高,新员工不大量招就算降本了吧,再优化优化各方面流程等提提效率,没想到降本的大刀直接落下来首先砍掉的是技术开发人员


裁员情况


公司北京总部这边,目前我们部门是裁了两个前端一个后端,其他部门也有有裁员,人数岗位就不清楚了


从被裁掉的同事那边了解到的消息,上周三下班后下午找他们谈的,周四交接,周五下班后就走了,按照法律规定赔偿


上周只是一个开始,应该是边裁边看,什么时候结束最终裁员比例目前还不清楚,由其他来源得到的消息来源说是这次裁员力度很大


现在如果不是核心项目员工,如果不是和领导关系比较好的员工,每个人头上都悬着一把达摩克利斯之剑


个人思考


看待裁员


我认为首先是放平心态吧


国际经济形去全球化,贸易战,疫情,到现在的各种制裁,俄乌战争等,极端气候频发,真是多灾多难的年代


裁员这几年大家也见多了,该来的总会来


我认为裁员好说,正常赔偿就行,好聚好散,江湖再见


企业层面


裁员也是企业激发组织活力的一种方式,正常看待就行,关于企业组织活力介绍的,这里推荐一本前段时间刚读完的一本书 《熵减:华为活力之源》



熵是来源于物理科学热力学第二定律的概念,热力学第二定律又称熵增定律。熵增表现为功能减弱直到逐渐丧失,而熵减表现为功能增强...



个人层面


1.如果公司正常走法律流程,拿赔偿走人,继续找工作,找工作的过程也能发现自己的不错,更加了解市场,甚至倒逼自己成长


2.如果公司只想着降低成本,不做人事,有那种玩下三滥手段的公司,一定要留好证据,拍照,录音,截图,保存到自己的手机或者云盘里,不想给赔偿或恶意玩弄手段的,果断仲裁,我们员工相对企业来讲是弱势群体,这时候要学会用法律武器保护自己(可能也是唯一的武器)



这年头行情不好,老板损失的可能只是近期收益,有的员工失去的可能是全家活下去的希望



日常准备


做好记录


日常自己工作上的重大成果,最好定期梳理一下,或者定期更新简历,也可以不更新简历,找地方记录下来,例如项目上的某个重大模块的开发升级,或者做的技术上的性能优化等,我是有写笔记博客的习惯,技术相关的有时间一般会写成文章发到社区里


保持学习


日常保持学习的基本状态,这个可能我们每个人都会有这个想法,但是能定期沉下心来去学习提升,系统地去提升自己的时候,很少能坚持下来,万事开头难,开头了以后剩下的是坚持,我自己也是,有些事情经常三天打鱼,两天晒网,一起加油


关注公司


如果公司有查考勤,或者重点强调考勤了,一般都是有动作了,我们公司这次就是,年中会后的第二周吧,大部门通报考勤情况,里面迟到的还有排名,没多久就裁员了


保护自己


有的公司可能流程操作不规范,也有的可能不想赔偿或者少赔偿,可能会在考勤上做文章,例如迟到啥的,如果公司有效益不好的苗头,一定要注意自己这方面的考勤,以及自己的绩效等,做好加班考勤截图,领导HR与自己的谈话做好录音,录屏等,后面可能用的上,也可能会让自己多一点点谈判筹码


经营关系


虽然裁员明面上都是根据工作表现来的,好多时候大家表现都差不多,这个时候就看人缘了,和领导关系好的,一般都不是优先裁员对象,和领导团队成员打成一片真的很重要



以前我还有过那种想法:


我一个做技术的,我认真做好我自己的工作不就行了?专心研究技术,经过多年的工作发现,很多时候真的不行,我们不是做的那种科研类的,只有自己能搞,国内的大部分软件开发岗可能都是用的开源的技术做业务相关的,这种没什么技术难度,技术上来看基本没有什么替代性的难度


可能可替代性比较难的就是某个技术人长期负责的某个大模块,然后写了一堆屎山吧,毕竟拉屎容易,吃屎难


越是优秀的代码,可读性越强,简洁优雅,像诗一样



关于简历


如果是刚毕业的,可能简历上还好,大部分都优化都是已经是有一定的工作经验了,简历的更新就比较重要了,尤其工作了两三年了,如果简历看起来内容很少,不是那么丰富或者看起来很简陋,在简历筛选这一关会降低自己的面试几率,这时候一定要丰富一下,也有一些可能不知道自己简历是否丰富的,网上有那种简历模板可以搜搜看看,也可以找大佬帮忙看看,也有技术圈提供简历优化的有偿服务


再找工作


我个人的感觉是如果还是继续老本行继续打工,这年头行情不好,最好第一时间找工作,不能因为拿了赔偿就想着休一个月再说之类的,我周围有那种本来准备休半个月或一个月的,结果一下子休了一年以上的,我面试的时候如果遇到那种空窗期很长的,如果第一轮技术面能力都差不多的情况,到第二轮的领导面或者HR面,他们有优先考虑让空窗期短的人加入


关于空窗期


基本所有的公司都会关注离职空窗期,如果这个空窗期时间长了,那么求职的竞争力会越来越小,我在面试的时候我也会比较关注空窗期,因为我会有如下思考(举个例子,纯属乐子哈)


1.为什么这个人求职者三个月多了不找工作,家里有矿?家里有矿还上班,工作不会是找个地方打发时间的吧



我朋友的朋友就是这样,北京土著,家中独子,前几年拆迁了,家里好几套房,自己开俩车,人家上班就是找地方交个社保,顺便打发一下时间




2.能力不行吗?找工作这么久都没找到,是太菜了吗?还是太挑剔了?长时间不敲代码,手也生疏了,来我们团队行不行呀,我们这里赶项目压力这么大,招进来万一上手一段时间干不了怎么办,自己还被牵连了



几年前在某家公司做团队leader的时候,我们做的又是AI类项目,用的技术也比较前沿,当时AI的生态还不完善,国内做AI的大部分还处于摸索阶段,项目中用的相关技术栈也没个中文文档,由于公司创业公司,价格给的很低,高手招不进来,没办法只能画饼招综合感觉不错的那种,结果好几个人来了以后又是培训,又是有把手把手教的,结果干了没多久干不动走了,留下的烂摊子还得自己处理



关于社保


如果自己家里没矿,最好还是别让社保断了,拿北京举例,社保断了影响医疗报销,影响买车摇号等等


如果实在没找到工作,又马上要断缴社保了,可以找个第三方机构帮忙代缴,几千块钱,这时候的社保补缴相对来讲代价就比较高了



我遇到的情况是,社保断了一个月,后来找到工作了,第三方机构补缴都补不了,后来一通折腾总算弄补缴上了



关于入职


先拿offer,每一家公司的面试都认真对待,抱着一颗交流开放互相尊重的心


如果自己跳槽频繁,再找公司,可能需要考虑一下自己是否能够长待了,跳槽越频繁,后面找工作越困难,没有哪个公司希望招的人干一年就走了


所以面试结束后,最好根据需要面试情况,以及网上找到的资料,分析一下公司的业务模式了,分析这家公司的行业地位,加入的业务线或者部门是否赚钱,所在的团队在公司属于什么情况,分析团队是否是边缘部门,招聘的业务线是否核心业务线,如果不是核心业务线,可能过段时间效益不好还会被砍掉,有时候虽然看拿了对应的赔偿,但是再找工作,与其他同级选手对比的话,竞争力会越来越低


不论是技术面试官,还是负责面试的HR,大部分也都是公司的普通员工,他们可能不会为公司考虑,基本都会为自己考虑的,万一招了个瘟神到公司或者团队里,没多久自己团队也解散了怎么整



这里也许迷信了,基于我的一些经历来看有些人确实会有一些人是看风水,看人分析运势的


之前在创业公司的时候,有幸和一些投资人,上市公司的总裁,央企董事长等所谓的社会高层接触过,越是那些顶级圈里的人,有些人似乎很看中这个,他们有人研究周易,有人信仰佛教,有人招聘必须看人面相,有人师从南怀瑾等等



再次强调


每个人的经历,认知都是不一样的,同样的人不同角度下的世界也是不一样的,有不同意见是非常正常的,欢迎探讨交流不一样的心得,互相学习,共同进步


作者:草帽lufei
来源:juejin.cn/post/7264236820725366840
收起阅读 »

Flutter:创建和发布一个 Dart Package

在 Dart 生态系统中使用 packages(包) 实现代码的共享,比如一些 library 和工具。本文旨在介绍如何创建和发布一个 package。 通常来讲,我们所说的 package 一般都是指 library package,即可以被其他的 pac...
继续阅读 »

在 Dart 生态系统中使用 packages(包) 实现代码的共享,比如一些 library 和工具。本文旨在介绍如何创建和发布一个 package。


通常来讲,我们所说的 package 一般都是指 library package,即可以被其他的 package 所依赖,同时它自身也可以依赖其他 package。本文中说的 package 也都默认是指 library package


1.package 的组成


下图展示了最简单的 library package 布局:








  • library package 中需要包括 pubspec.yaml 文件lib 目录





  • library 的 pubspec.yaml 文件和应用程序的 pubspec.yaml 没有本质区别。





  • library 的代码需要位于 lib 目录 下,且对于其他 package 是 公开的。你可以根据需要在 lib 下创建任意目录。但是如果你创建的目录名是 src 的话,会被当做 私有目录,其他 package 不能直接使用。目前一般的做法都是把代码放到 lib/src 目录下,然后将需要公开的 API 通过 export 进行导出。




2.创建一个 package


假设我们要开发一个叫做 yance 的 package。


2.1 通过 IDE 创建一个 package








我们来看看创建好的一个 package 工程的结构:





可以看到 lib 目录和 pubspec.yaml 文件已经默认给我们创建好了。


2.2 认识 main library


我们打开 lib 目录,会发现有一个默认和 package 项目名称同名的 dart 文件,我们把这个文件成为 main library。因为我的 package 名称是 yance,因此,我的 main libraryyance.dart





main library 的作用是用来声明所有需要公开的 API。


我们打开 yance.dart 文件:


library yance;

/// A Calculator.
class Calculator {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}

第一行使用 library 关键字。这个 library 是用来为当前的 package 声明一个唯一标识。也可以不声明 library,在不声明 library 的情况下,package 会根据当前的路径及文件生成一个唯一标记。


如果你需要为当前的 package 生成 API 文档,那么必须声明 library。


至于 library 下面的 Calculator 代码只是一个例子,可以删除。


前面说了 main library 的作用是用来声明公开的 API,下面我们来演示一下,如何声明。


2.3 在 main library 中公开 API


我们在 lib 目录下新建一个 src 目录,后面所有的 yance package 的实现代码都统一放在 src 目录下,记住,src 下的所有代码都是私有的,其他项目或者 package 不能直接使用。


我们在 src 目录下,创建一个 yance_utils.dart 文件,在里面简单写一点测试代码:


class YanceUtils {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}

好了,现在需求来了,我要将 YanceUtils 这个工具类声明为一个公开的 API ,好让其他项目或者 package 可以使用。


那么就需要在 yance.dart 这个 main library 中使用 export 关键字进行声明,格式为:


export 'src/xxx.dart';

输入 src 关键字,然后选择 src/ 这个路径:





然后再输入 yance_utils.dart 即可:


library yance;

export 'src/yance_utils.dart';

这样就完成了 API 的公开,yance_utils.dart 里面所有的内容,都可以被其他项目所引用:


import 'package:yance/yance.dart';

class MyDemo{
  void test() {
    var yanceUtils = YanceUtils();
    var addOne = yanceUtils.addOne(1);
    print('结果:$addOne}');
  }
}

此时,可能大家会有个疑问,使用 export 'src/xxx.dart' 的方式,会将该 dart 文件里所有的内容都完全公开,那假如该文件里的内容,我只想公开一部分,该如何操作呢?


需要使用到 show 关键字:


export 'src/xxx.dart' show 需要公开的类名or方法名or变量名

/// 多个公开的 API 用逗号分隔开

还是以 yance_utils.dart 为例子,我们在 yance_utils.dart 再添加一点代码:


String yanceName = "123";

void yanceMain() {
  print('调用了yanceMain方法');
}

class YanceUtils {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}

class StringUtils {
  String getStr(String value) => value.replaceAll("/""_");
}

此时,我想公开 yanceName 属性yanceMain() 方法YanceUtils 类,可以这样声明:


library yance;

export 'src/yance_utils.dart' show YanceUtils, yanceName, yanceMain;

使用 show 不仅可以避免导出过多的 API,而且可以为开发者提供公开的 API 的概览。


3.发布一个 package


开发完成自己的 package 后,就可以将其发布到 pub.dev 上了。


发布 package 大致需要 5 个步骤:





下面会一一解答每一个步骤。


3.1 关于 pub.dev 的一些政策说明





  • 发布是永久的


只要你在 pub.dev 上发布了你的 package,那么它就是永久存在,不会允许你删除它。这样做的目的是为了保护依赖了你 package 的项目,因为你的删除操作会给他们的项目带来破坏。





  • 可以随时发布 package 的新版本,而旧版本对未升级的用户仍然可用。





  • 对于那些已经发布,但不再维护的 package,你可以把它标记为终止(discontinued)。




进入到 package 页面上的 Admin 标签栏,可以将 package 标记为终止。








标记为终止(discontinued)的 package,以前发布的版本依然留存在 pub.dev 上,并可以被看到,但是它有一个清楚的 终止 徽章,而且不会出现在搜索结果中。


3.2 发布前的准备


3.2.1 首先需要一个 Google 账户


Google 账户申请地址:传送门




如果之前你登录过任何 Google 产品(例如 Gmail、Google 地图或 YouTube),这就意味着你已拥有 Google 帐号。你可以使用自己创建的同一组用户名和密码登录任何其他 Google 产品。



3.2.2 检查 LICENSE 文件


package 必须包含一个 LICENSE 文件。推荐使用 BSD 3-clause 许可证,也就是 Dart 和 Flutter 团队所使用的开源许可证。


参考:


Copyright 2021 com.yance. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.
    * Neither the name of Google Inc. nor the names of its
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3.2.3 检查包大小


通过 gzip 压缩后,你的 package 必须小于 100 MB


如果它所占空间过大,考虑将它分割为几个小的 package。或者使用 .pubignore 移除不需要的文件,或者减少包含资源或实例的数量。


3.2.4 检查依赖项


package 应该尽量只依赖于托管在 pub.dev 上的库,以避免不必要的风险。


3.3 编写几个重要的文件 🔺


3.3.1 README.md


README.md 的内容在 pub.dev 上会当做一个页面进行展示:





3.3.2 CHANGELOG.md


如果你的 package 中有 CHANGELOG.md 文件,同样会被作为一个页面(Changelog)进行展示:





来看一个例子:


# 1.0.1

Fixed missing exclamation mark in `sayHi()` method.

# 1.0.0

**Breaking change:** Removed deprecated `sayHello()` method.
Initial stable release.

## Upgrading from 0.1.x

Change all calls to `sayHello()` to instead be to `sayHi()`.

# 0.1.1

Deprecated the `sayHello()` method; use `sayHi()` instead.

# 0.1.0

Initial development release.

3.3.3 pubspec.yaml


pubspec.yaml 文件被用于填写关于 package 本身的细节,例如它的描述,主页等等。这些信息将被展现在页面的右侧。





一般来说,需要填写这些信息:





注意:


目前 author 信息已经不需要了,所以大家可以把 author 给删除掉。


3.4 预发布


预发布使用如下命令。执行预发布命令不会真的发布,它可以帮助我们验证填写的发布信息是否符合 pub.dev 的规范,同时展示所有会发布到 pub.dev 的文件。


dart pub publish --dry-run

比如,我运行的结果是:


chenyouyu@chenyouyudeMacBook-Pro-2 yance % dart pub publish --dry-run
Publishing yance 0.0.1 to https://pub.dartlang.org:
|-- CHANGELOG.md
|-- LICENSE
|-- README.md
|-- lib
|   |-- src
|   |   '-- yance_utils.dart
|   '
-- yance.dart
|-- pubspec.yaml
|-- test
|   '-- yance_test.dart
'
-- yance.iml
Package validation found the following potential issue:
* Your pubspec.yaml includes an "author" section which is no longer used and may be removed.

Package has 1 warning.

它提示我们author 信息已经不需要了,可以删除。


删除后,再次运行就没有警告了。





3.5 正式发布


当你已经准备好正式发布你的 package 后,移除 --dry-run 参数:


dart pub publish







点击链接会跳转浏览器验证账户,验证成功后,会有提示:





账户验证通过后,会继续执行上传任务:





此时,去 pub.dev 上就能看到发布成功的 package 了:





pub.dev 会检测 package 支持哪些平台,并呈现到 package 的页面上。


注意:


正式发布可能需要科学上网。


4.参考文章



作者:有余同学
来源:mdnice.com/writing/d5460df39ddd4649be9b102ccb2fb0b2
收起阅读 »

内向的我,到底应该怎么和领导相处啊

务必一而再,再而三,三而不竭; 千次万次,毫不犹豫地救自己于人间水火。 上一篇文章,我们谈到了双减是双减,你是你。 但凡有点野心的,都是既在卷自己,又在鸡孩子。 我们应该考虑的,不是要不要让孩子努力,而是怎样用正确的方法,让孩子快乐地学习。 文...
继续阅读 »

务必一而再,再而三,三而不竭;
千次万次,毫不犹豫地救自己于人间水火。





上一篇文章,我们谈到了双减是双减,你是你。


但凡有点野心的,都是既在卷自己,又在鸡孩子。


我们应该考虑的,不是要不要让孩子努力,而是怎样用正确的方法,让孩子快乐地学习。


文章发出后,有位做金融的读者联系东哥咨询,家庭的问题怎么都好解决,职场上挑战更大,尤其自己性格内向。


金融问,东哥你说,内向的人应该怎么和领导相处?




这个问题,东哥很能感同身受。


我也性格内向,轻度社恐。所以年轻时在职场中,也一样困惑。


每一个问题背后,都是其底层价值观的影响。


你问内向的人如何和领导相处,这是表面问题,而且有技巧能借用。


比如我们之前谈到的要做好向上管理,要相信领导永远是正确的。


要去讲故事、去争资源,去打最硬的仗,去赢得尊重,去成为老板手中最锋利的刀。


这些技巧,并没有触及根本,一点不重要。


什么问题重要?


第一,你的关注点是自己性格内向,也就是说你不满意自己的性格。


这是表面问题。


你真正不满意的,其实是自己的财富状态,职业发展,幸福指数。


如果你已经身价千万,财务自由,还会为自己性格内、不知道应该怎么和领导相处向苦恼么?


所以你的关注点,应该在财富上,而不是性格上。




金融说,财富要,但外向的人也很让人羡慕,我能怎么改善下自己的性格呢?


我说,科学家多年以来最感兴趣的一个问题是,到底人的哪些特征是天生的,哪些特征是受后天教育和环境影响的?


通过对同卵双胞胎几十年的研究,最后的共识是,先天因素远远大于后天因素。


首先,任何一种能够测量的特征,包括智商、兴趣爱好、性格、体育、幽默感,甚至爱不爱打手机,所有这些东西都是天生的。


其次,后天环境对智力和性格的影响非常有限。先天因素是主要的,后天因素是次要的。


家庭环境可以在一定程度上左右一对同卵双胞胎小时候的行为,以至于他们可能会有不同的爱好和个性。


但等他们长大以后,他们的先天特征会越来越突出,他们会越来越像,他们在摆脱家庭对他们“真实的自我”的影响。


这并不是说家教完全没用。


家教可以左右基因表达,可以鼓励孩子发挥他天生的特长,也可以压制他天生的性格缺陷。


只不过这个作用是有限的。


既然后天作用有限,就不应该花太多心力在上面。


雷茵霍尔德·尼布尔在他著名的《宁静祷词》里说的话,在过去的一个多世纪里,曾使无数人动容



愿上帝赐予我从容去接受我不能改变的,
赐予我勇气去改变我可以改变的,
并赐予我智慧去分辨这两者间的区别。



识别出来了性格不可变,这就是大智慧。


世人皆苦,性格外向的人也有他的苦恼,只是你不知道罢了。


比如你,智商在线,年轻时能耐得住寂寞寒窗苦读,现在名校毕业搞金融。


智商和勤奋,也都是天赋,没有这些特质的人,又去哪里说理?


内向而聪慧,外向却愚蠢,你选哪个?


你真正想要的,是在自己已有的好特质上,再加上更有利的特质,比如外向且聪慧。


这是一种不切实际的贪婪。




金融说,做自己是好,但这个内向的性格,会影响赚钱,影响积累财富。


如果我资产千万财务自由,就不用为性格苦恼,问题是我现在资产没有千万。


我说,谁告诉你性格内向影响赚钱?


现代社会需要互相协作,所以性格外向的确会有一些优势。


但同时现代世界的一个好处是,大家可以发挥各自强项分工合作,一起赚钱。


罗永浩在做英语培训的时候,发现自己的脾气不好,性格上的缺点在办公室里暴露得很多,很苦恼,还特意去问了冯唐。冯唐回答说



这个苦恼完全没有必要。
假如你需要做的事情一共有12件,那么你只要做好其中的六七件,就能成就这个企业在商业上的成功。
因此,你只要把自己擅长的那六七件事做好,其他的找人补就行了,千万不要想着把12件事全做好才能成就一个企业。



你去阿里和腾讯看看,那里面有很多人不爱跟人相处,但他们爱跟程序打交道,这就可以了。


像阿里和腾讯这种大企业,有非常多各种各样性格的人,但都没有影响他们取得成功。


所以真正影响赚钱的,是自己的强项。


不断加强自己的强项,做成 1 米宽 10000米深的优势,然后以自己为中心,寻找合作的人。


总想着改变自己,其实是因为自己的强项不够强,就像用提升弱项来弥补。


走偏了。




第二,你的关注点是和领导相处。


往更广了说,你关注的是与高能级者的相处之道。


和领导相处让你有压力,是因为他的能量密度比你高,你在仰视他。


仰视的结果,就是失去了平常心,动作变形。


几千年中央集权的结果,让整个国民对权力,有种天然的崇拜。


辫子不见了,无形的辫子还在,膝盖总是软的,总想找个皇上跪一下。


《遥远的救世主》是本奇书,里面丁元英和韩楚风喝酒的时候提到



中国的传统文化是皇恩浩大的文化,它的实用是以皇天在上为先决条件。
中国为什么穷?穷就穷在幼稚的思维,穷在期望救主、期望救恩的文化上。
这是一个渗透到民族骨子里的价值判断体系。



这种弱势文化,会让人丧失自己,也就无法得到对方平等的尊重。


要学会用平常心对待高能级的人。


平视他,平等的相处,平等的沟通,和他交换价值,互相利用。


金融问,道理是这么个道理,但怎么能达到这个状态?


我说,仰望高能级者的时候,其实是你有所图。


希望能从他那里,得到本不属于你的东西,所以就想着怎么去迎合和讨好。


也就是说



想得到的东西 = 尚有欠缺的能力 + 降低身价的讨好



这是贪婪。


要想得到你要的东西,最好的办法是让你自己配得起那样东西。


强者身边,从来都不缺想讨好他的人。


真正的强者,需要的是能给他创造价值的人,有自己的强项,能弥补他欠缺的东西的人。


所以与其仰视讨好,不如平等合作。


这时候强者是你赚钱的工具,你则是他的合作者,是以谓之双赢。


成年人在一起,总要互相能有所收获,关系才能长久。


只有一方长期的、单方面的输出,终究不免渐行渐远。




所以





  • 当你以为自己在因为性格苦恼时,其实是在为财富苦恼;



  • 当你在为财富苦恼时,其实是在为能力苦恼;



  • 感受到了能力不足,就想通过情绪价值弥补;



  • 当你努力弥补弱项的时候,就走偏了。



  • 没有人会因为你弱项没那么弱与你合作,只会因为你的强项足够强而产生链接。


所以,接受自己的性格,然后强化自己的强项,这才是赚钱的正道。


务必一而再,再而三,三而不竭,千次万次,毫不犹豫地救自己于人间水火。


好好爱自己。



作者:jetorz
来源:mdnice.com/writing/44e869ae99de41a3b393654c15ad3566
收起阅读 »

iOS 16 又又崩了

背景iOS 16 崩了: juejin.cn/post/715360…iOS 16 又崩了:juejin.cn/post/722551…本文分析的崩溃同样只在 iOS16 系统会触发,我们的 APP 每天有 2k+ 崩溃上报。崩溃原因:Cannot ...
继续阅读 »

背景

iOS 16 崩了: juejin.cn/post/715360…
iOS 16 又崩了:juejin.cn/post/722551…
本文分析的崩溃同样只在 iOS16 系统会触发,我们的 APP 每天有 2k+ 崩溃上报。

崩溃原因:

Cannot form weak reference to instance (0x1107c6200) of class _UIRemoteInputViewController. It is possible that this object was over-released, or is in the process of deallocation.
无法 weak 引用类型为 _UIRemoteInputViewController 的对象。可能是因为这个对象被过度释放了,或者正在被释放。weak 引用已经释放或者正在释放的对象会 crash,这种崩溃业务侧经常见于在 dealloc 里面使用 __weak 修饰 self。
_UIRemoteInputViewController 明显和键盘相关,看了下用户的日志也都是在弹出键盘后崩了。

崩溃堆栈:

0	libsystem_kernel.dylib	___abort_with_payload()
1 libsystem_kernel.dylib _abort_with_payload_wrapper_internal()
2 libsystem_kernel.dylib _abort_with_reason()
3 libobjc.A.dylib _objc_fatalv(unsigned long long, unsigned long long, char const*, char*)()
4 libobjc.A.dylib _objc_fatal(char const*, ...)()
5 libobjc.A.dylib _weak_register_no_lock()
6 libobjc.A.dylib _objc_storeWeak()
7 UIKitCore __UIResponderForwarderWantsForwardingFromResponder()
8 UIKitCore ___forwardTouchMethod_block_invoke()
9 CoreFoundation ___NSSET_IS_CALLING_OUT_TO_A_BLOCK__()
10 CoreFoundation -[__NSSetM enumerateObjectsWithOptions:usingBlock:]()
11 UIKitCore _forwardTouchMethod()
12 UIKitCore -[UIWindow _sendTouchesForEvent:]()
13 UIKitCore -[UIWindow sendEvent:]()
14 UIKitCore -[UIApplication sendEvent:]()
15 UIKitCore ___dispatchPreprocessedEventFromEventQueue()
16 UIKitCore ___processEventQueue()
17 UIKitCore ___eventFetcherSourceCallback()
18 CoreFoundation ___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__()
19 CoreFoundation ___CFRunLoopDoSource0()
20 CoreFoundation ___CFRunLoopDoSources0()
21 CoreFoundation ___CFRunLoopRun()
22 CoreFoundation _CFRunLoopRunSpecific()
23 GraphicsServices _GSEventRunModal()
24 UIKitCore -[UIApplication _run]()
25 UIKitCore _UIApplicationMain()

堆栈分析

崩溃发生在系统函数内部,先分析堆栈理解崩溃的上下文,好在 libobjc 有开源的代码,极大的提高了排查的效率。

_weak_register_no_lock

抛出 fatal errr 最上层的代码,删减部分非关键信息后如下。

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
objc_object *referent = (objc_object *)referent_id;
if (deallocatingOptions == ReturnNilIfDeallocating ||
deallocatingOptions == CrashIfDeallocating) {
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
deallocating =
! (*allowsWeakReference)(referent, @selector(allowsWeakReference));
}

if (deallocating) {
if (deallocatingOptions == CrashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of " <=== 崩溃
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
}
}

直接原因是  _UIRemoteInputViewController 实例的 allowsWeakReference 返回了 false。

options == CrashIfDeallocating 就会 crash。否则的话返回 nil。不过 CrashIfDeallocating 写死在了代码段,没有权限修改。整个 storeWeak 的调用链路上都没有可以 hook 的方法。

__UIResponderForwarderWantsForwardingFromResponder

调用 storeWeak 的地方反汇编

if (r27 != 0x0) {
r0 = [[&var_60 super] init];
r27 = r0;
if (r0 != 0x0) {
objc_storeWeak(r27 + 0x10, r25);
objc_storeWeak(r27 + 0x8, r26);
}
}

xcode debug r27 的值

<_UITouchForwardingRecipient: 0x2825651d0> - recorded phase = began, autocompleted phase = began, to responder: (null), from responder: (null)

otool 查看 _UITouchForwardingRecipient 这个类的成员变量

ivars          0x1cfb460 __OBJC_$_INSTANCE_VARIABLES__UITouchForwardingRecipient
entsize 32
count 4
offset 0x1e445d0 _OBJC_IVAR_$__UITouchForwardingRecipient.fromResponder 8
name 0x19c7af3 fromResponder
type 0x1a621c5 @"UIResponder"
alignment 3
size 8
offset 0x1e445d8 _OBJC_IVAR_$__UITouchForwardingRecipient.responder 16
name 0x181977f responder
type 0x1a621c5 @"UIResponder"

第一个 storeweak  赋值 offset 0x10 responder: UIResponder 取值 r25。

第二个 storeweak 赋值 offset 0x8 fromResponder: UIResponder 取值 r26。

XCode debug 采集 r25 r26 的值

到这里就比较清晰了,_UITouchForwardingRecipient 是在保存响应者链。其中_UITouchForwardingRecipient.responder = _UITouchForwardingRecipient.fromResponder.nextReponder(这里省略了一长串的证明过程,最近卷的厉害,没有时间整理之前的文档了)。崩溃发生在 objc_storeWeak(_UITouchForwardingRecipient.responder), 我们可以从 nextReponder 这个方法入手校验 responder 是否合法。

结论

修复方案

找到 nextresponder_UIRemoteInputViewController 的类,hook 掉它的 nextresponder 方法,在new_nextresponder 方法里面判断,如果 allowsWeakReference == NO 则 return nil
在崩溃的地址断点,可以找到这个类是 _UISizeTrackingView

- (UIResponder *)xxx_new_nextResponder {
    UIResponder *res = [self xxx_new_nextResponder];
    if (res == nil){
        return nil;
    }
    static Class nextResponderClass = nil;
    static bool initialize = false;
    if (initialize == false && nextResponderClass == nil) {
        nextResponderClass = NSClassFromString(@"_UIRemoteInputViewController");
        initialize = true;
    }

if (nextResponderClass != nil && [res isKindOfClass:nextResponderClass]) {
if ([res respondsToSelector:NSSelectorFromString(@"allowsWeakReference")]) {
BOOL (*allowsWeakReference)(id, SEL) =
(__typeof__(allowsWeakReference))class_getMethodImplementation([res class], NSSelectorFromString(@"allowsWeakReference"));
if (allowsWeakReference && (IMP)allowsWeakReference != _objc_msgForward) {
if (!allowsWeakReference(res, @selector(allowsWeakReference))) {
return nil;
}
}
}
}
return res;
}

友情提示

1. 方案里面涉及到了两个私有类,建议都使用开关下发,避免审核的风险。

2. 系统 crash 的修复还是老规矩,一定要加好开关,限制住系统版本,在修复方案触发其它问题的时候可以及时回滚,hook 存在一定的风险,这个方案 hook 的点相对较小了。

3. 我只剪切了核心代码,希望看懂并认可后再采用这个方案。

作者:yuec
链接:https://juejin.cn/post/7240789855138873403
来源:稀土掘金

收起阅读 »

喂!不用这些网站,你哪来的时间摸鱼?

一些我常用且好用的在线工具Postcat - 在线API 开发测试工具postcat.com/ API 开发测试工具Postcat 是一个强大的开源、免费的、跨平台(Windows、Mac、Linux、Browsers...)的 ...
继续阅读 »

一些我常用且好用的在线工具

Postcat - 在线API 开发测试工具
postcat.com/ API 开发测试工具


Postcat 是一个强大的开源、免费的、跨平台(Windows、Mac、Linux、Browsers...)的 API 开发测试工具,支持 REST、Websocket 等协议(即将支持 GraphQL、gRPC、TCP、UDP),帮助你加速完成 API 开发和测试工作。它非常适合中小团队及个人使用。

在保证 Postcat 轻巧灵活的同时,它设计了一个强大的插件系统,让您可以一键使用插件来增强它的功能。


因此 Postcat 理论上是一个拥有无限可能的 API 产品,可以从Logo 中看到,我们也形象地为它加上了一件披风,代表它的无限可能。

Excalidraw - 在线白板画图

ajietextd.github.io/ 一个开源的虚拟手绘风格的白板。创建任何漂亮的手绘图。



EmojiXD - Emoji表情

emojixd.com/ EmojiXD 是一本线上Emoji百科全书📚,收录了所有emoji


Carbon - 在线生成代码图片

carbon.now.sh/Carbon 能够轻松地将你的源码生成漂亮的图片并分享。


Pixso - 产品设计协作一体化工具

pixso.cn/ Pixso,一站式完成原型、设计、交互与交付,为数字化团队协作提效
原来不用注册 现在需要注册了


作者:前端小蜗
链接:https://juejin.cn/post/7243680457815261221
来源:稀土掘金

收起阅读 »

金三银四好像消失了,IT行业何时复苏!

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了, 这是要哪样 我心中不由一万个 草泥🐴 路过自我10连问我的心情自去年下半年以来,互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。...
继续阅读 »

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了, 这是要哪样 我心中不由一万个 草泥🐴 路过


自我10连问

我的心情

自去年下半年以来,互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。然而,站在这个应该是光明的时刻,举世瞩目的景象却显得毫无生气。令人失望的是,我们盼望已久的春天似乎仍未到来。
我的工作生涯
我已经从业近十年,然而最近两年一直在小公司中工作,

我的技术和经历并不出色。随着年龄的增长,是否我的技能也在快速提高呢?我们该如何前进呢 ,转产品,产品到达极限,转管理,可是不会人情事故,

我们该如何继续前进呢?目前还没有人给出答案。

第一家公司

我记得那是很早的时候了,那个时候简历投递出去,就马上会收到很多回复,不像现在 ,
失联招聘, 前程堪忧,boss直坑,
你辛苦的写完简历,满怀期待投递了各大招聘平台,可等来的 却是已读未回,等的心也凉透了。
好怀念之前的高光时刻 神仙打架日子
前面我面试 几乎一周都安排满了,都面试不过来,我记得那会最多时候一天可以跑三家面试哈哈哈,也是很拼命的,有面试机会谁不想多试试呢
我第一家进入的是一个外包公司,叫xxx东软集团, 那个时候也不不懂,什么是外包给公司,只看工资给的所有offer中最高的,然后就去了哈哈哈哈。
入职第一天,我背着我自己的电脑满怀着激动就去了,然后被眼前一幕吸引了,办公的人真多啊,办公室都是拿透明玻璃隔开那种,人挺多,我一想公司还挺大的,
随后我就被带到也是一个玻璃格子办公室,里面就三个人,加我一个4个。
我害怕极了,这个时候一个稍微有一些秃顶的 大叔过来了 哈哈哈(内心台词,早就知道这一行干就了,会秃头这不会就是下一个我把)
他把我安排在了靠近玻璃门的也就是大门位置,这是知道我准备随时跑路节奏吗。然后就去忙他自己的了。整个上午我就像是一个被遗忘在角落里的人一样。根本没人管我,就这样第一天结束了,我尴尬了做了一整天。
这工作和我想象的有点不太一样啊!
后面第三天还是如此,办公室里依旧是那么几个人,直到第四天,大叔来了,问我直到多线程吗,让我用多线程写一个抽奖的活动项目。(内心我想终于有事情干了,可是也高兴不起来谁知道怎么写)
不过好在,他也没有说什么时候交,只是说写完了给他看一下,经过我几天的,复制粘贴工程师 一顿谷歌,百度,终于是勉强写出来了。。。。。
后面,就又陆陆续续来了几个小伙伴,才开始新项目和开会,第一份工作大致就是这样开始了我的职业生涯。怎么说呢和我想象的有所不一样,但又有一些失望。
后面干了1年多,我就离职了原因是太累了没时间休息,一个项目接着一个项目的

第二家公司

在离开第一家公司时候,我休息了好长一段时间,调整了我自己的状态
也了解了什么是外包公司,什么是工作外派,也是我这一次就在投递简历,和面试时候刻意去避免进那种外包,和外派公司。
面试什么也还算顺利,不到半个月就拿到了offer。 但是工资总体来说比上一家是要少一点,但是我也接受了,是一家做本地生鲜电商公司,,本来生活科技有公司, 我觉得公司氛围,和公司都挺不错的,就入职了。
入职了我们那个项目经理还算很热情的,让同事帮我开git账号,开了邮箱,我自己拉取了公司项目,然后同事帮我运行调试环境第一天,项目什么都跑了起来,
你知道的,每次去一家新公司,开始新项目难的是项目复杂配置文件,和各种mave包依赖,接口,环境冲突,所以跑起来项目自己一个人摸索还是需要一些时间的。
在这家公司前期也还不错,公司维护自己项目,工作时间也比较自由和灵活,
大体流程是,每次有新的pm时候 产品经理就会组织各个部门开会
h5端-移动端-接口端开会讨论需求细节和实现,如果有问题头就会pass掉
然后产品经理就会把需求指派到每一个头,头把需求指派给组员,然后组员按照
redmine 上截止时间开发需求,
开发过程中自己去找对应接口负责方,其他业务负责方 去对接数据,没有问题了就可以提交给指定测试组测试了。
然后测试组头会把,测试分配给他们组员,进行测试。
有问题了就会在指派回来给对应负责各个开发同学去解决bug,直到测试完成。测试会让你提交堡垒环境 然后等待通知发布上线,
我们一般是晚上8点时候发布,发布时候,一般开发人员都要留守,直到发布上线没有问题了,才可以回家。如果弄得很晚的话,第二天可以晚点上班。
这一点是我觉得比较好的地方,工作时间弹性比较自由。
记得有一次生产事故。
我影响很深刻,东西上线了,然后产品经理说这个和他设计的预期的不符合要求,需要重新写,那一晚我们整组弄得很晚,就是为了等我弄好去吃饭。
你知道人在心急如焚情况下,是写不好代码的最后还是同事帮我一起完成了产品经理变态需求修改。。。。。。(也就在那时候我才知道产品经理和开发为什么不和了)
因为五行相克
因为经常这样发版,然后一起吃饭公司报销。我们组员和领导关系还算不错氛围还挺好。在这一家公司我待了挺久的。
离职原因
后期因为说公司项目战略升级。空降了一位携程cto,还带来了他的手下人,我们组头,职权被削弱了,我不在由原来头管理了。再加上后面一些其他原因。老同事一个一个走了。
最后这个组只剩下我,和一个进来不久新同事。 不久我也离职了。

第三家公司


这次离职后,我调整休息了差不多有一年,中间离开上海去了江苏,因为家里,女朋友等各种事情。后面我才又从新去了上海,开始找工作。
找工作期间投奔的同事,合同事住一起。
这次面试我明显感觉,有一些慌张了,可能是太久没上班原因,有一些底气不足。好在也是找到了工作虽然不太理想。
这个过程太曲折了,后面公司终究没有扛过疫情,可以说在疫情边缘倒闭了,钱赔偿也没拿到,。。。这里就不赘述了。
IT行业如何破局 大家有什么想法和故事吗。可以关注 程序员三时公众号 进行技术交流讨论
嗯~还想往下面写一点什么,,,下一篇分享一下我现在工作和未来思考


作者:程序员三时
链接:https://juejin.cn/post/7231608749588693048
来源:稀土掘金
收起阅读 »

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

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

一、自身情况


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



  1. 其实30岁的时候已经开始焦虑了,并且努力想找出路。

  2. 提升技术,努力争增加自己的能力。

  3. 努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你努力的成效很低。

  4. 大环境我们普通人根本改变不了。

  5. 自己大龄性价比不高,中年危机就是客观情况。

  6. 无非就是在本赛道继续卷,还是换赛道卷的选择了。


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


二、大环境情况




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




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




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




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




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




三、未来出路


未来的出路在哪里?


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


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


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


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



  1. 有存款的,并且了解一些行业的可以开店,比如餐饮店,花店,水果店等。

  2. 摆摊,国家也都改变政策了。

  3. 超市,配送员,外卖员。

  4. 开滴滴网约车。

  5. 有能力的,可以润出G。可以吸一吸GW“free的air”,反正都是要被ZBJ榨取的。


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


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


作者:可乐泡枸杞
链接:https://juejin.cn/post/7230656455808335930
来源:稀土掘金
收起阅读 »

一位大厂程序员的随想:6年前刚读硕士,6年后将35岁危机

全文4300字,整体目录如下 作者简介:持续探索副业的大厂奶爸程序员 2020年,华科硕士毕业,非科班转行成程序员 2022年,进入某互联网大厂,打工搬砖 2023年,开始探索副业,目前主攻IP+工具开发 正文开始 一位朋友转载我的故事到公众号后,突然就...
继续阅读 »

全文4300字,整体目录如下




作者简介:持续探索副业的大厂奶爸程序员 2020年,华科硕士毕业,非科班转行成程序员 2022年,进入某互联网大厂,打工搬砖 2023年,开始探索副业,目前主攻IP+工具开发



正文开始


一位朋友转载我的故事到公众号后,突然就让意识到


现在29岁的我


往回看6年,我刚进入华科读硕士


往后看6年,我将面临网上所说的35岁中年危机


因此,借此机会,聊下我对未来的思考和回顾下简单过去的6年



未来6年-战略上乐观,战术上悲观


看待未来,我需要保持乐观,只有这样,才能不为未来的不确定而过分焦虑


还是学生时代的时候,因为对这程序员这行业不清楚,当时就很害怕网上常说的35岁的失业危机,为此还在网上查了各种各样的信息,整天忐忑不已。


可真正进入了这个行业以后,才发现危机远没有想象中的恐怖,原来,恐惧真的源于对未知的不确定。


身边也有好些35以上的朋友,他们有的还在程序员这行,有的已经转行了。虽然整体来看,薪酬水平或者薪酬增长速度不如之前,但远没有到达山穷水尽的地步。


即使是现在ai时代的到来,我依然相信,只要程序员去积极的拥抱ai,使用ai去做更多创造性的工作,也不会突然就失业。


但同时,如果35岁的我,还是会被失业危机所困的话,那么一定就是平常的日子太过懈怠,处于温水煮青蛙的状态。


22年刚入大厂的半年里,基本就处于这个状态,除了工作外,剩下的时间基本都用来娱乐了,成长很是有限。


因此,我需要在战术上保持悲观,要不断成长,要确保自己将主要精力放下以下三方面的事情


1、做好主业,保持市场竞争力,被裁/失业时,能快速找到工作


2、开展第二曲线,降低未来失业可能带来的现金流锻炼的风险


3、爱护好自己的身体,照顾好家人,帮助朋友。


先来聊下第二点和第三点吧,第一点在文末聊。


未来6年-做好第二曲线


为什么开展


2022年过年期间,开始意识到现在的看似高薪工作并不稳定,需要在工作外,建立第二曲线(也就是副业),降低未来的风险。


原因有二,内心的渴望+外在的环境


内在的渴望就是,其实自己一直是一个很爱好学习的人,也希望做出点成绩获得外界认可的人。


在3月之前,也一直在保持学习,科学习的那点热情基本全用在了阅读各种书籍以及得到上,看了几十本书,学了好几本课程,可是成长却极为有限。


幸而在3月的时候遇见了生财有术,看见了更多的可能性,也提升了很多认知,因而,内在的渴望进一步扩大。


外在的环境,一方面是工作的不确定性,另一方面,是身上责任的加重。


自动20年当程序员以来,身边的朋友一茬接一茬的换,有的突然就被迫失业了,有的就跳槽了,有些朋友,甚至都没来得及告别,就已经后会无期了。


再加上网上的铺天盖地的悲观主义和程序员危机。想开展副业,抵抗未来的不确定的决心越来越强。 目前还没房贷车贷,这里的加重倒不是说现金流上的压力加重


只是觉得,作为一个父亲,应该为孩子去铺一条更好的道路,不希望等到我孩子需要我支持帮助的时候,我却面临中年危机。


同时,我也希望孩子从我这里获得更多的认知和经验,而仅仅只继续专注于程序员的话,这个希望是有点难以实现的。(因为我个人觉得,程序员这行,距离真实的商业事件挺远的)


这几个月的效果


到目前为止,从2023年3月算起,差不多开展5个月了,在金钱上的收获很少,累计也没超过500吧。


先后做过


1、小程序(做了2款小程序,但都是学习阶段的程序,未盈利)


2、小红书无货源店铺(赚了200多吧,其实还是朋友的支持)


3、公众号流量主(赚了没超过50吧)


说下后2个没赚大钱的最大原因吧:我有个很大的毛病,就是爱学习,但不注重学习的结果,在实际执行过程中,碰到点问题就会泄气。


同时,过分在意做事的时间成本,导致执行力不够。(后2个项目,其实只要投入时间认真去做,都不只赚我这点钱。)


不过虽然金钱上的收获不多,在技能、认知和人脉上还是提升了很多


人脉上,认识了好些其他行业的朋友,各行各业的都有。 认知上,知道了要多输出表达、要有流量意识、要懂得链接他人 技能上,也是突破了后端能力,会了一点vue app,小程序搭建能力。


当然,最重要的是,这个过程极大的提高了我对未来的信心


因为我知道,只要认真专注的执行某一个赚钱的领域,我就能一定能赚到一点钱。


不再是之前那种担心如果失业了,就前途一片阴暗的感觉了。


对接下来的思考


接下来的6年,为了发展好第二曲线。我需要做以下的事情:


1、需要克服执行力差、技术傲慢、纸上谈兵等一系列的问题,去扎实的投入实战中。


2、在过程中,尽早找到适合自己的长期事业,并专注的投入(我希望在30岁以前能够找到。)


3、相信积累的力量,不断坚持。


6年以后的我,一定能够发展好自己的第二曲线。


未来6年-爱护自己,照顾家人,帮助朋友


从6年后的视角看,其实最重要的是这三件事,爱护好自己,照顾好家人,帮助好朋友


爱护自己


健康是一切的起点,没有健康的话,其他所有的都是白搭。


现在的身体状况应该是挺糟糕的,肥胖而且不运动,6年后最容易出现的问题,应该就是肥胖带来的问题了。


也因此


1、需要有意识的去控制自己的体重,定期体检,适当运动。


2、平常养好身体,工作上不要太用力,压力不要太大。


照顾家人


6年后,孩子就到了上小学的年纪了。父母也都65左右了,这么看的话,主要是父母的健康问题需要考虑。


也因此


1、已经给父母买了医疗险,但还没给岳父母买,需要2023年落实


2、每年带父母/岳父母 体检。


帮助朋友


志同道合的朋友,于我来说,是不可或缺的,也是能极大的提升幸福感的。


也因此


1、积极拓展志同道合的朋友


2、维护好现有的朋友,真诚利他。


(最近建了个程序员副业群,欢迎私聊加入)


好,接下里回顾下过去的6年


过去6年-转行当程序员


为什么转行


我来自湖南农村,家里挺穷,是那种穷到连上大学学费都要借的那种。


2012-2016年在华科读本科,在校就是天天混日子,大四想考华科电气没考上,毕业时连份工作都没有,于是决定二战考研。考完研后,在湖南省长沙市新东方做了八年的小学奥数老师,保底薪资5k,钱少事多的一份工作。


2017年秋,以笔试和面试都是专业第一的成绩,顺利成为一位硕士。


在2017年开始读硕士时,实验室的师兄就丢给我一本《21天精通Java》,说:“你先学习这个哈,后面做实验会用到”。也因此,开始接触Java。(事实,我到现在都没有精通Java )


2018年,实验室接了头部水电企业的一个项目,需要给他们做一个系统,我就参与进来了,然后,还去这个头部企业公司内部实习了半年。


在那里工作,我看到那些公司的员工有的40 50岁了,每天都是在办公室上来了又走,每天的工作都规律的不行,中午午休2个半小时,下午5点半准时下班。有事没事去打个乒乓球,跑个步什么的。


那时候还年轻啊,也没有足够的经验认知,就觉得,这样安逸的生活,一眼看到头的生活,完全不是我想要的。我还年轻,还有大好年华,我要去闯荡,去见识更多的可能性,去看更多的世界。(事实证明,随便在哪工作,你都可以去看大千事件)


于是,从2018年开始就开始坚定的要转行。


转行成功的因素


现在看,非科班转行成功主要有3个因素:


一是学历给了我很大的加成。我是985本硕,在2020年的就业市场上,还是有很大竞争优势的。


二是实验室恰好有一两个项目和IT搭边。现在好多转行的人,做的项目基本都是往上那种通用的项目,这种项目,要是深耕下去的话,确实也能收获很多。但一般转行的人,但研究的比较浅,也因此,在项目上没有多少竞争优势。


三是我自己也还算刻苦。记得当时,经常一两点在那看《深入理解Java虚拟机》、《Java并发编程》等。花了3个月一页页的看完了《算法.第4版》。甚至还花了2个月恶补了计算机基础。同时,也在CSDN上输出自己的学习记录


最后,也是2020年的顺利的校招毕业,拿到当时挺高年薪的offer,进入了北京某头部地产当Java工程师


这是我当时的面试经历 app.yinxiang.com/fx/fc7e01fa…


过去6年- 跳槽到大厂的经历


想跳槽的原因


2020年7月进入公司,从2021年下半年开始,很明显的感觉整个部门的业务动荡。


再加上身边的人一个个的被裁了,虽然说我是校招+管培生,裁员短期内不会落到我头上,但我知道,这一天迟早会到来。


(后来也表明,22年开始,公司开始裁我们这些校招生了。)


当然,还有另外一个很重要的因素,当初和夫人异地恋,我们相约在深圳见面。


关于我在这家公司的情况,请见这个链接:北京,再见。下一站,深圳


跳槽的过程


我这个人脑子比较笨,技术底子也差。但肯下苦功夫 。


从2022年9月开始,以极客时间为主要学习渠道,开始疯狂的学习。主要学习的就是和八股文相关的课程。(记得那时候,身边的朋友都说,你是真的能学的进去阿,也有好几个朋友,被我卷的也开始看书学习了)。


从2021年12月开始,知道要为2022年的3月的黄金跳槽期做准备了。于是给自己列了个学习计划,并差不多严格执行了。


从21年12月开始,知道要为22年的3月的黄金跳槽期做准备了。于是给自己列了个学习计划,并差不多严格执行了。


与此同时,我发现思维导图很适合做这种八股文的笔记和辅助记忆,于是就在ProcessOn上持续记录学习笔记。(后来还将笔记分享给你100+朋友)


刘卡卡 | ProcessOn


一个人学习的道路总是艰辛的,经常感觉坚持不下去,感觉很孤独,没人交流。幸好在1月进入了知识星球代码随想录,里面都是为了找到好工作而奋斗的人,大家一起交流探讨,互相打卡监督,整个人的学习劲头也开始上来了。


也是在2022年3月底,面了差不多10家公司后,如愿以偿的拿到了现在的深圳大厂的工作。


过去6年- 大厂一年多以来的感想


2022年4月,成功进入大厂 。


前面3-4个月的时候,真的很累,一来是不并不适应大厂的自己干自己活的氛围,二来也是技术上也还待欠缺,三是业务复杂度很高,四是每天要应对Oncall处理。


但干了半年左右后,也就开始适应了。(人果然是一种适应性的动物。)


现在的我,在大厂内,就是当一名勤勤恳恳的螺丝钉,


同时在心态上,也有了很大的转变。


1、接受自己不爱竞争的性格,只要自己心里不卷的话,其他人也就卷不到我。


2、将工作看的很清晰,工作就是为了挣钱,因此,如果工作上有什么不如意的地方,切莫影响到自己的生活,不值当。


当然,工作中也不能躺平,要在日常的工作中去多做积累经验,沉淀知识,保持市场竞争力。


好了,洋洋洒洒写了4000多字了,就先到这吧,希望6年后的我,看到这篇文章的时候,能说一句:


你真的做到了,谢谢你这6年的努力


作者:刘卡卡
来源:juejin.cn/post/7263669060473520186
收起阅读 »

Android协程带你飞越传统异步枷锁

引言 在Android开发中,处理异步任务一直是一项挑战。以往的回调和线程管理方式复杂繁琐,使得代码难以维护和阅读。Jetpack引入的Coroutine(协程)成为了异步编程的新标杆。本文将深入探讨Android Jetpack Coroutine的使用、原...
继续阅读 »

引言


在Android开发中,处理异步任务一直是一项挑战。以往的回调和线程管理方式复杂繁琐,使得代码难以维护和阅读。Jetpack引入的Coroutine(协程)成为了异步编程的新标杆。本文将深入探讨Android Jetpack Coroutine的使用、原理以及高级用法,助您在异步编程的路上游刃有余。


什么是Coroutine?


Coroutine是一种轻量级的并发设计模式,它允许开发者以顺序代码的方式处理异步任务,避免了传统回调和线程管理带来的复杂性。它建立在Kotlin语言的suspend函数上,suspend函数标记的方法能够挂起当前协程的执行,并在异步任务完成后恢复执行。


Coroutine的优势



  • 简洁:通过简洁的代码表达异步逻辑,避免回调地狱。

  • 可读性:顺序的代码结构使得逻辑更加清晰易懂。

  • 卓越的性能:Coroutine能够有效地利用线程,避免过度的线程切换。

  • 取消支持:通过Coroutine的结构,方便地支持任务取消和资源回收。

  • 适用范围广:从简单的后台任务到复杂的并发操作,Coroutine都能应对自如。


Coroutine的原理


挂起与恢复


当遇到挂起函数时,例如delay()或者进行网络请求的suspend函数,协程会将当前状态保存下来,包括局部变量、指令指针等信息,并暂停协程的执行。然后,协程会立即返回给调用者,释放所占用的线程资源。一旦挂起函数的异步操作完成,协程会根据之前保存的状态恢复执行,就好像从挂起的地方继续运行一样,这使得异步编程变得自然、优雅。


线程调度与切换


Coroutine使用调度器(Dispatcher)来管理协程的执行线程。主要的调度器有:



  • Dispatchers.Main:在Android中主线程上执行,用于UI操作。

  • Dispatchers.IO:在IO密集型任务中使用,比如网络请求、文件读写。

  • Dispatchers.Default:在CPU密集型任务中使用,比如复杂的计算。


线程切换通过withContext()函数实现,它智能地在不同的调度器之间切换,避免不必要的线程切换开销,提高性能。


异常处理与取消支持


Coroutine支持异常处理,我们可以在协程内部使用try-catch块来捕获异常,并将异常传播到协程的外部作用域进行处理,这使得我们能够更好地管理和处理异步操作中出现的异常情况。


同时,Coroutine支持任务的取消。当我们不再需要某个协程执行时,可以使用coroutineContext.cancel()或者coroutinecope.cancel()来取消该协程。这样,协程会自动释放资源,避免造成内存泄漏。


基本用法


并发与并行


使用async函数,我们可以实现并发操作,同时执行多个异步任务,并等待它们的结果。而使用launch函数,则可以实现并行操作,多个协程在不同线程上同时执行。


val deferredResult1 = async { performTask1() }
val deferredResult2 = async { performTask2() }

val result1 = deferredResult1.await()
val result2 = deferredResult2.await()

超时与异常处理


通过withTimeout()函数,我们可以设置一个任务的超时时间,当任务执行时间超过指定时间时,会抛出TimeoutCancellationException异常。这使得我们能够灵活地处理超时情况。


try {
withTimeout(5000) {
performLongRunningTask()
}
} catch (e: TimeoutCancellationException) {
// 处理超时情况
}

组合挂起函数


Coroutine提供了一系列的挂起函数,例如delay()withContext()等。我们可以通过asyncawait()函数将这些挂起函数组合在一起,实现复杂的异步操作。


val result1 = async { performTask1() }.await()
val result2 = async { performTask2() }.await()

与jetpack联动


当使用Jetpack组件和Coroutine结合起来时,我们可以在Android应用中更加优雅地处理异步任务。下面通过一个示例演示如何在ViewModel中使用Jetpack组件和Coroutine来处理异步数据加载:


创建一个ViewModel类,例如MyViewModel.kt,并在其中使用Coroutine来加载数据:


import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import kotlinx.coroutine.Dispatchers

class MyViewModel : ViewModel() {

fun loadData() = liveData(Dispatchers.IO) {
emit(Resource.Loading) // 发送加载中状态

try {
// 模拟耗时操作
val data = fetchDataFromRemote()
emit(Resource.Success(data)) // 发送加载成功状态
} catch (e: Exception) {
emit(Resource.Error(e.message)) // 发送加载失败状态
}
}

// 假设这是一个网络请求的方法
private suspend fun fetchDataFromRemote(): String {
// 模拟耗时操作
delay(2000)
return "Data from remote"
}
}

创建一个Resource类用于封装数据状态:


sealed class Resource<out T> {
object Loading : Resource<Nothing>()
data class Success<T>(val data: T) : Resource()
data class Error(val message: String?) : Resource<Nothing>()
}

在Activity或Fragment中使用ViewModel,并观察数据变化:


class MyActivity : AppCompatActivity() {

private val viewModel: MyViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)

viewModel.loadData().observe(this) { resource ->
when (resource) {
is Resource.Loading -> {
// 显示加载中UI
}
is Resource.Success -> {
// 显示加载成功UI,并使用resource.data来更新UI
val data = resource.data
}
is Resource.Error -> {
// 显示加载失败UI,并使用resource.message显示错误信息
val errorMessage = resource.message
}
}
}
}
}

在以上示例中,ViewModel中的loadData()方法使用Coroutine的liveData构建器来执行异步任务。我们通过emit()函数发送不同的数据状态,Activity(或Fragment)通过观察LiveData来处理不同的状态,并相应地更新UI。


结论


Android Jetpack Coroutine是异步编程的高级艺术。通过深入理解Coroutine的原理和高级用法,我们可以写出更加优雅、高效的异步代码。掌握Coroutine的挂起与恢复、线程切换、异常处理和取消支持,使得我们能够更好地处理异步操作,为用户带来更出色的应用体验。

作者:午后一小憩
来源:juejin.cn/post/7264399534474297403

收起阅读 »

项目使用redis做缓存,除了击穿,穿透,雪崩,我们还要考虑哪些!!!

大家好,我是小趴菜,相信大家在项目中都是用过redis,比如用来做一个分布式缓存来提高程序的性能 当使用到了redis来做缓存,那么我们就必须要考虑几个问题,除了缓存击穿,缓存穿透,缓存雪崩,那么我们还需要考虑哪些问题呢? 高并发写 对于高并发情况下,比如直播...
继续阅读 »

大家好,我是小趴菜,相信大家在项目中都是用过redis,比如用来做一个分布式缓存来提高程序的性能


当使用到了redis来做缓存,那么我们就必须要考虑几个问题,除了缓存击穿,缓存穿透,缓存雪崩,那么我们还需要考虑哪些问题呢?


高并发写


对于高并发情况下,比如直播下单,直播下单跟秒杀不一样,秒杀是有限定的库存,但是直播下单是可以一直下的,而且是下单越多越好的。比如说我们的库存有10万个,如果这个商品特别火,那么可能一瞬间流量就全都打过来了。虽然我们的库存是提前放到redis中,并不会去访问MySql,那么这时候所有的请求都会打到redis中。


image.png


表面看起来确实没问题,但是你有没有想过,即使你做了集群,但是访问的还是只有一个key,那么最终还是会落到同一台redis服务器上。这时候key所在的那台redid就会承载所有的请求,而集群其它机器根本就不会访问到,这时候你确定你的redis能扛住吗???如果这时候读的请求很多,你觉得你的redis能扛住吗?


所以对于这种情况我们可以采用数据分片的解决方案,比如你有10万个库存,那么这时候可以搞10台redis服务器,每台redis服务器上放1万个库存,这时候我们可以通过用户的ID进行取模,然后将用户流量分摊到10台redis服务器上


image.png


所以对于热点数据来说,我们要做的就是将流量进行分摊,让多台redis分摊承载一部分流量,尤其是对于这种高并发写来讲


高并发读


使用redis做缓存可以说是我们项目中使用到的最多的了,可能由于平时访问量不高,所以我们的redis服务完全可以承载这么多用户的请求


但是我们可以想一下,一次reids的读请求就是一次的网络IO,如果是1万次,10万次呢?那就是10万次的网络IO,这个问题我们在工作中是不得不考虑的。因为这个开销其实是很大的,如果访问量太大,redis很有可能就会出现一些问题


image.png


我们可以使用本地缓存+redis分布式缓存来解决这个问题,对于一些热点读数据,更新不大的数据,我们可以将数据保存在本地缓存中,比如Guava等工具类,当然本地缓存的过期时间要设置的短一点,比如5秒左右,这时候可以让大部分的请求都落在本地缓存,不用去访问redis


如果这时候本地缓存没有,那么再去访问redis,然后将redis中的数据再放入本地缓存中即可


加入了多级缓存,那么就会有相应的问题,比如多级缓存如何保证数据一致性


总结


没有完美的方案,只有最适合自己的方案,当你决定采用了某种技术方案的时候,那么势必会带来一些其它你需要考虑的问题,redis也一样,虽然我们使用它来做缓存可以提高我们程序的性能,但是在使用redis做缓存的时候,有些情况我们也是需要考虑到的,对于用户访问量不高来说,我们直接使用redis完全是够用的,但是我们可以假设一下,如果在高并发场景下,我们的方案是否能够支持我们的业务


作者:我是小趴菜
来源:juejin.cn/post/7264475859659079736
收起阅读 »

基于css3写出的流水加载效果

web
准备部分 这里写入基本的html样式,这里还设置了水球的css样式,用于css样式中的计算--i:1是一种自定义的CSS变量,可能用于控制样式中的计数 <body> <div class="box"> <d...
继续阅读 »

准备部分



这里写入基本的html样式,这里还设置了水球的css样式,用于css样式中的计算--i:1是一种自定义的CSS变量,可能用于控制样式中的计数



<body>
<div class="box">
<div class="loader">
<!-- 这里设置了水球的css样式变量,用于css样式中的计算 -->
<span style="--i:1"></span>
<span style="--i:2"></span>
<span style="--i:3"></span>
<span style="--i:4"></span>
<span style="--i:5"></span>
<span style="--i:6"></span>
<span style="--i:7"></span>
<span style="--i:8"></span>
</div>
</div>
</body>

设置基本的css背景及其样式


image.png


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

.box {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #0c1b21;
}
/* 设置流水区域 */
.loader {
position: relative;
width: 250px;
height: 250px;
background: #666;
animation: animate 12s linear infinite;
}

下面进行详细的css设计



这里通过伪元素设计了第一个小球的效果,通过定位定位到左上角,,设置了大小为40,并且设置了颜色和圆角色设置,同时添加了阴影效果,形成了如下的圆球水滴效果,通过渐变函数linear-gradient是一个渐变函数,用于创建线性渐变背景,设置了颜色和倾斜的角度



image.png


    .loader span {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
/* 定位 伪元素,设置流水的珠子 */
.loader span::before {
content: '';
position: absolute;
top: 0;
width: 40px;
height: 40px;
/* 珠子颜色设置 */
background: linear-gradient(45deg, #c7eeff, #03a9f4);
border-radius: 50%;
/* 设置阴影 */
box-shadow: 0 0 30px #00bcd4;
}

接下来根据html中定义的css变量,来设置不同方向的数据


image.png


.loader span{
/* 设置不同方向的小球 */
transform: rotate(calc(45deg*var(--i)));
}

将小球向内收缩一部分


image.png


.loader span::before {
left: calc(50% - 20px);
}

动画设置


在这里设置了旋转动画,并且在整个区域添加入了动画


image.png


/* 这里设置了旋转动画 */
@keyframes animate {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}


接下来补一下html


这里设置了4个span元素,用来形成四个小球,在容器内沿着小球的方向进行转动,形成一个如下的的效果,并且通过在html中给span元素加入css变量来控制每个小球的延迟效果


ezgif.com-video-to-gif.gif


去掉背景颜色后


ezgif.com-video-to-gif.gif


<body>
<div class="box">
<div class="loader">
<!-- 这里设置了水球的css样式变量,用于css样式中的计算 --i:1是一种自定义的CSS变量,可能用于控制样式中的计数 -->
<span style="--i:1"></span>
<span style="--i:2"></span>
<span style="--i:3"></span>
<span style="--i:4"></span>
<span style="--i:5"></span>
<span style="--i:6"></span>
<span style="--i:7"></span>
<span style="--i:8"></span>
<!-- 这里设置了光晕效果,使得出现5个转动的小球 -->
<span class="rotate" style="--j:0"></span>
<span class="rotate" style="--j:1"></span>
<span class="rotate" style="--j:2"></span>
<span class="rotate" style="--j:3"></span>
<span class="rotate" style="--j:4"></span>
</div>
<div class="word">加载中</div>
</div>

</body>


.rotate {
animation: animate 4s ease-in-out infinite;
/* 设置延迟 */
animation-delay: calc(-0.2s*var(--j));
}
.loader {
filter: url(#fluid);
/* 去掉临时背景颜色为透明 */
background: transparent;
}

接下来补全所需要的html



这里不全了所需要的html,加入了svg部分,使用过svg,对图形进行高斯模糊处理,然后对图形的颜色进行变化,通过颜色矩阵来实现颜色变化,这里使用的颜色矩阵将每个像素的红、绿、蓝三个通道的颜色值分别乘以1,不变化;将透明度乘以20,增加透明度;最后将透明度减去10,进一步增加透明度。这段代码可能被使用在创建视觉效果中,比如给图像添加模糊效果并调整其透明度,从而实现一种"流体"或"柔和"的视觉效果
并且定义了一个filter的滤镜效果
“in"属性的值为"SourceGraphic”,表示将滤镜应用在源图形上,“stdDeviation"属性的值为"10”,表示高斯模糊的参数,即模糊程度



<body>
<div class="box">
<!-- 这里使用了svg,svg是可缩放矢量图形的标签,通过创建和操作svg,使得图形通过缩放而不失去真的在各种尺寸和分辨率下呈现 -->
<!-- 这段代码的作用是先对图形进行高斯模糊处理,然后对图形的颜色进行变换。具体的颜色变换可以通过颜色矩阵来实现,
这里使用的颜色矩阵将每个像素的红、绿、蓝三个通道的颜色值分别乘以1,不变化;将透明度乘以20,增加透明度;
最后将透明度减去10,进一步增加透明度。这段代码可能被使用在创建视觉效果中,比如给图像添加模糊效果并调整其透明度,
从而实现一种"流体"或"柔和"的视觉效果 -->

<svg>
<!-- 这是定义一个滤镜效果的元素,其中"id"属性的值为"fluid",用于给滤镜效果命名。 -->
<filter id="fluid">
<!-- 这是一个高斯模糊滤镜效果,用于对图形进行模糊处理。“in"属性的值为"SourceGraphic”,表示将滤镜应用在源图形上,
“stdDeviation"属性的值为"10”,表示高斯模糊的参数,即模糊程度。 -->

<feGaussianBlur in="SourceGraphic" stdDeviation="10"></feGaussianBlur>
<!-- 这是一个颜色矩阵滤镜效果,用于对图形的颜色进行变换。
"values"属性的值为一个包含20个数字的字符串,表示颜色矩阵的变换矩阵。 -->

<feColorMatrix values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 20 -10 "
>

</feColorMatrix>
</filter>
</svg>
<div class="loader">
<!-- 这里设置了水球的css样式变量,用于css样式中的计算 --i:1是一种自定义的CSS变量,可能用于控制样式中的计数 -->
<span style="--i:1"></span>
<span style="--i:2"></span>
<span style="--i:3"></span>
<span style="--i:4"></span>
<span style="--i:5"></span>
<span style="--i:6"></span>
<span style="--i:7"></span>
<span style="--i:8"></span>
<!-- 这里设置了光晕效果,使得出现5个转动的小球 -->
<span class="rotate" style="--j:0"></span>
<span class="rotate" style="--j:1"></span>
<span class="rotate" style="--j:2"></span>
<span class="rotate" style="--j:3"></span>
<span class="rotate" style="--j:4"></span>
</div>
<div class="word">加载中</div>
</div>
</body>

再继续设置svg的样式,这里将被svg偏移的容器位置归于中心位置,并且设置了文字效果及其设置了文字的缩放动画


    svg {
width: 0;
height: 0;
}

.word {
position: absolute;
color: #fff;
font-size: 1.2em;
animation: words 3s linear infinite;
}
/* 这里设置了文字的缩放动画 */
@keyframes words {
0% {
transform: scale(1.2);
}

25% {
transform: scale(1);
}

50% {
transform: scale(0.8);
}

75% {
transform: scale(1);
}

100% {
transform: scale(1.2);
}
}

效果展示


ezgif.com-video-to-gif (1).gif


总结



实现这个效果关键在最后的那个svg的使用,通过添加svg给了一个高斯模糊的效果,从而才会使得明显的小球变成了这种连在一起的流动液体样式的小球,要学习一下svg效果,在他也是属于html5里面的一部分内容,仅供自己学习记录和新的展示



作者:如意呀
来源:juejin.cn/post/7263064267560976442
收起阅读 »

如何自动打开你的 App?

相信大家在刷 某博 / 某书 / 某音 的时候,最能体会什么叫做 条条大路通 tao bao。经常是你打开一个 App,不小心点了下屏幕,就又打开了另一个 App 了。 那么这种自动打开一个 App 到底是怎么实现的呢? URL Scheme 首先是最原始的方...
继续阅读 »

相信大家在刷 某博 / 某书 / 某音 的时候,最能体会什么叫做 条条大路通 tao bao。经常是你打开一个 App,不小心点了下屏幕,就又打开了另一个 App 了。


那么这种自动打开一个 App 到底是怎么实现的呢?


URL Scheme


首先是最原始的方式 URL Scheme。



URL Scheme 是一种特殊的 URL,用于定位到某个应用以及应用的某个功能。



它的格式一般是: [scheme:][//authority][path][?query]


scheme 代表要打开的应用,每个上架应用商店的 App 所注册的 scheme 都是唯一的;后面的参数代表应用下的某个功能及其参数。


在 IOS 上配置 URL Scheme


在 XCode 里可以轻松配置


image.png


在 Android 上配置 URL Scheme


Android 的配置也很简单,在 AndroidManifest.xml 文件下添加以下配置即可


image.png


通过访问链接自动打开 App


配置完成后,只要访问 URL Scheme 链接,系统便会自动打开对应 scheme 的 App。


因此,我们可以实现一个简单的 H5 页面来承载这个跳转逻辑,然后在页面中通过调用 location.href=schemeUrl 或者 <a href='schemeUrl' /> 等方式来触发访问链接,从而自动打开 App


优缺点分析


优点: 这个是最原始的方案,因此最大的优点就是兼容性好


缺点:



  1. 通过 scheme url 这种方式唤起 App,对于 H5 中间页面是无法感知的,并不知道是否已经成功打开 App

  2. 部分浏览器有安全限制,自动跳转会被拦截,必须用户手动触发跳转(即 location.href 行不通,必须 a 标签)

  3. 一些 App 会限制可访问的 scheme,你必须要在白名单内,否则也会被拦截跳转

  4. 通过 scheme url 唤起 App 时,浏览器会提示你是否确定要打开该 App,会影响用户体验


DeepLink


通过上述缺点我们可以看出,传统的 URL Scheme 在用户体验上是存在一定缺陷的。


因此,DeepLink 诞生了。


DeepLink 的宗旨就是通过传统的 HTT P链接就可以唤醒app,而如果用户没有安装APP,则会跳转到该链接对应的页面。


IOS Universal Link


在 IOS 上一般称之为 Universal Link。


【配置你的 Universal Link 域名】


首先要去 Apple 的开发者平台上配置你的 domains,假设是: mysite.com


image.png


【配置 apple-app-site-association 文件】


在该域名根目录下创建一个 .well-known 路径,并在该路径下放置 apple-app-site-association 文件。


文件内容包含 appID 以及 path,path如果配置 /app 则表示访问该域名下的 /app 路径均能唤起App


该文件内容大致如下:


{
"applinks": {
"apps": [],
"details": [
{
"appID": "xxx", // 你的应用的 appID
"paths": [ "/app/*"]
}
]
}
}

【系统获取配置文件】


上面两步配置成功后,当用户 首次安装App 或者后续每次 覆盖安装App 时,系统都会主动去拉取域名下的配置文件。



即系统会主动去拉取 https://mysite.com/.well-known/apple-app-site-association 这个文件



然后根据返回的 appID 以及 path 判断访问哪些路径是需要唤起哪个App


【自动唤起 App】


当系统成功获取配置文件后,只要用户访问 mysite.com/app/xxx 链接,系统便会自动唤起你的 App。


同时,客户端还可以进行一些自定义逻辑处理:


客户端会接收到 NSUserActivity 对象,其 actionType 为 NSUserActivityTypeBrowsingWeb,因此客户端可以在接收到该对象后做一些跳转逻辑处理。


image.png


Android DeepLink


与 IOS Universal Link 原理相似,Android系统也能够直接通过网站地址打开应用程序对应的内容页面,而不需要用户选择使用哪个应用来处理网站地址


【配置 AndroidManifest.xml】
在 AndroidManifest 配置文件中添加对应域名的 intent-filter:


scheme 为 https / http;


host 则是你的域名,假设是: mysite.com


image.png


【生成 assetlinks.json 文件】


首先要去 Google developers.google.com/digital-ass… 生成你的 assetlinks json 文件。


image.png


【配置 assetlinks.json 文件】


生成文件后,同样的需要在该域名根目录下创建一个 .well-known 路径,并在该路径下放置 assetlinks.json 配置文件,文件内容包含应用的package name 和对应签名的sha哈希


【系统获取配置文件】


配置成功后,当用户 首次安装App 或者后续每次 覆盖安装App 时,系统会进行以下校验:



  1. 如果 intent-filter 的 autoVerify 设置为 true,那么系统会验证其



  • Action 是否为 android.intent.action.VIEW

  • Category 是否为android.intent.category.BROWSABLE 和 android.intent.category.DEFAULT

  • Data scheme 是否为 http 或 https



  1. 如果上述条件都满足,那么系统将会拉取该域名下的 json 配置文件,同时将 App 设置为该域名链接的默认处理App


【自动唤起 App】


当系统成功获取配置文件后,只要用户访问 mysite.com/app/xxx 链接,系统便会自动唤起你的 App。


优缺点分析


【优点】



  1. 用户体验好:可以直接打开 App,没有弹窗提示

  2. 唤起App失败则会跳转链接对应的页面


【缺点】



  1. iOS 9 以后才支持 Universal Link,

  2. Android 6.0 以后才支持 DeepLink

  3. DeepLink 需要依赖远程配置文件,无法保证每次都能成功拉取到配置文件


推荐方案: DeepLink + H5 兜底


基于前面两种方案的优缺点,我推荐的解决方案是配置 DeepLink,同时再加上一个 H5 页面作为兜底。


首先按照前面 DeepLink 的教程先配置好 DeepLink,其中访问路径配置为 https://mysite.com/app


接着,我们就可以在 https://mysite.com/app 路径下做文章了。在该路径下放置一个 H5 页面,内容可以是引导用户打开你的 App。


当用户访问 DeepLink 没有自动打开你的 App 时,此时用户会进入浏览器,并访问 https://mysite.com/app 这个 H5 页面。


在 H5 页面中,你可以通过浏览器 ua 获取当前的系统以及版本:



  1. 如果是 Android 6.0 以下,那么可以尝试用 URL Scheme 去唤起 App

  2. 如果是 IOS / Android 6.0 及以上,那么此时可以判断用户未安装 App。这种情况下可以做些额外的逻辑,比如重定向到应用商店引导用户去下载之类的


作者:龙飞_longfe
来源:juejin.cn/post/7201521440612974649
收起阅读 »

iOS组件化初探

安装本地库,cd到Example文件下,进行pod install:具体执行如下图:打开Example文件夹中的工程:此时可以看到导入本地库成功:导入头文件,此时就可以愉快的,使用了三、制作多个本地库四、添加资源文件之后cd到Example文件夹中,打开工程,...
继续阅读 »

一、创建本地化组件化

首先创建一个存储组件化的文件夹:例如

组件化文件夹

cd到这个文件夹中,使用下边命令创建本地组件库
(注:我在创建的过程中,使用WiFi一直创建失败,后来连自己热点才能创建成功,可能跟我的网络有关系,这里加个提醒)

pod lib create UIViewcontroller_category_Module

之后会出出现创建组件的选项,如下图:


组件化创建选项
① 组件化适用的平台
② 组件化使用的语言
③ 组件化是否包含一个application
④ 组件化目前还不清楚是啥,直接选none即可
⑤ 组件化是否包含Test
⑥ 组件化文件的前缀


至此组件创建完成,此时会自动打开你创建的工程

二、 创建组件化功能

关闭当前工程,打开你创建的工程文件夹,在classes文件中,放入你的组件化代码,文件夹具体路径如下:


安装本地库,cd到Example文件下,进行pod install:具体执行如下图:


打开Example文件夹中的工程:


此时可以看到导入本地库成功:


导入头文件,此时就可以愉快的,使用了


三、制作多个本地库

关闭工程,重新cd到最外层文件夹


使用:

pod lib create Load_pic_Module

后续创建步骤,选项参照一

四、添加资源文件


之后cd到Example文件夹中,打开工程,在Load_pic_Module.podspec,添加图片资源的搜索路径,具体如下图所示:

# 加载图片资源文件
s.resource_bundles = {
'Load_pic_Module' => ['Load_pic_Module/Assets/*']
}


之后在命令行中,执行pod install指令,效果如下图所示:


(注:每次对组件进行修改时,每次都需要进行一次pod install,这个很重要,切记)

五、添加本地其他依赖库

还是在Load_pic_Module工程中进行引入,在Podfile中进行本地库引入

# 添加本地其他依赖库
pod 'UIViewcontroller_category_Module', :path => '../../UIViewcontroller_category_Module'


执行pod install

六、添加外部引用库

有时候,也需要一些从网上下载的三方库,例如afn,masonry等

# 添加额外依赖库
s.dependency 'AFNetworking'
s.dependency 'Masonry'

添加位置如下


添加效果图


七、全局通用引入

作用:类似prefix header

#  s.prefix_header_contents = '#import "LGMacros.h"','#import "Masonry.h"','#import "AFNetworking.h"','#import "UIKit+AFNetworking.h"','#import "CTMediator+LGPlayerModuleAction.h"'
s.prefix_header_contents = '#import "Masonry.h"'

多个引入看第一条,单个引入是第二条
注:改完记得pod install

收起阅读 »

聊聊分片技术

今天来聊一聊开发中一个比较常见的概念“分片”技术。这个概念听起来好像是在讲切西瓜,但其实不是!它是指将大型数据或者任务分成小块处理的技术。 就像吃面条一样,太长了不好吃,我们要把它们分成小段,才能更好地享受美味。所以,如果你想让你的程序更加高效,不妨考虑一下...
继续阅读 »

今天来聊一聊开发中一个比较常见的概念“分片”技术。这个概念听起来好像是在讲切西瓜,但其实不是!它是指将大型数据或者任务分成小块处理的技术。


就像吃面条一样,太长了不好吃,我们要把它们分成小段,才能更好地享受美味。所以,如果你想让你的程序更加高效,不妨考虑一下“分片”技术!


1. “分片”技术定义


在计算机领域中,“分片”(sharding)是一种 把大型数据集分割成更小的、更容易管理的数据块的技术


一个经典的例子是数据库分片。


想象一家巨大的电商公司,拥有数百万甚至数十亿的用户,每天进行大量的交易和数据处理。这些数据包括用户信息、订单记录、支付信息等。传统的数据库系统可能无法应对如此巨大的数据量和高并发请求。


在这种情况下,公司可以采用数据库分片技术来解决问题。数据库分片是将一个庞大的数据库拆分成更小的、独立的片(shard)。


每个片都包含数据库的一部分数据,类似于一个小型的数据库。每个片都可以在不同的服务器上独立运行,这样就可以将数据负载分散到多个服务器上,提高了整个系统的性能和可伸缩性。


所以,分片技术提高了数据库的扩展性和吞吐量。


2. 分片技术应用:日志分片


好了,我们已经了解了分片技术的概念和它能够解决的问题。但是,你知道吗?分片技术还有一个非常有趣的应用场景——日志分片。


一个更加具体的应用场景是,手机端日志的记录、存储和上传


在日志分片中,原始的日志文件被分成多个较小的片段,每个片段包含一定数量的日志条目。这样做的好处是可以提高日志的读写效率和处理速度。当我们需要查找特定时间段的日志或者进行日志分析时,只需要处理相应的日志分片,而不需要处理整个大型日志文件。


日志分片还可以帮助我们更好地管理日志文件的存储空间。由于日志文件通常会不断增长,如果不进行分片,日志文件的大小会越来越大,占用大量的存储空间。而通过将日志文件分片存储,可以将存储空间的使用分散到多个较小的文件中,更加灵活地管理和控制存储空间的使用。


所以,分片技术不仅可以让你的日志更高效,还可以让你的存储更优雅哦!


总结一下,在手机端对日志进行分片可以带来如下的好处:





  • 减少数据传输量: 手机端往往有限的网络带宽和数据流量。通过将日志分片,只需要发送关键信息或重要的日志片段,而不是整个日志文件,从而减少了数据传输量,降低了网络负载。





  • 节省存储空间: 手机设备通常有有限的存储空间。通过分片日志,可以只保留最重要的日志片段,避免将大量无用的日志信息保存在设备上,节省存储空间。





  • 提高性能: 小型移动设备的计算能力有限,处理大量的日志数据可能会导致应用程序性能下降。日志分片可以减轻应用程序对处理和存储日志的负担,从而提高应用程序的性能和响应速度。





  • 快速故障排查: 在开发和调试阶段,日志是重要的调试工具。通过分片日志,可以快速获取关键信息,帮助开发者定位和解决问题,而不需要浏览整个日志文件。





  • 节省电池寿命: 日志记录可能涉及磁盘或网络活动,这些活动对手机的电池寿命有一定影响。分片日志可以减少不必要的磁盘写入和网络通信,有助于节省电池能量。





  • 安全性和隐私保护: 对于敏感数据或用户隐私相关的日志,分片可以帮助隔离和保护这些数据,确保只有授权的人员可以访问敏感信息。





  • 容错和稳定性: 如果手机应用程序崩溃或出现问题,分片日志可以确保已经记录的日志信息不会因为应用程序的异常终止而丢失,有助于在重启后快速恢复。




3.日志分片常见的实现方式


常见的日志分片实现方式有 3 种,一种是基于时间的分片,一种是基于大小的分片,还有一种是基于关键事件的分片


3.1 按时间分片


将日志按照时间周期进行分片,例如每天、每小时或每分钟生成一个新的日志文件。伪代码如下:


import logging
from datetime import datetime

# 配置日志记录
logging.basicConfig(filename=f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}.log",
                    level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# 记录日志
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")

3.2 按文件大小分片


将日志按照文件大小进行分片,当达到预设的大小后,生成一个新的日志文件。伪代码如下:


import logging
import os

# 设置日志文件的最大大小为5MB
max_log_size = 5 * 1024 * 1024

# 配置日志记录
log_file = "log.log"
logging.basicConfig(filename=log_file,
                    level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# 获取当前日志文件大小
def get_log_file_size(file_path):
    return os.path.getsize(file_path)

# 检查日志文件大小,超过最大大小则创建新的日志文件
def check_log_file_size():
    if get_log_file_size(log_file) > max_log_size:
        logging.shutdown()
        os.rename(log_file, f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}.log")
        logging.basicConfig(filename=log_file,
                            level=logging.DEBUG,
                            format='%(asctime)s - %(levelname)s - %(message)s')

# 记录日志
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")

# 检查并切割日志文件
check_log_file_size()

3.3 按关键事件分片


将日志按照特定的关键事件进行分片,例如每次启动应用程序或者每次用户登录都生成一个新的日志文件。伪代码如下:


import logging

# 配置日志记录
logging.basicConfig(filename="log.log",
                    level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# 记录日志
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")

# 在关键事件处切割日志
def split_log_on_critical_event():
    logging.shutdown()
    new_log_file = f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}.log"
    os.rename("log.log", new_log_file)
    logging.basicConfig(filename="log.log",
                        level=logging.DEBUG,
                        format='%(asctime)s - %(levelname)s - %(message)s')

# 在关键事件处调用切割函数
split_log_on_critical_event()

这些只是日志分片的简单示例,实际应用中,可能还需要考虑并发写入的处理。不同的应用场景和需求可能会有不同的实现方式,但上述示例可以作为日志分片的入门参考。


4.不同日志分片方式的优缺点


每种日志分片方式都有其优点和缺点,实际工作中选择哪种方式取决于项目需求、系统规模和性能要求。下面是它们各自的优缺点和选择建议:


4.1 按时间分片


优点:





  • 日志文件按时间周期自动切割,管理简单,易于维护和查找。



  • 可以按照日期或时间段快速定位特定时间范围的日志,方便问题排查和分析。


缺点:





  • 如果日志记录非常频繁,生成的日志文件可能会较多,占用较多的磁盘空间。


选择建议:





  • 适用于需要按照时间段来管理和查找日志的场景,如每天生成一个新的日志文件,适合于长期存档和快速回溯日志的需求。


4.2 按文件大小分片


优点:





  • 可以控制单个日志文件的大小,避免单个日志文件过大,减少磁盘空间占用。



  • 可以根据日志记录频率和系统负载自动调整滚动策略,灵活性较高。


缺点:





  • 按文件大小滚动的切割可能不是按照时间周期进行,导致在某个时间段内的日志记录可能分布在多个文件中,查找时稍显不便。


选择建议:





  • 适用于需要控制单个日志文件大小、灵活滚动日志的场景,可以根据日志记录量进行动态调整滚动策略。


4.3 按关键事件分片


优点:





  • 可以根据特定的关键事件或条件生成新的日志文件,使得日志按照业务操作或系统事件进行切割,更符合实际需求。


缺点:





  • 需要在代码中显式触发滚动操作,可能会增加一定的复杂性和代码维护成本。


选择建议:





  • 适用于需要根据特定事件进行日志切割的场景,如应用程序重启、用户登录等。


在实际工作中,通常需要综合考虑项目的实际情况来选择合适的日志分片方式。可以考虑以下因素:





  • 日志记录频率和数据量: 如果日志记录频率很高且数据量大,可能需要按文件大小分片来避免单个日志文件过大。





  • 日志存储要求: 如果需要长期存档日志并快速查找特定时间范围的日志,按时间分片可能更适合。





  • 日志文件管理: 如果希望日志文件按照特定的事件或条件进行切割,按关键事件分片可能更合适。





  • 磁盘空间和性能: 考虑日志文件大小对磁盘空间的占用和日志滚动对系统性能的影响。




所以,实际开发中通常需要根据项目的具体需求和系统规模,选择合适的日志分片方式。


在日志框架中通常可以通过配置来选择适合的滚动策略,也可以根据实际需求自定义一种滚动策略。


作者:有余同学
来源:mdnice.com/writing/e97842c4d3734ad8b8a1dd587342d985
收起阅读 »

量子力学与哲学的交叉:现实性,自由意志和意识

亲爱的读者, 欢迎回到我们的量子力学系列文章。在前面的几篇文章中,我们已经深入探讨了量子力学的起源、基本概念、实验验证以及应用领域。今天,我们将探讨量子力学与哲学之间的交叉点,涉及现实性、自由意志和意识等哲学问题,并探讨它们与量子力学的关系。 1. 现实性...
继续阅读 »


亲爱的读者,


欢迎回到我们的量子力学系列文章。在前面的几篇文章中,我们已经深入探讨了量子力学的起源、基本概念、实验验证以及应用领域。今天,我们将探讨量子力学与哲学之间的交叉点,涉及现实性、自由意志和意识等哲学问题,并探讨它们与量子力学的关系。


1. 现实性与测量问题


量子力学中的现实性问题是哲学上的一个重要问题。它与量子测量问题有密切关系。在经典物理学中,我们通常认为物体的性质是独立于我们的观测的,即物体具有客观的现实性。然而,在量子力学中,物体的性质通常被描述为概率性的叠加态,直到被观测或测量后才坍缩为确定的态。这种性质被称为“波函数坍缩”。


波函数坍缩: 当一个量子系统进行测量时,其波函数将坍缩为一个确定的态。例如,当我们测量一个电子的自旋时,它可能处于自旋向上或向下的态,但在测量前,它处于自旋向上和向下的叠加态。


这引发了现实性问题:在量子力学中,物体在测量前似乎没有确定的现实性,而只有在测量时才坍缩为确定的态。这种现象在双缝实验等实验中得到了证实,挑战了我们对现实性的直觉理解。


2. 自由意志与量子不确定性


量子力学中的不确定性原理是另一个与哲学有关的问题。不确定性原理指出,在同一时间,我们无法准确地同时测量一个粒子的位置和动量。这种不确定性表现为量子粒子的波粒二象性。


波粒二象性: 在量子力学中,粒子既可以像波一样传播,又可以像粒子一样具有确定的位置和动量。这使得我们在同一时间无法同时准确测量它的位置和动量。


这种不确定性被一些学者用来探讨自由意志的问题。自由意志是指人类是否有能力自主做出决策和行动。量子不确定性被认为可能为人类的自由意志提供了一种解释。根据这个观点,由于量子粒子的行为是不确定的,人类的决策和行动也可能受到量子不确定性的影响,从而具有一定的自由意志。


3. 意识与观测问题


在量子力学中,观测和测量对量子系统的状态产生重要影响。然而,关于观测的本质和意识的作用在哲学上引发了一些争议。


观测问题: 在量子力学中,当我们观测一个量子系统时,其波函数将坍缩为一个确定的态。这意味着观测的过程似乎在量子系统的行为中起到了特殊的作用。


意识的作用: 一些学者提出,意识可能在观测过程中起到了重要的作用。他们认为,观测的过程需要有意识的观察者,意识的存在才能导致波函数坍缩。然而,这个观点在学术界也引发了一些争议,因为它涉及到科学与哲学的交叉领域。


4. 实验与思想实验


为了探讨量子力学与哲学之间的关系,许多实验和思想实验被提出。其中最著名的实验之一是贝尔不等式实验,它用于检验量子力学中的非局部性和隐变量理论。贝尔不等式实验结果支持了量子力学的非局部性,即两个相互作用的粒子在某种程度上似乎可以瞬时相互影响,而不受它们之间的距离限制。


贝尔不等式实验: 贝尔不等式是由约翰·贝尔于1964年提出的,用于检验量子力学中的非局部性和隐变量理论。实验中,两个相互作用的粒子在不同的测量方向上进行测量,然后比较实验结果与贝尔不等式的预测。


薛定谔的猫思想实验: 薛定谔的猫是由著名物理学家埃尔温·薛定谔提出的一个思想实验。在这个实验中,一只猫被放置在一个密封的箱子里,箱子里还有一瓶氰化物。根据量子力学的叠加原理,当箱子被密封后,猫既处于生存叠加态又处于死亡叠加态,直到箱子被打开并进行观测时,猫的状态才会坍缩为生或死。





5. 结论


量子力学与哲学的交叉点是一个复杂而深刻的领域。许多哲学问题在量子力学的探索中得到了新的视角和解释。现实性问题挑战着我们对物体性质的理解,自由意志问题引发了我们对决策和行动的思考,而意识问题则涉及我们对观测和存在的认识。


尽管目前还有许多未解决的问题,量子力学与哲学的交叉研究仍在持续发展中。未来的研究可能会为我们带来更深入的理解和新的洞察力,从而推动我们对宇宙和人类本质的认知。


参考文献:


Bohr, N. (1935). "Can Quantum-Mechanical Description of Physical Reality Be Considered Complete?". Physical Review, 48(8), 696-702.


Einstein, A., Podolsky, B., & Rosen, N. (1935). "Can Quantum-Mechanical Description of Physical Reality Be Considered Complete?". Physical Review, 47(10), 777-780.


Bell, J. S. (1964). "On the Einstein Podolsky Rosen Paradox". Physics Physique Физика, 1(3), 195-200.


Schrödinger, E. (1935). "Die gegenwärtige Situation in der Quantenmechanik". Naturwissenschaften, 23(49), 807-812.


Wigner, E. P. (1961). "Remarks on the mind-body question". In The Scientist Speculates, 284-302. London: Heinemann.


希望这篇文章满足了您对量子力学与哲学交叉的了解需求。如果您还有其他问题或需求,请随时告诉我。谢谢!


作者:depeng
来源:mdnice.com/writing/9c9c807a9cfa46cda6c15d7315d342c0
收起阅读 »

记录实现音频可视化

web
实现音频可视化 这里主要使用了web audio api和canvas来实现的(当然也可以使用svg,因为canvas会有失帧的情况,svg的写法如果有大佬会的话,可以滴滴我一下) 背景 最近听音乐的时候,看到各种动效,突然好奇这些音频数据是如何获取并展示出来...
继续阅读 »

实现音频可视化


这里主要使用了web audio api和canvas来实现的(当然也可以使用svg,因为canvas会有失帧的情况,svg的写法如果有大佬会的话,可以滴滴我一下)


背景


最近听音乐的时候,看到各种动效,突然好奇这些音频数据是如何获取并展示出来的,于是花了几天功夫去研究相关的内容,这里只是给大家一些代码实例,具体要看懂、看明白,还是建议大家大家结合相关api来阅读这篇文章。

参考资料地址:Web Audio API - Web API 接口参考 | MDN (mozilla.org)


实现思路


首先画肯定是用canvas去画,关于音频的相关数据(如频率、波形)如何去获取,需要去获取相关audio的DOM 或通过请求处理去拿到相关的音频数据,然后通过Web Audio API 提供相关的方法来实现。(当然还要考虑要音频请求跨域的问题,留在最后。)


一个简单而典型的 web audio 流程如下(取自MDN):


1.创建音频上下文<br>
2.在音频上下文里创建源 — 例如 <audio>, 振荡器,流<br>
3.创建效果节点,例如混响、双二阶滤波器、平移、压缩<br>
4.为音频选择一个目的地,例如你的系统扬声器<br>
5.连接源到效果器,对目的地进行效果输出<br>

image.png


上代码


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
background-color: #000;
}
div {
width: 100%;
border-top: 1px solid #fff;
padding-top: 50px;
display: flex;
justify-content: center;
}
</style>
</head>
<body>
<canvas></canvas>
<div>
<audio src="./1.mp3" controls></audio>
</div>
<script>
// 获取音频组件
const audioEle = document.querySelector("audio");
// 获取canvas元素
const cvs = document.querySelector("canvas");
// 创建canvas上下文
const ctx = cvs.getContext("2d");

// 初始化canvas的尺寸
function initCvs() {
cvs.width = window.innerWidth;
cvs.height = window.innerHeight / 2;
}
initCvs();

// 音频的步骤 音频源 ==》分析器 ===》输出设备

//是否初始化
let isInit = false;
let dataArray, analyser;
audioEle.onplay = function () {
if (isInit) return;
// 初始化
const audCtx = new AudioContext(); //创建一个音频上下文
const source = audCtx.createMediaElementSource(audioEle); //创建音频源节点
analyser = audCtx.createAnalyser(); //拿到分析器节点
analyser.fftSize = 512; // 时域图变换的窗口大小(越大越细腻)默认2048
// 创建一个数组,接受分析器分析回来的数据
dataArray = new Uint8Array(analyser.frequencyBinCount); // 数组每一项都是一个整数 接收数组的长度,因为快速傅里叶变换是对称的,所以除以2
source.connect(analyser); // 将音频源节点链接到输出设备,否则会没声音哦。
analyser.connect(audCtx.destination); // 把分析器节点了解到输出设备
isInit = true;
};

// 绘制,把分析出来的波形绘制到canvas上
function draw() {
requestAnimationFrame(draw);
// 清空画布
const { width, height } = cvs;
ctx.clearRect(0, 0, width, height);
// 先判断音频组件有没有初始化
if (!isInit) return;
// 让分析器节点分析出数据到数组中
analyser.getByteFrequencyData(dataArray);
const len = dataArray.length / 2.5;
const barWidth = width / len / 2; // 每一个的宽度
ctx.fillStyle = "#78C5F7";
for (let i = 0; i < len; i++) {
const data = dataArray[i]; // <256
const barHeight = (data / 255) * height; // 每一个的高度
const x1 = i * barWidth + width / 2;
const x2 = width / 2 - (i + 1) * barWidth;
const y = height - barHeight;
ctx.fillRect(x1, y, barWidth - 2, barHeight);
ctx.fillRect(x2, y, barWidth - 2, barHeight);
}
}
draw();
</script>
</body>
</html>

我这里只用了简单的柱状图,还有什么其他的奇思妙想,至于怎么把数据画出来,就凭大家的想法了。


关于请求音频跨域问题解决方案


1.因为我这里是简单的html,音频文件也在同一个文件夹但是如果直接用本地路径打开本地文件是会报跨域的问题,所以我这里是使用Open with Live Server就可以了


image.png


2.给获取的audio DOM添加一条属性即可
audio.crossOrigin ='anonymous'

或者直接在 aduio标签中 加入 crossorigin="anonymous"


作者:井川不擦
来源:juejin.cn/post/7263840667826257957
收起阅读 »

Nginx 体系化之虚拟主机分类及配置实现

Nginx,这款备受推崇的高性能 Web 服务器,以其强大的性能和灵活的配置而广受欢迎。在实际应用中,虚拟主机是一项重要的功能,允许我们在单个服务器上托管多个网站。本文将深入探讨 Nginx 虚拟主机的分类和配置实现,帮助您构建一个高效多站点托管平台。 虚拟主...
继续阅读 »

Nginx,这款备受推崇的高性能 Web 服务器,以其强大的性能和灵活的配置而广受欢迎。在实际应用中,虚拟主机是一项重要的功能,允许我们在单个服务器上托管多个网站。本文将深入探讨 Nginx 虚拟主机的分类和配置实现,帮助您构建一个高效多站点托管平台。


虚拟主机的分类


虚拟主机是一种将单个服务器划分成多个独立的网站托管环境的技术。Nginx 支持三种主要类型的虚拟主机:


基于 IP 地址的虚拟主机(常用)


这种类型的虚拟主机是通过不同的 IP 地址来区分不同的网站。每个 IP 地址绑定到一个特定的网站或应用程序。这种虚拟主机适用于需要在同一服务器上为每个网站提供独立的资源和配置的场景。


基于域名的虚拟主机(常用)


基于域名的虚拟主机是根据不同的域名来区分不同的网站。多个域名可以共享同一个 IP 地址,并通过 Nginx 的配置来分发流量到正确的网站。这种虚拟主机适用于在单个服务器上托管多个域名或子域名的情况。


基于多端口的虚拟主机(不常用)


基于多端口的虚拟主机是一种将单个服务器上的多个网站隔离开来的方式。每个网站使用不同的端口号进行访问,从而实现隔离。这种方法特别适用于那些无法使用不同域名或 IP 地址的情况,或者需要在同一服务器上快速托管多个网站的需求。


虚拟主机配置实现


配置文件结构


Nginx 的配置文件通常位于 /etc/nginx/nginx.conf,在该文件中可以找到 http 块。在 http 块内,可以配置全局设置和默认行为。每个虚拟主机都需要一个 server 块来定义其配置。
使用 include 指令简化配置文件,通常情况下将基于 server 的配置文件放到一个文件夹中,由 include 引用即可


http{
include /usr/nginx/server/*.conf # 表示引用 server 下的配置文件
}

基于 IP 地址的虚拟主机实现


创建 IP 配置文件


/usr/nginx/server/ 中创建一个新的配置文件,例如 /usr/nginx/server/ip.conf


配置 IP


在新的配置文件中,为每个网站创建一个 server 块,并在其中指定监听的端口号和网站的根目录。例如:


# 基于 192.168.1.10 代理到百度网站
server {
listen 192.168.1.10:80;
server_name http://www.baidu.com;
root /var/www/baidu;
index index.html;
}
# 基于 192.168.1.11:80 代理到 bing 网站
server {
listen 192.168.1.11:80;
server_name http://www.bing.com;
root /var/www/bing;
index index.html;
}

最佳场景实践



  1. 资源隔离: 每个网站都有独立的 IP 地址、资源和配置,避免了资源冲突和相互影响。

  2. 安全性提升: 基于 IP 地址的虚拟主机可以增强安全性,减少不同网站之间的潜在风险。

  3. 独立访问: 每个网站都有独立的 IP 地址,可以实现独立的访问控制和限制。

  4. 多租户托管: 基于 IP 地址的虚拟主机适用于多租户托管场景,为不同客户提供独立环境。


基于域名的虚拟主机实现


创建 IP 配置文件


/usr/nginx/server/ 中创建一个新的配置文件,例如 /usr/nginx/server/domain.conf


配置 IP


在新的配置文件中,为每个网站创建一个 server 块,并在其中指定监听的域名和网站的根目录。例如:


# 通过 http://www.baidu.com 转发到 80
server {
listen 80;
server_name http://www.baidu.com;
root /var/www/baidu;
index index.html;
}

# 通过 http://www.bing.com 转发到 80
server {
listen 80;
server_name http://www.bing.com;
root /var/www/bing;
index index.html;
}

最佳场景实践


基于域名的虚拟主机为多站点托管提供了高度的定制性和灵活性:



  1. 品牌差异化: 不同域名的虚拟主机允许您为不同品牌或应用提供独立的网站定制,提升用户体验。

  2. 定向流量: 基于域名的虚拟主机可以将特定域名的流量引导至相应的网站,实现定向流量管理。

  3. 子域名托管: 可以将不同子域名配置为独立的虚拟主机,为多个应用或服务提供托管。

  4. SEO 优化: 每个域名的虚拟主机可以针对不同的关键词进行 SEO 优化,提升搜索引擎排名。


基于多端口的虚拟主机


创建多端口配置文件


/usr/nginx/server/ 中创建一个新的配置文件,例如 /usr/nginx/server/domain.conf


配置 IP


在新的配置文件中,为每个网站创建一个 server 块,并在其中指定监听的域名和网站的根目录。例如:


server {
listen 8081;
server_name http://www.baidu.com;
root /var/www/baidu;
index index.html;
}

server {
listen 8082;
server_name http://www.bing.com;
root /var/www/bing;
index index.html;
}

最佳场景实践


基于多端口的虚拟主机为多站点托管提供了更多的灵活性和选择:



  1. 快速设置: 使用多端口可以快速设置多个网站,适用于临时性或开发环境。

  2. 资源隔离: 每个网站都有独立的端口和配置,避免了资源冲突和相互干扰。

  3. 开发和测试: 多端口虚拟主机适用于开发和测试环境,每个开发者可以使用不同的端口进行开发和调试。

  4. 灰度发布: 基于多端口的虚拟主机可以实现灰度发布,逐步引导流量至新版本网站。


重载配置


在添加、修改或删除多端口虚拟主机配置后,使用以下命令重载 Nginx 配置,使更改生效:


nginx -s reload
作者:努力的IT小胖子
来源:juejin.cn/post/7263886796757483580

收起阅读 »

如果按代码量算工资,也许应该这样写

前言 假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢? 要在增加代码量的同时提高代码质量和可维护性,能否做到呢? 答案当然是可以,这可难不倒我们这种摸鱼高手。 耐心看完,你一定有所收获。 正文 1. 实现更多的...
继续阅读 »

前言


假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢?


要在增加代码量的同时提高代码质量和可维护性,能否做到呢?


答案当然是可以,这可难不倒我们这种摸鱼高手。


耐心看完,你一定有所收获。


giphy.gif


正文


1. 实现更多的接口:


给每一个方法都实现各种“无关痛痒”的接口,比如SerializableCloneable等,真正做到不影响使用的同时增加了相当数量的代码。


为了这些代码量,其中带来的性能损耗当然是可以忽略的。


public class ExampleClass implements Serializable, Comparable<ExampleClass>, Cloneable, AutoCloseable {

@Override
public int compareTo(ExampleClass other) {
// 比较逻辑
return 0;
}

// 实现 Serializable 接口的方法
private void writeObject(ObjectOutputStream out) throws IOException {
// 序列化逻辑
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 反序列化逻辑
}

// 实现 Cloneable 接口的方法
@Override
public ExampleClass clone() throws CloneNotSupportedException {
// 复制对象逻辑
return (ExampleClass) super.clone();
}

// 实现 AutoCloseable 接口的方法
@Override
public void close() throws Exception {
// 关闭资源逻辑
}

}


除了示例中的SerializableComparableCloneableAutoCloseable,还有Iterable


2. 重写 equals 和 hashcode 方法


重写 equalshashCode 方法绝对是上上策,不仅增加了代码量,还为了让对象在相等性判断和散列存储时能更完美的工作,确保代码在处理对象相等性时更准确、更符合业务逻辑。


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

// 重写 equals 方法
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

ExampleClass other = (ExampleClass) obj;
return this.age == other.age && Objects.equals(this.name, other.name);
}

// 重写 hashCode 方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}


giphy (2).gif


3. 增加配置项和参数:


不要管能不能用上,梭哈就完了,问就是为了健壮性和拓展性。


public class AppConfig {
private int maxConnections;
private String serverUrl;
private boolean enableFeatureX;

// 新增配置项
private String emailTemplate;
private int maxRetries;
private boolean enableFeatureY;

// 写上构造函数和getter/setter
}

4. 增加监听回调:


给业务代码增加监听回调,比如执行前、执行中、执行后等各种Event,这里举个完整的例子。


比如创建个 EventListener ,负责监听特定类型的事件,事件源则是产生事件的对象。通过EventListener 在代码中增加执行前、执行中和执行后的事件。


首先,我们定义一个简单的事件类 Event


public class Event {
private String name;

public Event(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

然后,我们定义一个监听器接口 EventListener


public interface EventListener {
void onEventStart(Event event);

void onEventInProgress(Event event);

void onEventEnd(Event event);
}

接下来,我们定义一个事件源类 EventSource,在执行某个业务方法时,触发事件通知:


public class EventSource {
private List<EventListener> listeners = new ArrayList<>();

public void addEventListener(EventListener listener) {
listeners.add(listener);
}

public void removeEventListener(EventListener listener) {
listeners.remove(listener);
}

public void businessMethod() {
Event event = new Event("BusinessEvent");

// 通知监听器:执行前事件
for (EventListener listener : listeners) {
listener.onEventStart(event);
}

// 模拟执行业务逻辑
System.out.println("Executing business method...");

// 通知监听器:执行中事件
for (EventListener listener : listeners) {
listener.onEventInProgress(event);
}

// 模拟执行业务逻辑
System.out.println("Continuing business method...");

// 通知监听器:执行后事件
for (EventListener listener : listeners) {
listener.onEventEnd(event);
}
}
}

现在,我们可以实现具体的监听器类,比如 BusinessEventListener,并在其中定义事件处理逻辑:


public class BusinessEventListener implements EventListener {
@Override
public void onEventStart(Event event) {
System.out.println("Event Start: " + event.getName());
}

@Override
public void onEventInProgress(Event event) {
System.out.println("Event In Progress: " + event.getName());
}

@Override
public void onEventEnd(Event event) {
System.out.println("Event End: " + event.getName());
}
}

最后,我们写个main函数来演示监听事件:


public class Main {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
eventSource.addEventListener(new BusinessEventListener());

// 执行业务代码,并触发事件通知
eventSource.businessMethod();

// 移除监听器
eventSource.removeEventListener(businessEventListener);
}
}

如此这般那般,代码量猛增,还顺带实现了业务代码的流程监听。当然这只是最简陋的实现,真实环境肯定要比这个复杂的多。


5. 构建通用工具类:


同样的,甭管用不用的上,定义更多的方法,都是为了健壮性。


比如下面这个StringUtils,可以从ApacheCommons、SpringBoot的StringUtil或HuTool的StrUtil中拷贝更多的代码过来,美其名曰内部工具类。


public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}

public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}

// 新增方法:将字符串反转
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}

// 新增方法:判断字符串是否为整数
public static boolean isInteger(String str) {
try {
Integer.parseInt(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}

6. 添加新的异常类型:


添加更多异常类型,对不同的业务抛出不同的异常,每种异常都要单独去处理


public class CustomException extends RuntimeException {
// 构造函数
public CustomException(String message) {
super(message);
}

// 新增异常类型
public static class NotFoundException extends CustomException {
public NotFoundException(String message) {
super(message);
}
}

public static class ValidationException extends CustomException {
public ValidationException(String message) {
super(message);
}
}
}

// 示例:添加不同类型的异常处理
public class ExceptionHandling {
public void process(int value) {
try {
if (value < 0) {
throw new IllegalArgumentException("Value cannot be negative");
} else if (value == 0) {
throw new ArithmeticException("Value cannot be zero");
} else {
// 正常处理逻辑
}
} catch (IllegalArgumentException e) {
// 异常处理逻辑
} catch (ArithmeticException e) {
// 异常处理逻辑
}
}
}


7. 实现更多设计模式:


在项目中运用更多设计模式,也不失为一种合理的方式,比如单例模式、工厂模式、策略模式、适配器模式等各种常用的设计模式。


比如下面这个单例,大大节省了内存空间,虽然它存在线程不安全等问题。


public class SingletonPattern {
// 单例模式
private static SingletonPattern instance;

private SingletonPattern() {
// 私有构造函数
}

public static SingletonPattern getInstance() {
if (instance == null) {
instance = new SingletonPattern();
}
return instance;
}

}

还有下面这个策略模式,能避免过多的if-else条件判断,降低代码的耦合性,代码的扩展和维护也变得更加容易。


// 策略接口
interface Strategy {
void doOperation(int num1, int num2);
}

// 具体策略实现类
class AdditionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
}
}

class SubtractionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
}
}

// 上下文类
class Context {
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public void executeStrategy(int num1, int num2) {
strategy.doOperation(num1, num2);
}
}

// 测试类
public class StrategyPattern {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;

// 使用加法策略
Context context = new Context(new AdditionStrategy());
context.executeStrategy(num1, num2);

// 使用减法策略
context = new Context(new SubtractionStrategy());
context.executeStrategy(num1, num2);
}
}

对比下面这段条件判断,高下立判。


public class Calculator {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;
String operation = "addition"; // 可以根据业务需求动态设置运算方式

if (operation.equals("addition")) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
} else if (operation.equals("subtraction")) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
} else if (operation.equals("multiplication")) {
int result = num1 * num2;
System.out.println("Multiplication result: " + result);
} else if (operation.equals("division")) {
int result = num1 / num2;
System.out.println("Division result: " + result);
} else {
System.out.println("Invalid operation");
}
}
}


8. 扩展注释和文档:


如果要增加代码量,写更多更全面的注释也不失为一种方式。


/**
* 这是一个示例类,用于展示增加代码数量的技巧和示例。
* 该类包含一个示例变量 value 和示例构造函数 ExampleClass(int value)。
* 通过示例方法 getValue() 和 setValue(int newValue),可以获取和设置 value 的值。
* 这些方法用于展示如何增加代码数量,但实际上并未实现实际的业务逻辑。
*/

public class ExampleClass {

// 示例变量
private int value;

/**
* 构造函数
*/

public ExampleClass(int value) {
this.value = value;
}

/**
* 获取示例变量 value 的值。
* @return 示范变量 value 的值
*/

public int getValue() {
return value;
}

/**
* 设置示例变量 value 的值。
* @param newValue 新的值,用于设置 value 的值。
*/

public void setValue(int newValue) {
this.value = newValue;
}
}

结语


哪怕是以代码量算工资,咱也得写出高质量的代码,合理合法合情的赚票子。


giphy (1).gif


作者:一只叫煤球的猫
来源:juejin.cn/post/7263760831052906552
收起阅读 »

我写了一个自动化脚本涨粉,从0阅读到接近100粉丝

web
点击在线阅读,体验更好链接现代JavaScript高级小册链接深入浅出Dart链接现代TypeScript高级小册链接linwu的算法笔记📒链接 引言 在CSDN写了大概140篇文章,一直都是0阅读量,仿佛石沉大海,在掘金能能频频上热搜的文章,在CSDN一点反...
继续阅读 »

点击在线阅读,体验更好链接
现代JavaScript高级小册链接
深入浅出Dart链接
现代TypeScript高级小册链接
linwu的算法笔记📒链接

引言


在CSDN写了大概140篇文章,一直都是0阅读量,仿佛石沉大海,在掘金能能频频上热搜的文章,在CSDN一点反馈都没有,所以跟文章质量关系不大,主要是曝光量,后面调研一下,发现情况如下


image.png


好家伙,基本都是人机评论,后面问了相关博主,原来都是互相刷评论来涨阅读量,enen...,原来CSDN是这样的,真无语,竟然是刷评论,那么就不要怪我用脚本了。


puppeteer入门



先来学习一波puppeteer知识点,其实也不难



puppeteer 简介


Puppeteer 是 Chrome 开发团队在 2017 年发布的一个 Node.js 包, 用来模拟 Chrome 浏览器的运行。



Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。 Chromium 和 Chrome区别



在学puppeteer之前我们先来了解下 headless chrome


什么是 Headless Chrome



  • 在无界面的环境中运行 Chrome

  • 通过命令行或者程序语言操作 Chrome

  • 无需人的干预,运行更稳定

  • 在启动 Chrome 时添加参数 --headless,便可以 headless 模式启动 Chrome


alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"  # Mac OS X 命令别名

chrome --headless --disable-gpu --dump-dom https://www.baidu.com # 获取页面 DOM

chrome --headless --disable-gpu --screenshot https://www.baidu.com # 截图


查看更多chrome启动参数 英文
中文


puppeteer 能做什么


官方称:“Most things that you can do manually in the browser can be done using Puppeteer”,那么具体可以做些什么呢?



  • 网页截图或者生成 PDF

  • 爬取 SPA 或 SSR 网站

  • UI 自动化测试,模拟表单提交,键盘输入,点击等行为

  • 捕获网站的时间线,帮助诊断性能问题

  • ......


puppeteer 结构


image



  • Puppeteer 使用 DevTools 协议 与浏览器进行通信。

  • Browser 实例可以拥有浏览器上下文。

  • BrowserContext 实例定义了一个浏览会话并可拥有多个页面。

  • Page 至少有一个框架:主框架。 可能还有其他框架由 iframe 或 框架标签 创建。

  • frame 至少有一个执行上下文 - 默认的执行上下文 - 框架的 JavaScript 被执行。 一个框架可能有额外的与 扩展 关联的执行上下文。


puppeteer 运行环境


查看 Puppeteer 的官方 API 你会发现满屏的 async, await 之类,这些都是 ES7 的规范,所以你需要: Nodejs 的版本不能低于 v7.6.0


npm install puppeteer 

# or "yarn add puppeteer"


Note: 当你安装 Puppeteer 时,它会自动下载Chromium,由于Chromium比较大,经常会安装失败~ 可是使用以下解决方案



  • 把npm源设置成国内的源 cnpm taobao 等

  • 安装时添加--ignore-scripts命令跳过Chromium的下载 npm install puppeteer --ignore-scripts

  • 安装 puppeteer-core 这个包不会去下载Chromium


puppeteer 基本用法


先打开官方的入门demo


const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'example.png'});

await browser.close();
})();

上面这段代码就实现了网页截图,先大概解读一下上面几行代码:



  1. 先通过 puppeteer.launch() 创建一个浏览器实例 Browser 对象

  2. 然后通过 Browser 对象创建页面 Page 对象

  3. 然后 page.goto() 跳转到指定的页面

  4. 调用 page.screenshot() 对页面进行截图

  5. 关闭浏览器


是不是觉得好简单?


puppeteer.launch(options)


options 参数详解


参数名称参数类型参数说明
ignoreHTTPSErrorsboolean在请求的过程中是否忽略 Https 报错信息,默认为 false
headlessboolean是否以”无头”的模式运行 chrome, 也就是不显示 UI, 默认为 true
executablePathstring可执行文件的路劲,Puppeteer 默认是使用它自带的 chrome webdriver, 如果你想指定一个自己的 webdriver 路径,可以通过这个参数设置
slowMonumber使 Puppeteer 操作减速,单位是毫秒。如果你想看看 Puppeteer 的整个工作过程,这个参数将非常有用。
argsArray(String)传递给 chrome 实例的其他参数,比如你可以使用”–ash-host-window-bounds=1024x768” 来设置浏览器窗口大小。
handleSIGINTboolean是否允许通过进程信号控制 chrome 进程,也就是说是否可以使用 CTRL+C 关闭并退出浏览器.
timeoutnumber等待 Chrome 实例启动的最长时间。默认为30000(30秒)。如果传入 0 的话则不限制时间
dumpioboolean是否将浏览器进程stdout和stderr导入到process.stdout和process.stderr中。默认为false。
userDataDirstring设置用户数据目录,默认linux 是在 ~/.config 目录,window 默认在 C:\Users{USER}\AppData\Local\Google\Chrome\User Data, 其中 {USER} 代表当前登录的用户名
envObject指定对Chromium可见的环境变量。默认为process.env。
devtoolsboolean是否为每个选项卡自动打开DevTools面板, 这个选项只有当 headless 设置为 false 的时候有效

puppeteer如何使用



下面介绍 10 个关于使用 Puppeteer 的用例,并在介绍用例的时候会穿插的讲解一些 API,告诉大家如何使用 Puppeteer:



01 获取元素及操作


如何获取元素?



  • page.$('#uniqueId'):获取某个选择器对应的第一个元素

  • page.$$('div'):获取某个选择器对应的所有元素

  • page.$x('//img'):获取某个 xPath 对应的所有元素

  • page.waitForXPath('//img'):等待某个 xPath 对应的元素出现

  • page.waitForSelector('#uniqueId'):等待某个选择器对应的元素出现



Page.$(selector) 获取单个元素,底层是调用的是 document.querySelector() , 所以选择器的 selector 格式遵循 css 选择器规范




Page.$$(selector) 获取一组元素,底层调用的是 document.querySelectorAll(). 返回 Promise(Array(ElemetHandle)) 元素数组.




const puppeteer = require('puppeteer');

async function run (){
const browser = await puppeteer.launch({headless:false,defaultViewport:{width:1366,height:768}});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
const input_area = await page.$("#kw");
await input_area.type("Hello Wrold");

const search_btn = await page.$('#su');
await search_btn.click();

}

run();

02 获取元素属性


Puppeteer 获取元素属性跟我们平时写前段的js的逻辑有点不一样,按照通常的逻辑,应该是现获取元素,然后在获取元素的属性。但是上面我们知道 获取元素的 API 最终返回的都是 ElemetHandle 对象,而你去查看 ElemetHandle 的 API 你会发现,它并没有获取元素属性的 API.


事实上 Puppeteer 专门提供了一套获取属性的 API, Page.$eval() 和 Page.$$eval()


Page.$$eval(selector, pageFunction[, …args]), 获取单个元素的属性,这里的选择器 selector 跟上面 Page.$(selector) 是一样的。


const value = await page.$eval('input[name=search]', input => input.value);
const href = await page.$eval('#a", ele => ele.href);
const content = await page.$eval('
.content', ele => ele.outerHTML);

const puppeteer = require('puppeteer');

async function run (){
const browser = await puppeteer.launch({headless:false,defaultViewport:{width:1366,height:768}});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
const input_area = await page.$("#kw");
await input_area.type("Hello Wrold");

const search_btn = await page.$('#su');
await search_btn.click();

await page.waitFor('div#content_left > div.result-op.c-container.xpath-log',{visible:true});

let resultText = await page.$eval('div#content_left > div.result-op.c-container.xpath-log',ele=> ele.innerText)
console.log("result Text= ",resultText);



}

run();

03 处理多个元素




const puppeteer = require('puppeteer');

async function run() {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 1280,
height: 800,
},
slowMo: 200,
});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
const input_area = await page.$('#kw');
await input_area.type('Hello Wrold');
await page.keyboard.press('Enter');
const listSelector = 'div#content_left > div.result-op.c-container.xpath-log';
// await page.waitForSelector(listSelector);
await page.waitFor(3 * 1000);

const list = await page.$$eval(listSelector, (eles) =>
eles.map((ele) => ele.innerText)
);
console.log('List ==', list);
}

run();


04 切换frame


一个 Frame 包含了一个执行上下文(Execution Context),我们不能跨 Frame 执行函数,一个页面中可以有多个 Frame,主要是通过 iframe 标签嵌入的生成的。其中在页面上的大部分函数其实是 page.mainFrame().xx 的一个简写,Frame 是树状结构,我们可以通过page.frames()获取到页面所有的 Frame,如果想在其它 Frame 中执行函数必须获取到对应的 Frame 才能进行相应的处理



const puppeteer = require('puppeteer')

async function anjuke(){
const browser = await puppeteer.launch({headless:false});
const page = await browser.newPage();
await page.goto('https://login.anjuke.com/login/form');

// 切换iframe

await page.frames().map(frame => {console.log(frame.url())})
const targetFrameUrl = 'https://login.anjuke.com/login/iframeform'
const frame = await page.frames().find(frame => frame.url().includes(targetFrameUrl));

const phone= await frame.waitForSelector('#phoneIpt')
await phone.type("13122022388")
}

anjuke();

05 拖拽验证码操作


const puppeteer = require('puppeteer')

async function aliyun(){
const browser = await puppeteer.launch({headless:false,ignoreDefaultArgs:['--enable-automation']});
const page = await browser.newPage();
await page.goto('https://account.aliyun.com/register/register.htm',{waitUntil:"networkidle2"});

const frame = await page.frames().find(frame=>{
console.log(frame.url())
return frame.url().includes('https://passport.aliyun.com/member/reg/fast/fast_reg.htm')

})

const span = await frame.waitForSelector('#nc_1_n1z');
const spaninfo = await span.boundingBox();
console.log('spaninfo',spaninfo)

await page.mouse.move(spaninfo.x,spaninfo.y);
await page.mouse.down();

const div = await frame.waitForSelector('div#nc_1__scale_text > span.nc-lang-cnt');
const divinfo = await div.boundingBox();

console.log('divinfo',divinfo)
for(var i=0;i<divinfo.width;i++){
await page.mouse.move(spaninfo.x+i,spaninfo.y);
}
await page.mouse.up();
}

aliyun();


06 模拟不同设备


const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];

puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.emulate(iPhone);
await page.goto('https://www.baidu.com');
// 其他操作...
await browser.close();
});

07 请求拦截



const puppeteer = require('puppeteer');
async function run () {
const browser = await puppeteer.launch({
headless:false,
defaultViewport:{
width:1280,
height:800
}
})
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
const blockTypes = new Set(['image', 'media', 'font']);
const type = interceptedRequest.resourceType();
const shouldBlock = blockTypes.has(type);
if (shouldBlock) {
interceptedRequest.abort();
} else {
interceptedRequest.continue();
}

});
await page.goto('https://t.zhongan.com/group');
}

run();

08 性能分析



const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.tracing.start({path: 'trace.json'});
await page.goto('https://t.zhongan.com/group');
await page.tracing.stop();
browser.close();
})();

09 生成pdf



const URL = 'http://es6.ruanyifeng.com';
const puppeteer = require('puppeteer');
const fs = require('fs');

fs.mkdirSync('es6-pdf');

(async () => {
let browser = await puppeteer.launch();
let page = await browser.newPage();
await page.goto(URL);
await page.waitFor(5000); // 等待五秒,确保页面加载完毕

// 获取左侧导航的所有链接地址及名字
let aTags = await page.evaluate(() => {
let eleArr = [...document.querySelectorAll('#sidebar ol li a')];
return eleArr.map((a) =>{
return {
href: a.href.trim(),
name: a.text
}
});
});

// 先将本页保存成pdf,并关闭本页
console.log('正在保存:0.' + aTags[0].name);
await page.pdf({path: `./es6-pdf/0.${aTags[0].name}.pdf`});

// 遍历节点数组,逐个打开并保存 (此处不再打印第一页)
for (let i = 1, len = aTags.length; i < len; i++) {
let a = aTags[i];
console.log('正在保存:' + i + '.' + a.name);
page = await browser.newPage();
await page.goto(a.href);
await page.waitFor(5000);
await page.pdf({path: `./es6-pdf/${i + '.' + a.name}.pdf`});
}
browser.close();
})


10 自动化发布微博


const puppeteer = require('puppeteer');
const {username,password} = require('./config')

async function run(){
const browser = await puppeteer.launch({
headless:false,
defaultViewport:{width:1200,height:700},
ignoreDefaultArgs:['--enable-automation'],
slowMo:200,
args:['--window-size=1200,700']})
const page = await browser.newPage();

await page.goto('http://wufazhuce.com/',{waitUntil:'networkidle2'});
const OneText = await page.$eval('div.fp-one-cita > a',ele=>ele.innerText);
console.log('OneText:',OneText);


await page.goto('https://weibo.com/',{waitUntil:'networkidle2'});
await page.waitFor(2*1000);
await page.reload();

const loginUserInput = await page.waitForSelector('input#loginname');
await loginUserInput.click();
await loginUserInput.type(username);

const loginUserPasswdInput = await page.waitForSelector('input[type="password"]');
await loginUserPasswdInput.click();
await loginUserPasswdInput.type(password);

const loginBtn = await page.waitForSelector('a[action-type="btn_submit"]')
await loginBtn.click();

const textarea = await page.waitForSelector('textarea[class="W_input"]')
await textarea.click();
await textarea.type(OneText);

const sendBtn = await page.waitForSelector('a[node-type="submit"]');
await sendBtn.click();
}

run();

CSDN的脚本


image.png



这里注意CSDN有反扒机制,规则自己琢磨就行,我贴了伪代码,核心代码就不开放,毕竟自己玩玩就行了



const puppeteer = require('puppeteer');

async function autoCommentCSDN(username, password, targetBlogger, commentContent) {
const browser = await puppeteer.launch({ headless: false }); // 打开有头浏览器
const page = await browser.newPage();

// 登录CSDN
await page.goto('https://passport.csdn.net/login');
await page.waitForTimeout(1000); // 等待页面加载

// 切换到最后一个Tab (账号登录)
// 点击“密码登录”
const passwordLoginButton = await page.waitForXPath('//span[contains(text(), "密码登录")]');
await passwordLoginButton.click();


// 输入用户名和密码并登录
const inputFields = await page.$$('.base-input-text');

await inputFields[0].type( username);
await inputFields[1].type( password);
await page.click('.base-button');

await page.waitForNavigation();

// // 跳转到博主的首页
await page.goto(`https://blog.csdn.net/${targetBlogger}?type=blog`);

// // 点击第一篇文章的标题,进入文章页面
await page.waitForSelector('.list-box-cont', { visible: true });
await page.click('.list-box-cont');
// // 获取文章ID
console.log('page.url()',page.url())

// await page.waitForTimeout(1000); // 等待页面加载


await page.goto('https://blog.csdn.net/weixin_52898349/article/details/132115618')


await page.waitForTimeout(1000); // 等待页面加载

console.log('开始点击评论按钮...')

console.log('page.url()',page.url())

// 获取当前页面的DOM内容

const bodyHTML = await page.evaluate(() => {
return document.body.innerHTML;
});

console.log(bodyHTML);

// await page.waitForSelector('.comment-side-tit');

// const commentInput = await page.$('.comment-side-tit');

// await commentInput.click();

// 等待评论按钮出现

// 点击评论按钮

// await page.waitForSelector('.comment-content');
// const commentInput = await page.$('.comment-content textarea');
// await commentInput.type(commentContent);
// const submitButton = await page.$('.btn-comment-input');
// await submitButton.click();

// console.log('评论成功!');
// await browser.close();

}

// 请替换以下参数为您的CSDN账号信息、目标博主和评论内容
const username = 'weixin_52898349';
const password = 'xxx!';
const targetBlogger = 'weixin_52898349'; // 目标博主的CSDN用户名
const commentContent = '各位大佬们帮忙三连一下,非常感谢!!!!!!!!!!!'; // 评论内容

autoCommentCSDN(username, password, targetBlogger, commentContent);




作者:linwu
来源:juejin.cn/post/7263871284010106938
收起阅读 »

清朝项目太臭怎么办?TS重构它!

web
多图预警,流量党慎入 图片:均图片在上,描述在下 全文:6474字 阅读需要约28分钟 项目背景 最近公司要求给一个老项目加功能,具体就是把原来免费的服务改成付费的,然后再加一点其他的功能,我之前看到过那个项目的线上,这么一说,顿感不妙。截个图简单感受一下:...
继续阅读 »

多图预警,流量党慎入


图片:均图片在上,描述在下


全文:6474字 阅读需要约28分钟



项目背景


最近公司要求给一个老项目加功能,具体就是把原来免费的服务改成付费的,然后再加一点其他的功能,我之前看到过那个项目的线上,这么一说,顿感不妙。截个图简单感受一下:


image.png


农业数字云页面


image-20230804132617537


告警平台页


image.png
天气监测页面(navBar直接没了,我天😅)


image-20230804132711975


气象大数据页,好家伙直接空壳页面,也是没有navBar了,好像进入了一个全新的大屏项目。


报错什么的咱就不说了,光log信息就看的眼花缭乱,点几下给你log十几行出来,你说log就log吧,把名字取好也行,不过各位,看图😅。


好好好,虽然线上看起来没那么漂亮,可能人家代码写的工整呢?


那咱接下来看看代码,我草!等会jym,咱先不看代码,光这目录结构就把俺老猪吓一跳:


image-20230804133247244


image-20230804134709932


image-20230804133320761


image-20230804133541655


牛蛙牛蛙,今天算开眼了,根目录下命名个entries,好家伙下面还有components、views、App.vue(这是把另一个项目整个搬过来了是吧😅),根目录下同时拥有:utils、tools、service,好家伙,直接跟俺摆迷魂阵!更离谱的是,view下面还有view,view下的文件夹还有components,组件和页面完全分不清啊!敢问掘友们见过这种史诗级屎目录吗?


被目录结构震惊之后,先平复一下心情,看一下代码,代码写的还是挺工整的


做好准备,图来了:


先拿个vue组件看看:


image-20230804134138764


(那个分页函数是我后来写的)嗯~中规中矩,我还可以接受,毕竟命名什么的还算规范,看个十几分钟就没问题了,继续看个组件


image-20230804134418622


啊~行,配合模板部分还是能看懂的,继续来:


image-20230804134856519


啊?我可以接受两个函数长得差不多,但是功能应该也差不多吧?你这addControl2里面的功能和addControl完全不一样啊!!一个是绑定控制地图时的监听回调,一个是添加地图控件,我承认这两种确实都能称之为 Control...但是


vue组件写的还都可以接受


image-20230804135816345


接口封装也行,命名和注释都有


image-20230804140404214


这哥们自己用正则写了个时间格式化函数,我只能说很强,但是没必要。


好吧,看到这,虽然每一项都还算过得去,但是如果让我维护这样的项目,实在是太累了(我截图不是个例各位,每个文件中都有类似的)。改了一周后,我申请了重构,没想到当即被领导拍板,重构吧,交给你了。


敲定技术栈


因为旧的项目变量和全局变量、路由传参、mixin混用,导致页面内的变量来源难以追踪,很多时候知道是这个值引起的问题,但是就是找不到这个值哪来(可能被路由传了好多级才过来)。由此,我准备用 ts + pinia 做类型和状态管理。


旧项目(webpack)每次冷启动或打包都要花费 20s 左右的时间,热更新在 1 - 2s之间还行。由于这个网站一直在使用阿里云云效在线部署,所以我也没特意看打包后的大小,但是估计要在几十上下,其实这些问题都不大,主要优化一些静态文件和无用的代码就好了。但是我还是因此选择了使用 vite


因为旧项目使用了vue2、mixin和vuex和vue-router导致 变量污染问题严重 。为了解决这个问题,除了上面提到的 ts + pinia ,还选用了vue3,因为vue3天然不支持mixin,使用组合式API很容易可以更清晰的实现mixin可以做到的所有功能。


在新项目中,使用vue-router时,只作为备用方案,因为在重构时进行了新一轮的需求整理,直接将告警平台移除,后面两个页面的核心功能合并到农业数字云,在此场景下,新版的农业数字云只需要一个路由就可以承载所有的功能了。并且,使用了pinia-plugin-persistedstate插件对pinia做了全局的持久化,这里要说明一点,很多人觉得在本地存储太多数据不太好,尤其是像我这样一股脑的全部持久化,其实,在localStorage允许的情况下,把一些常用数据甚至是状态信息存储在本地,是效率最高也是性能最好的方式。


最终技术栈为:vue3 + pinia + vue-router + ts


开干


概述


因为是旧的项目,所以接口基本都全,整理出来的新需求,后端大哥跟我一起做,速度很快,我们第一周就已经完成了整个项目的99%。


由于重构系统并没有出新的设计图,导致我页面端两眼一抹黑,只能靠模仿旧的大致样式来做(因为和旧版逻辑架构完全不一样了,所以大部分还是重做的),最终做出了一个更清爽的前端页面:


image-20230804143240923


image-20230804143358520


登录页也不是一个单独的页面了,几乎所有的功能都改成了轻量级的弹窗。


这个项目完整的展开后是这种样子:


image-20230804143517623


对比旧的


image-20230804143615322


重构和优化


页面变化


可以看到明显变化的地方有:



  1. 新增了行政区域级地块

  2. 不再对播种、未播种和样板田分类,而是使用tag徽标

  3. 不再对每个地块单独展示服务项,而是选中地块时使用同一个区域展示

  4. 移除了信息重复的图表,移除了冗余的图例、地图控件、农事记录

  5. 灾害信息亦作为服务项、不再单独列出

  6. 新增了时间轴替代了原本的轮播图


代码变化


不明显的变化在于:


移除所有生产环境的log

image-20230804144029382


控制台干净到一尘不染,生产环境的log全部被移除了,且使用ts编写,运行阶段基本不会出错,使用了autofit.js,一行轻松适配设计稿下任何分辨率(就是强无敌)


移除所有第三方大型UI组件库

image-20230804144258724


未使用任何大型UI组件库(我管你能不能树摇),这也使得该项目体积被压缩到了极限,打包后仅有 1M+


清晰代码结构

image-20230804144439678


代码结构清晰,现在内容比较少,但不难看出结构还是比较规范的,基本沿用了vite创建的默认结构,且静态文件基本没有,这也是保证打包体积的重要因素。


使用pinia持久化存储

image-20230804144806244


因为全篇使用了 pinia 持久化存储,开发时不用关心数据什么时候get什么时候set了,随时使用store,即可得到、设置数据,极大的提高了开发幸福感。


接口分类封装

image-20230804145117239


接口分类封装,调用时可以清楚的知道在调用哪个厂商的接口,并且有ts加持,绝不会少一个、错一个参数。


简化拦截器

image-20230804145317150


极简拦截器,拦截器本质上就是一个错误截获器,只需要保证后续流程不崩溃就可以了,所以这里只做了最简单的拦截器,非常好用。


简化路由、使用组件

image-20230804145538621


不出所料的,这个项目没有用到路由切换,所有的内部功能都以轻量化弹窗的形式展示,唯一需要跳转的是支付页面,但那是另一个项目,我们的解决方案是直接带着用户token跳转,简单粗暴。


基于mqtt理念的发布订阅范式

image-20230804145855326


我选择完全相信vue3的watch,以这种方式编码乍一看会很难阅读,但是实际上是仿照了mqtt的消息订阅机制,把watch当成一个subscribe,把store.xxx当成一个topic,你会发现这种写法再好理解不过了,并且这种做法很爽,你如果需要订阅一个数据的变化而做出一些操作的话,就写watch就好了。


清除冗余代码

image-20230804150605361


没有冗余代码,没有!函数、变量命名清晰规范,主打的就是一个清晰,你看我这段代码需要注释吗?


编码规范

image-20230804150758323


规范、规范、还是他妈的规范,这就是规范的节流器,都跟着写!


使用ts定义类型

image-20230804150944425


明确的类型,定义类型可能多花五分钟,但是会在编码时节省一小时。


开发轻量级必要组件

image-20230804151157258


自定义selecter、toast等组件,不需要的一点不写,主打的就是一个轻量。


打包


整个开发流程下来,打包体积达到了惊人的1.94M(少林功夫好耶,太棒辽啊🥳),线上运行表现基本令人满意(少林功夫,够劲!顶呱呱呀🥳)


image.png


完事


虽然在第一周改完99%后,又加了大大小小的新需求和新改动,不过在规范的开发面前都迎刃而解,编码成了一种享受,对于这种轻量级项目,就要用轻量级的方法去实现,搞得笨重的像一头装甲熊(没说沃利贝尔),没有任何意义,关于这次重构,除了技术上的学习,还有理念上的进步,往往一个前端项目,只需要保证页面不卡顿、不报错、不崩溃就行了,不要剪了芝麻丢了西瓜、头重脚轻、舍本逐末、南辕北辙。我是德莱厄斯,共勉。


作者:德莱厄斯
来源:juejin.cn/post/7263315523537928250
收起阅读 »

近年来项目研发之怪现状

简述 近年来,机缘巧合之下接触了不少toG类项目。项目上颇多事情,令人疑惑频频。然而屡次沟通,却都不了了之,长此以往,心力愈发交瘁,终究心灰意冷,再无劝谏之心。 令人困惑的项目经理 孟子说天时不如地利,地利不如人和。而项目上遇到的很多事情,天时、地利终为...
继续阅读 »

简述



近年来,机缘巧合之下接触了不少toG类项目。项目上颇多事情,令人疑惑频频。然而屡次沟通,却都不了了之,长此以往,心力愈发交瘁,终究心灰意冷,再无劝谏之心。



令人困惑的项目经理



孟子说天时不如地利,地利不如人和。而项目上遇到的很多事情,天时、地利终为少数,多数在人和。



立项开工,项目经理自然是项目上的第一把手。既为第一把手,自要有调兵遣将,排兵布阵的能耐。


当然用我们业内的话来说,可分为下面几类:


第一等的自然是懂业务又懂技术,这样的项目经理可运筹帷幄之中,决胜千里之外,当然这般的项目经理可遇而不可求。


这第二等的懂业务不懂技术,或者懂技术不懂业务,这样的项目经理,辅以数名参将,只要不瞎指挥,也可稳扎稳打,有功无过。


第三等的项目经理,业务与技术皆是不懂,如这般的项目经理,若尽职尽责,配先锋、军师、参将、辎重,最好再辅之以亲信,也可功成身退。若其是领导亲信,那更可说是有惊无险了。


而这第四等的,业务与技术不懂也就罢了,既无调兵遣将之才,又无同甘共苦之心,更是贻误战机,上下推诿。若其独断专横,那便是孔明在世也捧不起来。



有这般一个项目,公司未设需求经理,常以项目经理沟通需求。工期八月,立项后,多次催促,却不与甲方沟通,以至硬生生拖了两月之后才去。然而不通业务,不明技术。甲方被生耗两个月才沟通需求,这样的情况下,如何能顺利进行,以至于项目返工现象,比比皆是。多次提及需求管理,亦是左耳进右耳出。类类数落问题,甲方、研发、产品都有问题,独独他自身若皎皎之明月,灿灿之莲花。然而纵是项目成员承星履草,夜以继日,交付一版之后。举目皆是项目经理之间的恭维之词。



我有很多朋友是优秀的项目经理。言必行,行必果。沟通起来非常愉悦。偶尔遇到一个这样的人,确实让我大开眼界。


其实我也想过,这并非是项目经理职位的问题,实在是个别人自身的问题,这样的人,在任何岗位都是令人恼火的。


技术人员的无力感


我们互联网从业者经常听到一个词,技术债。技术债是令人难受的,尤其是在做项目的时候。做产品,我们可以制定完善的迭代周期,而项目,当需求都无法把控的时候,那么就意味着一切都是可变的。


糟糕的事情是,我遇到了这样的项目。前期无法明确的需求,项目期间,子虚乌有的需求管理,项目中不断的需求变更,deadline的不断临近,最终造就了代码的无法维护。


我从未想过,在同一个紧迫的时间阶段,让研发进行需求研发、bug修复、代码解耦,仿佛每一件事情都很重要。当然,我更建议提桶跑路,这样的项目管理,完全是忽视客观现实的主观意识。


前端规范难落地


公司是有前端规范的,然而前端规范的落地却很糟糕。如果使用TS,那么对于诸多时间紧,任务重,且只有一名前端开发人员的项目来说,显得太过冗余了。所以依旧使用js,那么代码中单个性化不会少见。使用esLint怎么样呢?这当然很棒,直到你发现大部分成员直接将esLint的检查注释了。或许还可以依靠团队内不断的宣讲与code Review,这是个好主意,然而你会发现,公司的code Review也是那么形式化的过程。


或许对一些企业来说,代码的规范性不重要,所谓的技术类的东西都显得没那么重要。只有政府将钱塞到它的口袋里这件事,很重要。


崩盘的时间管理


那么,因为各方面的原因,项目不可避免的走向了失控。时间管理的崩溃,项目自然开始了不断的延期。在私下里,一些擅长酒桌文化的甲方与项目经理,开始了酒桌上的攀谈,推杯换盏之间,开始了走形式的探讨。灯红酒绿之间,公司又开始了例行的恭维。


当然,我依旧无法理解,即使管理的如此糟糕,只要在酒桌上称兄道弟,那便什么问题都没有了?若是如此,项目经理面试的第一道题,一定是酒量如何了。

作者:卷不动咯
来源:juejin.cn/post/7263372536791433275

收起阅读 »

即时通讯 IM 永久免费版+高额赠费,环信发布「着陆」计划!

环信一直致力于提供稳定、安全、易用的即时通讯云服务,10年来积累了40余万开发者,我们提供了永久免费版、专业版、旗舰版等版本。如果您希望从第三方切换接入环信的 IM 服务,欢迎了解并加入环信「着陆」计划,我们为所有用户(包括免费版)提供全套迁移方案及文档,高额...
继续阅读 »

环信一直致力于提供稳定、安全、易用的即时通讯云服务,10年来积累了40余万开发者,我们提供了永久免费版、专业版、旗舰版等版本。如果您希望从第三方切换接入环信的 IM 服务,欢迎了解并加入环信「着陆」计划,我们为所有用户(包括免费版)提供全套迁移方案及文档,高额赠费,同时环信技术支持团队将免费7*12小时为您的迁移工程保驾护航。

环信「着陆」计划适用客户




「着陆」计划方案概述


环信「着陆」计划提供如下完善的迁移方案、迁移服务保障、业务政策,这套方案经受过拥有「数千万级用户」和「数亿级消息量」的客户项目验证,迁移过程敏捷、平稳、安全,已经成功为多家客户实现了系统迁移。

迁移方案分为【一次性迁移方案】和【平滑迁移方案】,两个方案均可以达到从第三方IM厂商迁移到环信 IM 系统的能力。


【一次性迁移方案】


适用于新应用上架,可以强制所有的老应用升级至新应用,此方式下不存在新老应用的兼容问题。
优势:集成时间短、工作量小、维护一套系统;
劣势:有可能损失用户;
角度:需要客户判断,让用户强制更新APP是否会导致用户卸载不更新或者使用环境无法更新的情况,如果几率小可以采用该方案。

【平滑迁移方案】


在迁移过程中,环信 IM 服务器和原 IM 服务器同时提供服务,新应用和旧应用并存,支持新旧应用互通。待用户逐步更新至新应用,原 IM 服务器停止服务。
优势:不损失用户,客户无感知
劣势:前期需要客户维护两套系统,工作量多几个环节。
角度:需要客户判断,如果需要新老用户同时并存,保证用户的体验和避免损失,建议采用平滑迁移方案。


项目迁移整体进程


迁移环节分为5步,根据业务复杂度不同预计2周~4周完成:



迁移服务保障

为保障每一位客户平稳有序迁移至环信系统,「着陆」计划提供以下全程免费的迁移服务保障,协助客户做好数据快速迁移工作,实现新老系统的快速切换和上线。

1. 详尽的《环信IM平滑迁移手册》
该手册从迁移准备、环境搭建、迁移步骤、数据导入、消息格式转换、服务端集成等流程均有详细文档说明。

2. 迁移解决方案讲解及建议
根据客户具体的业务场景,制定系统迁移解决方案,环信解决方案架构师将提供具体的方案讲解、培训及建议。

3. 搭建中转服务器方案指导
根据客户具体场景,提供专业的服务端解决方案,指导协助客户搭建中转服务器等。

4、全程免费技术支持
提供全程即时沟通渠道,创建1vs1技术支持群,7x24h提供免费技术咨询,高效解决问题。


迁移福利政策


从其他三方平台迁移至环信 IM 系统的客户,均享有以下业务政策





项目迁移案例

某垂直招聘领域客户,拥有6000万注册用户、300万日活的招工大数据平台,最初使用友商 IM作为Android、iOS、20多个小程序平台的沟通通道,后期因为IM系统稳定性等多种原因迁移到环信。

采用环信VIP集群,确保系统的安全、稳定和资源冗余能力

项目迁移从6月初开始接触,经过1个月的方案论证于7月初开始进行集成,先后完成了三个客户端SDK+服务端SDK集成、中转服务器的开发等工作,并于7月底完成原始数据的迁移。待测试完成后,在8月份分批开放小程序、iOS端和Android上线。

项目期间,环信除了提供标准迁移接口,同时还为该客户单独开发了一些接口,用于满足特殊业务场景的需求。

迁移完成后,线上业务稳定有序,项目圆满交付。


加入「着陆」计划


环信秉承以客户为本,不负每一位客户的信赖与支持,诚邀您加入「着陆」计划,与环信携手并进,共铸辉煌!如果你的项目正在或可能要涉及到 IM 系统迁移,或想了解《环信IM平滑迁移手册》详细说明,欢迎通过以下联系方式与我们聊聊。



业务联系电话:400-622-1776

快速注册环信:https://console.easemob.com/user/register

了解环信 IM:https://www.easemob.com/product/im

环信「着陆」计划:https://www.easemob.com/event/landing


收起阅读 »

恋人没在身边?这些小动作出轨几率竟高达76%!

“前几天我突然注意到,我老公最近总是在深夜加班,有时候甚至一周加班3次以上,快12点才下班回家。你说,他是不是在外面有了新欢?”我的好闺蜜李芳跟我吐露,一脸失落。 常与异性同事出去应酬 “上周我老公又说要和一个新来的女同事叫林萍一起出去谈项目。我...
继续阅读 »

“前几天我突然注意到,我老公最近总是在深夜加班,有时候甚至一周加班3次以上,快12点才下班回家。你说,他是不是在外面有了新欢?”我的好闺蜜李芳跟我吐露,一脸失落。



常与异性同事出去应酬



“上周我老公又说要和一个新来的女同事叫林萍一起出去谈项目。我提出一起去,他还拒绝了。他们公司这些应酬我都不放心,谁知道喝多了会发生什么。”



李芳愤愤不平地说。


其实,我理解李芳的担心。经常与异性同事单独出去应酬,的确很容易引起歧义。但作为老公,他也有自己的社交圈子。如果李芳表达疑虑后,老公还能体贴她的感受,不再频繁应酬就最好不过了。但如果老公不改,李芳也要学会给他合理的空间,不要过分限制,免得适得其反。


主动帮异性同事搬重物



“还有一次,他们单位搬家,我老公就主动帮那个女同事林萍一起搬箱子。也不知道他们在搬箱子的时候聊了些什么,搬完还笑得那么开心。”



说到这里,李芳红了眼眶,泪水在眼眶里打转。


帮忙搬东西本身无可厚非,但过分殷勤确实也容易引发类似误会。作为朋友,我建议李芳不要先入为主,与老公沟通后再下判断。如果仅是顺手帮同事的举动,也不必过分猜疑。


经常参加异性同事的生日party



“上个月那个女同事林萍生日,我老公还特意去参加了她的生日party,送了礼物。他居然都不跟我提前说一声,我要不是看到他朋友圈,都不知道这件事。”



李芳愤愤道。


参加同事生日party本是正常的社交活动,但老公不告知李芳确实有些过分。作为朋友,我劝李芳要学会委婉地表达自己的感受,让老公理解你的想法,而不是一味地指责。相处之道需要双方不断磨合,这也需要时间。


过于关注异性同事的社交动态



“我还发现我老公最近老是看那个林萍的朋友圈,所有的照片都点赞。我跟他提这事,人家说只是礼貌性赞一下,又不是什么大不了的事。太不正常了!我都开始怀疑他俩之间有事了。”



说到这,李芳已经禁不住泪水盈眶。


关注同事动态固然正常,但过分频繁的点赞确实也容易引起误会。不过我认为李芳不应仅凭此就随意猜疑,更好的方法是与老公坦诚交流这个问题,给老公一个解释的机会,也让老公注意到李芳内心的不安,从而做出调整。


......


经过与李芳的交流,我理解她的疑虑和不安。但与其胡思乱想导致误会,不如与老公好好沟通,让他知道你的想法,给他一个解释的机会。只有双方互相理解信任,婚姻才能长久。相信李芳也能做出正确的选择,与老公建立平等和睦的关系。


作者:wuxuan0208
来源:mdnice.com/writing/93d11978e60f43f698ffe292bd2e6f61
收起阅读 »

成功人士早已悉知的20条道理

1.对抗内卷的最好方法是找到自己独特的生态位,也就是自己的独特性,不可替代性 2.人对自己失去东西的恐惧远大于想要获得东西的欲望 3.我们喜欢那些认为我们是对的人和观点,我们讨厌那些认为我们是错的人和观点 4.能说会道不是一个好销售,投其所好才是 5....
继续阅读 »

1.对抗内卷的最好方法是找到自己独特的生态位,也就是自己的独特性,不可替代性


2.人对自己失去东西的恐惧远大于想要获得东西的欲望


3.我们喜欢那些认为我们是对的人和观点,我们讨厌那些认为我们是错的人和观点


4.能说会道不是一个好销售,投其所好才是


5.同一层面的问题不可能在同一层面得到解决,只有在高于他的层面才能解决


6.能力驱动成功,但是当能力无法被衡量时社会网络驱动成功


7.任何一家公司,最重要的三个因素就是人,钱,事


8.人在财务状况越糟糕的时候,赌性也就越强,长此以往恶性循环


9.由于大多数人都是不自信的,仅仅表现得很自信就已经超越了大多数人


10.只要比普通人忍受更多痛苦,总有普通人得不到的机会向你打开


11.天下没有卑微的工作,赚钱机会就在泥里面去找


12.认清自己的优势和弱势,信息差永远存在


13.这个世界永远有懒人,也就永远总有靠信息差赚钱


14.有街头智慧的人往往具有几个特质:察言观色,捕捉需求,极端务实,没有幻想,专注目标,忍受痛苦


15.任何的知识智慧和想法,如果没有变成结果,他都只是你的潜力,而不是你的实力


16.###### 任何事业干的出色的人,都会告诉你,脑力劳动最大的门槛其实就是 体力


17.那些最能把书本智慧和街头智慧结合起来的人,最后都具有极强的逆袭能力。


18.如何挖掘大城市的受益?建立更多链接,进入好公司,参加同行交流、建立跨行人脉


19.有三样东西可以让人的友情变得深厚,就是健康,财富,后代。可以从这三个方面拉近人与人的关系


20.在三十岁之前,大多数人用钱来赚钱的能力,远不如自己通过劳动来赚钱


作者:美人薇格
来源:mdnice.com/writing/98d59933de5749baa9c66a23d1b3fdd1
收起阅读 »