注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

使用Compose DeskTop实现一个带呼吸灯的秒表

前言 Compose Multiplatform是由Jetbrain团队维护的一个基于Kotlin和Jetpack Compose用于跨多平台的共享UI的声明式框架,目前支持的平台除了Android以外,还有iOS,Web和桌面,如此厉害的技术怎么能不亲自上手...
继续阅读 »

前言


Compose Multiplatform是由Jetbrain团队维护的一个基于Kotlin和Jetpack Compose用于跨多平台的共享UI的声明式框架,目前支持的平台除了Android以外,还有iOS,Web和桌面,如此厉害的技术怎么能不亲自上手尝试一下呢,所以这篇文章要讲的就是使用Compose Desktop开发一个桌面版的秒表应用


准备工作


在开发之前,我们要确定下使用的开发环境,这里我使用的编辑器是IntelliJ IDEA 2022.3.3这个版本,JDK环境用的是11,貌似是最低要求。


image.png

如何创建项目就不说了,现在很多文章都有详细讲解,我们直接开始吧


创建视图


首先我们的秒表肯定是有开始计时与结束计时两个状态,所以我们的界面上需要一个按钮来控制这两个状态,那么第一步就是在main函数的Window组件内绘制出这个按钮


image.png

其中turnOn变量就是我们控制状态的开关,通过Button的点击事件来改变,并且按钮的文案也随着状态的更改显示不同的文字,Clock就是我们绘制秒表的函数,并且接收turnOn这个变量控制秒表的计时。好了以后我们看一下效果


image.png

正如我们预想的一样,一个简单的桌面应用就出来了,接下来就开始绘制我们的秒表


绘制外框


秒表的外框通常来讲就是个圆,而使用Canvas绘制圆有两种选择,一种是使用drawCircle,另一种是使用drawPath,但考虑到drawCircle无法定义边框的大小,所以我们直接使用drawPath函数,绘制Path的话我们需要定义几个变量,分别是中心点坐标,Rect的左上坐标和右下坐标,代码如下


image.png

表盘选取在水平居中位置绘制,其中在中心点的y坐标,以及Rect的y坐标上都加上100的原因主要是为了如果边框的粗细设置的比较大的话,表盘不会被视图遮挡,现在我们就在Canvas中绘制定义好的Path


image.png

一个简单的边框就绘制好了,我们看下效果


111.png

又大又圆,边框绘制完毕,接下去就是表盘的刻度了


绘制刻度


刻度的样式每一种表盘上都不一样,我们这边就简单一些,就在5,10,15这样的刻度上显示文字,其他位置用圆点代替,不然60个数字画一圈怕是太密密麻麻了,那怎么做呢?我们分两步来,第一步先画数字,以下是我们需要用到的变量


image.png

由于绘制数字的方向是在一个圆周上的,所以我们定义一个数组angList存放绘制角度,同时也相对应的定义另一个数组textList存放数字的文案,circleRdius是表盘半径,用来计算圆周坐标用,现在就是要绘制文案了,我们使用DrawScopedrawText函数,有的人会说DrawScope下面哪来的drawText啊?那是因为drawText是在Compose 1.3.0版本推出的,所以如果找不到drawText的话,那就赶紧去更新版本吧,我们看下drawText这个函数都提供了哪些参数


image.png

可以看到必填的参数是前两个,一个是TextMeasurer对象,用来测量文案的,另一个不用多说,设置text,然后topLeft这个属性也是需要的,总不能12个数字都叠在一起吧,知道了要填的参数,我们现在就调用一下drawText


image.png

我们使用rememberTextMeasurer函数创建了一个TextMeasurer对象,并且使用pointXpointY分别计算了每个数字坐上的x,y坐标,两个函数的代码如下


image.png

这里至于为什么要在Offset函数中分别对计算出来的x,y减去20,主要是因为虽然计算出来的坐标是刚好在圆周上,但是当文字绘制出来以后,整体布局会有点偏右下,所以得在结果坐标上再减去20,让文字可以刚好看起来在圆周坐标的中心位置,现在我们运行下代码看下效果怎样


image.png

可以看到数字都画上去了,效果还行,接下来就是圆点刻度,同样定义需要用到的变量


image.png

degreeColor是绘制圆点刻度的颜色,pointAngleList跟上面的anglist一样,是存放圆点角度的数组,虽然说这个数组的大小定义为60,但是在lambda表达式中我们判断了如果计算出来的角度在anglist中已经存在,那么就不赋值用0代替,最终绘制的时候我们判断如果角度为0,那么就不绘制,所以0度的刻度不会被绘制在表盘上,而绘制圆点我们直接使用drawCircle函数,代码如下


image.png

因为同样也是在圆周上,所以计算圆点的坐标也用到了pointx与pointy函数,我们再看下效果


image.png

有内味儿了是不,我们接下来开始画指针


绘制指针


指针其实就是一根line,我们使用drawLine函数就能绘制出来,另外我们在中心点位置再绘制一个圆点,当作是把指针固定在表盘上的一样,代码如下


image.png

其中pointerColor是指针和圆点的颜色,运行一遍代码,我们看到指针已经绘制上去了


image.png

但是指针跟刻度不一样,它得是能绕着圆点动的,怎么动呢?我们看到上面那根静态指针绘制的角度是在angList[0]上,那是不是不停的改变角度,我们的指针就动起来了呢?我们来定一个数组来存在所有需要经过的角度


image.png

totalList就是存放所有角度的数组,至于intervalSize是什么呢,我们知道有的秒表上指针是一格一格走的,间隔比较大,有的间隔比较小,看起来的效果就比较丝滑,intervalSize就是定义指针走动的频率大小的值,并且是能够被360整除的,数组定义好了,我们再给数组下标创建个动画


image.png

这里创建了一个循环动画,因为totalList遍历完一遍以后,代表着一分钟过去了,角度又得重新开始遍历,所以我们给数组的下标值定义了一个循环动画,另外我们还使用LaunchedEffect函数,来监听外部传来的turn值的变化,turn为true的时候,angleIndex的初始值目标值不同,动画开启,turn为false的时候,angleIndex的初始值目标值相同,动画暂停。我们更新下CanvasdrawLine的代码,让drawLine里面获取角度的下标值的变量变成angleIndex


image.png

我们看下效果


aaa1.gif


文字时间


一个秒表的表盘绘制完毕,我们再加点东西,一般性一个秒表底下都会有个文字时间在跳动,差不多由分,秒,毫秒组成,我们这边也加上这些东西,并且在分与秒之间用文字“分”隔开,秒与毫秒之间用“秒”字隔开,那么这五个Text我们要计算出它们topLeft的坐标


image.png

文案的y坐标很容易,就是在表盘底部y坐标上再加点距离就好,至于横坐标,就是找出中间一块区域再五等分,坐标定义完毕,我们先把两个中文绘制出来,x坐标取timeXList下标为1和3的值


image.png

接着我们想一下毫秒位置的数字怎么展示,毫秒位置是在一秒内从0跳到99,然后再从0跳到99,这不又是个循环动画吗,我们仿照指针的动画,将毫秒的动画创建出来


image.png

同样的,因为毫秒的动画也跟随着turn值的变化而改变,所以我们将这个过程也在LaunchEffect中添加上


image.png

现在我们可以在Canvas中将毫秒也绘制出来了


image.png

这边还做了一个处理,当毫秒的值为个位数的时候,我们在数字的边上再加上一个0,让数字跳动的时候看起来效果好一些,毫秒的位置已经绘制完毕,秒的位置也一样,因为它也是从0到60变化的一个循环动画,所以它的代码与毫秒基本差不多


image.png

现在我们再看下效果


aaa2.gif


还剩下分的位置,分就不能用循环动画来实现了,它是一个逐渐递增的过程,当秒的位置为从59变回0的时候,分的位置加一,那么我们就需要一个变量来记录分的值


image.png

minuteValue用来记录分钟的值,然后我们在Canvas里面判断当mainSecondText刚到59的时候,就准备开始给minuteValue加一,为什么是开始准备而不是立马加一呢,因为如果那样做的话,显示的效果是秒的位置一到59秒的时候,分就加一了,这就不符合实际了,我们希望是当59变为0的那会分才加一,所以我们还需要一个状态位,当mainSecondText变为59的时候,状态位打开,直到mainSecondText变成0的时候,状态位才关闭,这个时候分才加一,我们把状态位命名为addMinute


image.png

给分钟设置值的代码如下


image.png

再运行一遍代码看看效果如何


aaa3.gif


完美的衔接起来了,这样一个秒表的功能就基本完成了,我们稍微在点缀一下,如标题所示,加个呼吸灯


呼吸灯效果


在做这个效果之前,这里有个问题,大家是否知道在Compose里面如何给视图设置渐变色?使用drawable吗?Compose里可不兴这些,咱回忆下我们在调用drawpath函数的时候,编辑器是不是会给出这样的提示


image.png

有两个drawPath的函数,这俩函数的区别是在第二个参数上,一个是Color,另一个是Brush,我之前通常都是用Color的,因为Brush是个啥我也不知道,但是当我看到Brush里面的代码以后


image.png

看到第一行注释没,这个其实就是用来做渐变效果的,它比我们传统Android里面设置渐变功能还要丰富,不但渐变的颜色没有限制,方向也没有限制,也就是说你可以在任意两个点之间设置若干种颜色的渐变,现在我们就在我们秒表的边框上设置三种颜色的渐变吧


image.png

首先设置好我们要渐变的颜色值,然后将这个存放颜色值的circleColorList当作参数传入drawPathBrush


image.png

边框的粗细也加大到了30,这样也能清晰的看到渐变效果,现在运行后的效果如下


image.png

效果出来了是不,现在是三个颜色的渐变,那既然刚刚说了Brush的渐变颜色可以是若干个,那么我们在circleColorList中再添加几个颜色试试


image.png

从刚刚的三个变成了六个颜色的数组,再运行一下看看效果会怎么样呢?


image.png

是不是跟刚刚的那个效果图比起来,这个时候的边框渐变色更多了呢,到了这里,咱有个想法,通过之前的循环动画,我们能不能将Brush里面的渐变色值也循环起来呢,比如先设置的是circleColorList下标为0,1,2的颜色,接下去就是显示下标为1,2,3的颜色,以此类推,下标值到了数组末尾,下一个再从头开始,这么做到底会有什么效果呢,我们试一下


image.png

如上述代码所示,我们创建了一个初始值为0,目标值为circleColorList.lastIndex的循环动画,动画时长为两秒,接下去,我们通过判断不同的下标值场景来选取不同的颜色来绘制边框


image.png

由于是三种颜色的渐变,所以场景选择了如果colorIndex为数组最后一个下标,colorIndex为数组倒数第二个下标,以及其他情况,现在我们再来看看边框效果


aaa4.gif


是不是就像表盘周围安置了一个呼吸灯一样,但是这个呼吸灯还不是很完善,因为我们看到的效果,这个呼吸的过程是慢慢从浅色开始,逐渐变深,然后由深变浅是一瞬间的过程,感觉像是这个呼吸被打断了一样,造成这个效果的原因是我们circleColorList数组里面的色值,根据下标的递增是逐渐变深的,但是缺少逐渐变浅的过程,所以我们应该在circleColorList中再增加几个色值,也就是将原来的色值顺序倒转一下添加进去,就像下面这样


image.png

这样就满足了我们呼吸灯由浅变深和由深变浅的两个过程,我们再看看效果


aaa5.gif


总结


Compose DeskTop的秒表功能完成了,这也是我Compose Multiplatform的第一个demo,先选择DeskTop主要是因为几个跨平台里面只有DeskTop与Android的代码算是真正意义上的一套代码跨平台使用,Web主要是多了几个Dom组件,Android里面没法使用,而iOS现在也只是刚刚发布Alpha版,我还在摸索学习中,所以先用DeskTop开个场,后面别的平台的小应用也会相继推出。


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

使用 Compose 时长两年半的 Android 开发者,又有什么新总结?

大家好啊,我是使用 Compose 时长两年半的 Android 开发者,今天来点大家想看的东西啊,距离上次文章也已经过去一段时间了,是时候再次总结一下了。 期间一直在实践着之前文章说的使用 Compose 编写业务逻辑,但随着业务逻辑和页面越来越复杂,在使用...
继续阅读 »

大家好啊,我是使用 Compose 时长两年半的 Android 开发者,今天来点大家想看的东西啊,距离上次文章也已经过去一段时间了,是时候再次总结一下了。

期间一直在实践着之前文章说的使用 Compose 编写业务逻辑,但随着业务逻辑和页面越来越复杂,在使用的过程中也遇到了一些问题。


Compose Presenter


上一篇文章中有提到的用 Compose 写业务逻辑是这样写的:


@Composable
fun Presenter(
action: Flow<Action>,
)
: State {
var count by remember { mutableStateOf(0) }

action.collectAction {
when (this) {
Action.Increment -> count++
Action.Decrement -> count--
}
}

return State("Clicked $count times")
}

优点在之前的文章中也提到过了,这里就不再赘述,说一下这段时间实践下来发现的缺点:



  • 业务复杂后会拆分出非常多的 Presenter,导致在最后组合 Presenter 的时候会非常复杂,特别是对于子 Presenter 的 Action 处理

  • 如果 Presenter 有 Action,这样的写法并不能很好的处理 early return。


一个一个说


组合 Action 处理


每调用一个带 Action 的子 Presenter,就至少需要新建一个 Channel 以及对应的 Flow,并且需要增加一个对应的 Action 处理,举个例子


@Composable
fun FooPresenter(
action: Flow<FooAction>
)
: FooState {
// ...
// 创建子 Presenter 需要的 Channel 和 Flow
val channel = remember { Channel<Action>(Channel.UNLIMITED) }
val flow = remember { channel.consumeAsFlow() }
val state = Presenter(flow)
LaunchedEffect(Unit) {
action.collect {
when (it){
// 处理并传递 Action 到子 Presenter中
is FooAction.Bar -> channel.trySend(it.action)
}
}
}

// ...

return FooState(
state = state,
// ...
)
}

如果页面和业务逻辑复杂之后,组合 Presenter 会带来非常多的冗余代码,这些代码只是做桥接,没有任何的业务逻辑。并且在 Compose UI 中发起子 Presenter 的 Action 时也需要桥接调用,最后很容易导致冗余代码过多。


Early return


如果一个 Presenter 中有 Action 处理,那么需要非常小心的处理 early return,例如:


@Composable
fun Presenter(
action: Flow<Action>,
)
: State {
var count by remember { mutableStateOf(0) }

if (count == 10) {
return State("Woohoo")
}

action.collectAction {
when (this) {
Action.Increment -> count++
Action.Decrement -> count--
}
}

return State("Clicked $count times")
}

count == 10 时会直接 return,跳过后面的 Action 事件订阅,造成后续的事件永远无法触发。所以所有的 return 必须在 Action 事件订阅之后。


当业务复杂之后,上面两个缺点就成为了最大的痛点。


解决方案


有一天半夜我看到了 Slack 的 Circuit 是这样写的:


object CounterScreen : Screen {
data class CounterState(
val count: Int,
val eventSink: (CounterEvent) -> Unit,
) : CircuitUiState
sealed interface CounterEvent : CircuitUiEvent {
object Increment : CounterEvent
object Decrement : CounterEvent
}
}

@Composable
fun CounterPresenter(): CounterState {
var count by rememberSaveable { mutableStateOf(0) }

return CounterState(count) { event ->
when (event) {
is CounterEvent.Increment -> count++
is CounterEvent.Decrement -> count--
}
}
}

这 Action 原来还可以在 State 里面以 Callback 的形式处理,瞬间两眼放光,一次性解决了两个痛点:



  • 子 Presenter 不再需要 Action Flow 作为参数,事件处理直接在 State Callback 里面完成,减少了大量的冗余代码

  • 在 return 的时候就附带 Action 处理,early return 不再是问题。


好了,之后的 Presenter 就这么写了。期待再过半年的我能再总结出来一些坑吧。


为什么 Early return 会导致事件订阅失效


可能有人会好奇这一点,Presenter 内不是已经订阅过了吗,怎么还会失效。

我们还是从 Compose 的原理开始说起吧。

先免责声明一下:以下是我对 Compose 实现原理的理解,难免会有错误的地方。

网上讲述 Compose 原理的文章都非常多了,这里就不再赘述,核心思想是:Compose 的状态由一个 SlotTable 维护。

还是结合 Early return 的例子来说,我稍微画了一下 SlotTable 在不同时候的状态:


@Composable                                          
fun Presenter(
action: Flow<Action>, count != 10 | count == 10
)
: State {
var count by remember { mutableStateOf(0) } | State | State |
if (count == 10) { | State | State |
return State("Woohoo") | Empty | State |
} | | |
action.collectAction { | State | Empty |
when (this) { | State | Empty |
Action.Increment -> count++ | State | Empty |
Action.Decrement -> count-- | State | Empty |
} | | |
} | | |
return State("Clicked $count times") | State | Empty |
}

count != 10 的时候,SlotTable 内部保存的状态是包含 Action 事件订阅的,但是当 count == 10 之后,SlotTable 就会清空所有之后语句对应的状态,而之后正好包含了 Action 事件订阅,所以订阅就失效了。

我觉得这是 Compose 和 React Hooks 又一个非常相似的地方,React Hooks 的状态也是由一个列表维护的

再举一个例子:


@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column {
var boolean by remember {
mutableStateOf(true)
}
Text(
text = "Hello $name!",
modifier = modifier
)
Button(onClick = {
boolean = !boolean
}) {
Text(text = "Hide counter")
}

if (boolean) {
var a by remember {
mutableStateOf(0)
}
Button(onClick = {
a++
}) {
Text(text = "Add")
}
Text(text = "a = $a")
}
}
}

这段代码大家也可以试试。当我做如下操作时:



  • 点击 Add 按钮,此时显示 a = 1

  • 点击 Hide counter 按钮,此时 counter 被隐藏

  • 再次点击 Hide counter 按钮,此时 counter 显示,其中 a = 0


因为当 counter 被隐藏时,包括变量 a 在内所有的状态都从 SlotTable 里面清除了,那么新出现的变量 a 其实是完全一个新初始化的一个变量,和之前的变量没有任何关系。


总结


过了大半年,也算是对 Compose 内部实现原理又有了一个非常深刻的认识,特别是当我用 C# 自己实现一遍声明式 UI 之后,然后再次感叹:SlotTable 真是天才般的解决思路,本质上并不复杂,但大大简化了声明式

作者:Tlaster
来源:juejin.cn/post/7222897518501543991
UI 的状态管理。

收起阅读 »

git stash 用过嘛?

各位在摸鱼的时候...不是... 各位在写代码的时候离不开的git其实发现有很多小技巧,老铁们,今天咱们就来唠一唠 笔者由于能力水平非常高...不是... 笔者的司由于前端人手紧缺,不得不自己负责多个项目,同时存在一个项目中多个更新迭代同时开发的情况,所以笔者...
继续阅读 »


各位在摸鱼的时候...不是...


各位在写代码的时候离不开的git其实发现有很多小技巧,老铁们,今天咱们就来唠一唠


笔者由于能力水平非常高...不是...


笔者的司由于前端人手紧缺,不得不自己负责多个项目,同时存在一个项目中多个更新迭代同时开发的情况,所以笔者每天的工作状态就是切分支,切分支...


当一个分支的内容还没开发完,不得不切换分支去改造另一个线上bug时,此时当前分支的内容一定要推到远程分支嘛,答案,不一定


这时,我们应该想到git stash


# 保存当前未commit的代码
git stash
# 保存当前未commit的代码并添加备注git stash save "订单详情"

#
应用最近一次的stashgit stash apply

#
应用最近一次的stash,随后删除该记录git stash pop

#
删除最近的一次stashgit stash drop

删除stash的所有记录git stash clear

#
查看所有记录git stash list
#还原某个版本git stash apply stash@{stash_id}

所有的命令我都罗列于此


所以话不多说,开干


git stash save '订单详情组件封装完成'

//此时我们可以切换分支
经过一系列的操作之后
//切换到当前分支

1.懒人操作
- 如果不想麻烦and墨迹,直接操作
git stash apply

2.如果想秀一波
- 那么好,操作
git stash clear

啊......

其实我想操作git stash list

来吧,继续操作~

当误操作git stash clear时,我们应该打印所有的提交列表
git log --graph --oneline --decorate $( git fsck --no-reflog | awk '/dangling commit/ {print $3}' )
如果在这个输出的内容中可以找到你提交 那么可以通过提交的id来找回

当内容无法找到时间,我们继续操作
git log --graph --oneline --decorate $( git fsck --no-reflog | awk '/dangling commit/ {print $3}' ) >1.txt
这个命令是将输出内容输出到1.txt文件中,在文件中可以通过搜索你保存时的关键字来进行找到id

再次执行
git stash apply id

3.当我们手不抖~

执行找到stash的列表
git stash list

还原某个版本
git stash apply stash@{stash_id}
大功告成

各位这波操作怎样,哈哈哈


其实就是日常工作(mo yu)的小技巧,希望对各位有帮助!


作者:凌云空间
来源:juejin.cn/post/7221825086667014205
收起阅读 »

踏平坎坷成大道---前端还有希望吗

最近在头条还有一些其他平台经常会看到一些论调“前端已死”,各种找工作难之类的信息,同时在自己身边也有不少朋友、同事持有同样的看法;作为前端开发接近10年的老菜鸟也想谈谈自己的一些看法【个人拙见,欢迎喷,喷的时候给出建议】 先说观点,前端未死 要知后续情节请...
继续阅读 »

最近在头条还有一些其他平台经常会看到一些论调“前端已死”,各种找工作难之类的信息,同时在自己身边也有不少朋友、同事持有同样的看法;作为前端开发接近10年的老菜鸟也想谈谈自己的一些看法【个人拙见,欢迎喷,喷的时候给出建议】



先说观点,前端未死



要知后续情节请看下面分解,如有雷同实属巧合;头条或一些平台的网友言论有很多带节奏的成分,而不是真实的!



为什么很多人说前端已死


找工作难



  • 不可否认的是这两年受疫情影响、国际国内经济形式的影响,我们身边能看到、能感受到各行各业的日子都不好过,各种内卷、各种无奈;我所在的公司在年前也进行了一波人员的精简,组内成员减半,这个已经是不争的事实;




  • 我们发现刷BOSS、刷智联看到的岗位似乎比前几年同期少了很多,发信息得到回复的次数也在减少,似乎简历就像石头一样沉入招聘的大海,隔壁王奶奶就跟我吐槽过多次;




  • 发现身边、各种论坛、网络上充斥着各种悲观的论调,讨论的热度不比36度的夏天差多少;




面试要求高



  • 隔壁王奶奶经常说,前几年简单问问Vue\React原理、生命周期、JS基础,聊聊项目经验就能收到几份Offer,自己还在选择哪家公司更有钱景; 而现在呢? 一上来就问源码、算法、数据结构、还有让直接手写源码的,就问各位看官慌不慌、惊不惊?




  • 说实话,在前端岗位摸打滚爬接近10年,感觉自己老了却还是个菜鸟,很多原理性的东西真的很弱,计算机基础也很差(早期培训了),上班干活也就每天那一、二、三3板斧;诸位有没有跟我一样的,面试很没底呢!




有没有核心技能

既然面试要求那么高,那么卷,这个时候就要问问自己到底有没有核心竞争力呢?很庆幸目前我{ age: 36 }目前尚有一份可维持生计的工作(可能也快了),短期还能跟大家一期扯淡;自我感觉10年下来我并没有掌握核心技能,诸位呢?如果一定要回答自己有什么信心敢出去面试,大概有这么几点吧:



  1. 脸皮够厚,不怕被拒绝,能怼人

  2. 得益于早些年坚持锻炼,现在加班熬夜还能和年轻人一战

  3. 长期的积累,基础还行,目前还学得动

  4. 得益于长期的积累,对待产品、项目有一些心得和自己的看法


前端的路在哪?



在回答该问题之前我想说的是:当下的局面对刚刚入行、初级前端是很不友好的,对于已经在行业内站稳了,有几年工作经验的前端朋友而言,处境并没有那么悲观;所以,这里只聊聊已经有几年经验的情况;





  • 首先、我们通常讲的前端大部分还是停留在HTML + CSS + JS的模式,与后端相对, 那前端就是在画页面,调数据,写交互,诸位的工作是这样吗?




  • 其次,前端在项目中、在团队中的核心价值是什么?




  • 最后,面对当下日益复杂的业务,前端能抢谁的饭碗,而谁又会抢前端的饭碗?




前端在团队、项目中的核心价值是什么?


要回答上面的问题,需要先弄清楚前端在团队、项目中的核心价值是什么,我个人认为,前端的核心价值在于:【用户体验】【用户体验】【用户体验】,前端不是页面仔,也不是接口仔,更不是切图仔!前端应该是一个胶水性质的岗位,哪里有漏堵哪里;在产品经理-设计师、UI设计师-UE交互设计师、UI\UE - 前端、 前端-后端、 后端-测试、
产品经理-项目经理,这些诸多环节之间做一个适配器的角色,在项目的全程去理清开发的每个环节,为项目的高质量交付发挥不可替代的作用;



说了那么多,前端就是要充当适配器的角色呀,那么是不是就意味着我们需要掌握的技能就要很多呢?要接触很多的面呢? 答案是肯定的;合格的前端技能至少是一超多强的,一精多能的,这也就是在面试中对初级不友好,对老鸟相对友好一点的根结!而这些技能的积累,产品、项目的把握能力需要一定的积累,短期怕是无法达到的;三点,重新审视自己的岗位:



  1. 【站在产品经理的角度去理解前端】

  2. 【站在项目经理的角度去理解前端开发的过程】

  3. 【站在用户的角度去理解前端的产出】


前端能抢谁的饭碗? 谁又来抢前端的饭碗?

这个嘛....(⊙o⊙)…,の,打败自己的从来都不是自己的同行,想这些是无意义的,把眼光盯在本公司同事的身上是无意义的;有个很好笑的现象是:很多人总是气不过自己身边的同事、朋友比自己每个月多几千块钱,而不生气身价百万、千万、甚至过多的成功人士; 所以我说,如果真的要抢,那应该从其他与之(前端)有关联的行业找出路;


如何破局?


如何提高薪资,如何不被抛弃? 这个问题思考了很久,反复拷问自己接下来该怎么走?最近几年也做了一些尝试,有的放弃了,有的没下文了;在划水之余关注了BOSS招聘一年多时间,看到了一些信息,供大家参考;





  1. 如果已经有多年的经验,前端架构师是不错的选择(最近1年在BOSS上看到,架构师的需求在不断增加),建议:一定要会NodeJS,最好学学其他后端语言、服务器、数据库,可以不写,但是一定要知道之间的区别,能做什么,适合做什么,如何选择;




  2. 大前端方向现在还有很大空间: Vue\React + Flutter(或类似) + 小程序,这里有个高的要求:建议去学一下原生的Android、IOS开发,至少知道是如何运行的,和混合式开发等区别是什么,如何同前端交互;




  3. 前端 + GIS 方向,这个是很推荐的!(最近1年看到gis岗位的招聘增加非常多)




  4. 前端 + 3D + GIS (各位看官,去看看BOSS吧,如果这些你同时拥有,还会担心工作岗位吗)




以上的都是硬实力,也就是面试时候绕不开的; 接下来的软实力,就是作为适配器角色的核心:



  • 沟通能力;

  • 对产品的理解能力;

  • 项目把控能力;


这个需要积累,决定前端的尽头最终走向哪的,前端最终的出路就是不是前端,可能是?


....


...


..


.


BOSS!
谁说前端出生就不能成为老板呢? 前端走向产品经理一定要强于一出来就是做产品的产品经理(当然其他岗位也可以),总之方向很多吧!


聊到

作者:风雪中的兔子
来源:juejin.cn/post/7220220100384817210
这里吧,该撸代码了!

收起阅读 »

必须会的前端基础通用优化方法

web
为什么要做优化? 虽然现在网速越来越快,客户端性能越来越好。但是还是有很多人在使用老旧的设备,不稳定的或者2G、3G网络。另外用户现在对应用的体验要求也越来越高,用户不仅会拿你的应用和同行业的竞争对手去做比较,并且会跟使用过的做的最好的应用去比较。 优化应用的...
继续阅读 »

为什么要做优化?


虽然现在网速越来越快,客户端性能越来越好。但是还是有很多人在使用老旧的设备,不稳定的或者2G、3G网络。另外用户现在对应用的体验要求也越来越高,用户不仅会拿你的应用和同行业的竞争对手去做比较,并且会跟使用过的做的最好的应用去比较。


优化应用的性能可以提升用户体验从而提高留存率,转化率。谷歌、微软和亚马逊的研究都表明,性能可以直接转换成收入。比如,Bing搜索网页时延迟2000ms会导致每用户收入减少4.3%。BBC发现他们的网站加载时间每增加一秒,他们就会失去10%的用户。


下面分享一些操作简单但是效果明显的优化方法。


1、使用HTTP 2.0


HTTP 2.0通过支持首部字段压缩和多路复用技术,让应用更有效地利用网络资源,减少感知的延迟时间。


二进制分帧机制是HTTP 2.0大幅度提高网页性能的核心,它定义了如何封装HTTP消息并在客户端与服务器之间传输。HTTP 1.x的版本都是通过文本的方式传递数据,而HTTP 2.0将传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码。


HTTP 2.0在二进制分帧的基础上实现了多路复用技术,可以在同一连接上同时发送多个请求和响应,解决了HTTP 1.x的队头阻塞问题,提高了并行处理能力和性能,突破了HTTP 1.x中每个连接每次只交付一个响应的限制。


HTTP 2.0使用HPACK算法对请求和响应头部进行压缩,减少了数据传输量, 可以显著减少每个请求的开销,提高了网络传输速度。而且,它还支持服务器到客户端的主动推动推送机制。


体验demo:http2.akamai.com/demo


开启http2的方法也非常简单,下面以nginx为例


server {
-      listen       443 ssl;
+      listen       443 ssl http2;
       ...
}

2、缓存资源


浏览器发出的所有HTTP请求首先会转至浏览器缓存,用于检查是否存在可满足请求的有效缓存响应。如果存在匹配,则从缓存中读取响应,从而消除网络延迟和传输产生的数据成本。


HTTP缓存是一种提高负载性能的有效方式,因为它减少了不必要的网络请求。所有浏览器都支持该功能,并且不需要太多设置。默认情况下,大部分Web服务器内置支持设置缓存相关表头的设置。


3、缩小和压缩传输的资源


对传输的资源进行缩小和压缩可以有效减少负载大小,进而缩短页面加载时间。


像webpack中已经内置了缩小代码的插件,不需要做额外的工作就可以直接使用,可以删除空格和不需要的代码。


压缩是使用压缩算法修改数据的过程。目前使用最广泛的压缩格式是Gzip,但可以有限考虑使用Brotli,2015年谷歌推出的Brotli压缩算法能在Gzip的基础上将数据再压缩20~25%,现在大部分的浏览器已经支持这种压缩格式,国外很多站点已经开始使用,但是国内还没有开始大规模的应用。很多托管平台、CDN和反向代理服务器默认情况下都会对资产进行压缩编码,或者经过简单的配置就可以轻松实现。下面以Express为例配置一下动态压缩。


const express = require('express');
const compression = require('compression');

const app = express();

app.use(compression());
app.use(express.static('public'));

const listener = app.listen(process.env.PORT, () => {
    console.log(`Your app is listening on port ${listener.address().port}`)
})


4、使用CDN(内容分发网络)


由离用户更近的服务器向用户提供数据,可以显著减少每次TCP连接的网络延迟,增大吞吐量。选择一个可靠的CDN服务提供商进行简单的配置就可以,如阿里云、腾讯云、百度云等。


5、图片处理


对图片处理可以很好得对图片就行优化,经过图片处理优化的图像可以节省40%~80%的大小。虽然通过构建脚本也可以实现图片处理的效果,但在实践中一般使用第三方提供的图像CDN,第三方图像CDN也可以提供更多形式的图像处理方式。通过向文件地址传递参数来获取合适的图像,而不是直接获取原文件。


比如在chrome浏览器中使用WebP格式图片,WebP是由谷歌开发的一种新型图片格式,相比JPEG和PNG格式,WebP图片可以更好地压缩图片大小,从而提高页面加载速度。


6、优先加载关键资源


优先加载关键资源,延迟加载次要资源。优先加载关键资源可以减少页面加载时间,加快页面的渲染速度,提高用户体验。可以对网站进行分析,确定哪些资源是关键资源,然后将非关键资源设置为延迟加载。


7、利用chrome性能工具


Chrome浏览器的Lighthouse扩展程序可以对网站进行测试并生成一个性能报告。Lighthouse生成的报告包含了网站性能、可访问性、最佳实践和SEO等方面的评估结果,以及优化建议。分析测试结果,找出需要改进的方面,并根据建议进行优化。


总之,前端优化是提高用户体验、提高网站性能、减少成本和支持更多设备的关键因素之一。上述优化方法可以帮助开发人员优化应用程序的性能,提高用户体验和满意度,从而提高留存率和转化率,增加收入。


作者:liupl
来源:juejin.cn/post/7219241334926180410
收起阅读 »

项目很大,得忍一下

web
背景 常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的rou...
继续阅读 »

背景


常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的router层爆炸,打包速度直线下降,开发过程中,开了hmr稍微有点改动也要等个几秒钟,恨不得立刻重启一个新项目。但是现实告诉你,忍住,别吐,后面还有更多的业务活动加进来。那么怎么解决这个问题呢,这个时候mp的思路是个不错的选择。


关键点


打包慢,本质原因是依赖庞大,组件过多。开发过程中,我们开新的业务组件时,往往和其他业务组件是隔离的,那么我们打包的时候是不是可以把那些不相干的业务组件隔离出去,当然可以。打包工具,从入口开始进行扫描,单页面的模块引入基本都是借助router,所以,关键的是如果我们能够控制router的数量,其实就能够控制编译和打包规模了。


问题


router在vue项目中我们常用的是全家桶的库vue-router,vue-router最多提供了懒加载,动态引入功能并不支持。有小伙伴说router的引入路径可不可以动态传入,我只能说小伙子你很机智,但是vue-router并不支持动态的引入路径。因此我们换个思路,就是在入口的位置控制router的规模,通过不同规模的router实例来实现router的动态引入。当然这需要我们对router库进行一定改造,使其变的灵活易用


一般的router


通常的router如下:



// router.js

/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const routes = [

{

path: '/routermap',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'routermap',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

const router = new Router({

mode: 'history',

routes

})

router.afterEach((to, from) => {

///

})

export default router

// 引入 entry.js

import router from './router.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们可以不断的往routes数组中添加新的router item来添加新的业务组件,这也是我们的项目不断变大的根本,这样既不好维护,也会导致后面的编译效率


易于维护和管理的router


其实好的管理和维护本质就是分门别类,把类似功能的放在一起,而不是一锅粥都放在一起,这样基本就能解决追踪维护的功能,对应router管理其实也不是很复杂,多建几个文件夹就行如下:


router.png


对应routes/index.js代码如下:



import testRouter from './test.js'

const routes = [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...testRouter,

// 可以扩展其他router

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

// test.js

/**

* 测试相关页面路由映射

*/


/*global require*/

export default [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]


我们通过把router分为几个类别的js,然后在通过router item的数组展开合并,就做到了分门别类,虽然看似简单,但是可以把管理和维护效果提升几个数量级。


支持mp的router


虽然上面支持了易于管理和维护,但是实际上我们如果只是单一入口的话,导出的还是一个巨大的router。那么如何支持多入口呢,其实也不用想的过于复杂,我们让类似test.js的router文件既支持router item的数组导出,也支持类似routes/index.js一样的router实例导出即可。所谓既能分也能合才是最灵活的,这里我们可以利用工厂模式做一个factory.js,如下:



/**

* app 内的页面路由映射

*/


/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const RouterFactory = (routes) => {

return new Router({

mode: 'history',

routes: [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...routes,

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

})

}

export default RouterFactory


这个factory.js产出的router实例和routes/index.js一模一样所以我们只需组装一下test.js即可,如下:



/*global require*/

import RouterFactory from './factory'

export const testRouter = [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]

export default RouterFactory(developRouter)

// routes/index.js的引入变化一下即可

import testRouter from './test.js'

// 修改为=》

import { testRouter } from './test.js'


那么我们的入口该如何修改呢?也很简单:



// testEntry.js

import router from './routes/test.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们建立了一个新的入口文件 testEntry.js 这个入口只引入了test相关的模块组件


如何灵活的和编译命令做配合呢


根据上面,我们进行mp改造的基础已经做好,关于如何多入口编译webpack或者其他打包里面都是基础知识,这里就不多赘述。这里主要聊一下如何灵活的配合命令做编译和部署。


既然router我们都可以分为不同的文件,编译文件我们同样可以拆分为不同的文件,这也使得我们的命令可以灵活多变,这里我们以webpack做为示例:


config.png


config1.png


config2.png


config3.png


根据上图示例 我们的webpack的配置文件仅仅改动了entry,我们稍微改造一下build.js,使其能够接受不同的编译命令:



// build.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.prod.conf'),

'app': require('./webpack.app.conf')

}

let webpackConfig = configMap[page]

// dev-server.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.dev.conf'),

'app': require('./webpack.app.dev.conf')

}

let webpackConfig = configMap[page]


对应的脚本配置:



// package.json

"scripts": {

"dev": "node build/dev-server.js",

"build": "node build/build.js",

"build:app": "node build/build.js app",

"dev:app": "node build/dev-server.js app"

},


以上app对应test。最后,我们只需要在命令行执行相应命令,即可实现我们可控的router规模的开发,基本随便来新的需求,咱都可以夜夜做新郎,怎么搞都是飞速。当然部署的话我们也可以单独执行一部分页面的部署命令到单独的域名,换个思路也可以作为一种预发测试的部署方法。



#
整体项目的开发编译

npm run dev

#
单独的app,也即test项目的开发编译

npm run dev:app

#
整体项目的部署

npm run build

#
单独的app,也即test项目的部署

npm run build:app


结语


以上,即如何利用mp思路,提高我们的编译开发效率。时常有人会在提高网页性能的时候说到mp,但mp本质上并不能提高页面的性能,比如白屏优化。而路由中使用懒加载其实才是提高部分网页性能的出力者,关于白屏优化,本篇文章不作展开讨论。


作者:CodePlayer
来源:juejin.cn/post/7218866717739696183
收起阅读 »

Android 获取短信验证码并自动填充

Android 获取短信验证码并自动填充(踩坑小米、荣耀、OPPO) 前言 最近弄了个短信自动填充功能,一开始觉得很简单,不就是动态注册个广播接收器去监听短信消息不就可以了吗?结果没这么简单,问题就出在机型的适配上。小米的短信权限、荣耀、OPPO的短信监听都是...
继续阅读 »

Android 获取短信验证码并自动填充(踩坑小米、荣耀、OPPO)


前言


最近弄了个短信自动填充功能,一开始觉得很简单,不就是动态注册个广播接收器去监听短信消息不就可以了吗?结果没这么简单,问题就出在机型的适配上。小米的短信权限、荣耀、OPPO的短信监听都是坑,暂时就用这三个手机测了,其他的遇到了再补充。


下面简单讲讲:


权限


申请权限


短信属于隐私权限,Android 6.0后需要动态申请权限。首先在manifest里面注册权限:


<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />

在需要用的地方,动态申请下:


String[] smsPermission = {Manifest.permission.READ_SMS, Manifest.permission.RECEIVE_SMS};

小米短信权限问题


本来这样权限问题就搞定了,但是在小米手机上就不行。小米手机会把短信归类到通知类权限里:
pic


在 ContextCompat.checkSelfPermission 的时候会直接返回true,并且不会弹出权限对话框,而是在实际使用的时候才会咨询用户,按理说好像和我们逻辑没有冲突,但是在使用receiver进行监听前,不是得确保有权限么?实际效果也是,在没有权限时,不能获取到短信的广播。


小米短信权限解决


在网上找了找办法,好像也没多少博文,但是大致有了思路:不是用的时候才申请么?那我就先用一下,再去用receiver监听。下面是方法:


// 读取一下试试,能读取到就有权限
boolean flag = false;
try {
Uri uri = Uri.parse("content://sms/inbox");
ContentResolver cr = context.getContentResolver();
String[] projection = new String[]{"_id"};
Cursor cur = cr.query(uri, projection, null, null, "date desc");
if (null != cur) {
cur.close();
}
lag = true;
}catch (Exception e) {
e.printStackTrace();
}

这里仅针对小米手机啊,对小米手机的判断我只是用 android.os.Build.MANUFACTURER 简单判断了下,如果有更高要求请查找资料。


使用Receiver进行监听


编写SmsReceiver


这里也是网上随便找了个代码,能用,但是在荣耀手机上却是偶尔能收到一次,后面几次就收不到了,打了log也没进入到onReceive中,这就很离奇了,排查了很久。同样的代码,在小米手机上是没问题的,那就只可能是适配问题了。


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.util.Log;

public class SmsReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
//Toast.makeText(context, "收到信息", Toast.LENGTH_LONG).show();
Log.d("SmsReceiver", "onReceive: " + intent.getAction());
if(intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")){
//intent.getExtras()方法就是从过滤后的意图中获取携带的数据,
// 这里携带的是以“pdus”为key、短信内容为value的键值对
// android设备接收到的SMS是pdu形式的
Bundle bundle = intent.getExtras();
SmsMessage msg;
if (null != bundle){
//生成一个数组,将短信内容赋值进去
Object[] smsObg = (Object[]) bundle.get("pdus");
//遍历pdus数组,将每一次访问得到的数据方法object中
for (Object object:smsObg){
//获取短信
msg = SmsMessage.createFromPdu((byte[])object);
//获取短信内容
String content = msg.getDisplayMessageBody();
Log.d("SmsReceiver", "onReceive: content = " + content);
//获取短信发送方地址
String from = msg.getOriginatingAddress();
Log.d("SmsReceiver", "onReceive: from = " + from);

// TODO ...
}
}
}
}
}

使用方法:


// 使用广播进行监听
IntentFilter smsFilter = new IntentFilter();
smsFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
smsFilter.addAction("android.provider.Telephony.SMS_DELIVER");
if (smsReceiver == null) {
smsReceiver = new SmsReceiver();
}
smsReceiver.setCallbackContext(callbackContext);
context.registerReceiver(smsReceiver, smsFilter);

接触监听,最好在收到短信的时候就取消注册广播:


context.unregisterReceiver(smsReceiver);

解决OPPO手机无法接收短信广播问题


本来小米荣耀都搞定了,给测试一测,结果又不行了。收不到广播,用下面的ContentObserver还总拿不到对的数据。找了下资料,发现OPPO手机需要在短信APP进行设置。


ps. 后面发现华为、荣耀都是这样,会对验证码进行保护。可以使用ContentObserver 监听,能触发onChange,但是拿不到Uri,不过可以使用查询,拿到倒叙的第一条数据,取出其中的date属性,比对监听时的时间,如果短信两分钟有效,那就看看第一条数据是不是在两分钟内,如果不是,那就是没拿到,问题就出在用户开启了短信验证码保护,可以提示用户自行输入验证码(毕竟这个不是我们的锅)。


解决方法:
在短信 -> 短信设置里面 -> 打开禁止后台应用读取验证码


解决荣耀无法连续监听短信的问题


既然上面的方法没用了,只能找新的办法喽,网上很多提供了两种办法,第二种就是通过ContentResolver去监听短信添加的更新动作,其实也和广播类似,代码如下:


import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.provider.Telephony;
import android.util.Log;

import androidx.annotation.RequiresApi;

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class ReadSmsObserver extends ContentObserver {

private final Context context;

public ReadSmsObserver(Handler handler, Context context) {
super(handler);
this.context = context;
}

private static final String SMS_INBOX_URI = "content://sms/inbox";//API level>=23,可直接使用Telephony.Sms.Inbox.CONTENT_URI,用于获取cusor
// private static final String SMS_URI = "content://sms";//API level>=23,可直接使用Telephony.Sms.CONTENT_URI,用于注册内容观察者
private static final String[] PROJECTION = new String[]{
Telephony.Sms._ID,
Telephony.Sms.ADDRESS,
Telephony.Sms.BODY,
Telephony.Sms.DATE
};

@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange);
Log.d("ReadSmsObserver", "onChange: ");
// 当收到短信时调用一次,当短信显示到屏幕上时又调用一次,所以需要return掉一次调用
if(uri.toString().equals("content://sms/raw")){
return;
}
// 读取短信收件箱,只读取未读短信,即read=0,并按照默认排序
Cursor cursor = context.getContentResolver().query(Uri.parse(SMS_INBOX_URI), PROJECTION,
Telephony.Sms.READ + "=?", new String[]{"0"}, Telephony.Sms.Inbox.DEFAULT_SORT_ORDER);
if (cursor == null) return;
// 获取倒序的第一条短信
if (cursor.moveToFirst()) {
// 读取短信发送人
String address = cursor.getString(cursor.getColumnIndex(Telephony.Sms.ADDRESS));
Log.d("ReadSmsObserver", "onChange: address = " + address);
// 读取短息内容
String smsBody = cursor.getString(cursor.getColumnIndex(Telephony.Sms.BODY));
Log.d("ReadSmsObserver", "onChange: smsBody = " + smsBody);

// TODO 传递出去,最好切下线程

}
// 关闭cursor的方法
cursor.close();
}
}

用的时候要注册和取消注册:


// 使用ContentResolver进行监听
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (smsObserver == null) {
smsObserver = new ReadSmsObserver(new SmsHandler(), context);
}
smsObserver.setCallbackContext(callbackContext);
context.getContentResolver().registerContentObserver(
Uri.parse("content://sms/"), true, smsObserver);
}

取消注册:


context.getContentResolver().unregisterContentObserver(smsObserver);

解决OPPO手机无法拿到最新短信问题


很神奇啊,每次使用ContentObserver去监听短信变化,明明onChange触发了,但是去拿短信就是拿不到最新的,开了上面的设置也不行,弄了好久。


最后想的解决办法是,两种方式同时监听,在onChange触发后等待三秒钟(开始试了1s还不行),看看有没有onReceive,如果有就直接使用onReceive的短信,如果没有再验证onChange内拿到的短信,看看是不是有效时间内的,连倒叙第一个都在有效时间外,那就是没拿到了,直接舍弃了。


PS. 后续更新,感觉这些问题都可能是手机系统开了短信验证码保护。


思路是这样,做起来不麻烦,用个handler就可以解决,读者自行处理吧。


结语


这些机型的兼容性搞起来真头疼,上面两种方法可以兼容起来使用,收到一条短信后直接取消注册就行了。


作者:方大可
来源:juejin.cn/post/7222897518501003319
收起阅读 »

周末闲来无事,做了一个能动的宣传页

web
最近在用可画(canva),制作一些素材,海报活动页面,这不情人节快到了吗?基于海报模版,设计自己的页面倒是简单,但是都是静态页面,想着能不能让页面的元素都动起来(Everybody跟我一起嗨嗨嗨!!)。 两个方案 纯CSS animate库 CSS基于ani...
继续阅读 »

创建项目

最近在用可画(canva),制作一些素材,海报活动页面,这不情人节快到了吗?基于海报模版,设计自己的页面倒是简单,但是都是静态页面,想着能不能让页面的元素都动起来(Everybody跟我一起嗨嗨嗨!!)。


两个方案


纯CSS animate库


CSS基于animate库



  1. 利用animate动效,给页面上所有的image和text元素加上className,借助--var全局css变量属性,给元素依次加上delay、duration、index序号、初始化信息rotate、offset、easing等等,我会在码上掘金给一个css的demo版本。CSS版本相对简单一些,只需要循环给所有元素加上对应动画,计算执行时间,延迟时间,页面就可以动起来了。


// 定义的数据结构 Image\Text
[{
"id": "Image/Text-xx",
"type": "Image/Text",
"name": "图片/文本",
"css": {
"top": 0,
"left": 0,
"width": 414,
"height": 736,
"zIndex": 1,
"opacity": 1,
"fontSize": 18,
},
"animationObj": {
{
"delay": 1000,
"duration": 3030,
"type": "flipInY",
"easing": '',
"index": 8,
"rotate_angle": -6.6,
"offset": -112.5,
}
},
"value": "文本内容",
"src": "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/935920813a0c4151bbf452ef3c53ab7f~tplv-k3u1fbpfcp-watermark.image"
}]


码上掘金-CSS版


下面是纯css的版本:
code.juejin.cn/pen/7123482…


JS animejs库


animejs库


使用JS的关键就是编写对应帧属性,通过时间轴timeline方法给元素加上动画。现在js版本还只是一个demo中的demo,下次再给jym,感兴趣的jy可以自己想想。


时间轴可让你将多个动画同步在一起。
默认情况下,添加到时间轴的每个动画都会在上一个动画结束时开始。

<div class="demo-content params-inheritance-demo">
<div class="line">
<div class="square shadow"></div>
<div class="square el" style="transform: translateX(0px) scale(1); opacity: 0.5;"></div>
</div>
<div class="line">
<div class="circle shadow"></div>
<div class="circle el" style="transform: translateX(7.22878e-10px) scale(1); opacity: 0.5;"></div>
</div>
<div class="line">
<div class="triangle shadow"></div>
<div class="triangle el" style="transform: translateX(2.30924px) scale(1.00924) rotate(180deg); opacity: 0.5;"></div>
</div>
</div>

<script src="https://lib.baomitu.com/animejs/3.2.1/anime.min.js"></script>


.demo-content {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
width: 290px;
height: 100%;
}
.line {
width: 100%;
padding: 1px 0px;
}
.square,
.circle {
pointer-events: none;
position: relative;
width: 28px;
height: 28px;
margin: 1px;
background-color: #005bb7;
font-size: 14px;
}
.triangle {
pointer-events: none;
position: relative;
width: 0;
height: 0;
border-style: solid;
border-width: 0 14px 24px 14px;
border-color: transparent transparent #005bb7 transparent;
}
.shadow {
position: absolute;
opacity: .2;
}

var tl = anime.timeline({
targets: '.params-inheritance-demo .el',
delay: function(el, i) { return i * 200 },
duration: 500,
easing: 'easeOutExpo',
direction: 'alternate',
loop: true
});

tl
.add({
translateX: 250,
// override the easing parameter
easing: 'spring',
})
.add({
opacity: .5,
scale: 2
})
.add({
// override the targets parameter
targets: '.params-inheritance-demo .el.triangle',
rotate: 180
})
.add({
translateX: 0,
scale: 1
});

code.juejin.cn/pen/7123478…


码上掘金太卡了吧,能不能优化下


作者:一起重学前端
来源:juejin.cn/post/7123482707983613965
收起阅读 »

本地运行的前端代码,如何让他人访问

web
有时候,我前端写好了项目,想要给其他人看一下效果,可以选择将代码部署到test环境,也可以选择让外部通过ip来访问,不过前提是在同一个局域网下(比如连接同一个WiFi),下面介绍第二种方式。 修改dev命令 首先我们需要先修改host地址,此处以vue3项目举...
继续阅读 »

有时候,我前端写好了项目,想要给其他人看一下效果,可以选择将代码部署到test环境,也可以选择让外部通过ip来访问,不过前提是在同一个局域网下(比如连接同一个WiFi),下面介绍第二种方式。


修改dev命令


首先我们需要先修改host地址,此处以vue3项目举例


image.png


页面启动之后如下


image.png


正常情况下,script下的dev命令是不会指定host的,我们可以在下面看到Local的地址为默认的127.0.0.1,此时把这个网址发给别人肯定跑不起来。


所以我们可以指定host,比如0.0.0.0,允许所有ip访问


"dev": "vite --host=0.0.0.0",

修改完host后,windows系统的话,我们还需要关闭防火墙(苹果不需要)。重新启动项目可以看到


QQ截图20230406204123(1)(1).png


Network那里的网址,打马赛克的地方其实就是本机的ip地址,window输入cmd打开命令提示符,然后输入ipconfig即可查到ip地址,苹果的话,点击wifi小图标,同时按住option键即可查到ip地址。


在其他电脑或者手机访问


浏览器中输入url即可看到相关页面,此方法也适用于手机端调试


Screenshot_2023-04-06-20-51-03-21_439a3fec0400f89.jpg


作者:笨笨狗吞噬者
来源:juejin.cn/post/7218916720323706935
收起阅读 »

知道尤雨溪为什么要放弃 $ 语法糖提案么?

web
前言 最近看到一篇文章: 《最新,Vue 中的响应性语法糖已废弃》 本文标题中的 $ 语法糖指的就是上文中的响应式语法糖 (Reactivity Transform),那为什么不写 Reactivity Transform 呢?因为这个名实在是太长了… 看了一...
继续阅读 »

前言


最近看到一篇文章:


《最新,Vue 中的响应性语法糖已废弃》


本文标题中的 $ 语法糖指的就是上文中的响应式语法糖 (Reactivity Transform),那为什么不写 Reactivity Transform 呢?因为这个名实在是太长了…


看了一圈评论发现大家觉得被废弃是因为分不清是正常变量还是响应式变量的居多:



下面这个评论说的有一定道理:



Vue 的官网现在已经变成这样了:



以后会不会变成这样:



23次方,一共8种不一样的写法。不对,无虚拟 DOM 模式只能用 Composition API,所以应该不到 8 种写法,你看这不就分裂了嘛!虽说这几种不同的写法也能看懂吧,但每个人都有不同的偏好不同的写法总归不太好。而且你能保证 Vue 不会又改写法吗?Vue 总是受人启发:受 Angular 启发的双向绑定、受 React 启发的虚拟 DOM、受 React Hooks 启发的 Composition API、受 Svelte 启发的语法糖(一开始用的是 Svelte 的 label 写法)、受 Solid 启发的 Vapor Mode无虚拟 DOM 模式




  • 高情商:集百家之长

  • 低情商:方案整合商




开玩笑的哈~ Vue 还是有很多自己的东西的,不过它确实老是抄袭各种框架受各种框架的启发,太杂糅了。今天受这个框架启发做出来这种新 feature、明天又受那个框架启发做出来了另一种新 feature… 估计等 Vue4 出来的时候肯定又是受到了什么其他框架的启发…


我在《无虚拟 DOM 版 Vue 即将到来》这篇文章下看到这样一条评论:



大家觉得这个人说的有没有道理呢?反正我现在感觉 Vue 的各个方案有点太杂糅了,有点像是方案整合商集百家之长,以后指不定就发展成这样了:



当你去网上搜索一些解决方案时,能看到数十种不同的写法是一种什么体验……


不过这条评论真的是高情商:





  • 低情商:Vue 这是啥流行抄啥

  • 高情商:只用 Vue 就能体会到各种流行的技术趋势




跑题了,咱们来说一说 $ 语法糖,它可绝不只有分不清到底是不是响应式变量这一个缺点,它的缺点比优点多得多,我们来具体分析一下。


分析


我们也不要一上来就说这个语法糖有多么多么的不好,如果真这么不好的话尤总也不至于费这么大劲来推动这个提案了对不?这个语法糖在某些情况下确实会大幅改善我们的开发体验,但在另一些情况下不仅不会帮助我们改善体验,反而会增加我们的心智负担,我们来看下面这个案例:


let x = $(0)
let y = $(0)

const update = e => {
 x = e.x
 y = e.y
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))

$watch([x, y], ([x, y]) => console.log(x, y))

看上去很美好是不是,我们终于不用再写 .value 了。



如果看不明白这种写法的话可能是之前没有对其进行过了解


建议先阅读一下这篇《Vue3又出新语法 到底何时才能折腾完?》



不过像这种逻辑我们通常都会提取出去封装成一个函数,因为有可能有很多个组件都用到了获取鼠标位置这个逻辑,你不想在每个用到该逻辑的组件里都复制一遍相同的逻辑吧?那我们就这样:


// useMouse.js
export const useMouse = (dom = window) => {
  let x = $(0)
  let y = $(0)

  const update = e => {
    x = e.x
    y = e.y
}
  onMounted(() => dom.addEventListener('mousemove', update))
  onUnmounted(() => dom.removeEventListener('mousemove', update))

  return { x, y }
}

import { useMouse } from './useMouse.js'

let { x, y } = useMouse()

$watch([x, y], ([x, y]) => console.log(x, y))

如果这么写你就会惊讶的发现根本不生效,因为编译过后就相当于:


import { ref } from 'vue'

export const useMouse = (dom = window) => {
 let x = ref(0)
 let y = ref(0)

 const update = e => {
   x.value = e.x
   y.value = e.y
}
 onMounted(() => dom.addEventListener('mousemove', update))
 onUnmounted(() => dom.removeEventListener('mousemove', update))

 return {
   x: x.value,
   y: y.value
}
}

这就相当于把一个普通值给 return 出去了,普通值是没法在取值或改值时运行一些其他逻辑的,所以我们还不能把值直接 return 出去,而是把这个响应式变量本身给 return 出去:


import { ref } from 'vue'

export const useMouse = (dom = window) => {
 let x = ref(0)
  let y = ref(0)

  const update = e => {
   x.value = e.x
    y.value = e.y
}
  onMounted(() => dom.addEventListener('mousemove', update))
  onUnmounted(() => dom.removeEventListener('mousemove', update))

  return { x, y }
}

所以编译必须还要有还原的功能,把响应式的值给还原成响应式变量:


export const useMouse = (dom = window) => {
 let x = $(0)
 let y = $(0)

 const update = e => {
   x = e.x
   y = e.y
}
 onMounted(() => dom.addEventListener('mousemove', update))
 onUnmounted(() => dom.removeEventListener('mousemove', update))

 return $$({ x, y })
}

但这样又要写 .value 了:


import { useMouse } from './useMouse.js'

let { x, y } = useMouse()

console.log(x.value, y.value)

因为编译器是分析不出来一个函数的返回值到底是不是响应式变量的,所以就又得引入一个 API 来告诉编译器这个函数的返回值有响应式变量:


import { useMouse } from './useMouse.js'

let { x, y } = $fromRefs(useMouse())

console.log(x, y)

大家不觉得这样很麻烦吗?而且搞出那么多莫名其妙的 $ 、$$ 变量。写一堆这玩意真的没感觉比 .value 好到哪去,而且我们还要随时记得某个变量是响应式的,不然在传递的过程中就有可能失去响应性:


// logValue.js
// 接收一个响应式变量并在其变化时将其打印出来

export const logValue = arg => { // 在提案中并未找到如何用语法糖转换函数的参数
 // 也就是说在这种情况下可能没有什么完美的解决方案 那就又要写 .value 了:
 console.log(arg.value)
 // 不过也不是没有解决方案 我们可以用 $computed 来关联一下:
 let argument = $computed(() => arg.value)
 // 这样就可以不用写 .value 了:
 console.log(argument)
 // 但缺点就是太麻烦了 参数少的时候还可以 参数多的时候还能每个都这么写吗?
 // 而且还要为变量取个不同的名字 这对于我们这些英文不好的人来说简直就是场灾难
 $watch(argument, value => console.log(value))
}

import { logValue } from './logValue.js'

let a = $(0)

logValue(a) // 这么传就错啦
logValue($$(a)) // 一定要写成这样

// 假如有函数是需要响应式变量和普通变量混着传的:
let b = 0
logValue($$(a), b, { a: $$(a), b }) // 写成这样真的很乱

还有需要把 ref 变量传给 reactive 字段的情况:


let a = $(0)

const obj = reactive({ a })

console.log(obj.a) // 0
a++
console.log(obj.a) // 还是 0


// 必须写成这样
const obj = reactive({ a: $$(a) })
console.log(obj.a) // 0
a++
console.log(obj.a) // 1

所以说语法糖只能某些情况下改善我们的开发体验,前提就是你不要把响应式变量传来传去的。但 Vue3 的核心卖点之一不就是 Composition API 么?中文官网管这个叫组合式 API,关键词是组合Vue 还把提取出去的可复用函数叫 Composables,翻译过来就是可组合的,如果不把响应式变量传来传去那还组合个P呀!


这个问题可不是只有 Vue 有,来看下 Solid.js 吧:


import { createSignal } from 'solid'

export const useMouse = (dom = window) => {
 const [x, setX] = createSignal(0)
 const [y, setY] = createSignal(0)

 dom.addEventListener('mousemove', ({ x, y }) => {
   setX(x)
   setY(y)
})

 return {
   x: x(),
   y: y()
}
}

同样会有响应式值与响应式变量的问题,只不过就是把 .value 变成了 ()


// 假如有个响应式变量 a

// 打印的是响应式值
console.log(a.value) // Vue
console.log(a()) // Solid

//打印的是响应式变量
console.log(a) // Vue & Solid

是不是看过很多文章说 Solid.js 和 React Hooks 很像、写起来很舒服、什么比 React 还 react 之类的文章?实际上真的就只是 API 设计的相似而已,只要我们想,我们同样也可以把 Vue 的 API 封装成 React 那样:


import { ref } from 'vue'

const useState = value => {
 const result = ref(value)
 const getter = () => result.value
 const setter = newValue => result.value = newValue
 return [getter, setter]
}

const [num, setNum] = useState(0)
setNum(1)

那是不是这样封装一下,Vue 也变得比 React 还 react 了?应该不难看出这只是在自欺欺人罢了,我们传值时照样还得区分到底应该传的是响应式变量本身还是响应式变量的值。


Vue2 为何没这个问题


不知大家有没有思考过:为什么 Vue2 时代大家从来就没听说过丢失响应性、没听过要出什么语法糖之类的问题呢?听过最多有关于语法糖的可能就是 v-model 的双向绑定功能其实就是 @input="xxx" + :value="xxx" 的语法糖。


这是因为 Vue2 时代用的都是 this.xxx,咱们所有的响应式变量全都挂载到了 this 上。取值时 this.xxx 会触发 getter、改值时 this.xxx = xxx 会触发 setter


你可以简单的理解成这样:


// 用 Vue3 来写一段伪代码
import { reactive, watchEffect } from 'vue'

const this = reactive({
a: 1,
b: 2,
c: 3
})

watchEffect(() => console.log(this.a))
this.a++

当然这只是一段伪代码,真这么写是会报错的:



因为 this 是一个关键字,正因为它是一个关键字所以咱们用 this.xxx 才会显得这么的自然。而我们现在的响应式变量都需要自己起名,自己起的名不是关键字,所以用 xx.xxx 就老觉得麻烦,就老想给它解构:


import { reactive, watchEffect, toRefs } from 'vue'

const user = reactive({
name: 'AngularBaby',
age: 34,
beautiful: true
})

console.log(user.name) // 有些人觉得这样写很麻烦
const { name } = user // 就老想给它解构
console.log(name) // 结果就是失去了响应性

// 想要保持响应性 写法就变得更麻烦了
const { name } = toRefs(user)
console.log(name.value)

而且之前用 this 还有一个显著的好处就是只要写法正确,操作 this 上的属性就不用担心响应式的问题,没有那么多心智负担。甚至有人会简单的理解为只要是 this.xxx 就一定会有响应:


export default {
data () {
return { a: 1 }
},
mounted () {
this.a = 2 // 没有心智负担 因为我们知道自己是在改变 this 上的属性
this.a++ // 正确改变 this 上的属性就会存在响应

let b = 2 // 也没有心智负担 因为我们知道这不是 this 上的属性
b++ // 我们不会期待这段代码会有任何的响应
}
}

这样很容易区分哪些是响应式变量而哪些不是,即使有人真的写成了这样:


export default {
data () {
return { a: 1 }
},
mounted () {
let { a } = this
a++ // 我们不会期待这段代码会有任何的响应
}
}

这里也很容易能够看出来我们这样并没有修改 this 上的属性,所以并不会正确响应也是理所应当的一件事。


还有复用逻辑,Vue2 时代有很多人用 Mixins 来复用逻辑:


import mouse from 'mouse.mixin.js'
import position from 'position.mixin.js'

export default {
mixins: [mouse, position],
mounted () {
this.x // 哪来的 x ?
this.y // 哪来的 y ?
// 除了 xy 还有没有其他的未知 this.xxx ?
}
}

可以看到 Mixins 存在很多的弊端,比方说数据来源不清晰、容易产生冲突变量之类的。如果不去看源码的话谁能知道 this.x 到底是 mouse 中的 x 还是 position 的 x 呢?正是由于 Vue2 没有一个完美的复用机制,所以尤大才下定决心将 Vue3 改造成函数式。但函数式没了 this 就又失去了 Vue2 时期的那种… 我不知该怎么形容 Vue2时期的 this.xxx 哈,舒服?自然?反正我是比较喜欢 this.xxx 这种写法的,虽然这种写法是受 Angular 启发(集百家之长)


而且我还比较喜欢的一点就是一些全局挂载的属性:


this.$el
this.$refs
this.$nextTick(() => { /* ... */ })

直接 this.$xxx 就出来了,不用引,既方便又快捷。当然这种方式也有不少坏处,比方说容易被覆盖、不利于 Tree Shaking 之类的…


但我还真的蛮喜欢这种写法的:


// main.js
import Vue from 'vue'

Vue.prototype.$toast = msg => { /* ... */ }

this.$toast('Success!')

如今就会变得就稍麻烦一些:


import toast from './toast.js'

toast('Success!')

虽说后者其实更好,但有没有这样一种可能:既恢复到 Vue2 时期用 this 的便捷、又能享受到 Vue3 组合式的好处:


// 幻想中的写法

this.$data.a = 1 // 相当于 Vue2 时期的 data: { a: 1 } 最终会挂载到 this 上变成 this.a
this.$computed.b = () => this.a * 2 // 相当于 Vue2 时期的 computed: { b () { return this.a * 2 } } 最终会挂载到 this 上变成 this.b

this.$watch.b = value => console.log(value) // 相当于 Vue2 时期的 watch: { b: value => console.log(value) }

let timer
this.$mounted = () => {
timer = setInterval(() => this.a++, 1000)
}
this.$unMounted = () => clearInterval(timer)

复用逻辑:


// 幻想中的写法

import useMouse from './useMouse.js'

({ x: this.$computed.x, y: this.$computed.y } = useMouse())
this.$effect = () => console.log(this.x, this.y)

// 如果用数组解构将会更加的便捷
[this.$computed.x, this.$computed.y] = useMouse()
this.$effect = () => console.log(this.x, this.y)

这样我们的心智负担就又能回到 this 时期了:只要改变 this 属性就会存在响应,否则就无响应,那这个方案有实现的可能吗?在 ES5 时代无可能,但在 ES6 Proxy 的加持下我认为还是可以实现的,那么接下来我们就来试一下。


实验


首先我们回顾一下 Vue3.0 没有 setup 语法糖时期的写法:


<template></template>

<script>
import { defineComponent } from 'vue'

export default defineComponent({
setup () {
console.log(this) // undefined
}
})
</script>

原版的 this 指向为 undefined,那我们怎么改变它的指向呢?我们可以自己写一个 defineComponent


// defineComponent.js

import { defineComponent, reactive } from 'vue'

export default options => {
const { setup } = options
if (typeof setup === 'function') {
options.setup = setup.bind(reactive({}))
}
return defineComponent(options)
}

这样 setup 的指向就变成了 reactive({}),当我们在操作 this 的时候就相当于在操作 reactive({})。但这样并不能满足我们的需求,我们想要的是当我们 this.$data.a 的时候会在 this 上挂载个 a 属性,所以我们要把 reactive 换成一个 Proxy


// createThis.js
import { defineComponent, reactive } from 'vue'

const createData = target => new Proxy({}, {
get: (_, key) => Reflect.get(target, key),
set (_, key, value) {
if (Reflect.getOwnPropertyDescriptor(target, key)) {
console.error(`this.$data.${key} is already defined!`)
return false
}
return Reflect.set(target, key, value)
}
})

export default () => {
const that = reactive({})
const $data = createData(that)
return new Proxy(that, {
get (target, key) {
if (key === '$data') {
return $data
}
return Reflect.get(target, key)
},
set (target, key, value) {
if (key === '$data') {
return console.warn('this.$data is readonly!')
}
return Reflect.set(target, key, value)
}
})
}

// defineComponent.js

import { defineComponent } from 'vue'
import createThis from './createThis.js'

export default options => {
const { setup } = options
if (typeof setup === 'function') {
const that = createThis()
options.setup = (...args) => {
setup.apply(that, args)
return that
}
}
return defineComponent(options)
}

也就是说我们利用 Proxy 来把 $data 给代理出去了,当我们访问 $data 的时候其实已经是另一个代理对象了,在这个代理对象上设置的属性全部都设置到 this 上。this 现在就相当于 reactive({}),所以 this.$data.a = 1就相当于 reactive({ a: 1 }),我们来试一下:



完美运行,只要你能搞懂上面的那段代码,那么接下来的 $computed$watch$watchEffect$readonly$shallow$nextTick$mounted$unMounted 等一大堆 API 相信你也知道该怎么做了,我就不在这里占用过多的篇幅了。这里直接用码上掘金贴上源码及用法,向大家展示一下可行性:



当然这源码并不是把所有 API 都实现了,目前只实现了 this.$datathis.$computedthis.$watchthis.$mounted 等几个常用的 API 供大家参考,感兴趣的可以去把全部的 API 都实现一下,我这里犯懒就先不实现那么全乎了。



这么好的东西为啥犯懒不实现呢?因为这玩意有一定的弊端。对了,掘金好像在文章中屏蔽了来自码上掘金alert,必须点查看详情才能看到。为了防止大家也犯懒不点进去看,这里直接给大家贴上动图:



我们的写法类似于下面这样:


export default defineComponent({
setup () {
this.$data.count = 0
this.$watch.count = (value, oldValue) => alert(`验证 this.$watch:按钮上的值将会从 ${oldValue} 变为 ${value}`)

this.$computed.doubleCount = () => this.count * 2
this.$watch.doubleCount = value => alert(`验证 this.$computed:${this.count} 的双倍是 ${value}`

this.$mounted = () => alert('验证 this.$mounted:已挂载')
}
})

怎么样,是不是很好玩?我是蛮喜欢这种 this 混合着函数式的写法。但刚刚说了这玩意有一定的弊端,只能拿来当玩具玩玩所以我才懒得实现的那么全乎。那么它究竟有多大的弊端呢?


弊端


Vue3 比 Vue2 更优秀的一个点是支持 tree shaking,在你仅仅只用了 Vue 的某几项功能的情况下打包体积会小很多。但我们刚刚的做法无疑是开了历史的倒车,又回去了!并且随着 Vue3.2 的崛起,setup 语法糖得到了大多数人的认可,因为它确实很方便。但这样我们就无法修改 this 指向了:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
this.$data.a = 1 // 怎么修改 this 指向
</script>

有人可能会说加个函数不就得了:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
import setup from './setup.js'

setup(() => {
this.$data.a = 1
})
</script>

这样虽然可以修改 this 指向,但随之而来的就是 <template> 模板里面访问不到 a 这个变量了,除非我们写成这样:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
import { toRefs } from 'vue'
import setup from './setup.js'

const { a, b, c, d, e, f } = toRefs(setup(() => {
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6
}))
</script>

我相信没人会愿意写成这样,所以我们必须借助 babel 插件来完成编译,思路是把 this 编译成 reactive({}),类似于下面这样:


// 编译前
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6

// 编译后
import { reactive } from 'vue'
import createThis from 'createThis.js'
import createData from 'createData.js'

const that = createThis(reactive({}))
createData(that)

that.$data.a = 1
that.$data.b = 2
that.$data.c = 3
that.$data.d = 4
that.$data.e = 5
that.$data.f = 6

不过这样还是会引入我们刚刚写的那些代码,虽然代码量并不高,但如果压根就不引入任何额外的代码才好,所以如果能编译成这样才是最完美的:


// 编译前
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6

console.log(this.a)

// 编译后
import { reactive } from 'vue'

const that = reactive({
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
f: 6
})

console.log(that.a)

但如果这样编译的话又有可能发生如下情况:


import useXxx from './useXxx'

this.$data.a = 1

useXxx.call(this)

这样会被编译成:


import { reactive } from 'vue'
import useXxx from './useXxx'

const that = reactive({ a: 1 })

useXxx.call(that)

万一这个 useXxx 里写了这样一段逻辑:


// useXxx.js

expurt default function () {
this.$watch.a = value => console.log(value)
}

这样就不会按照我们所期待方式去运行了,因为在编译后就相当于:


// 伪代码

const obj = reactive({ a: 1 })

useXxx.call(obj)

function useXxx () {
this.$watch.a = value => console.log(value)
}

这样会直接报错,因为 reactive({ a: 1 }).$watch 是 undefinedundefined.a 会报错,所以并没有特别完美的解决方案。最好是检测如果没把 this 作为参数传走或者没有哪个函数用了 fn.call(this) 来把 this 指向当前上下文的话,就按照最完美的方式(不引入任何杂七杂八的代码)编译。否则就引入一点运行时,反正也没多少:


// 编译前
import useMouse from 'useMouse'

this.$data.a = 1
this.$watch.a = value => console.log(value)

this.$mounted = () => window.addEventListener(...)
this.$unmounted = () => window.removeEventListener(...)

[this.$computed.x, this.$computed.y] = useMouse.call(this)

// 编译后
import { reactive } from 'vue'
import createThis from 'createThis'
import createData from 'createData'
import createWatch from 'createWatch'
import createMounted from 'createMounted'
import createUnmounted from 'createUnmounted'

const that = createThis(reactive({
a: 1
}))
createData(that)
createWatch(that)
createMounted(that)
createUnmounted(that)

that.$data.a = 1
that.$watch.a = value => console.log(value)

that.$mounted = () => window.addEventListener(...)
that.$unmounted = () => window.removeEventListener(...)

[that.$computed.x, that.$computed.y] = useMouse.call(that)

但仔细一想还是有可能有 bug,比方说你这个组件里没用到 this.$readonly,但 useMouse 用了的话,那岂不是又要报错。那就在 Vue 组件之外也编译,如果在外面有用到 this.$xxx,那就在相应的位置:


// 编译前
export default function useMouse () {
this.$readonly.a = 1
}

// 编译后
import createReadonly from 'createReadonly'

export default function useMouse () {
createReadonly(this)
this.$readonly.a = 1
}

缺陷


这种写法不仅仅是有弊端,还有一个非常严重的缺陷。虽然刚刚我们设想了一下用编译的方案来解决弊端的可能,但有个最大的缺陷是连编译都无法解决的。这个最大的缺陷就是对 TS 的支持,如果不用 TS 还好,但如果你的项目里有用 TS,那么这种写法就完全没法用:



不知怎么才能让 TS 也支持这种想法,查了国内外很多资料,最后找到了这两篇文章:



《TypeScript plugin 实践 —— 类型,编辑器与业务价值》


《基于 TypeScript 的开发者体验增强 - 朝夕相处却始终被忽视的领域》



也不知道这个 TS Language Service 有没有可能能够实现我们这种语法,感兴趣的小伙伴可以好好研究一下。我们目前只实现了运行时方案,但编译方案才是未来。写这篇文章的目的是希望给大家提供一个思路,看看大家觉得这个想法怎么样。万一大家觉得这个想法非常好,把它推给官方,官方实现了呢?



当然上述的那些话也可能仅仅只是过于美好的想象,现实很有可能是压根儿就没有人对这个想法感兴趣,官方也认为这是在开历史的倒车并且对 TS 支持不好不予实现。



往期精彩文章



作者:Veev
来源:juejin.cn/post/7222874734185922597
收起阅读 »

实战:快速实现iOS应用中集成即时通讯IM和UI

准备熟练objective-c语言有一台mac电脑,并安装了xcode 和 cocoapods目标手把手教大家在iOS应用中集成即时通讯IM 功能内容篇幅较长,需要内心平和耐心看下去,务必戒躁.阅读本文并按照本文进行对接预计时长2小时注册Appkey和user...
继续阅读 »

准备

熟练objective-c语言

有一台mac电脑,并安装了xcode 和 cocoapods


目标

手把手教大家在iOS应用中集成即时通讯IM 功能

内容篇幅较长,需要内心平和耐心看下去,务必戒躁.阅读本文并按照本文进行对接预计时长2小时


注册Appkey和username

本教程以集成环信IM为例

注册环信账号并登录到console后台:

https://console.easemob.com/user/register


第一步 点击添加应用:



第二步 创建应用


示例:


第三步,找到刚创建的appkey,并点击查看详情



第四步 创建两个user,并相互加好友







加好友:





至此,在console部分操作完成,并得到了一个appkey和两个user

appkey:1168171101115760#abc ,

username1: user1 ,

password1:1 ,

username2: user2 ,

password2: 1



创建一个简单项目simple,整合IM和UI

创建一个简单项目




工程创建成功之后,使用cocoapods进行环信IM集成

示例:



最终效果



第一部分 环信UI库集成方式

环信官方提供了UI库

那么我们有几种方式进行集成呢?

1.利用pod远端拉取集成

2.利用pod集成本地库

3.直接拖入项目


实际上这套UI库并不是最理想的UI库,因为在真正做项目的过程中,我们需要进行大量的改造,以达到符合产品设计的样式.所以这里推荐第二种方式和第三种方式.


第一种方式

参考:https://www.imgeek.net/video/76

第二种方式

参考:https://www.imgeek.org/video/91


我们将会在这里演示第三种方式


第二部分 集成前需要了解

问:集成UI库是否可以直接使用?

答:这里需要注意,实际上UI库仅仅提供了UI功能,并没有提供逻辑部分,所以无法直接拿来使用.


问:那我应该怎么使用?关于逻辑部分在哪里?还需要我自己进行实现吗?

答:不需要,我们可以从官方demo中取出我们需要的UI部分.


问:那么我需要以最快速度集成UI,都需要做什么工作?

答:

1.下载demo<https://www.easemob.com/download/im

跑通demo<https://www.imgeek.net/video/76>

2.根据产品设计的情况,从第一部分提到的三种方式中选择合适的集成方式.

3.从demo中取出相关界面逻辑并放入在自己的项目中.


第三部分 上手干

这里将会演示第三种集成方式

1.首先我们已经跑通了demo,所以我们当前demo的路径如下:




其中:

EaseIM是可运行的项目

EaseUI是官方提供的UI库


2.首先将UI部分拖入项目




把其中多出的plist删除

在podfile中加入


pod 'EMVoiceConvert', '0.1.0'

效果如下:




此时UI部分集成完成.


下一步集成逻辑部分,需要从Demo中提取



创建pch文件(如果项目中有则不用创建)


并在podfile中加入



创建pch文件(如果项目中有则不用创建)







最后配置项目权限(这里没有进行配置推送)






到这里,我们完成了将IM整合进项目中。


完善代码及IM的UI使用方式

第一步,我们在项目中创建一个负责做配置项的helper 和一个音视频做回调处理的类

类名分别为

EMAppConfig如下所示



EMAppCallHelper如下所示



第二步 在appdelegate中完善




宏定义appkey(定义已经存在,需要修改定义的值)



(另,声网id添加或修改位置在这里)






使用示例如下




实现收发消息

第一部分 参考现有Demo写法


1.发消息的逻辑参考



2.收消息的回调



第二部分,主动实现


如何主动发消息(这里发消息是直接调用SDK发消息)


//构建一个消息体
    EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithText:@"你好,环信"];
    
    //构建一条消息
    EMChatMessage *message = [[EMChatMessage alloc] initWithConversationID:"user2" body:body ext:@{}];
    
    //设置消息的聊天类型为单聊消息
    message.chatType = EMChatTypeChat;
    
    //将消息发送出去
    [EMClient.sharedClient.chatManager sendMessage:message progress:^(int progress) {
    } completion:^(EMChatMessage * _Nullable message, EMError * _Nullable error) {
        if(error){
            NSLog(@"发送失败(%d):%@",error.code,error.errorDescription);
        }else{
            NSLog(@"发送成功");
        }
    }];


如何收到消息





另:代理可以添加多份,不过如果在不需要代理的情况下一定要移除,否则会被强引用,无法释放


至此,我们已将IM和UI整合至项目中。大功告成!



阅读推荐:深度改造聊天界面的cellhttps://www.imgeek.org/video/121

收起阅读 »

Flutter必学的Getx状态管理库

什么是 GetX? 一个简单、高效、强大的管理状态、路由管理库 学习目标 掌握使用GetX管理状态 了解基础GetX状态管理的原理 GetX状态管理的优势 精确渲染,只会渲染依赖状态变化的组件而不会全部组件渲染一遍 安全性高,当程序出现错误时,不会因为重...
继续阅读 »

什么是 GetX?


一个简单、高效、强大的管理状态、路由管理库


学习目标



  • 掌握使用GetX管理状态

  • 了解基础GetX状态管理的原理


GetX状态管理的优势



  1. 精确渲染,只会渲染依赖状态变化的组件而不会全部组件渲染一遍

  2. 安全性高,当程序出现错误时,不会因为重复更改状态导致崩溃

  3. 有个GetX永远不需要声明状态组件, 忘记StatefulWidget组件

  4. 实现MVC架构,将业务逻辑写到控制器中,视图层专注于渲染

  5. 内置了防抖/节流、首次执行等功能

  6. 自动销毁控制器,无需用户手动销毁


用法


1.1声明响应式状态


有三种声明方式,使用哪一种都可以 推荐第三种


1.1.1 使用声明,结合Rx{Type}


final name = RxString(''); // 每种内置的类型都有对应的类
final isLogged = RxBool(false);
final count = RxInt(0);
final balance = RxDouble(0.0);
final items = RxList<String>([]);
final myMap = RxMap<String, int>({});

1.1.2 泛型声明 Rx


final name = Rx<String>(''); 
final isLogged = Rx<Bool>(false);
final count = Rx<Int>(0);
final balance = Rx<Double>(0.0);
final number = Rx<Num>(0);
final items = Rx<List<String>>([]);
final myMap = Rx<Map<String, int>>({});
// 自定义类 声明方法
final user = Rx<User>();

1.1.3以.obs作为值(推荐使用)


final name = ''.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String, int>{}.obs;
// 自定义类 声明方法
final user = User().obs;

2.1 使用响应状态到视图中


有两种方法使用状态:



  1. 基于Obx收集依赖状态

  2. 基于GetX<Controller>获取对应的控制器类型


2.1.1基于Obx收集依赖状态


十分简单,我们只需要使用静态类即可达到动态更新效果。



  1. 创建一个 Controller


// HomeController 可以写到一个专门管理控制器的文件中,这样方便维护
// 就像 React 需要把 Hook 单独提取一个文件一样
class HomeController extends GetxController {
var count = 0.obs;
increment() => count++;
}


  1. 导入创建的 Controller 并使用它


class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
// 寻找Controller
HomeController c = Get.find<HomeController>();
return Obx(
() => Scaffold(
body: ElevatedButton(
// 通过`c.count.value`使用状态,也可以不使用.value,.value可选的
child: const Text("${c.count}"),
onPressed: () => c.count++, // 改变状态,
),
),
);
}
}

2.1.2 基于GetX<Controller>获取对应的控制器类型


这种做法需要三个步骤



  1. 声明一个控制器


// HomeController 可以写到一个专门管理控制器的文件中,这样方便维护
class HomeController extends GetxController {
var count = 0.obs;
increment() => count++;
}


  1. GetMaterialApp类中初始化时导入对应的控制器


// main.dart
void main() {
runApp(GetMaterialApp(
// 如果不写这一步那么GetX将无法找到HomeController控制器
initialBinding: InitBinding(),
home: const Home(),
));
}
class InitBinding implements Bindings {
@override
void dependencies() {
Get.put(HomeController());
}
}


  1. 在对应组件或页面中使用GetX<Controller>实现数据的响应


class Home extends StatelessWidget {
const Home({super.key});

@override
Widget build(BuildContext context) {
// 这样就可以正常使用了
return Obx<HomeController>(
builder: (c) => Scaffold(
body: ElevatedButton(
child: const Text(c.count.value),
onPressed: () => c.count++,
),
),
);
}
}

3.1 监听状态更新的工具函数



  • 当依赖的值发生变化后会触发回调函数


var count = 0.obs;

// 每当 count 发生改变的时候就会触发回调函数执行
ever(count, (newCount) => print("这是count的值: $newCount"));

// 只有首次更新时才会触发
once(count, (newCount) => print("这是count的值: $newCount"));

/// 类似于防抖功能频繁触发不会每次更新,只会停止更新count后的 1秒才执行(这里设置成了1秒)
debounce(count, (newCount) => print("这是count的值: $newCount"), time: Duration(seconds: 1));

/// 类似于节流功能 频繁更新值每秒钟只触发一次 (因为这里设置成了1秒)
interval(count, (newCount) => print("这是count的值: $newCount"), time: Duration(seconds: 1));

GetX状态管理的疑惑


1.1 哪些地方可以使用.obs



  • 可以直接在类中赋值使用


class RxUser {
final name = "Camila".obs;
final age = 18.obs;
}


  • 直接将整个类都变成可观察对象


class User {
User({String name, int age});
var name;
var age;
}

final user = User(name: "Camila", age: 18).obs;

1.1.2 一定要使用xxx.value获取值吗?


这个并没有强制要求使用xxx.value获取值,可以直接使用xxx这能让代码看起来更加简洁


1.2 可观察对象是类如何更新?



  • 两种方式可以更新,使用其中一种即可


class User() {
User({this.name = '', this.age = 0});
String name;
int age;
}
final user = User().obs;

// 第一种方式
user.update( (user) {
user.name = 'Jonny';
user.age = 18;
});

// 第二种方式
user(User(name: 'João', age: 35));

// 使用方式
Obx(()=> Text("名字 ${user.value.name}: 年龄: ${user.value.age}"))

// 可以不需要带.value访问,需要将user执行
user().name;

GetX状态管理的一些原理


1.1.1.obs原理是什么?


var name = "dart".obs



  • 源码只是通过StringExtensionString扩展了一个get属性访问器

  • 原理还是通过RxString做绑定


tips: 如果想查看源码的话可以通过 control键 + 左击.obs就可以进入源码里面了


1.2 Obx的基本原理是什么?



  • 简而言之,Obx其实帮我们包裹了一层有状态组件


var build = () => Text(name.value)

Obx(build);


继承了一个抽象ObxWidget类,将传递进来的build方法给了ObxWidget,还得看看ObxWidget做了什么



ObxWidget继承了有状态组件,并且build函数让Obx类实现了



_ObxWidget主要做了两件事情



  1. 初始化的时候监听依赖收集,销毁时清空依赖并关闭监听。这是Obx的核心

  2. Obx实现的build函数传递给了RxInterface.notifyChildren执行



NotifyManager是一个混入,主要功能



  • subject属性用于传递更新通知

  • _subscriptions属性用于存储RxNotifier实例的订阅列表

  • canUpdate方法检查是否有任何订阅者

  • addListener用于将订阅者添加到订阅列表中,当 RxNotifier 实例的值发生变化时,它将通过 subject 发出通知,并通知所有订阅者

  • listen方法监听subject变化并在变化时执行回调函数

  • close关闭所有订阅和释放内存等

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

有趣的futu android面试体验

futu面试算是我面过的最特殊的了,大部分问题都是智力或者算法题,幸好我平常有刷算法的习惯,即使是项目相关的题,也是非常开放式的,几乎没看到八股 整体体验面试官还是比较专业的,其中的实现队列题和概率题我没有考虑到的点都和我进行了友好讨论,提醒了我,尤其是三面的...
继续阅读 »

futu面试算是我面过的最特殊的了,大部分问题都是智力或者算法题,幸好我平常有刷算法的习惯,即使是项目相关的题,也是非常开放式的,几乎没看到八股


整体体验面试官还是比较专业的,其中的实现队列题和概率题我没有考虑到的点都和我进行了友好讨论,提醒了我,尤其是三面的那道概率题,挺有意思,也是多亏面试官提示


不算难,但是需要扎实的基础,大家可以作为参考


一面:


项目


recyclerview刷新设计,分页,复杂布局,网络流量 刷新包含整个刷新,单个item刷新, 局部刷新策略,diff对比后台刷新id


MVVM的viewModel与android中的ViewModel的区别


如何分享一个抽象列表框架(如recyclerview)技术给客户端同事,包含pc, ios等


如何做一个需求,输入框可以输入,输入字符串变化时都会有回调产生对应效果


算法/智力




  1. 数组实现队列,入队出队


    增加扩容功能,


    修改为循环队列







  1. 9个砝码,一个轻的,最少次数称出来?


    称的次数与砝码数量有什么关系?






  1. 数组中和大于等于target的长度最小的连续子数组


二面:




  1. 多线程


    static a = 0;

    thread1: a += 1;

    thread2: a += 1;

    最后的结果区间







  1. 已知公司OA数据库有一个员工信息表,


    包含员工ID,员工姓名,入职月份(如201801),和离职月份。


    财务审核时发现 201803 到 201808 这6个月,


    当时所有在职员工都少发了工资,


    现在老板需要了解有多少人受影响需要获得补偿。


    请写出查询语句。




  2. a b c 轮流投掷一个硬币,直到正面出现即胜利,求c获胜的概率




  3. 在一个字符串中,找出不包含重复字符的最长子字符串的长度




aa => a => 1


abcdaf => bcdaf => 5


abcd => abcd => 4


abacad=>bac=>3


afbcdef => afbcde => 6


三面



  1. 编码:输入有字符串s1和s2,判断s2是否包括s1的排列 例如, 输入s1=abc,s2=abcd, 输出True; 输入s1=abc,s2=acbd, 输出True; 输入s1=abc,s2=ambnc, 输出False; (排列的解释:字符串abc,则abc的排列包括abc、acb、bac、bca、cab 和 cba)


先用了排序,复杂度


然后用数组,空间换时间




  1. 90% 返回0,10%返回1,如何包装此方法让返回0与1都是50%




  2. 做过的最难/有挑战性的项目


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

环信 uni-app Demo升级改造计划——Vue2迁移到Vue3(一)

前言由于环信uni-app Demo 为早期通过工具从微信小程序转换为的 uni-app 项目,经过实际的使用以及复用反馈,目前已经不适用于当前的开发使用,因此开启了整体升级改造计划,目前一期计划将 vue2 代码进行手动转换为 vue3+vite,并剔除原项...
继续阅读 »

前言

由于环信uni-app Demo 为早期通过工具从微信小程序转换为的 uni-app 项目,经过实际的使用以及复用反馈,目前已经不适用于当前的开发使用,因此开启了整体升级改造计划,目前一期计划将 vue2 代码进行手动转换为 vue3+vite,并剔除原项目中已经无用的项目代码,下面记录一下升级操作,如果升级过程,对大家有所帮助,深感荣幸~

前期准备

  • 【重要】阅读 uni-app 官网文档 Vue2 升级 Vue3 指南文档地址
  • 调研迁移到 Vue3 中原有的 Demo 中哪些三方库或者方法将不可用主要 uview UI 库不支持 Vue3)。
  • 下载并运行环信官网 uni-app 项目(原项目master分支)。Demo下载地址
  • 在 HubilderX 中创建容器项目所谓容器项目即为创建一个空白的 Vue3 模板,用以逐步将 Vue2 的项目代码逐步挪到此项目中。
  • 在空白项目中引入 uni-ui 组件,主要为了使用其组件替换原项目 uviewUI 组件
  • 确认升级流程以及方式本次升级采用渐进式语法修改形式,主要方式为迁移一个组件则将修改一个组件的语法为 vue3,如该组件依赖多个组件则先切断相组件的连接注释大法,后续逐步放开并配套修改。

核心迁移步骤

第一步、导入环信 uni-app SDK

原有 Vue2 版本 uni-app-demo 项目为本地引入 SDK 包,对于有些习惯 npm 安装导入的同学不太友好,目前 uniSDK 已经支持 npm 安装并导入,因此将原有本地引入 js 文件改为通过 npm 安装 SDK 并 import 导入 SDK。

//第一步 打开终端执行 npm install easemob-websdk
//第二步 复制原demo中的utils文件夹至空白项目中
//第三步 找到utils文件夹中的WebIM.js 文件中的导入SDK方式改写为impot 导入 easemob-websdk/uniApp包,具体代码如下。
/* 原项目引入SDK代码 */
import websdk from '../newSDK/uniapp-sdk-4.1.2';
/* 改写后的代码 */
import websdk from 'easemob-websdk/uniApp/Easemob-chat';

第二步、CommonJS 导入导出改写为 ESM

这种改写原因两点:

1、CommonJS 规范在 Vite 中使用本身并不支持,如果支持则需要进行单独配置。

2、原始项目中既有 CommonJS 导入方式,也有 ESM 导入,借此机会进行统一。

进行到此主要是先将原始项目中的 CommonJS 导出 WebIM 实例改为 ESM 导出,后续会在语法改造过程中将所有 CommonJS 规范改写为 ESM 导出,后续将不在本文中提及,实例代码如下

/* 原始项目utils/WebIM.js的导入导出WebIM实例代码段 */
//导入方式
let WebIM = (wx.WebIM = require('./utils/WebIM')['default']);
//导出方式
module.exports = {
default: WebIM,
};

/* 改写后导入导出 */
//导入方式
import WebIM from '@/utils/WebIM.js';
//导出方式
export default WebIM;

第三步、迁入 App.vue 组件

完整的复制原始项目中的 App.vue 组件(uni 的 Vue3 模板中也支持 Vue2 代码,因此可以放心进行 CV)

App.vue 组件涉及到的改动为注释掉暂时没有引入的 js 文件,后续进行引入,去除 scss 中的 uview 样式代码,引入后续将要完全剔除 uview 组件。

App.vue 中代码较多此示例做了大量的缩减,大致调整之后的结构如下。

<script>
import WebIM from '@/utils/WebIM.js';
//这些导入暂时注释,后续再进行引入
//let msgStorage = require("./components/chat/msgstorage");
//let msgType = require("./components/chat/msgtype");
//let disp = require("./utils/broadcast");
//let logout = false;

//import { onGetSilentConfig } from './components/chat/pushStorage'
export default {
//export default的代码块原封不动,此处先进行了删除,实际迁入不用动。
data (){
return {

}
}
}
</script>
<style lang="scss">
@import './app.css';
/*注意这行代码删除 @import "uview-ui/index.scss"; */
</style>

第四步 牛刀小试~ 迁入 Login 组件

先迁入一个 Login 组件热热身,毕竟从登录开始,原始项目中有注册、Token 登录、等等但目前暂不需要所以只需迁入 Login 组件。

在迁入前我们先了解并思考一下,Vue2 的 Options API 与 Vue3 Composition API 一些特点,主要目的是用较小的代价进行 Vue3 语法改造。
Vue3 模版支持 setup 语法糖,因此可以直接使用使用 setup 语法糖方式进行语法改造。

<script setup>
/* 原始代码片段 */
let WebIM = require("../../utils/WebIM")["default"];
let __test_account__, __test_psword__;
let disp = require("../../utils/broadcast");
data() {
return {
usePwdLogin:false, //是否用户名+手机号方式登录
name: "",
psd: "",
grant_type: "password",
psdFocus: "",
nameFocus: "",
showPassword:false,
type:'text',
btnText: '获取验证码'
};
},
/* 改造后的代码 */
//使用reactive替换并包裹原有data中的参数
import { reactive } from 'vue'
import disp from '@/utils/broadcast.js'; //修改为ESM导入
const WebIM = uni.WebIM; //从挂载到uni下的WebIM中取出WebIM并赋值用以替换原有单独require导入的WebIM
const loginState = reactive({
usePwdLogin: true, //是否用户名+手机号方式登录
name: '',
psd: '',
grant_type: 'password',
psdFocus: '',
nameFocus: '',
showPassword: false,
type: 'text',
btnText: '获取验证码',
});

//methods中的方法提取到外层中,例如将login 登录IM进行调整
//登录IM
const loginIM = () => {
runAnimation = !runAnimation;
if (!loginState.usePwdLogin) {
if (!__test_account__ && loginState.name == '') {
uni.showToast({
title: '请输入手机号!',
icon: 'none',
});
return;
} else if (!__test_account__ && loginState.psd == '') {
uni.showToast({
title: '请输入验证码!',
icon: 'none',
});
return;
}
const that = loginState;
uni.request({
url: 'https://a1.easemob.com/inside/app/user/login/V2',
header: {
'content-type': 'application/json',
},
method: 'POST',
data: {
phoneNumber: that.name,
smsCode: that.psd,
},
success(res) {
if (res.statusCode == 200) {
const { phoneNumber, token, chatUserName } = res.data;
getApp().globalData.conn.open({
user: chatUserName,
accessToken: token,
});
getApp().globalData.phoneNumber = phoneNumber;
uni.setStorage({
key: 'myUsername',
data: chatUserName,
});
} else if (res.statusCode == 400) {
if (res.data.errorInfo) {
switch (res.data.errorInfo) {
case 'UserId password error.':
uni.showToast({
title: '用户名或密码错误!',
icon: 'none',
});
break;
case 'phone number illegal':
uni.showToast({
title: '请输入正确的手机号',
icon: 'none',
});
break;
case 'SMS verification code error.':
uni.showToast({
title: '验证码错误',
icon: 'none',
});
break;
case 'Sms code cannot be empty':
uni.showToast({
title: '验证码不能为空',
icon: 'none',
});
break;
case 'Please send SMS to get mobile phone verification code.':
uni.showToast({
title: '请使用短信验证码登录',
icon: 'none',
});
break;
default:
uni.showToast({
title: res.data.errorInfo,
icon: 'none',
});
break;
}
}
} else {
uni.showToast({
title: '登录失败!',
icon: 'none',
});
}
},
fail(error) {
uni.showToast({
title: '登录失败!',
icon: 'none',
});
},
});
} else {
if (!__test_account__ && loginState.name == '') {
uni.showToast({
title: '请输入用户名!',
icon: 'none',
});
return;
} else if (!__test_account__ && loginState.psd == '') {
uni.showToast({
title: '请输入密码!',
icon: 'none',
});
return;
}
uni.setStorage({
key: 'myUsername',
data: __test_account__ || loginState.name.toLowerCase(),
});
console.log(111, {
apiUrl: WebIM.config.apiURL,
user: __test_account__ || loginState.name.toLowerCase(),
pwd: __test_psword__ || loginState.psd,
grant_type: loginState.grant_type,
appKey: WebIM.config.appkey,
});
getApp().globalData.conn.open({
apiUrl: WebIM.config.apiURL,
user: __test_account__ || loginState.name.toLowerCase(),
pwd: __test_psword__ || loginState.psd,
grant_type: loginState.grant_type,
appKey: WebIM.config.appkey,
});
}
};
</script>

改造中会遇到了原 Vue2 中原 data 部分参数通过使用 reactive 包裹并重命名,需要注意把语法中的 this.、me.、this.setData 进行替换为包裹后的 state 命名,另外 template 中也要同步进行替换,这一点在后续所有组件改造中都会遇到。

Login 组件需要 page.json 中进行路由的配置,只有配置成功之后我们方可运行项目并展示页面!

此时就可以启动项目运行观察一下看看页面是否可以正常的进行展示,当然是运行到小程序还是 H5 以及 App 上自行选择。

第五步、 迁入“Home 页中的”三个 Tab 页面【conversation 会话列表,mian 联系人页、Setting 我的页面】

迁移各组件,此处使用 conversation 组件作为示例,其余两个组件完全相同的步骤,全部示例代码将在文章末尾给出地址。

在原项目中包括已迁移进来的 App.vue 组件中有下面这样一个方法,其作用即为环信 IM 连接成功之后触发 onOpened 该监听回调,进行路由跳转进入到会话页面,因此不难理解,open 之后首个跳转的页面即为 conversation。

    onLoginSuccess: function (myName) {
uni.hideLoading();
uni.redirectTo({
url: "../conversation/conversation?myName=" + myName,
});
},
  • 在原始项目中 copy conversation(会话)组件至容器项目相同目录下,另外不要忘记顺手在 page.json 下配置路由。

  • 开始改写会话组件中的代码

//script 标签增加 setup 使其支持setup语法糖
<script setup>
/* 引入所需组合式API */
//computed 用以替换options API中的计算属性,Vue3中计算属性使用略有差异。
import {reactive,computed} from 'vue'
/* 引入所需声明周期钩子函数替换原有钩子函数,该写法uni-appvue2升级vue3指南有提及 */
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app';
/* 调整disp为import导入 */
// let disp = require("../../utils/broadcast");
import disp from '@/utils/broadcast';
/* 调整WebIM引入直接从uni下取 */
// var WebIM = require("../../utils/WebIM")["default"];
const WebIM = uni.WebIM
let isfirstTime = true;
/* components中的组件暂时注释,template中的组件引入也暂时注释,
* 另options API中的components中的组件注册也暂时注释
*/

// import swipeDelete from "../../components/swipedelete/swipedelete";
// import longPressModal from "../../components/longPressModal/index";

/* data 提出用reactive包裹并命名 */
const conversationState = reactive({
// 内容省略...
});

/* onLoad替换 */
onLoad(() => {
//所有通过this. 进行方法方法调用全部删除
disp.on('em.subscribe', onChatPageSubscribe);
//监听解散群
disp.on('em.invite.deleteGroup', onChatPageDeleteGroup);
//监听未读消息数
disp.on('em.unreadspot', onChatPageUnreadspot);
//监听未读加群“通知”
disp.on('em.invite.joingroup', onChatPageJoingroup);
//监听好友删除
disp.on('em.contacts.remove', onChatPageRemoveContacts);
//监听好友关系解除
disp.on('em.unsubscribed', onChatPageUnsubscribed);
if (!uni.getStorageSync('listGroup')) {
listGroups();
}
if (!uni.getStorageSync('member')) {
getRoster();
}
readJoinedGroupName();
});
/* onShow替换 */
onShow(() => {
uni.hideHomeButton && uni.hideHomeButton();
setTimeout(() => {
getLocalConversationlist();
}, 100);
conversationState.unReadMessageNum =
getApp().globalData.unReadMessageNum > 99
? '99+'
: getApp().globalData.unReadMessageNum;
conversationState.messageNum = getApp().globalData.saveFriendList.length;
conversationState.unReadNoticeNum =
getApp().globalData.saveGroupInvitedList.length;
conversationState.unReadTotalNotNum =
getApp().globalData.saveFriendList.length +
getApp().globalData.saveGroupInvitedList.length;
if (getApp().globalData.isIPX) {
conversationState.isIPX = true;
}
});
/* 计算属性改写 */
const showConversationName = computed(() => {
const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
return (item) => {
if (item.chatType === 'singleChat' || item.chatType === 'chat') {
if (
friendUserInfoMap.has(item.username) &&
friendUserInfoMap.get(item.username)?.nickname
) {
return friendUserInfoMap.get(item.username).nickname;
} else {
return item.username;
}
} else if (
item.chatType === msgtype.chatType.GROUP_CHAT ||
item.chatType === msgtype.chatType.CHAT_ROOM
) {
return item.groupName;
}
};
});
const handleTime = computed(() => {
return (item) => {
return dateFormater('MM/DD/HH:mm', item.time);
};
});
/* 将methods中方法全量提取到外层与onLoad onShow等API平级 */
const listGroups = () => {
return uni.WebIM.conn.getGroup({
limit: 50,
success: function (res) {
uni.setStorage({
key: 'listGroup',
data: res.data,
});
readJoinedGroupName();
getLocalConversationlist();
},
error: function (err) {
console.log(err);
},
});
};

const getRoster = async () => {
const { data } = await WebIM.conn.getContacts();
if (data.length) {
uni.setStorage({
key: 'member',
data: [...data],
});
conversationState.member = [...data];
//if(!systemReady){
disp.fire('em.main.ready');
//systemReady = true;
//}
getLocalConversationlist();
conversationState.unReadSpotNum =
getApp().globalData.unReadMessageNum > 99
? '99+'
: getApp().globalData.unReadMessageNum;
}
console.log('>>>>好友列表获取成功', data);
};
const readJoinedGroupName = () => {
const joinedGroupList = uni.getStorageSync('listGroup');
const groupList = joinedGroupList?.data || joinedGroupList || [];
let groupName = {};
groupList.forEach((item) => {
groupName[item.groupid] = item.groupname;
});
conversationState.groupName = groupName;
};

//还有很多方法就不一一展示,暂时进行了省略...
/* onUnload */
onUnload(() => {
//页面卸载同步取消onload中的订阅,防止重复订阅事件。
disp.off('em.subscribe', conversationState.onChatPageSubscribe);
disp.off('em.invite.deleteGroup', conversationState.onChatPageDeleteGroup);
disp.off('em.unreadspot', conversationState.onChatPageUnreadspot);
disp.off('em.invite.joingroup', conversationState.onChatPageJoingroup);
disp.off('em.contacts.remove', conversationState.onChatPageRemoveContacts);
disp.off('em.unsubscribed', conversationState.onChatPageUnsubscribed);
});
</script

在做这三个组件迁移的时候主要的注意事项为,this 的替换,template 中的默认从 vue2 中 data 取的参数也要替换为被 reactive 包裹后的变量名。

启动运行调整

建议迁移一个组件调试一个组件,运行到 H5 端,从登录页面登录进去,并点击三个页面进行切换,观察是否有相应的报错,发现即进行修改并重新运行测试。

第六步、迁入复杂度最高的聊天相关组件。

以单聊作为说明示例:

1)迁入单聊入口组件[pages/chatroom]

chatroom 组件(groupChatroom 作用相同)为单聊功能聊天的入口组件,pages 中其他组件发起单聊聊天时均会跳转至该组件,而该组件同时又承载 components 下的 chat 组件作为容器形成聊天功能。

将 chatroom 组件 copy 至容器项目 pages 下并配置路由映射,为了语义化将 chatroom 更名为 singleChatEntry,并进行语法改造,此时 singleChatEntry 如下:

不要忘了,路由路径配套也要从 chatroom 更名为 singleChatEntry

<template>
<chat
id="chat"
ref="chatComp"
:chatParams="chatParams"
chatType="singleChat"
></chat>
</template>

<script setup>
import { ref, reactive } from 'vue';
import {
onLoad,
onUnload,
onPullDownRefresh,
onNavigationBarButtonTap,
} from '@dcloudio/uni-app';
import disp from '@/utils/broadcast';
import chat from '@/components/chat/chat.vue';

const chatComp = ref(null);
let chatParams = reactive({});
onNavigationBarButtonTap(() => {
uni.navigateTo({
url: `/pages/moreMenu/moreMenu?username=${chatParams.your}&type=singleChat`,
});
});
onLoad((options) => {
let params = JSON.parse(options.username);
chatParams = Object.assign(chatParams, params);
// 生成的支付宝小程序在onLoad里获取不到,这里放到全局变量下
uni.username = params;
uni.setNavigationBarTitle({
title: params?.yourNickName || params?.your,
});
});
onPullDownRefresh(() => {
uni.showNavigationBarLoading();
chatComp.value.getMore();
// 停止下拉动作
uni.hideNavigationBarLoading();
uni.stopPullDownRefresh();
});

onUnload(() => {
disp.fire('em.chatroom.leave');
});
</script>
<style>
@import './singleChatEntry.css';
</style>

2)完整迁入 components 组件

image.png

components 组件结构如上图,由于音视频功能已经废弃本次迁移决定剔除,但目前迁移方案采取“抓大放小,后续清算”的策略先一起迁入,后续剔除。

引入之后运行起来之后会发现有很多 require not a function 字眼的错误,同样我们要将所有 CommonJS 的导出修改为 ESM 导出,剩下的则是一点一点的去进行语法改造,整个 chat 下其实涉及组件非常多,因为 IM 所有消息的收发,以及渲染均囊括在此组件。

这里提一下 msgpackager.js、msgstorage.js、msgtype.js、pushStorage.js 几个 js 文件的作用。

msgpackager.js 主要为将收发的IM消息进行结构重组

msgstorage.js 将收发消息进行本地缓存

msgtype.js 消息类型以及聊天类型的常量文件

pushStorage.js 推送处理相关

迁入进去之后将开始针对大大小小十几个文件进行语法以及引入改造,另外其中个别文件还牵扯到使用的 uviewUI 那么则需要进行重写,最终经过改造以及剔除不再使用的组件以及音视频相关代码之后,结构如图:
image.png

有一点较为基础但是还是要强调注意的事项要提一下,在 components/chat 下的组件改造中经常出现父子组件的调用,那么父组件在使用子组件的方法的时候,由于 Vue3 中不能再通过类似$ref 直接去调用子组件中的方法或者值,子组件需要通过 defineExpose 主动进行暴露方可使用,这个需要进行注意。

迁移中发现 H5 的录音采用的 recorder-core.js 库,js 按需导入中有用到 require,那么需要改写为 import 导入,但是发现实例化时发现依然不是一个构造函数,通过改写从 window 下访问即正常使用,相关代码如下:

    /* 原代码片段 */
handleRecording(e) {
const sysInfo = uni.getSystemInfoSync();
console.log("getSystemInfoSync", sysInfo);
if (sysInfo.app === "alipay") {
// https://forum.alipay.com/mini-app/post/7301031?ant_source=opendoc_recommend
uni.showModal({
content: "支付宝小程序不支持语音消息,请查看支付宝相关api了解详情"
});
return;
}
let me = this;
me.recordClicked = true;
// h5不支持uni.getRecorderManager, 需要单独处理
if (sysInfo.uniPlatform === "web") {
import("../../../../../recorderCore/src/recorder-core").then((Recorder) => {
require("../../../../../recorderCore/src/engine/mp3");
require("../../../../../recorderCore/src/engine/mp3-engine");
if (me.recordClicked == true) {
clearInterval(recordTimeInterval);
me.initStartRecord(e);
me.rec = new Recorder.default({
type: "mp3"
});
me.rec.open(
() => {
me.saveRecordTime();
me.rec.start();
},
(msg, isUserNotAllow) => {
if (isUserNotAllow) {
uni.showToast({
title: "鉴权失败,请重试",
icon: "none"
});
} else {
uni.showToast({
title: `开启失败,请重试`,
icon: "none"
});
}
}
);
}
});
} else {
setTimeout(() => {
if (me.recordClicked == true) {
me.executeRecord(e);
}
}, 350);
}
}
/* 调整后代码片段 */
const handleRecording = async (e) => {
const sysInfo = uni.getSystemInfoSync();
console.log('getSystemInfoSync', sysInfo);
if (sysInfo.app === 'alipay') {
// https://forum.alipay.com/mini-app/post/7301031?ant_source=opendoc_recommend
uni.showModal({
content: '支付宝小程序不支持语音消息,请查看支付宝相关api了解详情',
});
return;
}
audioState.recordClicked = true;
// h5不支持uni.getRecorderManager, 需要单独处理
if (sysInfo.uniPlatform === 'web') {
// console.log('>>>>>>进入了web层面注册页面');
// #ifdef H5
await import('@/recorderCore/src/recorder-core');
await import('@/recorderCore/src/engine/mp3');
await import('@/recorderCore/src/engine/mp3-engine');
if (audioState.recordClicked == true) {
clearInterval(recordTimeInterval);
initStartRecord(e);
audioState.rec = new window.Recorder({
type: 'mp3',
});
audioState.rec.open(
() => {
saveRecordTime();
audioState.rec.start();
},
(msg, isUserNotAllow) => {
if (isUserNotAllow) {
uni.showToast({
title: '鉴权失败,请重试',
icon: 'none',
});
} else {
uni.showToast({
title: `开启失败,请重试`,
icon: 'none',
});
}
}
);
}
// #endif
} else {
setTimeout(() => {
if (audioState.recordClicked == true) {
executeRecord(e);
}
}, 350);
}
};

3)启动进行后续调整测试

启动之后验证发现更多的是一些细节问题,同样边改边验证。

后续总结

在首期迁移 vue2 升级 vue3 的工作中其实难度并没有很大,主要的工作量集中在语法的修改变更上,好在 uni-app 中可以同步去写 vue2 与 vue3 两种语法代码,这样有助于在引入之后陆续进行语法变更,另外迁移之后开发体验启动速度确实快了很多,接下来就可以腾出手针对 uni-app-demo 源码代码进行整体质量提升,敬请期待…

收起阅读 »

【2023】22届前端程序员毕业一年的心路历程

没有任何光环的,一个普通人的前端之路 启程 要讲是如何对前端感兴趣的,我大约可以从大一的C语言讲到大四的微信小程序,想了想这不是本篇文章的重点,所以掐住话头(。 自觉大学过得十分散漫,临近毕业我才开始认真对待自己的前途,好在专业选的不错——计科,啥也不会又啥都...
继续阅读 »

没有任何光环的,一个普通人的前端之路


启程


要讲是如何对前端感兴趣的,我大约可以从大一的C语言讲到大四的微信小程序,想了想这不是本篇文章的重点,所以掐住话头(。


自觉大学过得十分散漫,临近毕业我才开始认真对待自己的前途,好在专业选的不错——计科,啥也不会又啥都懂一点。


大四那年寒假我从学校建的就业群里参加了一个线上面试培训班(虽是商业但全程免费),从接触前后端的概念到决定学前端只用了十分钟,那个时候我连html都不太会写,就被拉去接触企业级的前端开发流程,可想而知有多手忙脚乱,“蓝湖、禅道、yapi...”,这些现在习以为常的东西对于当时的我来说根本形不成概念,中途跟老师提了一次退出,理由是不想拖大家后腿,好在老师十分鼓励我继续跟进。于是我一边跟着培训进度一边在b站学习前端基础,虽然到结课也没学完,培训课也因为人数不够被腰斩。


留下来的同学被打包送进面试背题班(没错就是背八股文),明确的目的使得效率突飞猛进,回想起来仍觉得那一个月过得十分抓马,但也是在那里我真正叩响了前端的大门。


实习前夕


培训课一个月结束,寒假还剩半个月,对就业十分心虚的我决定提前返校,由于疫情学校不提前开放,我便借住在同学的校内公寓里,那段日子每天一睁眼就是打开电脑看视频课,跟着敲代码,直到学完vue以后,我才有了些许底气面对我的简历(培训老师倾情指导,洋洋洒洒写了好多我根本不懂的技能,只能硬着头皮一个一个学。


寒假前我在班级群里保存了一份招聘实习生的材料,负责人说我联系的时间有些晚,但还是接受了我的简历并通知我去面试,面试前一天培训老师和我通了电话,告诉我可能会遇到哪些提问,以及怎么答。


人生第一次求职面试就这样顺利结束,虽然实习工资近乎于倒贴上班hhh(长沙的行情令人闻之落泪)


经验之谈


入行前端于我而言是一段兵荒马乱的时光,给后来者的建议就是早做准备!早点实习!真的会少很多焦虑与迷茫。


另外,学校的资源一定要充分利用,出了社会就更没人为你操心了。


实习阶段


实习的过程中我写了一个门户网站,也参与了一个大型管理后台的需求迭代


内容少而简单,三个月时间就是夯实基础以及查漏补缺,不过很大一个原因就是我学的刚好是我要用的,真真是一点新东西都没有出现,也是很神奇_(:з」∠)_

大概是以下这些:


html + js + css

vue2 + webpack + axios + vuex

element-ui

git


这段阅历虽然浅薄但也不可或缺,让我明白学过跟真正会用是两码事,也真正实践了企业级的开发流程。
临别前负责人找我谈话,语重心长地建议我去大城市发展,我点点头,但对于自身的实力十分不自信,并没抱太多希望。


春招的末班车


因为考研错过秋招,又因为实习错过春招,在确定不转正后我才把精力放到了春招末班车上。

不得不承认机会真的所剩无几,为数不多的几场招聘会一眼望去全是销售,偶尔有前端岗位也不对本校学生开放,简历改了又改,真正能投出去的屈指可数。


当然也不是全无机会,要是我当时甘心留在长沙,从我实习能接触到的技术与眼界来看,也许就不会有这篇文章了。


转折点就在那些不对本校学生开放的前端岗位上,招聘摊位的人并没有接我的简历,但我本着排队都排了那么久的心态还是扫了易拉宝上的企业春招二维码,投递了线上的简历。


大约是幸运女神的眷顾,一面二面终面,耗时近两周,最终一路绿灯拿到了深圳的offer,后来从导师那里得知,我投递简历的那一天,也是春招通道关闭的最后一天。


真正的修炼之路


一个事实是,即使抓住了机会,我也时常焦虑,放眼同期入职的都是双一流的研本,说没有压力神都不信。


疏解压力的办法只有比别人加倍努力。


试用期三个月,第一个月vue2就被公司淘汰了,前端技术更新迭代的速度令我咋舌,那段时间我几乎把所有的技术栈都更新了一遍。


稳定下来的vue技术栈大致如下:


html + js + ts + css + less

vue3 + vite + axios + pinia

ant-design-vue

git + docker + nginx + devops


学会这些大致能跟上团队的开发速度,但代码依旧不优雅,知识也不成体系。
转正后我忙于一个又一个需求,新技能的学习基本依托于业务,例如被动学习了fabric、echarts、g2plot等等。


要是组织架构没有变动,身边的同事没有被优化,我大约会持续这样状态很久,直到遇到下一个瓶颈。


变动带来了不安,也推动了新一轮的自我学习,转到新的业务线后,自己明显感觉能控制开发节奏了,独立完成的需求越来越多,遇到知识盲区的次数也越来越少,部门的业务和技术栈大差不差,好处是不用花费精力去适应另一套生态,坏处也很明显,没办法从工作中学到技术层面的新东西了。


也是在这段时间,我能够抽出时间自主学习,先是用hexo搭建了个人博客,又尝试着用react自己设计开发,期间去重修了nodejs,补充了一些后端的技能。我导师说他初中就开始做这些事情了(我发誓我初中对网络的概念还停留在4399...( _ _)ノ|


技术之外


从我转正到22年年底,一共参加过两次答辩,一次是转正答辩,一次是年终述职答辩,两次的结果都不错,刚毕业的我很喜欢从一些虚无缥缈的事情上寻找价值,比如被领导夸了两句就觉得自己像一个快乐的小陀螺,到现在还是如此。


与此同时,工作带给我的精神内耗一直很严重,总觉得自己什么都不会,也不聪明,不懂表达,追求不高,喜欢给自己也给别人画饼。


唯一的优势大约是我尚且年轻,仍然对无垠的未来充满期待,并且愿意为之付出努力。


23年的flag


1、系统学习前端,不再浅尝辄止;

2、继续学习英语,关注前沿技术,坚持博客产出;

3、早睡早起,饮食规律,平凡且健康就很好。


写下这篇文章更多的是记录自己曾经和当下的状态,期冀今后的每一年我都比现在更加从容。


作者:兀米米
来源:juejin.cn/post/7216223889487511608
收起阅读 »

总结:用chatGPT整理现有商业模式

上周用chatGPT做了几个简单的测试。 用历史的角度询问了岳飞的去世。 并且让它帮我写了一段快速排序的javascript代码,以及解释了一下vue2的双向数据绑定原理。 跟朋友谈论了chatGpt目前的能力和未来的发展。 我们都认为chatGpt作为一...
继续阅读 »

上周用chatGPT做了几个简单的测试。


用历史的角度询问了岳飞的去世。
image.png


并且让它帮我写了一段快速排序的javascript代码,以及解释了一下vue2的双向数据绑定原理。


image.png


跟朋友谈论了chatGpt目前的能力和未来的发展。


我们都认为chatGpt作为一个高效的工具,在不远的将来,将很大程度上的代替一部分人的工作岗位。


正如公司与人才的关系一样,人才与某个岗位挂钩,本质上是可以把人当成工具来看的。而当对chatGTP的投产比高于人的时候,那么从公司的角度一定会选择chatGTP而不是选择人。


未来大家都能用上chatGTP了,那么在这样高效率的工具面前,利用这个高效工具拿到结果的能力,就不仅仅只是专业能力。站在全局的角度去搜索和整合信息的能力,会显得更为重要。


聪明的人能够把chatGPT发挥10倍的作用,普通人只能发挥1倍的作用,从而产生了差距。


介于AI的爆发早晚都会到来,那么我就通过AI查阅一下如果他来了,我们可以看到现有的商业模式有哪些,并且哪些模式能跟AI结合到一起呢?


所以,今天这一篇文章主要是通过chatGTP对目前常见的一些商业模式的收集,希望对大家有帮助。


01传统商业模式


传统的商业模式是:企业通过生产和销售产品或服务来获得利润。


比如卖衣服、买鞋子、卖蜂蜜等等实物的方式。这种模式的核心是降低成本和提高销售量。


目前我们这种模式做的比较成功的公司有:Coca-Cola、walmart、Procter & Gamble、McDonalds和Ford。


Coca-Cola是全球性的饮料生产企业。


walmart是全球最大的零售企业。


Procter & Gamble是全球性的消费品制造商。


McDonalds是全球性的快餐企业。


Ford是全球性的汽车制造商。


02订阅模式


订阅模式是:企业提供订阅服务来为企业带来稳定的现金流和客户关系。


这种模式的优势在于可以提供更好的客户体验和更稳定的收益。


目前我们这种模式做的比较成功的公司有:Netflix、Amazon Prime、Spotify、The New York Times和Dollar Shave Club。


Netflix是全球性的在线流媒体服务提供商。


Amazon Prime是亚马逊提供的一个订阅服务,订阅用户可以享受免费快递、流媒体服务和其他福利。


Spotify是一家全球性的音乐流媒体服务提供商,可以通过订阅服务来获取音乐和广播。


The New York Times是全球知名的报纸和新闻网站,通过定于服务向用户提供新闻和其他内容。


Dollar Shave Club是一家提供订阅式刮胡刀和理发产品的企业,通过订阅服务向用户提供每月定期配送的产品。


03平台模式


平台模式的企业,提供一个平台来连接买家和卖家,从而获得利润。


比较常见的国内平台有淘宝、天猫、美团、滴滴打车等等。


这种模式的优势在于可以快速扩展规模和提高利润率。


目前做的比较成功的企业有:Airbnb、Uber、Amazon Marketplace、Upwork和eBay。


Airbnb是一家在线的短期住宿预定平台,这个平台提供者房屋出租和旅游体验预定服务。


Uber是一家提供打车服务的在线平台,通过连接司机和乘客,提供便捷的打车服务。


Amazon Marketplace是亚马逊提供的一个在线市场平台,卖家可以在上面销售自己的商品。


Upwork是一个在线自由职业者和雇主的平台,雇主可以在上面发布任务,自由职业者可以在上面寻找机会。


eBay是一家全球性的在线拍卖和购物平台,卖家可以在上面销售商品,买家可以在上面购买商品。


04开放式商业模式


开放式商业模式的企业通过:开放自己的技术、数据和知识产权,与其他公司和个人合作,从而获得更大的市场和创新机会。


目前做的比较成功的企业有:Android、IBM Linux、Tesla、Salesforce和Wikipedia。


Android是一款由Google开发的开放式移动操作系统,允许开发者在其上开发应用程序。


IBM Linux是IBM 在 2000 年将其操作系统Linux开放源代码,从而为Linux社区的发展做出了重大贡献。


Tesla是一家全球知名的汽车制造商,其开放商业模式在于其开放自己的电池技术和充电网络,与其他汽车制造商合作,从而扩大了市场和创新机会。


Salesforce是一家提供云计算解决方案的企业,其开放商业模式在于其开放自己的API接口,允许其他企业和开发者在其上开发应用程序。


Wikipedia是一个免费的在线百科全书,其开放商业模式在于其开放自己的内容和编辑权限,允许用户编辑和更新条目。


05企业社会责任模式


企业社会责任模式的企业通过履行社会责任来提高品牌形象和市场份额。


这种模式的优势在于可以赢得消费者和员工的认可和支持。


目前做的比较成功的企业有:Patagonia、Ben &Jerry's、TOMS和The Body Shop。


Patagonia是一家知名的户外用品制造商,其企业社会责任模式在于其对环境和气候变化的重视和投资,例如推广可持续农业和使用环保材料。


Ben &Jerry's是一家知名的冰淇淋制造商,其企业社会责任模式在于其对社会公正和环保的关注和投资,例如推广可持续农业和支持社会公益事业。


TOMS是一家知名的鞋类品牌,其企业社会责任模式在于其对弱势群体的关注和投资,例如每出售一双鞋,就会捐赠一双鞋给需要的人。


The Body Shop是一家知名的化妆品品牌,其企业社会责任模式在于其对环境、社会公正和动物权益的关注和投资,例如推广环保材料和支持社会公益事业。


06服务模式


服务模式的企业通过提供各种服务,例如咨询、培训、维修等,从而获得利润。


目前做的比较成功的企业有:Uber、Airbnb、Amazon Prime和Netflix。


Uber是一家科技公司,提供网络预订的出租车服务。


Airbnb是一家在线市场,提供旅游住宿服务。


Amazon Prime是亚马逊公司提供的一个订阅服务,用户每年支付一定的费用,即可享受包括免费配送、视频流媒体等在内的各种优惠和服务。


Netflix是一家在线视频流媒体服务提供商,为用户提供了大量的电影和电视节目。


07建立生态系统模式


建立生态系统模式企业通过建立一个完整的生态系统来为客户提供全方位的服务,并从中获得利润。


目前做的比较成功的企业有:苹果公司的ioS生态系统、谷歌的Android生态系统和微软的Azure云计算生态系统。


苹果公司的ioS生态系统是苹果公司通过其iOS生态系统建立了一个完整的生态系统,包iPhone、iPad、Mac电脑、Apple Watch等设备、App Store、iTunes Store、Apple Music等服务。


谷歌的Android生态系统是谷歌通过其Android生态系统建立了一个大规模的生态系统,包括数百万款应用程序、各种服务和设备,例如Pixel手机、Google Home等。


微软的Azure云计算生态系统是微软通过其Azure云计算生态系统建立了一个全球性的云计算平台,为用户提供了各种云服务,如laas、 Paas、SaaS等。


08生产线模式


生产线模式的企业通过控制生产线上的每一个环节来提高效率和降低成本。


目前做的比较成功的企业有:丰田生产线模式、摩根汽车生产线模式和富士康生产线模式。


丰田生产线模式是丰田生产线模式是一种高效的、精益的生产方式,它的核心是“精益生产”和“精益流程”。


摩根汽车生产线模式是摩根汽车是一家英国的小型汽车制造商,采用传统的手工制造方式和高度定制化的生产线模式。


这种模式的成功在于,它能够为客户提供独一无二的汽车,满足客户对于个性化和品质的需求。


摩根汽车凭借这种模式在高端汽车市场上获得了成功。


富士康生产线模式是富士康是一家全球性的电子制造服务提供商,采用高度自动化和标准化的生产线模式。


总结


以上,就是我通过chatGPT帮我整理的8大商业模式,希望大家能根据自己的资源选择能结合的商业模式。


如果这篇文档对你有帮助,欢迎点赞、关注或者在评论区留言,我会第一时间对你的认可进行回应。精彩内容在后面,防止跑丢,友友们可以先关注我,每一篇文章都能及时通知不会遗失。


作者:程序员摩根
来源:juejin.cn/post/7217360688263413817
收起阅读 »

“勇敢的去走自己的夜路”——走出我的“浪浪山”

引子 2022年,经历过太多太多的故事,也发生了太多太多的事故。 这一年,迷途失措且努力,未来可期却恍惚,我错失了太多的机会,幸然遇到了大家,让我们一同努力见证Cool(小陈同学)的改变,这一年我经历过比赛失利,国奖失之交臂,也遇到了求职季的滑铁卢。但有幸的是...
继续阅读 »

引子


2022年,经历过太多太多的故事,也发生了太多太多的事故。


这一年,迷途失措且努力,未来可期却恍惚,我错失了太多的机会,幸然遇到了大家,让我们一同努力见证Cool(小陈同学)的改变,这一年我经历过比赛失利,国奖失之交臂,也遇到了求职季的滑铁卢。但有幸的是遇到了一堆可爱的掘友,利用掘金的资源也找到了一个工作。


这一年,我走了很远的路,吃了很多的苦,踩了很多的坑,才将这份年终总结交付与星球大伙。也曾有幸与掘友一起分享只属于我们的“情书”


第一节:对你,同样对自己说


今天是 2023年1月1日,这一年,半分努力,半分浑噩,忽隐忽现的理想,支撑着自己踽踽独行。几年前,他应征入伍,算不上什么好选择,也或许并没有选择的权利。北方干冷的空气,窗前停驻的麻雀,以及战友豪迈的言语曾一度让我觉得,南京或许会是我最终的归宿。


在南京的第二个年头,这一年我21周岁,报国的赤心和热血似乎都正热时,我做出了人生的第一个计划,“退役复学”。


感恩军旅生活,让我真正的热爱祖国与持续学习,在二零年上旬,新冠疫情爆发了,一个八十八线小城市的我,除了紧张的气氛外,到也没受到多大的影响,在家依旧忙碌,直到2022年2月10日,我记得非常清楚,写了一天前端(三件套的弱鸡)代码的我,结束了当天的笔记小结,打开了B站,悄然间随机看见了关注了好久的鱼皮居然真的开了学习圈子(编程导航),这让一个对编程说不上爱的萌新,从此爱上了coder与share,(一个利他的博主谁又能不爱呢?),曾经把编程视为作业的我,我发现我能用他code出一个全新的世界,我便一发不可收拾爱上了它(这里的它指的是编程)。


(一)身体是革命的本钱


but 「熬夜 + 不规律的作息 + 不健康的饮食」+ 「年轻」= 无事发生


“年轻人”,似乎总是有一种得天独厚的优势,有精力,有体力。而这对于我这个退役选手更是easy了,这些不太好的习惯也似乎在年轻一代的大水潭中泛不出多少涟漪,凭借着这份“本钱” ,自然能更加心安理得,反正:我还年轻,我还可以熬。


(2) 继续战斗,也请先照顾好自己


疫情消耗掉了大半年的时间,大学断断续续的锻炼,把熬夜换成了早起,开始按时吃早餐,解封后的日子,趁着南方冬天来的很晚,与几个战友开始了跑步的活动,这一阶段体能上确实有了很大的提升,我很享受跑步后,被风吹过的感觉(皮一下:我也曾吹过未来女朋友曾吹过的风)。


说来也很神奇,每次当我没什么精神,只要去跑步,回来冲个澡就会精神百倍,所以我一般傍晚的时候有空就会去跑跑,然后就可以再晚上全身心的写代码,整理笔记(当然最后就是发到星球上面,感受大家阅读后的“指责与指导,哈哈哈”)。


运动本不应被当做一种应该做的任务,而应被看作一种休闲的方式,没必要与别人比较强度,组别,只有自己舒服就是最好的标准。


所以,不管是真的热爱也罢,苦于生计也罢,即使继续战斗也请先照顾好自己


(二) 随波逐流只会让你接近平均值


普通人的危机感总来自他人,而想要成为一个优秀的人,危机感必须来自自身,随波逐流只会让你靠近平均值,总有一种恍惚感,懂得越多,越觉得自己像这个世界的孤儿,与同龄人格格不入,总是自负的认为他人幼稚,就像鲁迅先生说过:“人类的悲欢并不相通......”。听着他们谈论着我 “早就走过的路”,“早就见过的风光”,我也只觉得他们吵闹。


可惜,我在某些时候,总是小气的,心中惰于学习,更不愿将自己的 “财富” 与他人分享,总忧虑别人以己为石,跳向远方,患得患失的一种矛盾,让自己无奈又颓靡。
后来我遇到了鱼皮,我发现分享的乐趣后我便不再随波逐流,持续性努力,以下是我在星球这一年输出的笔记



(ps:请大佬过目,记得留赞)如下:



大数据笔记:wx.zsxq.com/dweb2/index…


运维Devops笔记:wx.zsxq.com/dweb2/index…


低代码Lowcode笔记:wx.zsxq.com/dweb2/index…


yarn的学习:wx.zsxq.com/dweb2/index…


软件设计师:wx.zsxq.com/dweb2/index…


NodeJS笔记:wx.zsxq.com/dweb2/index…


机器学习方面:wx.zsxq.com/dweb2/index…


Vue+pinpa笔记:wx.zsxq.com/dweb2/index…


MySQL笔记:wx.zsxq.com/dweb2/index…


华为鸿蒙认证:wx.zsxq.com/dweb2/index…


软件工程笔记:wx.zsxq.com/dweb2/index…


力扣刷题攻略:wx.zsxq.com/dweb2/index…


ES6模块暴露笔记:wx.zsxq.com/dweb2/index…


ACM算法思维导图:wx.zsxq.com/dweb2/index…


Bootstrap笔记:wx.zsxq.com/dweb2/index…


网络安全资源贴:wx.zsxq.com/dweb2/index…


(三) 不被枯井遮住双眼,保持谦虚及自尊


目光短浅带来的问题是致命的,当你有一天觉得自己好像还不错,好像已经登到峰顶了。那就需要反思一下自己或许已经陷入了“枯井”中,你会这样想,那大概率是被枯井遮住了双眼,你看不到枯井之外的世界,为了一点点成就就沾沾自喜,虽然阶段性的成功也很值得高兴,但千万不要走进这份舒适区中,温水煮青蛙的例子也不少见,走出枯井后,你就会发现外面的世界还是在一个枯井中,你要做的就是不断的往上爬。


永远不要看不起任何人,即使一位在你眼中普普通通的人,他的技术或许逊色你不少,但是他在思想和创造性上总能给你意想不到的惊喜。即使我的学校很普通,但是我的身边仍然有着一批充满韧劲的朋友,希望能通过考研,亦或者对于技术的钻研,弥补自己高考的遗憾,我记得大二那年,我常常在凌晨一点半两点收到微信弹窗大家一起交流一些问题。备战比赛的三点一线生活,学技术的通宵达旦,为了目标不断努力,这样的人仍然值得我尊重与学习,我认为他们拥有了一名大学生应该有的“灵魂”


除此之外,请千万保持自尊,自尊并非别人给的,而是自己给的,如果遇到比自己弱的人就有“自尊”,遇到比自己强的人就畏畏惧惧,没有“自尊”,那么这种自尊就没有任何意义了,闻道有先后,术业有专攻,应当尊重任何在某个方向的前辈,但是也没必要过于拘束,见贤思齐,见不贤而内自省即可。


(四) 远离总是给你负面情绪的人


但是如果你遇到了一些人,总时时刻刻,在学习以及生活上给你一些负面的观点,这种人会严重影响你坚定往枯井上爬的信念,不管你们是什么关系,我给你一个建议——赶快跑(这里现代化的叫法喊:润),有多快,跑多快,如果你们不幸要发生必要的交互,请将这段关系限制在最小范围内,切勿投入感情


(五) 传道授业:若要学知识,必得为人师


这一年我很喜欢读一本书,那就是《软技能:代码之外的生存指南》(下面我会提到)其中有一个章节给我印象很深,即第33章,传道授业:若要学知识,必得为人师,下面我摘了一段:



在你传道授业的时候都会发生什么 当我们初次接触某个课题的时候,我们对于自己对此了解多少往往都会高估。我们很容易自欺欺人,以为已经对某样东西 了如指掌,直到我们试着去教会别人的时候,才能发现事实并非如此。你有没有被别人问过一个非常简单的问题,却震惊地发现自己不能清晰地解答。你刚开始会说:“这个,很明显……”,接下来只有“哦……”。这种情况在我身上屡屡发生。我们自认为已经透彻理解了这个话题,实际上我们只是掌握了表面知识。这就是传道授业的价值。在你的知识集合里面,总有一部分知识你并没有理解透彻到可以向别人解释,而“教”的过程能够迫使你面对这一部分。作为人类,我们的大脑善于模式识别。我们能够识别模式,并且套用这些固定的模式去解决许多问题, 而没有做到“知其然”也“知其所以然”。这种肤浅的理解力无碍于我们完成工作,因而不易被察觉。然而一旦我们试着向别人解释某件事情的运作原理或背后的原因的时候,我们在认知上的漏洞就会暴露出来。不过这并非坏事。我们需要知道自己的弱点,然后才能对症下药。在教别人的时候,你迫使自己面对课题中的难点,深入 探索,从只知皮毛变成完全理解。学习是暂时的,而理解是永久性的。我可以背诵九九乘法表,但是一旦理解了乘法的运算原理,即使突然记性不好,我也可以重做一张乘法表。



我已经记不清很多年前我初中亦还是我高中的一位任课老师曾经说过这么一句话:能教会别人,自己也就没问题了。可惜那个时候的自己压根没提起学习的欲望,当然了,也或许与我自己根本不喜欢枯燥的应试教育有点关系。我也没理解这句话的意思。大学这几年,我很喜欢与朋友交流技术方面的事情,每个人都有很多我意想不到的理解与想法。还有更多时候我更加喜欢帮助朋友解决一些问题,当你什么时候可以将别人的一个问题,用通俗的解释说明 + 简洁却又富有代表意义的实例 + 补充一些自己的理解与看法,说给别人听得时候,最起码,我认为你对于这块内容就真的入门了。当你能够滔滔不绝的讲解给别人一块内容,能合理的安排讲解的引子与顺序的时候,这说明这一块的知识已经在你脑海中有了一条清晰的体系。同时你通过与别人交流的时候,再根据别人对你提供的一些方向好好反思斟酌一下,不断的修改。相信我,当你成功与他人讲解/交流你的知识后,你会爱上这种感觉的。


但是老板和老师可不会等你,很多时候我们都不得不 “填鸭子” 式的学习一些内容,例如根据老师的要求,强制使用一些指定的框架或者技术,或者根据业务/项目组长的需要和安排,你需要快速的学习一些你并不熟悉的内容,凭借我们多年 “应试” 的本事,大家总能很快的就找到这种套路,例如怎么快速搭建环境,怎么配置,如何快速的用起来。但是千万别止步于此,不然终究只是一个CRUD工程师,这也不一定是坏事,当你熟悉如何用一款框架或技术后,再去看一些源码,或许会事半功倍。



作为人类,我们的大脑善于模式识别。我们能够识别模式,并且套用这些固定的模式去解决许多问题, 而没有做到“知其然”也“知其所以然”。



(六) 别让情绪扼杀你状态


(1) 所谓迷茫,都不过是想的太多


总在独处时,开始怀疑自己,我是谁,我在干什么,以后该怎么办......在我理智的那两天,我都会把这种状态归咎于闲的蛋疼。但是确确实实在那种状态下,什么事情都没法下手,最严重的的一种状态,就是会有一种深深的无力感,感觉距离目标实在太远了。这种无力感,会瞬间摧毁你的勇气,让你不敢下手去做些什么。就像是一场噩梦,你明知道应该醒来,却无法挣脱。



鼠鼠我啊是家里唯一的大学生,大学入了党,工作也没有让家里操心,家里人都认为我有出息了,只有鼠鼠觉得鼠鼠是个废物,鼠鼠以前也会想着让妈妈为自己骄傲,让家里人可以开心的生活,可是浪浪山不如鼠鼠所愿,鼠鼠在浪浪山清楚的认识到了阶层的差距,身边的人正活着曾经难以想象的生活,鼠鼠也才知道人生可以那么精彩,它就在我眼前,又好像远在天边。鼠鼠在家里是最强天赋,在浪浪山却是擦锅布,我好像永远走不出浪浪山了,鼠鼠想回下水道,鼠鼠下辈子不想做鼠鼠。



这种状态,都不过是因为想的太多,我们总是在刚起步的时候,就想着终点在哪里;总是在刚学习一项技能的时候,就想着攻克技术难题;总是在与人初次见面之后,就想推心置腹;总是在今天都没有过好的时候,就想着明天该怎么办。我就是这样一个人,常常纠结于各种各样的学习路线上,每次在学习不同的技术的选择上,进行纠结,但其实这两者我明明是有足够的时间兼学的,还有时候明明知道基础要一步一步走扎实,但是却想到后面还有各种各样的新式技术,高级技术等着我,就会又开始所谓的迷茫。


其实这种所谓的迷茫,很多时候都是源自于我们想的太多了,路要一步一步走,饭要一口一口吃,想的太多,就会感到迷茫和焦虑。最好的办法就是,立足当下,安于寂寞,不要太着急看到极大的成果,放平心态,只有你的心里想通了,你的状态就会迅速回归,重振旗鼓


(2) 你总需要一个人走一段路


孤独伴随着,几年前来到几百公里外上学的我,亦或是年后即将开始找实习,找工作的我。


我想我总会有一段时间感觉到莫名的孤独,想找个人聊聊天,却又不想去找,自己戴着耳机,漫无目的的走在路上。以及每次晚上或者凌晨写完东西,躺在床上有一种说不出的感觉,特别的是,我并不感到忧伤,只是感觉空落落的,也不想认识新的朋友,也不想联系家人,却也不知道有些话该和谁说。


即使你人缘很好,常常有三五好友一起相伴,但是总会有一些空隙感到孤独,这源自于你的内心还是不够强大,有的人独行却乐在其中,有的人三无成伴却又内心孤独,因为孤独的人心中并无足乐者,灵魂还是被空虚填满。


所以,请充实自己的生活,多出去走走,多与人交往,给自己多找点自己感兴趣的事情去做,即使感到孤独,也没必要太过沮丧,只需要告诉自己,沮丧,孤独,都是正常的,我们要在自己走的这段路上,让自己成为一个更加闪亮的人。走过自己的一段夜路,终将会有柳暗花明又一村的“闹市”。


(七) 恋爱的本质是「撞」而不是「寻」


(1) 你真的想要谈恋爱吗?


有时候总会想,谈恋爱是「一定要」还是「可以要」亦或者 「没必要」。


总有那么几天,好似陷入了爱情的怪圈。让你平淡无奇的生活荡起了阵阵涟漪,打破了你安稳的生活轨迹。


大部分时候,或许只是你想要摆脱这种“孤零零”的状态,又或者看着别人的“幸福”与“快乐”,激起了你的那份欲望,而欲望总会在你的忍耐中冲昏你的头脑,让你开始憧憬爱情,并且费力的去「寻」去「找」,试图去接触不同的人,试图找出哪一个是适合自己的,或许你会觉得,主动去寻找自己的幸福是一件很美好的事情,不过于我而言,这并不是爱情,我只能把它叫做权衡利弊后的一个选择。


或许有的时候,你只是想找一个人陪你,那也或许并不是爱情。你结束了一天的忙碌,合上了笔记本电脑,关掉了手机,疲惫的倚靠在椅子上,狭小的房间中,只有那盏台灯在一片死寂中发出微弱的光。连点一只烟的动作都觉得多余,他只想一个人安静的待一会,也不知道在想什么。但如果无由头的想起了一些事情,一些人,这个引火线,就会瞬间将情绪点燃,无尽的孤独涌上心头,这个时候,你渴望有一个人陪在你身边,陪你说说话,哪怕陪着你坐一坐,起码让你知道你并不是一个人。自此以后,你开始标榜自己「需要人陪」,看似高尚的理由,其实只是你害怕寂寞的一种借口,就算你真的找到了一个陪着你的人,那你真的爱她吗,可能你只是在你漆黑的房间中又添置了一盏台灯,这样能让你的眼中看起来更加明亮。


(2) 三观一致真的很重要!


这几年也接触过一些异性,或许也有动心过,但是你会发现,不同人看待,处理事物的方式会有截然不同的结果,你认为简直不可理解的事情,在其眼中似乎也稀松平常,或许你不懂她,也许她不懂你,三观这个词的定义实在太模糊,最简单的方法就是看你们在一起的感觉,给你的感觉如果是很舒服的,那么可以进一步了解一下,害,没什么好说的了, 希望你可以找一位能符合你心中期望的另一半。


(3) 顺着人生轨迹走吧,别为了一个人停下来


千万不要陷入单恋的漩涡中,这是致命的,对的人是不需要主动找的,你只需要顺着人生轨迹走,在合适的年纪做合适的 “正事” ,自然而然就会遇到那个人了,如果等到七老八十,也没有遇到,或许这也就是命。或许说的太悲观了,但我仍认为,与其让自己为了追求一个不确定,也或许没有回应的爱情,不如自己欣赏自己孤岛中的美丽。但话也不能太绝对,或许有一天我就会因为所谓的爱情,陷入盲目。爱情这东西,谁说的好呢。但我只要不断告诉自己,一定不要停下来


第二节 这一年我都做了些什么


(一) 学习 + 技术输出


(1) 行百里者半九十


按照原来的计划,从 Java --> JavaWeb --> SSM --> SpringBoot 这个主线就算结束了,其中夹杂着 SQL,Linux,Web,小程序,设计模式等等的一些支线。不过,根据自己的情况和具体需要吗,其实我已经做出了一些重点的调整,我会在后面的目标中去提到。


(2) 一年和球友一起 输出了一百多万字的笔记



先放地址【Java全栈方向】:http://www.yuque.com/icu0/wevy7f


欢迎大佬们关注一下小弟。



一年中,一边学习,一边做总结,做整理,陆陆续续一年也写了200来篇笔记(也可能是文章或者感悟)(不一定纯后端/前端,还有 Linux ,计网等等)记得某个大佬说过写博客和笔记不一定能做到对别人有帮助,但起码对自己有帮助。但是我一直通过大白话概括 + 做图 + 简单示例 + 官方说明的方式写文章,也在努力希望能对别人也有帮助。



(二) 超爱买书的购物狂


这一年买了不少书(买了 == 看了),还有好多想买的都在我的购物车里吃灰,再买我真怕自己变成一个光收藏的 “读书人” 了,来盘点盘点这一年我看了比较有感觉的书(没感觉的和没怎么读的就不提了,如果给我多一点时间,我争取出一篇介绍自己读的书籍的文章)




一件恐怖的事情:我利用一年时间看过了这些书



第三节 明年今日,记得要回头看看



We already walked too far, down to we had forgotten why embarked. ——纪伯伦《先知》


译文:我们已经走得太远,以至于忘记了为什么而出发。



2022年度回顾



2023年新的目标


技术上:


只有写1-2月的,所以我放一个链接,欢迎大家监督我学习



http://www.yuque.com/icu0/qeowns… 《Cool的三两事》



生活上:


  1. 孝敬父母

  2. 勤运动

  3. 照顾好自己的身体

  4. 不要熬夜

  5. 与人交谈,沉稳思虑而后动

  6. 多读书,多出去走走,善待他人


学业上:


  1. 英语四级

  2. 拉取开源项目,为开源项目提PR

  3. 持续输出技术型文章

  4. 专升本上岸


总而言之,2022喜忧参半,有“春风得意马蹄疾,一日看尽长安花”的喜悦,也有“停杯投箸不能食,拔剑四顾心茫然”的忧愁,但我希望我的2023能有“长风破浪会有时,直挂云帆济沧海”。



个人独白:


以上内容皆是一名专科生的自白,感谢自己在大专三年没有一天是“浑浑噩噩式”学习,也没有一天因为当前的荣誉而骄傲满足,同时感谢部队两年的栽培,让我站在低谷依旧能仰望天空,扎根大地,心有猛虎,细嗅蔷薇。


作者:Cool
来源:juejin.cn/post/7187012953659899965
收起阅读 »

异地技术团队管理的三大模式六项注意

1 为什么会有异地团队 当一个企业成长到一定程度后,往往会在多地建立研发中心或者业务中心,这里企业的考量可能会有如下的一些点: 人才资源:不同的城市和地区可能具有独特的人才资源,通过在多个城市建立研发中心,公司可以吸引和招聘到更多具有不同技能和背景的优秀人...
继续阅读 »

1 为什么会有异地团队


当一个企业成长到一定程度后,往往会在多地建立研发中心或者业务中心,这里企业的考量可能会有如下的一些点:




  1. 人才资源:不同的城市和地区可能具有独特的人才资源,通过在多个城市建立研发中心,公司可以吸引和招聘到更多具有不同技能和背景的优秀人才。这有助于公司在保持竞争力,并确保能够获取到足够的人才来支持研发和业务需求。
    比如深圳是中国的高新技术产业中心,其在硬件制造、消费电子、通信技术等方面具有很强的竞争力,对硬件制造、物联网、人工智能等领域拥有丰富经验的工程师较多,并且由于深圳地理位置优越,靠近香港,拥有国际化的人才环境,因此在跨境项目和多元文化沟通方面具备优势;
    又如北京是中国的政治、文化和教育中心,拥有众多顶级高校和研究机构,拥有大量理论研究和技术创新方面的顶尖人才,北京的互联网行业较为成熟,尤其是在互联网+政务、在线教育、大数据等方面有较多经验的人才。




  2. 市场覆盖:在多个城市设立研发中心有助于公司更好地了解和适应不同地区的市场需求。这可以让公司更迅速地响应市场变化,提供更符合客户需求的产品和服务。




  3. 成本优化:不同地区的劳动力成本、房地产成本和生活成本可能存在差异。在多个城市建立研发中心可以让公司充分利用各地的成本优势,降低整体运营成本。如一些深圳/北京的公司,会把一些研发中心放到西安、成都、武汉、长沙等城市。




  4. 政策支持:一些城市为了吸引优秀企业入驻,可能会提供各种政策支持,如税收优惠、低息贷款、用地优惠等。在多个城市建立研发中心可以让公司充分利用这些政策优势,降低研发成本。




除此之外,还有风险分散的考虑,技术合作与创新等等,最终都是帮助公司获得更多的资源和优势,提高整体竞争力。


2 异地团队会有什么问题


以技术团队为例,当有多个技术团队在不同的城市后,与所有技术团队在同一个地方相比,会有一些问题出现,主要分为以下的 4 个方面:


2.1 团队建设和凝聚力打造困难


由于缺乏面对面交流和互动,异地团队成员之间可能难以建立信任和凝聚力。而团队建设和凝聚力是影响团队绩效的重要因素。当技术团队分布在不同城市时,团队建设和凝聚力可能受到以下方面的影响:




  1. 面对面交流机会少
    当团队成员分布在不同城市时,他们的面对面交流机会将大大减少。面对面交流有助于加深团队成员之间的了解、建立信任和加强团队凝聚力。例如,共同参加团队活动、庆祝生日等场合,能增强团队成员之间的情感联系。而分布在不同城市的团队成员可能很难享受到这些互动的机会。




  2. 困难的团队文化塑造
    一个健康的团队文化对于团队建设和凝聚力至关重要。在异地团队的情况下,公司需要付出更多的努力来塑造统一的团队文化。例如,各地团队可能在工作习惯、价值观、沟通方式等方面存在差异,这些差异可能导致团队凝聚力降低。




  3. 时空的隔阂
    异地团队面临地理距离的挑战,以及各地工作安排导致的时间不一致的问题。这种情况下,团队成员可能较难以达到理想的实时沟通,而在中国实时沟通是大部分公司的必备品,大家更习惯于实时的沟通,而不是异步的非实时沟通。




  4. 缺乏有效的团队认同感
    异地团队成员可能会感到自己与其他团队成员的联系较弱,这会导致他们缺乏对整个团队的认同感。例如,一个异地团队成员可能对其他城市团队的工作情况和成果了解较少,难以形成归属感和共同的目标。




2.2 项目管理及实时协同难度大


异地团队成员可能难以实时协作,尤其是涉及紧急问题或需要即时反馈的情况。项目管理及协同难度增大主要表现在以下的 3 个方面:




  1. 沟通成本上升:当团队成员分布在不同城市时,团队之间的沟通成本会显著增加。团队成员需要通过电话、电子邮件、即时通讯等工具进行沟通,这可能导致信息传递的延迟和误解。例如,一个团队成员在深圳提出一个需求变更,另一个团队成员在上海可能需要数小时甚至一天后才能了解到这一变更,从而影响项目进度。




  2. 快速应对变化的能力变弱:异地团队可能在应对突发事件和变更需求时存在局限。假设一个重要客户要求对产品进行紧急修改,跨城市的团队成员可能需要在短时间内协调资源和安排工作,而地理隔离使得这一过程变得更加困难。




  3. 时间管理和跨团队协调困难:不同城市的团队可能存在不同的工作时间和节假日安排(比如某个城市因为办公场地原因而全员居家),这可能导致某些任务在协作过程中出现延迟。例如,在一个紧急 bug 修复的情况下,由于一个城市的团队正在度假,另一个城市的团队需要独自解决问题,可能导致修复速度变慢。




2.3 监督和管理困难


在异地团队中,监控和评估团队成员的绩效可能较为困难。管理者需要找到合适的方法和指标,以便对团队成员的工作成果进行公平、准确的评估。监督和管理困难主要包括以下的一些情况:




  1. 工作状态难以掌握:由于地理隔离,管理者可能无法直接了解团队成员的工作状态和情况。例如,一个城市的团队可能遇到了技术难题,导致项目进度受阻,但管理者由于无法亲自与团队成员交流,可能难以及时发现问题并采取相应措施。




  2. 绩效评估困难:在异地团队中,评估团队成员的工作绩效可能变得更加困难。由于缺乏面对面交流,管理者可能无法准确评估团队成员的工作质量和效率。例如,一个城市的团队成员可能在某个任务上花费了较长时间,但管理者无法确定这是否是由于技术难题还是工作效率低下。




  3. 难以建立信任和团队凝聚力:地理隔离可能导致管理者难以建立与团队成员的信任关系,从而影响团队凝聚力。例如,一个城市的团队成员可能对管理者的决策表示质疑,由于无法进行面对面沟通,管理者可能无法充分解释决策背后的原因,从而导致信任度降低。




  4. 协调和调动资源困难:异地团队的管理者可能在协调和调动资源方面面临挑战。当项目需求发生变化或出现紧急问题时,管理者需要快速协调各地团队的资源,但地理隔离可能使这一过程变得更加复杂。例如,在一个紧急项目中,管理者需要从多个城市的团队中调集人力资源,但由于异地情况,这可能导致资源调配的速度和效果受限。




整体来说,主要是由于沟通与协作问题导致的各种延展性问题。缺失的面对面沟通、缺少肢体语言、表情语言等,可能导致信息传递不畅、误解和沟通成本的增加。我们无法彻底解决这些问题,但是能通过一些手段来缓解。


3 三大模式


为解决上面这些问题,我们在工作中发现了一些在不同的环境和场景中具有普遍适用性的解决或缓解问题方法,以模式的形式表述出来。


3.1 代理模式(Proxy Pattern)


代理模式在团队管理中可以被用于创建一个协调人或代表角色,负责处理某个团队或多个团队之间的沟通与协作。代理角色在此情景下充当一个中介,处理跨团队的需求、问题解决和资源协调。代理模式有助于简化沟通流程,提高团队协作效率。


具体实施方案:



  • 为每个团队或职能领域设立代理角色,如前端代理、后端代理、QA 代理和移动端代理。

  • 代理角色负责处理跨团队的需求和问题,同时将反馈和解决方案传递给相应团队。

  • 组织定期的代理角色会议,让代理们相互沟通和协作,以确保团队目标的达成。

  • 建立代理角色的沟通汇报机制,如定期晨会、周报和项目维度的回顾会。


3.2 门面模式(Facade Pattern)


门面模式提供了一个统一的接口来访问子系统中的一组接口。在团队管理中,可以创建一个统一的协调角色(如项目经理或技术负责人),该角色负责协调团队成员的工作,并充当各个团队之间的沟通桥梁。这有助于确保团队之间的沟通更加高效,降低沟通成本。


具体实施方案



  • 设立项目经理、技术负责人或者某个业务模块的 DRI 角色,负责跨团队协调和沟通。

  • 为每个团队成员分配具体的职责和任务,以便在项目经理或技术负责人的协调下高效协作。

  • 定期召开跨团队会议,确保团队之间的沟通畅通,及时解决问题。


门面模式和代理模式看起来有一点相似,其本质上是有区别的,区别在于授权的范围,门面模式不用太关注其内部实现,而代理模式在管理上要更深入细节一些。


在实际应用中,我们通常在各职能和各业务模块中使用代理模式,而针对不同的区域使用门面模式,由当前地区的负责人提供统一的输出。


3.3 观察者模式(Observer Pattern)


观察者模式在团队管理中可以应用于实时通知和信息共享。当一个团队成员对项目状态或任务完成情况进行更新时,其他相关成员可以作为观察者实时收到通知。这种模式有助于保持团队成员之间的信息同步,提高沟通效率。


具体实施方案



  • 为团队成员创建一个共享平台,如任务管理工具、项目管理系统等。

  • 当某个团队成员更新任务状态或项目信息时,系统自动通知其他相关成员。

  • 通过观察者模式,确保团队成员之间的信息同步,减少冗余沟通。


4 六项注意


4.1 相互信任


信任是团队协作的命脉。要想促进并保持长久的关系,你就必须信任他人,他们也必须信任你。与此同时,他们还必须相互信任。


信任来自相互理解对方的价值观、个人经历和立场。为了实现这一目标,我们必须承认自己的弱点,我们必须开放。这样我们才能够建立起共同的价值观和彼此信任。


信任在异地团队中有如下的好处:



  1. 提高团队凝聚力:信任关系有助于增强团队成员间的默契,从而提高团队凝聚力。当团队成员信任彼此时,他们更愿意携手合作,共同解决问题。

  2. 提高工作效率:信任关系可以促使团队成员更加开放地分享信息、资源和建议,从而提高整体工作效率。当团队成员相互信任时,他们更可能分享自己的想法和专业知识,共同解决问题。

  3. 降低沟通障碍:信任有助于消除团队成员间的沟通障碍,提高沟通效果。当团队成员彼此信任时,他们更愿意倾听对方的意见,以开放的态度接受建议和批评。

  4. 降低管理成本:信任关系有助于减轻管理压力,降低管理成本。当团队成员相互信任时,他们更可能自我管理,减少管理者的介入。

  5. 增加创新和风险承担:信任关系有助于创造一个安全的环境,使团队成员更愿意尝试新的想法和承担风险。当团队成员彼此信任时,他们更可能勇于创新和承担失败的风险。


建立相互信任关系的方法,以下是一些常见的方法:




  1. 增加沟通



    • 定期开展团队会议,让团队成员分享项目进展、遇到的困难和解决方案。

    • 鼓励一对一交流,让团队成员有机会深入了解彼此的工作、兴趣和需求。

    • 举办团队活动,如团队建设、庆祝活动和知识分享,促进团队成员间的互动和信任。




  2. 增加透明度



    • 使用项目管理工具,让团队成员能够实时查看项目进度和任务分配。

    • 定期分享业务战略、目标和团队绩效,让团队成员了解公司的发展方向。




  3. 赋予责任和权力



    • 根据团队成员的专长和兴趣分配任务,让他们在完成任务时有更大的自主权。

    • 鼓励团队成员在解决问题时提出建议和改进方案,展现对他们的信任。




  4. 鼓励支持和合作



    • 创建一个支持性的氛围,让团队成员在遇到问题时不惧于寻求帮助。

    • 鼓励团队成员互相学习、分享经验,以解决共同面临的问题。




  5. 表扬和认可



    • 在团队会议上表扬团队成员的优秀表现和努力。

    • 为表现突出的团队成员提供奖励,如奖金、晋升和表彰。




  6. 建立公平的环境



    • 确保团队中的决策过程透明,鼓励团队成员参与讨论和决策。

    • 设定明确的激励和奖惩




4.2 仪式感


在异地管理中,仪式感是一种有意识地营造正式或非正式场景,以传递重要信息、强化文化价值观、增强团队凝聚力和提升员工信任感的方式。


在异地团队中,恰当的仪式感具有以下好处:



  1. 增强团队凝聚力:仪式感有助于让团队成员感受到归属感和团队精神,从而增强团队凝聚力。

  2. 传递公司文化和价值观:通过仪式感,可以传递公司的文化和价值观,帮助团队成员更好地理解和认同这些价值观。

  3. 提升员工士气和信任感:仪式感可以激发团队成员的积极性和参与感,从而提高员工士气和信任感。

  4. 建立清晰的期望和目标:仪式感有助于确立团队成员的期望和目标,提高工作效率和执行力。


那如何建立恰当的仪式感呢?



  1. 定期召开团队会议:固定时间、地点召开团队会议,让团队成员汇报进展、分享经验、讨论问题。如每周一召开全体成员参加的在线例会,或者对于管理团队,定期如开包含问题同步和处理,学习分享的管理例会。

  2. 庆祝重要节点和成就:为团队的重要成就和里程碑设立庆祝活动,以增强团队成员的归属感和自豪感。如在项目完成时,举办在线庆祝活动,表彰优秀团队成员。

  3. 组织团队建设活动:定期组织线上或线下的团队建设活动,增进团队成员间的联系和互动。如每季度举办一次线上游戏比赛,增强团队成员之间的合作和交流。

  4. 激励和认可:对团队成员的努力和成果给予表扬和认可,提高他们的信任感。如每月颁发「最佳团队贡献者」奖项,表扬表现优秀的团队成员。

  5. 传递公司文化:通过仪式感传递公司文化,帮助团队成员理解和认同公司的价值观。如每年举办一次公司文化分享活动,邀请公司领导和团队成员分享公司文化和价值观。


4.3 严格目标管理,注重结果


在异地技术团队管理中,严格的目标管理和注重结果至关重要,因为这有助于确保项目按时完成、质量达标,并提高团队成员的工作效率和执行力。


以下是实行严格目标管理和注重结果导向的好处:



  1. 明确工作目标:设定清晰的目标和期望,帮助团队成员明确工作重点,避免资源浪费和目标模糊。

  2. 提高工作效率:明确的目标和期望有助于团队成员更高效地完成任务,降低拖延和低效的可能性。

  3. 便于评估和改进:结果导向的管理使团队可以通过衡量实际成果来评估工作效果,从而找出不足并进行改进。

  4. 激发团队成员积极性:目标明确、注重结果的管理方式有助于激发团队成员的积极性和责任心,鼓励他们为实现目标而努力。

  5. 有利于项目按期完成:严格的目标管理和注重结果有助于确保项目按计划进行,按时完成,避免延期。


那么如何实施严格的目标管理和注重结果导向?有如下 7 个方法



  1. 设定明确的目标:为项目和团队设定明确、可衡量、可达成的目标。如在项目开始时,为团队设定一个明确的项目交付日期,并明确交付内容的具体要求(也就是大家常说的 deadline 是第一生产力)。

  2. 制定详细的计划:为实现目标制定详细的计划和进度表,包括任务分配、时间安排等。如使用项目管理工具(如Trello、Jira等)制定详细的任务列表和时间表,如果没有这些工具,搞个在线表格也是极好的。

  3. 定期检查进度:定期与团队成员沟通,了解项目进度和遇到的问题,确保项目按计划进行。如定期的项目晨会(可以按周,或按天,也可以一周两次,根据实际情况调整),让团队成员报告各自的任务进展和遇到的问题。

  4. 强调结果导向:鼓励团队成员关注实际成果,以实现预定目标。在管理过程中对团队成员的绩效评估更注重实际完成的任务和贡献,而非工作时长或其他表面指标(不要卷加班)。

  5. 及时反馈和调整:根据实际进度和成果,及时给予团队成员反馈,调整目标或计划。如当发现某个任务进度落后时,及时与相关成员沟通,分析原因,并调整计划或提供支持。如当发现某个任务进度落后时,及时与相关成员沟通,分析原因,并调整计划或提供所需资源,以确保项目仍能按时完成。

  6. 定期总结和复盘:项目结束后,与团队成员一起总结经验教训,分析成功与失败的原因,以便在未来项目中持续改进。如项目结束后,组织团队进行复盘会议,总结项目的优点和不足,制定改进措施。或者迭代结束后做一些回顾。


我们在团队管理中,目标管理是一个非常重要的点,一定要自己主导,不能授权,作为一个技术团队的负责人,方向是你来定的,未来在你的手里


4.4 扁平、弹性的组织架构


在异地技术团队管理中,组织架构至关重要,因为组织架构会影响团队的沟通效率、决策速度、责任分配和协作。适合异地技术团队的组织架构应具备以下特点:扁平化、模块化、弹性和高度协作。




  1. 扁平化:扁平化的组织结构有助于提高沟通效率,减少信息传递过程中的失真和延迟。扁平化组织中,每个成员能够直接向上级汇报,决策速度更快,执行力更强。




  2. 模块化:将工作划分为具体的、相对独立的模块,有助于提高团队的协作效率。每个模块可以由一个或多个团队负责,这样可以减少跨团队协作的复杂度,降低沟通成本。




  3. 弹性:适应不断变化的项目需求和团队规模,组织架构需要具备一定的弹性。弹性的组织架构可以快速调整资源分配和团队规模,以满足项目发展的需要。




  4. 高度协作:鼓励团队成员之间的协作和互助,以提高工作效率和质量。高度协作的团队可以更好地应对复杂问题,减少重复劳动和资源浪费。




以下是如何实现适合异地技术团队的组织架构:



  1. 利用技术手段优化沟通:使用沟通和协作工具(如钉钉、企业微信、飞书、Microsoft Teams等)提高沟通效率,方便团队成员跨地域、跨部门协作。

  2. 决策下放:授权团队成员在其负责领域做出决策,提高决策速度。如将需求评审的决策权下放至小组 leader 或 DRI,甚至一线开发,让他们根据自己的专业知识对需求进行评估和调整。

  3. 设立技术负责人或项目经理:在每个地区或团队设立技术负责人或项目经理,负责协调团队成员的工作,确保项目顺利进行。如在各城市的团队中各设立一名项目经理,负责当地团队的项目进度和资源协调。

  4. 定期进行跨团队沟通:组织定期的跨团队会议,让各个团队分享进展、问题和解决方案。这有助于提高团队间的了解和协作。如每两周组织一次跨团队分享会议,让各个团队汇报自己的进展和挑战,共同寻找解决方案。

  5. 提供培训和支持:为团队成员提供技能培训和支持,以便他们更好地适应组织架构变化。如提供关于敏捷开发、跨部门协作等方面的培训课程,帮助团队成员提高工作效率和协作能力。

  6. 鼓励创新和变革:建立一种鼓励创新和变革的文化,让团队成员敢于尝试新方法,优化工作流程。如设立创新奖励计划,对于提出改进方案并成功实施的团队成员给予奖励。或者团队负责人亲自来参与或推进一些创新的事项,如最近的比较热的 AI。


4.5 统一的技术栈


在异地技术团队管理中,统一技术栈非常重要,因为它能为团队带来以下好处:



  1. 提高协作效率:统一技术栈能确保团队成员之间更容易进行技术交流和协作,避免因技术差异导致的沟通障碍和额外工作量。

  2. 降低维护成本:使用相同的技术栈,使得维护、调试和优化工作更加简单,减少因为技术差异导致的额外成本。

  3. 增强团队能力:统一技术栈有助于团队成员互相学习,提高整体技术能力,使得团队在面对复杂项目时更具备应对能力。

  4. 简化招聘和培训:统一技术栈使得招聘和培训过程更加简单,因为公司可以针对特定技术栈进行招聘和培训,提高招聘效率和培训质量。


为实现统一技术栈,我们可以采取以下方法:



  1. 制定技术规范和标准:制定统一的技术规范和标准,确保各地团队遵循相同的技术实践。如制定统一的编码规范、代码审查标准和自动化测试要求。

  2. 组织上增加架构设计的职能或者技术通道的职能组织: 通过组织的方式构建技术栈统一的土壤。

  3. 搭建技术共享平台:创建内部技术分享平台,让团队成员分享技术心得、问题解决方案和最佳实践,有助于统一技术理念和实践。如搭建一个内部的技术博客平台,鼓励成员撰写和分享技术文章。

  4. 统一基建和开发流程中的系统: 通过使用工具的统一达到技术栈的统一。

  5. 选型时充分调查和论证:在技术选型阶段,充分调查并论证各种技术方案的优缺点,确保选择的技术栈适合公司的业务需求和发展战略。

  6. 定期评估和调整:定期评估技术栈的合理性和有效性,根据项目需求和团队能力进行调整,以保持技术栈的统一性和先进性。如每年定期组织技术栈评审和审查,了解目前所使用的技术栈是否仍然满足业务需求,或者是否有新技术可以更好地支持业务发展。


通过以上方法,异地技术团队可以实现技术栈的统一,从而提高协作效率、降低维护成本、增强团队能力,并简化招聘和培训过程。这将有助于提高团队整体的研发效能,使得公司在面对市场竞争和业务挑战时更具备优势。


4.6 高效的沟通机制


异地团队最突出的问题是沟通问题,在我们平常的沟通过程中需要选择合适的沟通渠道和做有准备的沟通。良好的沟通有如下的好处:



  1. 能提高沟通效率:采用合适的沟通方式可以确保信息准确、及时地传递给相关人员,避免因沟通不畅导致的误解和冲突。

  2. 增强团队凝聚力:良好的沟通方式有助于增进团队成员之间的理解和信任,提高团队凝聚力。

  3. 减少资源浪费:有效的沟通方式能够减少不必要的会议和重复工作,降低资源浪费。

  4. 支持项目管理:清晰的沟通方式有助于确保项目进度、需求和问题得到及时解决,保障项目顺利进行。


我们可以通过如下的一些方式达到比较高效的沟通机制:



  1. 明确沟通目标和内容:在沟通开始前,明确沟通的目的、内容和预期结果。如:在项目会议开始前,列出讨论议题、相关人员和预期决策。

  2. 选择合适的沟通渠道:根据沟通内容和参与人员,选择合适的沟通渠道。如:对于紧急问题,可以使用电话或即时通讯工具(如微信、钉钉等)进行沟通;对于团队日常工作,可以使用邮件或者项目管理工具(如Jira、Trello等)进行沟通。

  3. 建立沟通规范:制定团队沟通规范,确保沟通有效进行。如:要求团队高效会议,或者要求团队成员在邮件中使用清晰的主题行、合理的收件人列表以及简洁明了的正文。

  4. 鼓励开放和诚实的沟通:营造一个鼓励团队成员开放、诚实地表达观点和需求的氛围。如:在团队会议上,鼓励成员提出问题、建议和想法,避免惩罚性的反馈。

  5. 定期进行沟通培训:为团队成员提供沟通技巧培训,以便他们更好地进行沟通。如:提供关于有效沟通、团队协作等方面的培训课程。


值得注意的是,异地的沟通中尽量少用邮件,邮件适用于传达信息和事实,撰写时还需要注意措辞,以防误会的发生。


单纯的文字无法传递情绪,如果要传达你的想法时,最好拿起电话进行视频,通过视频也能制造多次「见面」的机会更有利于建立信任。


现在用 IM 类工具也比较多了,在清晰的文字表达的基础上,多用表情包。


5 后记


上面说了这么多,有点啰嗦,简单点来说就是:多见见,多一起喝点酒,多一起搞定一些事情,保证基本的机制、流程、标准、工具和系统,也就差不多了。


异地的问题表象是见不着,核心要解决的是效率的问题。


技术团队的管理更多的还是人的问题,还是需要有情感的交流和因为长时间的一起工作而产生的向心力。
我们所做的这些仅能缓解这些问题。


当然,可能有同学会更喜欢异地/远程的工作协同模式,此处因人而异,从个人的角度来看:从团队的角度,从效能的角度,本地化团队会是更高效的选择。


当然以上的模式和注意事项在非异地团队的情况下也是可以使用的,而且效果会更好,因为这些的本质是授权管

作者:潘锦
来源:juejin.cn/post/7219651766707044389
理和过程管理的逻辑。

收起阅读 »

Low-Code,一定“low”吗?

web
作者:京东保险 吴凯 前言 低代码是一组数字技术工具平台,基于图形化拖拽、参数化配置等更为高效的方式,实现快速构建、数据编排、连接生态、中台服务。通过少量代码或不用代码实现数字化转型中的场景应用创新。本文将重点介绍低代码相关知识,包括低代码的定义与意义、相关概...
继续阅读 »

作者:京东保险 吴凯


前言


低代码是一组数字技术工具平台,基于图形化拖拽、参数化配置等更为高效的方式,实现快速构建、数据编排、连接生态、中台服务。通过少量代码或不用代码实现数字化转型中的场景应用创新。本文将重点介绍低代码相关知识,包括低代码的定义与意义、相关概念、行业发展等,同时介绍京东的低代码工具,期望能帮助大家更好地认识与理解低代码。


一、低代码介绍


2014年,Forrester(著名研究咨询机构)提出“低代码”的术语,定义为“利用很少或几乎不需要写代码就可以快速开发应用,并可以快速配置和部署的一种技术和工具”。或者说是“(能力)多(出品)快(质量)好(功夫)省”。



这个定义体现出低代码的核心价值:


1、低代码开发平台能够实现业务应用的快速交付。低代码开发的重点是开发应用快,不像传统意义上仅仅是一个应用的开发,而是通过可视化的开发,达到“设计及交付”的目的,提高开发效率。


2、低代码开发平台能够降低业务应用的开发成本。低代码开发投入更低,主要体现在开发时间短,可以快速配置和部署,同时也更容易使非开发人员上手。


二、我们为什么用低代码


低代码可以降本增效,一方面低代码的出现避免了“反复造轮子”的问题,其通过可视化的编程方式实现“千人千面”的效果,驱使技术回归本源--支持业务。另一方面低代码的生命周期贯穿整个软件开发周期(设计、开发、测试、交付),周期上的各角色都可以在同一个低代码开发平台上紧密协作,由传统的开发方式变为敏捷开发,实现了快速交付的目的。


低代码的使用场景:


1、构建新的SaaS应用,而借助低代码平台可以快速有效地构建、测试和推出应用。低代码与SaaS的结合,可以为企业提供独特的业务解决方案。


2、基于Web的门户网站是提供自助服务的数字化工具。使用低代码开发平台,更简单、更快速地构建个性化应用,打造数字化平台。


3、历史系统的迁移或升级。基于低代码技术:一方面,最大限度地保留遗留系统的代码,保留其“公共数据服务”;另一方面,基于遗留系统的开发环境和能力构建相应的“功能适配器”,然后在此基础上,通过低代码技术快速定制新业务和流程的交互式UI与业务逻辑。


4、应用复杂性低,业务流程相对简单,95%的应用场景可以通过低代码完成。



三、低代码会使程序员失业吗


回答这个问题,我们首先需要搞明白:低代码和零代码的区别。作为程序员,大家都会把低代码认为是零代码,这也是会被误解程序员失业的原因之一。


低代码,意味着反复迭代的代码质量高,在必要的时候,也会进行代码的编写;BUG更少,减少了测试环节的工作量。


零代码,字面意思:完全不需要任何代码即可完成应用开发,从软件开发效率看,**零代码是低代码的最终形态。**零代码平台由于采用全部都是封装模块进行搭建,所有控件都已经被固化了,所以用零代码平台搭建的系统想要进行扩展是有些困难的。


现实是,编码的最终目的是支持业务,业务逻辑的复杂与否依旧需要人来掌握,低代码只是写的少,并不是不写代码,这并不会导致程序员的失业



四、低代码的行业现状


2021年11月11日,Forrester发布《The State Of Low-Code Platforms In China》,这是低代码概念提出者第一次将视角聚焦在中国。Forrester认为,低代码目前在国内主要应用于银行、保险、零售、医疗、政府、制造、电信和建筑行业。比如,为了针对各个业务单元量身定制各种业务需求,中国建设银行采用云枢为其分布式开发团队构建统一的低代码开发平台(LCDP)。另外,报告指出:中国企业数字化转型过程中,有58%的决策者正在采用低代码工具进行软件构建,另有16%的决策者计划采用低代码。


目前,国内的低代码开发平台不断涌现,Forrester划分了9类低代码平台厂商:


▪数字流程自动化(BPM):炎黄盈动(AWS PaaS)、奥哲(云枢)


▪公有云:阿里巴巴(宜搭)、百度(爱速搭)、华为(应用魔方)、微软(Power Platform)、腾讯(微搭)


▪面向专业开发者的低代码开发平台:ClickPaaS、葡萄城(活字格)、Mendix、Outsystems


▪面向业务开发者的低代码开发平台:捷德(Joget DX)、轻流


▪AI/机器学习:第四范式(HyperCycle)


▪BI:帆软(简道云)


▪协作管理:泛微(E-Builder)


▪流程自动化机器人(RPA):云扩(ViCode)、来也(流程创造者)


▪数字化运营平台:博科(Yigo)、金蝶(金蝶云·苍穹)、浪潮(iGIX)、用友(YonBIP)


由此可知,中国的低代码市场正在飞速发展,各种低代码工具的发布问世,也意味着低代码未来将成为主流的开发方式。


五、业内的低代码平台


1、Out-System


OutSytems 作为国外著名的低代码开发平台,出发点就是简化整个应用开发和交付的过程,让开发人员可以快速响应市场的需求变化。通过可视化和模型驱动的开发方式,大幅减少时间和成本。并通过预构建的连接器加速集成后端系统,同时还提供了一个集中式的控制台来管理应用的版本、发布以及部署。


OutSytems 生成的应用可以不依赖于 OutSytems 运行。数据是直接存储到数据库,这样就可以通过任何标准的 ETL、 BI或其他第三方数据工具来访问数据。


官网:

http://www.outsystems.com/demos/


2、阿里-云凤蝶


云凤蝶是蚂蚁金服体验技术部的重点研发项目,是面向中后台产品的快速研发平台,主要用户面向工程师,使用场景专注在标准化的中后台产品研发,目标是为了提高效率。


云凤蝶的核心思路是将组件生产和组件组装这两部分工作进行职责分离,通过建立一条组件组装流水线,打通 npm 组件的一键导入流程,从而完成一条产业链式的分工协作,最终实现规模化的快速生产。


淘系的“乐高”系统以及蚂蚁金服的“金蝉”系统、“云凤蝶”系统成微阿里系主要的低代码开发工具。


3、京东-星链


星链是京东科技消金基础研发部开发的一款研发效能提升工具,主要为面向后端服务研发需求,因此前端简洁可视化开发界面需要满足极致的细节,并依赖其自身后端的能力来实现用户的低代码。


核心概念:


VMS可视化微服务应用,是星链的基本单元,同时VMS也是一种模型,各种配置均在模型中。支持京东中间件(JSF、定时任务、JMQ,缓存服务、分布式配置等),服务流程编排,DEBUG调试等;


Serverless部署,星链的部署及配置均由系统自动分配。用户只需关注系统的开发,资源的使用情况。


地址:jddlink.jd.com/


结论


低代码,一定不“low”,却更low-code。


参考:


2021年低代码平台中国市场现状分析报告

http://www.authine.com/report/56.h…



作者:京东云开发者
来源:juejin.cn/post/7217449801633808439
收起阅读 »

思考:如何做一名合格的面试官?

背景 关于招聘,在最近一段时间有幸又参与到了面试工作。这次面试也包含一些相对高级一点的岗位,如何招到合适的人成为了最近一段时间一直在思考的一个点。 整体上感觉自己还是没有章法,没有清晰的思路(做不到因人而异),这种情况对自己对他人都是不负责任的。 因此,简单做...
继续阅读 »

背景


关于招聘,在最近一段时间有幸又参与到了面试工作。这次面试也包含一些相对高级一点的岗位,如何招到合适的人成为了最近一段时间一直在思考的一个点。


整体上感觉自己还是没有章法,没有清晰的思路(做不到因人而异),这种情况对自己对他人都是不负责任的。


因此,简单做一些总结思考,边面边想边改进吧。


image.png


招聘者的目标


首先,作为招聘者,都希望能找到一些厉害的人,成本不应该是他要考虑的问题。但现实总是相反。


面试官:这是我这次招聘的需求,要这个...... 那个...... 总之,能力越强越好。


公司:这次招聘成本范围已发给你了,注意查收。


面试官:......


所以,在成本有限的情况下,面试官要做的就是找到那些会发光的人。对面试官来说,招到一个即战力(不亏),招到一个高潜力(赚翻了)。


因此,招聘者的目标都是希望能够招到 能力 > 成本 的人。


image.png


梳理招聘需求


招聘的需求是需要面试官提前梳理好的,面试官作为团队的组建者,一定要提前规划好招聘需求。比如:



  1. 我要找技术强的。(我不懂的他来)

  2. 我要找态度好的。(我说啥都听)

  3. 我要找有责任心的。(不用我说就把活干得很漂亮)

  4. 我要找学习能力强的。(自我提升,啥需求都能接的住)

  5. 最好还能带团队。(这样我就轻松了)

  6. 最后一定要稳定。(这样我就一直轻松了)


哈哈,先开个玩笑。虽然我真的想......


image.png


现实就像上边提到的,招聘者希望的求职者模样。虽然知道不可能,但还是忍不住想要(我控制不住我几己呀!),所以在有限的面试时间内,问了很多方方面面的问题......


面试结束后:我问了那么多才答上来那么几个问题,不行,下一个......


面了几天后:人怎么这么难招呢?


image.png


所以真正的招聘需求应该是下边这样的:



  1. 我要招一个领导者还是执行者:这个一定要想清楚,两者考察维度完全不一样。

  2. 我要技术强的:想好哪方面技术强,不要妄图面面俱到。

  3. 我要找有责任心的,学习能力强的,稳定的:想好怎么提问,如何判断。


如果能够做到上边的三点,相信招进来的人应该都是OK得。PS:先做到不亏。


领导者Or执行者


为什么把这个作为第一点,上边也提到过,两者考察维度完全不一样。一场面试,时间就那么点,所以要有针对性。


先说领导者


如果招聘领导者。试想一下领导者有哪些特点,什么样的人你愿意让他成为领导者。



  1. 业务理解程度深,不仅限于产品规划的业务需求,还要有自己的理解和看法。

  2. 技术能力强,通常一个技术方案便能提现出来,方案好不好,考虑全面不全面。

  3. 抗压能力强,能够承担工作压力,这里不是指加班(当然加班也算),更多的是来自于技术,业务的困难和挑战。


以上三点并不代表全部,仅做参考。那么如何在面试中确认呢?




  1. 业务理解程度主要通过追问细节的方式来确认。在你不了解的情况下,依然能够给你讲明白,这个业务是做什么的,关键核心点是什么,核心点有什么难度和挑战,最后是怎么解决的,解决的是否完美,不完美的原因。如果能够回答的不错那就基本合格了。最后可以再多问一下: 有没有哪些产品提出的需求,你认为不合理或者不适合当前产品现状的?这个问题只要回答的有一定高度,那就完美了。


    ps: 如果面试者认为没有什么难度和挑战,只能证明他自己没有深度参与或主导该业务。再简单的系统,也不可能一点问题都没有,如果真的没有,那么完全没有必要安排团队去专门负责。没有简单的系统,只有简单的思考。


    举个栗子,用户管理(用户CRUD)系统我们一听可能都觉得很简单,早期,用户注册要填一堆的东西,现在都是各种登录渠道,非常的方便。站在现在的角度,对于早期的用户管理来说,如何提升用户注册效率,增加用户量就是一个有难度有挑战的事情。




  2. 技术能力,我简单分为有效技术能力和无效技术能力。无效技术能力代指无用且无聊的八股,当然也不是所有的八股都无用。有效技术能力我理解就是解决问题的能力,而解决问题不在于使用的技术手段与否高明,是否先进,只要贴合业务场景,我都会认为有技术能力。反而那些八股回答的头头是道,解决实际项目问题无一用到的会严重减分。




  3. 抗压能力,项目经验是能反应出来一些信息的:有难度,有挑战的事情你不会交给一个不合适的人来做的,所以如果简历的项目经验中有类似的经验那么就证明别人已经帮你筛选过了。PS:别忘了鉴别一下。




再说执行者


还是试想一下,好的执行者有哪些特质:



  1. 注重细节,考虑问题全面。

  2. 责任心强,不会随便应付了事。

  3. 技术OK,至少基础没有问题。


同样,上述三点仅做参考。




  1. 注重细节,直接体现其实跟方案的完善程度有关,所以问问技术方案的异常情况是如何考虑的。另外,直接体现其实就是BUG比较少,当然这个一般人肯定不会说自己BUG多,所以可以问问,对于如何减少BUG量,有没有心得。(这个问题目前来看基本没啥用,哈哈)




  2. 责任心强。责任心如何体现的我也说不太清楚,思考以后认为加班算一方面,有加薪、晋升算一方面,面试中很难直接体现,只能凭感觉。和第一条注重细节一样,我面试全凭聊完之后的直觉,这种大家都会有,而且一般来说准确率也不错。


    PS: 其实在面试沟通过程中,一问一答,很多事情靠直觉、面向也能猜个七七八八,玄学的东西这里不多说。说一点有科学依据的,人的性格简单分为外向、内向,两者都有其各自的特质,通常来说,内向者的特质更多时候适合于执行者。感兴趣的可以去了解一下两种性格特质,有益于团队管理。




  3. 技术OK。这个不做多说了,一定要多问实际使用的,不用的就不要问了,可能用的适当问一下,实际使用的也可以拔高一下往深了问问。比如:mysql都在用,mysql的八股可以多问几个。JVM这种开发基本不用的,简单问一下得了(我一般是不问的)。






这一篇先到这里把,关于技术强、责任心其实也简单提了一下。关于这两点,后续结合实际情况再更新吧。


作者:FishBones
来源:juejin.cn/post/7219943233799323704
收起阅读 »

在前端领域摸爬滚打7年,我终于掌握了这些沉淀技巧

我做开发多年,常常有人问我「软件开发难学吗?」「前端和后端哪个比较简单?」「培训后是否好找工作呢?」这些问题单拎出来比较棘手,三言两语说不清楚,需要你对开发有一个系统了解,问题才能迎刃而解。 所以,我想和你分享我的学习和工作经历,希望这对于正在准备成为一名程序...
继续阅读 »

我做开发多年,常常有人问我「软件开发难学吗?」「前端和后端哪个比较简单?」「培训后是否好找工作呢?」这些问题单拎出来比较棘手,三言两语说不清楚,需要你对开发有一个系统了解,问题才能迎刃而解。


所以,我想和你分享我的学习和工作经历,希望这对于正在准备成为一名程序员的你有所帮助。


我的经历可能会为新手提供一些有用的建议和思路。


01 萌芽之初,点燃编程学习的梦想


对于一些90后的朋友来说,网游填满了他们的高中时期,甚至是初中。


他们经常因为不走寻常路去打游戏,在回来时被门卫大爷逮个正着。尽管我没有沉迷于游戏,但我仍然被游戏所吸引。


在游戏中,我一直认为只有玩家和 NPC 的存在,但是,玩得越多,你会发现还有一些不寻常的角色,那就是“工作室”。部分“工作室”利用一些技术手段批量、自动地在游戏中完成任务以赚取游戏产出。


虽然这种行为不可取,但是他们使用的技术确实让我感兴趣。


这时候,代码的种子已经悄悄埋藏在我的内心深处,等待发芽。


高中毕业后,卸下学业负担,我开始利用暑期学习了一些脚本精灵、Tc 简单编程和易语言编程,这也是我第一次接触编程基础语法,如条件判断、循环、遍历和条件选择,再加上社区提供的一些识图插件,我就像一个蹩脚的裁缝,东拼西凑,左缝右补,费劲巴拉缝制成一件衣服,却不合身。


虽然实现了自动登录游戏的功能,但很不幸运的是,这样的小功能也还是过不去游戏的自检程序,万物皆有裨益,万事皆可为师,正是这一次编程体验促使了我后来的专业选择。


02 踏上编程学习之路,从安卓到前端,每一步都算数


英语是我成长路上的一块绊脚石,在选择专业时,我想躲开英语,于是选择了同为计算机系下的软件外包服务专业,结果发现,只要是技术,英语的要求都是一样的。


当然,我选择这个专业还有另外一个动机 -- 它开设了Android课程。毕竟,那时我刚拿到一款安卓手机,能在手机上开发自己的App是何等酷炫的体验啊!


那时,有一本厚重的《疯狂 Android 讲义》成了我的启蒙之书,我翻过无数遍,上课、参加编程比赛、实习工作、这本书我一直在用,为我第一份工作立下了汗马功劳。


临近毕业,是先就业还是先培训,许多软件相关专业的毕业生都面临着这样的选择。


所以,你要想明白,你到底需要的是什么?


我选择参加培训是出于两个原因:第一是为了将平时自学的知识整合起来,第二是希望能够认识更多的小伙伴,以便进行技术交流。编程最忌讳的就是闭门造车,不进行沟通交流。


然而,选择参加培训并不是每个人的选择。


如果你有能力自己阅读技术书籍,并且知道如何获取最新的技术信息,那么参加培训完全没有必要。


只有当你需要别人的指点和帮助来梳理技能,或者需要更好的机会来进行技术交流时,参加培训才是一个好的选择。


但是,如果你仅仅因为听说培训完就能很赚钱而选择花钱加入,那么你就要好好思考一下了,周围打水漂的人确实不在少数。


培训结束后,2015 年 12 月 7 号,我入职了第一家公司,担任 Android 开发工程师。


人生有时候做一个决策,一个行动,当时只道是寻常,当它的价值在未来某一刻兑现时,你会感谢当时努力的自己。


如果没有大学时翻过无数遍的《疯狂 Android 讲义》,我不可能找到这份工作。


03 学前端到底在学什么


工作后,我第一次真正进入团队开发模式(我是不会告诉你我当初使用百度云盘定时同步代码的,炸过一次硬盘),由于业务需要一定的前端支持(合同模板),所以在一次小组会议上,组长建议我们要着手学习前端技术(Angular1.x)。


到了17年左右,公司的业务开始由原 Pad 端转移到手机端。我和其他几个新入职的小伙伴经过一上午的 Vuejs2.x 培训后,就开始上手开发了。


也是在这次前端项目开发中,我第一次接触到了闭包导致循环失灵的问题,第一次把一个页面写到 3 千多行(烂,不懂拆分)。


由于这次前端项目开发的经验不足,导致迭代两年后,项目能编译出 200MB 的内容。我只能通过各种查找和大量的 webpack 参数调试,将产物压缩回了20MB 左右。对于我来说,这也是一次很大的成长。


我非常推荐各位小伙伴在工作中多承担,因为开发经验绝非是你熟背八股题得到的,开发经验只能是来自大量的项目实战。


多做练习,多遇困难,多做总结,得到的才是自己的。开发经验决定了你的下一个项目能否走得更顺利。


选择成为前端程序员是一件比较苦的事情,因为这个领域的技术更新非常频繁,如果你不持续学习,那么你就会落后,这也是“前端很累”的一个根本原因。


实际上,现在还有一些人对前端存在偏见,因为他们认为不就一个 JavaScript,能有多难?


但是事实上,很多前端构建技术的底层实现并不是用 JavaScript 语言编写的,而是基于了其它编程语言如 Golang(代:ESBuild)和Rust(代表:SWC)“包装”起来的,利用这些语言的特点来弥补 JavaScript 的不足。


前端学习的基础是 JavaScript,但不仅仅是 JavaScript,如果你认为学习 JavaScript 就是学习前端,那么你可能会走进死胡同。


04 正确的学习编程方式一定是这样的


在学校里,老师一定告诉过你两个正确的学习方式,其中一个是要做笔记,另一个是要能够向同学清晰地讲解。


繁多的技术是不可能靠记忆实现的,因此做笔记和写博客是记录学习过程和分享学习成果的捷径。


现在,我也发现很多在校的同学积极在各大技术社区分享自己的学习经验,这也印证了这条成长途径的正确,同时也激励我们这些已经做了多年程序员的伙伴要更加努力。


不论你是学习新的编程语言还是新的框架,都需要为其配置对应环境,但有很多框架的环境配置其实对于第一次接触的小伙伴来说并不友好,就比如我最初在从Android转前端的时候就因为安装NodeJsNpm这些东西而烦恼,因为当时莫名其妙就提示你Python2的模块找不到了,要不就是安装依赖超时了,在环境搭建问题上花费太长时间真的不划算。


为了避免环境搭建影响学习进度,我们可以使用一些在线的 IDE 环境,例如 CodePen、CodeSandBox、Stackblitz、JSRun 等。


但是,它们在依赖安装、操作习惯和响应速度上仍然有一些上手难度。


我最近一段时间一直在使用 1024Code  社区提供的在线 IDE,它提供了很多热门语言和框架的代码空间模板,免配置环境,即开即用随时学习新技术。


它支持多人开发和在线分享,无论是和朋友一起开发项目还是找大佬请教问题,都非常轻松。


05 学习编程,高效沉淀需要技巧


我发现之前写博客时做的案例很难沉淀下来。往往只是写完一遍,很少再打开运行。


但是在 1024Code 中,可以以卡片的形式记录每一个案例,也可以将一系列案例放到一个集合中归类。


此外,1024Code 还支持在个人主页中渲染 Markdown,为小伙伴打造炫酷的个人主页提供了便利。


最令人赞叹的是,1024Code 紧跟最近比较火的 ChatGPT,将其接入到了 IDE 中,让你在编码的同时可以更快速地查找解决方案。下面我给大家简单地展示一下:


在社区主页中,案例以卡片的形式展示。你可以点击你感兴趣的案例,一键运行。边浏览源码,边跟着作者提供的 README 进行学习。


如果你想在此基础上练习或二次开发,还可以 fork 一份到自己的工作空间。如果你发现作者的代码有不合理的地方,还可以在评论区大胆地给他留言,大家可以共同成长。



1024Code 提供了众多空间模板,涵盖了多种编程语言和框架,例如针对数据统计和 AI 模型训练的 Python,以及让许多程序员感到头疼的 C++。


此外,它还支持其它主流的热门编程语言和框架。



Markdown 是编程小伙伴们最常用的笔记格式之一,因此无需专门学习其语法。只需要多看几遍,就可以自然而然地掌握。


此外,你还可以参考社区中其他小伙伴的主页,来打造自己独特的个人主页。



接下来,我要展示一段时间以来我制作的合集。


最初,这个合集是为了帮助那些不熟悉滴滴 LF 框架如何使用 Vue3+TS 编写的小伙伴们而制作的。


我还将合集地址提交到了 LF 仓库,希望能够帮助那些正在转向 Vue3+TS 的小伙伴们。



最重磅的就是 ChatGPT 了。


在使用 1024Code 的 IDE 进行开发过程中,如果遇到问题,你可以快速打开 ChatGPT 来协助你查找答案,而不需要离开当前页面。


ChatGPT 支持上下文连续问答模式,虽然它不能解决你所有的问题,甚至会给出错误的答案,但对于一些常规类编程问题或正在做毕业设计的小伙伴们,它还是能够显著提升效率的。



总结


最后,我再为你做一些总结、建议和对未来的期待:




  1. 我建议你要有很强的动力来学习编程,因为坚持并不是易事;




  2. 我建议你坚守自己慎重选择的专业,因为不忘初心方得始终;




  3. 我建议你在面对技术培训时要清醒认知,因为明确目标的选择才适合自己;




  4. 我建议你在工作中抓住一切学习的机会,因为努力的人很多,只有不断学习才能跟上技术的发展;




  5. 我建议你在编程学习时要善用工具、做好笔记、写博客,不断沉淀自己的知识和经验;




最后的最后,愿我们所有付出都将是沉淀,所有美好终会如期而至。


作者:小鑫同学
来源:juejin.cn/post/7209648356530929721
收起阅读 »

python | 写一个记仇本

最近背着老婆买了一个switch卡带,这货居然给丈母娘讲,害得我被丈母娘说还小了,不买奶粉买游戏,太气人了,我连夜用python写了个《记仇本》,画个圈圈把她记下来。 本片文章,主要关注于python代码,而html和css将暂时被忽略。 记仇本展示 如题所...
继续阅读 »

最近背着老婆买了一个switch卡带,这货居然给丈母娘讲,害得我被丈母娘说还小了,不买奶粉买游戏,太气人了,我连夜用python写了个《记仇本》,画个圈圈把她记下来。


本片文章,主要关注于python代码,而htmlcss将暂时被忽略。



记仇本展示


如题所述,项目已经写好了,是基于local_storage存储在本地的项目,地址如下:



该项目运行时是基于brython, 你可能想问,为什么不使用原生python来编写网页呢,这个有个误区是,网页是由html代码编写的,而原生python想要操作DOM非常难,所以brython正是为这个来操作的。


初始打开页面,因为没有数据展示,所以只有一个增加按钮。



当我们点击【画个圈圈记下来】按钮后,会刷新为新增页面,例如:



此时,我们只需要输入信息,比如 记老婆的仇,缘由为 买switch游戏透露给丈母娘,还得被骂。



此时点击记仇,就可以存储到页面上了。



此时若点击已原谅,则可以删除该记录。


brython 之 local_storage


你可能细心发现了,哎,关掉了浏览器,下次打开,怎么还会有记录在上面呢,这是因为用了local_storage,那么,什么是local_storage呢?


哎,我们使用的是brython中的local_storage但是,它可不是python定义的哦,而是HTML 5提供的存储数据的API之一,可以在浏览器中保持键值对数据块。


现在来展示使用一下brython存储和删除的操作。


导入库:


from browser.local_storage import storage

存储数据,例如键值信息juejinName存储为pdudo


storage[juejinName] = "pdudo"

查询的话,直接使用storage[变量]就好,若为空,则返回


v = storage[juejinName]

循环所有的key,需要引入window库,再使用for...in来完成


from browser import window
for key in window.localStorage:
print(key)

也可以直接使用for...in来遍历storage


而删除数据呢?只需要像删除字典一下


del storage[juejinName]

storage是不是操作起来和字典非常类似呢?减少了开发者的学习成本。


上述案例,已经放到下面的链接中了:



制作项目


有了上述前置条件后,我们再看该项目,便可以总结为 针对localStorage的增删查,首先当页面加载出来的时候,我们需要先遍历一下localstorage数据,从而映射为一个table,例如:


  for key in window.localStorage:
tr = html.TR()
datas = json.loads(storage[key])

delBtn = html.BUTTON("已原谅")
delBtn.dataset["id"] = datas["id"]
delBtn.className = "confirm-btn"
delBtn.bind("click",delGrudges)

td = html.TD(delBtn+" "+time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(datas["id"]))))
tr <= td

for tdVal in datas["whos"],datas["Text"]:
td = html.TD(tdVal)
tr <= td

tb <= tr

userWindows <= tb

上述代码是遍历localStorage,而后在定义删除按钮,等将其他值组合完毕后,全部加载进table中,而后再页面上显示。


而添加数据呢?


def saveGrudges(ev):
getWhoVal = document["whos"].value
getTextVal = document["textArea"].value

if getWhoVal == "" or getTextVal == "":
return

document["saveBtn"].unbind("click")


ids = int(time.time())
datas = {
"id": ids,
"whos": getWhoVal,
"Text": getTextVal
}

storage[str(ids)] = json.dumps(datas)

上述代码,显示获取inputtextarea框中的值,再判断是否用户没有输入,我们将数据组装为一个字典,而后转换为字符串,再存入localstage中。


还有其他操作,这个可以直接看代码说明,brython很简单的。


总结


这篇文章,是善用localStorage来作为键值对存储,以此来保证打开和关闭浏览器,不会对数据产生影响,整个项目就是围绕这个localStorage增删改查来操作的。


作者:真的不能告诉你我的名字
来源:juejin.cn/post/7222229682027462693
收起阅读 »

为什么面试聊得很好,转头却挂了?

本文首发自公粽hao「林行学长」,欢迎来撩,免费领取20个求职工具资源包。 了解校招、分享校招知识的学长来了! 四月中旬了,大家面试了几场? 大家有没有这样的感受:面试的时候和面试官聊得火热朝天,气味相投。 面完觉得自己稳了,已经在畅想入职事宜了,结果一封感谢...
继续阅读 »

本文首发自公粽hao「林行学长」,欢迎来撩,免费领取20个求职工具资源包。


了解校招、分享校招知识的学长来了!


四月中旬了,大家面试了几场?


大家有没有这样的感受:面试的时候和面试官聊得火热朝天,气味相投。


面完觉得自己稳了,已经在畅想入职事宜了,结果一封感谢信让人瞬间清醒。


image.png


不少同学应该有这样的经历。


学长也曾经有过:面试两小时,自觉面试问题回答得不错,但是面试官只说:你回去等消息吧。


经历过面试的同学应该懂”回去等消息“这句话的杀伤力有多大。


在此也想先和那些面试多次但还是不通过的朋友说:千万别气馁!


找工作看能力,有时候也看运气,面试没有通过,这并不说明你不优秀。


所有,面试未通过,这其中的问题到底出在哪呢?


01 缺乏相关经验或技能


如果应聘者没有足够的经验或技能来完成职位要求,或者面试的时候没有展现自己的优势,那么失败很常见。


而面试官看重也许就是那些未展现的经验或技能,考察的是与岗位的匹配程度。


02 没有准备充分


每年学长遇到一些同学因为时间安排不当,没有任何了解就开投简历。


而被春招和毕业论文一起砸晕的同学更是昏头转向。


如果没有花足够的时间和精力来了解公司和职位,并准备回答常见的面试问题,那么可能表现不佳。


03 与招聘人员沟通不畅


在面试过程中,面试官真的非常看重沟通效果!


如果应聘者无法清晰地表达自己的想法,或者不能理解面试官的问题,那么可能会被认为不适合该职位。


04 缺乏信心或过度紧张


学长也很理解应届生的局促感,以及面对面试官的紧张。


image.png


但是如果面试场上感到非常紧张或缺乏自信,那么可能表现得不自然或不真诚。


好像,面试的时候需要表现得自信、大方,才能入面试官的眼。


05 不符合公司文化或价值观


企业文化,也成为考察面试者的一个利器。


如果应聘者的个人品格、行为或态度与公司文化或价值观不符,那么可能无法通过面试。


比如一个一心躺平的候选人,面对高压氛围,只会 Say goodbye。


image.png


06 其他候选人更加匹配


如果公司有其他候选人比应聘者更加匹配该职位,那么应聘者可能无法通过面试。


一个岗位,你会面对强劲的对手。


同样学历背景,但有工作经验比你丰富的;


工作经验都 OK,但有其他学历背景比你合适,或稳定性比你高的面试者。


经常有同学发帖吐槽面试经历:一场群面,只有 Ta 是普通本科生,其余人均 Top 学校研究生学历。


面试不容易,祝大家都能斩获心仪的 Offer!


最后,欢迎来关注公粽hao「林行学长」,聊聊职场,聊聊工作,聊聊生活,林行学长和你一起成长~


作者:林行学长
来源:juejin.cn/post/7221131892426096701
收起阅读 »

使用fabric从零开始打造互动白板(一)

web
最近由于公司业务需要,原本使用第三方的互动白板功能,准备自行实现。一方面为了节省开支,另一方面自行实现定制化也更强一些,能够和现有业务更好的结合,用以满足第三方互动白板无法实现的一些功能。 一、功能整理 既然需求明确了,于是就开始着手整理白板所需的功能。由于...
继续阅读 »

最近由于公司业务需要,原本使用第三方的互动白板功能,准备自行实现。一方面为了节省开支,另一方面自行实现定制化也更强一些,能够和现有业务更好的结合,用以满足第三方互动白板无法实现的一些功能。



一、功能整理


既然需求明确了,于是就开始着手整理白板所需的功能。由于我们的直播课都是大班课,只需要讲师在白板上操作,用户端只用来展示,也就不需要太多复杂的功能,结合几个第三方互动白板,归纳整理出了如下几个需要实现的功能点:



  • 自由画笔

  • 文字书写

  • 橡皮擦

  • 画三角、圆形、矩形

  • 画直线和箭头

  • 清空画布

  • 撤销重做

  • 画布缩放

  • 插入PPT图片及切换控制


二、技术选择


观察了现有的互动白板,都是在Canvas进行操作,为了节约开发时间于是找到了fabric这个库,这个库本身就实现了不少功能,可以大大的减少开发时间。


结合我熟悉的技术栈,最终选定了使用Vite+Vue3+TypeScript进行demo版本的构建。


相关代码放在github上,链接地址:使用vite+typescript+fabric创建的互动白板项目


三、页面结构


参考其他白板的布局进行了页面结构的搭建。白板使用一个容器进行包裹,左侧是工具区域,包括画笔、橡皮擦、画线、清空画布等各种绘制工具;左下角是撤销、重做和画布缩放控制区域;右上角是插入PPT文件控制区域;右下角是PPT控制区域。最后提供了一个容器进行的白板预览。


效果图如下:


demo.png


页面结构代码如下:


<template>
<div>
<div class="canvas-wrap">
<div class="tool-box-out">
<ToolBox></ToolBox>
</div>
<div class="redo-undo-box">
<RedoUndo></RedoUndo>
</div>
<div class="zoom-controller-box">
<ZoomController></ZoomController>
</div>
<div class="room-controller-box" v-show="!isPreviewShow">
<div class="page-controller-mid-box">
<div className="page-preview-cell" @click="insertPPT">
<img style="width: 28px" :src="folder" alt="文件"/>
</div>
</div>
</div>
<div class="page-controller-box" v-show="isShowPPTControl">
<div className="page-controller-mid-box">
<PageController></PageController>
<div className="page-preview-cell" @click="handlePreviewState(true)">
<img :src="pages" alt="PPT预览"/>
</div>
</div>
</div>
<div class="preview-controller-box" v-show="isShowPPTControl&&isPreviewShow">
<PreviewController @handlePreviewState="handlePreviewState"></PreviewController>
</div>
<canvas id="canvas" width="800" height="450"></canvas>
</div>
<div class="canvas-wrap">
<canvas id="canvas2" width="800" height="450"></canvas>
</div>
</div>
</template>

四、初始化白板


为了方便后续使用,这里对fabric进行封装,后续拓展也能更加灵活。相关代码如下:


import { fabric } from "fabric";

class FabricCanvas {
constructor(canvasId: string) {

// 初始化画布,默认可绘制
this.canvas = new fabric.Canvas(canvasId, {
isDrawingMode: true,
selection: false,
includeDefaultValues: false, // 转换成json对象,不包含默认值
});
}
}

使用示例:


const canvas = new FabricCanvas('canvas');

五、工具栏相关功能实现


页面框架搭建完成之后,就开始各种功能的开发。这里将fabric封装成一个类,所有需要实现的方法在类中实现,方便后续灵活调用。


选择


选择功能实现比较简单,只需要关闭绘制模式,然后将画布设置可选中。相关代码如下:


this.canvas.isDrawingMode = false;
this.canvas.selection = true;
// 设置鼠标光标
this.canvas.defaultCursor = 'auto';

自由画笔


fabric提供了各种丰富的笔刷,实现自由绘制这里调用了PencilBrush类,并将isDrawingMode设置为true即可。相关代码如下:


  public drawFreeDraw() {
this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
this.canvas.freeDrawingBrush.color = '#ff0000'
this.canvas.freeDrawingBrush.width = 5
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 自由画笔
canvas.drawFreeDraw();

文字书写


文字输入使用fabric提供的IText方法,实现文字编辑和修改,并且让输入框自动获取焦点,方便输入。相关代码如下:


  public drawText(text: string, options?: ITextOptions): void {
const textObj = new fabric.IText(text, {
editingBorderColor: '#ff0000',
padding: 5,
...options
});
this.canvas.add(textObj);
this.canvas.defaultCursor = 'text'
this.currentShape = textObj;
// 文本打开编辑模式
textObj.enterEditing();
// 文本编辑框获取焦点
textObj.hiddenTextarea.focus()
this.setActiveObject(textObj);
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制文本
canvas.drawText('Hello World!', { left: 50, top: 250, fontSize: 24, fill: 'red' })

橡皮擦


fabric内置了EraserBrush用来实现橡皮擦功能,不过默认构建排除了该功能,避免库文件过大。如需使用,需要进入node_modules/fabric目录执行下面的命令重新构建:


node build.js modules=ALL exclude=gestures,accessors requirejs minifier=uglifyjs

构建完成之后就可以使用EraserBrush来实现橡皮擦功能了,相关代码如下:


public eraser(options?: any): void {
this.canvas.freeDrawingBrush = new fabric.EraserBrush(this.canvas, options);
this.canvas.freeDrawingBrush.width = 10
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 橡皮擦
canvas.erase({ width: 10 });

画三角、圆形、矩形


画三角形、圆形、矩形方法相似,直接调用fabric封装的对应方法即可。


这里以绘制矩形为例,相关代码实现如下:


public drawRect(options: IRectOptions): void {
const rect = new fabric.Rect({ ...this.options, ...options });
this.canvas.add(rect);
this.currentShape = rect;
this.canvas.defaultCursor = 'crosshair'
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawRect({ left: 50, top: 150, width: 100, height: 50, fill: 'green', stroke: 'black' });

画直线和箭头


画直线功能fabric直接内置了,调用对应的方法即可,这里重点讲画箭头。画箭头的本质是在直线的一端加上一个三角形,根据起始点使用三角函数计算好三角形的方向,这样就组合成一个箭头了。这里搬运网友的画箭头方法,直接对画直线功能进行拓展,并封装成fabric中的功能模块,方便后续调用。相关代码如下:


import { fabric } from 'fabric';

fabric.Arrow = fabric.util.createClass(fabric.Line, {
type: 'arrow',
superType: 'drawing',
initialize(points: number[], options: any) {
if (!points) {
const { x1, x2, y1, y2 } = options;
points = [x1, y1, x2, y2];
}
options = options || {};
this.callSuper('initialize', points, options);
},
_render(ctx: any) {
this.callSuper('_render', ctx);
ctx.save();
const xDiff = this.x2 - this.x1;
const yDiff = this.y2 - this.y1;
const angle = Math.atan2(yDiff, xDiff);
ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);
ctx.rotate(angle);
ctx.beginPath();
// Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)
ctx.moveTo(5, 0);
ctx.lineTo(-5, 5);
ctx.lineTo(-5, -5);
ctx.closePath();
ctx.fillStyle = this.stroke;
ctx.fill();
ctx.restore();
},
});

fabric.Arrow.fromObject = (options: any, callback: any) => {
const { x1, x2, y1, y2 } = options;
return callback(new fabric.Arrow([x1, y1, x2, y2], options));
};

export default fabric.Arrow;

封装好的代码,直接导入调用即可。相关代码如下:


import Arrow from "./objects/Arrow";
// 绘制箭头
public drawArrow(x1: number, y1: number, x2: number, y2: number, options?: ILineOptions) {
const arrow = new Arrow([x1, y1, x2, y2], { ...this.options, ...options });
this.canvas.add(arrow);
this.currentShape = arrow;
this.canvas.defaultCursor = 'crosshair'
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawArrow(10, 50, 100, 50, { stroke: 'blue', strokeWidth: 2 })

通过鼠标绘制图形


实际使用白板过程中,上面这些图形、线条的绘制都是通过鼠标拖动进行,这样更加灵活一些。


通过鼠标绘制图形,需要对鼠标的mouse:downmouse:movemouse:up事件进行监听,相关代码如下:


// 监听鼠标事件
this.canvas.on("mouse:down", this.onMouseDown.bind(this));
this.canvas.on("mouse:move", this.onMouseMove.bind(this));
this.canvas.on("mouse:up", this.onMouseUp.bind(this));

这里以绘制矩形为例,实现通过通过鼠标绘制一个矩形框。



  1. 当鼠标按下时,在鼠标按下的地方绘制一个宽高为0的矩形。相关代码如下:


// 是否处于绘制状态
private isDrawing = false;
// 鼠标起点坐标x
private startX = 0;
// 鼠标起点多表y
private startY = 0;

// 鼠标按下事件处理函数
private onMouseDown(event: IEvent) {
// 如果当前有活动的元素则不进行后续绘制
const activeObject = this.canvas.getActiveObject();
if (!event.pointer || activeObject) return;

// 切换成绘制状态
this.isDrawing = true;
// 记录当前坐标点
const { x, y } = event.pointer;
this.startX = x;
this.startY = y;

// 在当前坐标绘制一个矩形
this.drawRect({
left: x,
top: y,
width: 0,
height: 0,
});
}


  1. 在鼠标移动的过程中,动态的修改矩形的宽高,并实时渲染。相关代码如下:


// 鼠标移动事件处理函数
private onMouseMove(event: IEvent) {
if (!this.isDrawing || !event.pointer || !this.currentShape) return;

// 计算宽高
const { x, y } = event.pointer;
const width = x - this.startX;
const height = y - this.startY;

// 设置宽高
this.currentShape.set({
width,
height,
});

// 更新画布
this.canvas.renderAll();
}


  1. 当鼠标抬起后,改变绘制状态。相关代码如下:


// 鼠标抬起事件处理函数
private onMouseUp() {
this.isDrawing = false;
this.currentShape = null;
}

如果想要更加灵活的在各种图形和线条中自由的进行切换,并通过鼠标绘制,在提供的demo中也进行了对应的封装。
相关代码请在github中进行查看,对fabric的各种功能封装


清空画布


清空画布直接调用画布的清除方法即可,相关代码如下:


// 清空画布
public clearCanvas() {
this.canvas.clear();
}

不过该方法会清除画布上包含背景的所有内容,如果不想画布背景也被清除,可以遍历画布上的所有对象进行移除。相关代码如下:


// 移除所有对象
public removeAllObject() {
this.canvas.getObjects().forEach((obj) => {
this.canvas.remove(obj);
});
}

六、工具栏布局
将工具栏封装成ToolBox组件,并在组件中实现各种工具的切换。


工具栏显示效果
组件布局代码如下:


<template>
<div class="tool-mid-box-left">
<div class="tool-box-cell-box-left" v-for="item in tools" :key="item.shapeType">
<div class="tool-box-cell"
@click="clickAppliance(item.shapeType)">
<img :src="item.shapeType === currentShapType ? item.iconActive : item.icon" :alt="item.name"/>
</div>
</div>
<div class="tool-box-cell-box-left">
<div class="tool-box-cell"
@click="clickClear">
<img :src="clear" alt="清屏"/>
</div>
</div>
</div>
</template>

相关功能事件实现的代码如下:


const currentShapType = ref<string>("pencil")

// 设置当前工具
function clickAppliance(type: DrawingTool) {
currentShapType.value = type;
canvas?.value.setDrawingTool(type)
}

// 清屏事件处理
function clickClear() {
canvas?.value.clearCanvas()
}

设置当前绘制工具


// 设置绘图工具
public setDrawingTool(tool: DrawingTool) {
if(this.drawingTool === tool) return;
this.canvas.isDrawingMode = false;
this.canvas.selection = false;

this.drawingTool = tool;
if (tool === "pencil") {
this.drawFreeDraw();
} else if (tool === "eraser") {
this.eraser();
} else if (tool === "select") {
this.canvas.selection = true;
this.canvas.defaultCursor = 'auto'
}
}

其他功能说明


为避免文章太长,撤销重做、画布缩放、插入PPT图片及切换控制等功能的实现在后续文章中介绍。


如果等不及,可以直接在github上查看相关代码实现:使用vite+typescript+fabric创建的互动白板项目


六、参考资料



作者:江阳小道
来源:juejin.cn/post/7221348552513077305
收起阅读 »

你到底值多少钱?2023打工人薪酬指南

大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。 作为打工人,你最关心什么?技能,成长,发展还是薪酬? 刚毕业时,我为了赢得面试官的好感,说了很多违心话,如:“工资不要紧,主要是想学习”,又或者是“我对贵司的这块技术非常感兴趣”。 现在想想,呸!恶心,...
继续阅读 »

大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。



作为打工人,你最关心什么?技能,成长,发展还是薪酬


刚毕业时,我为了赢得面试官的好感,说了很多违心话,如:“工资不要紧,主要是想学习”,又或者是“我对贵司的这块技术非常感兴趣”。


现在想想,呸!恶心,哪怕是花钱培训呢,也不要再傻乎乎的说出“为块术”这种违心的话了。


图1:为块术.png


那时候年轻,不知道起薪高的好处,现在被各种压涨幅,各种倒挂,干最累的活,拿最少的钱,吃最硬的大饼。


2023年,后疫情时代的“元年”,我想明白了,我背上行囊,背井离乡来北漂,就为了3件事:挣钱,挣钱,还是TM的挣钱


既然要挣钱,首先要明确自己的价值。想必大家也对自己值多少钱感兴趣吧?可苦于薪酬保密协议,很难和身边人对比,难以了解自己的价值。没关系,我最近读了几份有趣的报告:



  • 《看看你该赚多少?2023薪资指南(亚太版)》连智领域

  • 《2023年市场展望与薪酬报告》任仕达

  • 《2023⾏业薪酬⽩⽪书》嘉驰国际


今天我就通过这几份报告和大家聊聊,在职场中“我”到底价值几何,“我”拿到怎样的薪资才没有辜负我的才华。


Tips



  • 本文重点分享信息技术岗位,互联网行业,金融行业,软件行业的数据,其余数据可自行阅报告,文末附下载方式;

  • 个人价值不单单由工作年限决定,更多的是与工作年限所匹配的能力。


应届生薪资指南


如果你经常逛各种论坛,可能会看到“今年春/秋招的白菜价是25K”,“XXXX给我开了20K的侮辱性Offer”这类言论。那么20K真的是侮辱性Offer吗?低于白菜价的Offer到底要不要接?


来看嘉驰国际统计到的信息技术行业应届生薪资数据:


图2:2023信息技术行业应届生平均薪资.png


数据似乎与看到的言论相反,一线城市中,本科毕业生薪资中位数是8.8K,只有25%的毕业生拿到了超过10K的薪资。热门城市(北京,上海,广州,深圳和杭州)中也只有北京,上海和深圳的应届生薪资中位数超过了8K


那么网上流传的“白菜价”是怎么回事?其实不难理解,“白菜价”是少数顶尖院校(115所211院校,含39所985院校)的学生拿到顶尖互联网大厂的平均薪资水平,而大部分应届毕业生是很难拿到这个薪资的。


我国拥有2759所普通高等院校,本科1270所,高职(专科)1489所,顶尖院校(115所211院校,含39所985院校)仅占本科院校的9%,普通高等院校的4.1%。


所以对于大部分的普通人院校的毕业生来说,没有所谓的“白菜价”。根据自身的硬性条件合理决定自身的价值范围,不要被HR忽悠,也不要有太过离谱的期望


插句题外话,我16年毕业于某双非院校,第一份工作8.5K,但我们年级的“神”,第一份工作18K。讲这个事情有两层意思:



  • 某些大佬真的可以挣脱本科院校的枷锁

  • 身边的个例并不能反应真实的平均情况


Tips



互联网的天花板


了解完应届生的薪资后,你一定会很想了解未来自己的天花板在哪。注意,标题是互联网的天花板,并非某个职业,也并非每个人都能达到天花板。


先来看互联网行业的年固定收入成长曲线:


图3:互联网年固定收入天花板.png


接着是互联网行业年总收入的成长曲线:


图4:互联网年总收入天花板.png


以我个人观察到的情况,互联网行业中,主管/高级通常对应阿里巴巴的技术职级序列的P6和P6+,经理/资深则对应的是P7,而总监/专家则是P8及以上的职级。


一个很惨淡的事实,对于大部分人来说,P7是通过勤奋可以达到的天花板


如果不是太差,当你达到P7时你的年固定收入会来到50W上下,总收入(奖金和少量股票)会在60W到70W徘徊;而其中的佼佼者,年固定收入会来到70W,总收入触摸到7位数的边界;佼佼者中的一部分会跨过P7这道坎来到P8,普通的P8年薪会在60W上下,总收入(奖金和股票)接近100W,而顶尖的P8薪资会超过100W,总收入更是超过150W。


如果说P7是普通人勤奋的天花板,那普通人想要晋升为P8就需要额外的借助机遇和人脉才有可能达成


Tips:正文部分只展示互联网行业的年固定收入成长曲线和年总收入成长曲线,附录部分提供其他行业的收入曲线。


上海地区研发岗位薪酬


任仕达在《2023年市场展望与薪酬报告》中,给出了信息技术行业中各个技术岗位的薪酬数据,但只有上海地区的数据较为全面,我们重点关注几个“奋战”在一线的技术岗位的薪酬数据:


图5:上海职位薪酬.png
可以看到,对于研发工程师来说,薪资的天花板都非常接近,最突出的是移动开发工程师,稍微落后的是Python开发工程师


当然,技术岗位的天花板远不是职业的终点,技术岗位之后是偏向管理的岗位,例如项目管理和技术管理等。


在大部分互联网公司中,产品经理也是一线岗位,但无论平均薪酬还是薪酬上限,都高于研发岗位(AI类除外)。


结语


了解市场上的薪酬行情,有助于你在求职市场上擦亮自己的双眼,一来可以防止HR恶意压薪资,二来可以清楚自身的定位。


因文章篇幅限制,仅展示部分数据,点击王有志,回复【薪酬报告】即可下载报告。




好了,今天就到这里了,Bye~~


附录


各行业应届生薪资数据


附1:2023各行业应届生平均薪资.png


电子商务年收入成长曲线


年固定收入成长曲线:


附2:电子商务年固定收入天花板.png


年总收入成长曲线:


附3:电子商务年总收入天花板.png


企业软件年收入成长曲线


年固定收入成长曲线:


图4:互联网年总收入天花板.png


年总收入成长曲线:


附5:企业软件年总收入天花板.png


作者:王有志
来源:juejin.cn/post/7217601930917838885
收起阅读 »

new 一个对象时,js 做了什么?

web
前言 在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。 new 的作用 我们先通过例子来了解 n...
继续阅读 »

前言


在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。


new 的作用


我们先通过例子来了解 new 的作用,示例如下:


function Person(name) {
this.name = name
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const t = new Person('小明')
console.log(t.name) // 小明
t.sayName() // 小明

从上面的例子中我们可以得出以下结论:





  • new 通过构造函数 Person 创建出来的实例对象可以访问到构造函数中的属性。




  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来。





构造函数 Person 并没有显式 return 任何值(默认返回 undefined),如果我们让它返回值会发生什么事情呢?


function Person(name) {
this.name = name
return 1
}
const t = new Person('小明')
console.log(t.name) // 小明

在上述例子中的构造函数中返回了 1,但是这个返回值并没有任何的用处,得到的结果还是和之前的例子完全一样。我们又可以得出一个结论:



构造函数如果返回原始值,那么这个返回值毫无意义。



我们再来试试返回对象会发生什么:


function Person(name) {
this.name = name
return {age: 23}
}
const t = new Person('小明')
console.log(t) // { age: 23 }
console.log(t.name) // undefined

通过上面这个例子我们可以发现,当返回值为对象时,这个返回值就会被正常的返回出去。我们再次得出了一个结论:



构造函数如果返回值为对象,那么这个返回值会被正常使用。



总结:这两个例子告诉我们,构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用。


实现 new


首先我们要清楚,在使用 new 操作符时,js 做了哪些事情:



  1. js 在内部创建了一个对象

  2. 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数连接起来

  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)

  4. 返回原始值需要忽略,返回对象需要正常处理


知道了步骤后,我们就可以着手来实现 new 的功能了:


function _new(fn, ...args) {
const newObj = Object.create(fn.prototype);
const value = fn.apply(newObj, args);
return value instanceof Object ? value : newObj;
}

测试示例如下:


function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};

const t = _new(Person, "小明");
console.log(t.name); // 小明
t.sayName(); // 小明

以上就是关于 JavaScript 中 new 操作符的作用,以及如何来实现一

作者:codinglin
来源:juejin.cn/post/7222274630395379771
个 new 操作符。

收起阅读 »

CSS链接悬停效果的的小创意

web
前言 每次写a标签的时候我都烦躁,为什么默认是蓝色的,就跟奇怪,关键他那个颜色也用不上,今天想让平凡的a标签也能做出令人眼前一亮的效果,大家以后直接cv多好 悬停滑动高亮链接效果 鼠标悬停后,链接有一个过渡动画,填充背景色,我们从链接周围的填充开始,然后添加相...
继续阅读 »

前言


每次写a标签的时候我都烦躁,为什么默认是蓝色的,就跟奇怪,关键他那个颜色也用不上,今天想让平凡的a标签也能做出令人眼前一亮的效果,大家以后直接cv多好


悬停滑动高亮链接效果


鼠标悬停后,链接有一个过渡动画,填充背景色,我们从链接周围的填充开始,然后添加相同值的负边距以防止填充破坏文本流。我们将使用box-shadow而不是 background 属性,因为它允许我们转换。


a { 
box-shadow: inset 0 0 0 0 #54b3d6;
color: #54b3d6;
margin: 0 -.25rem;
padding: 0 .25rem;
transition: color .3s ease-in-out, box-shadow .3s ease-in-out;
}
a:hover {
box-shadow: inset 100px 0 0 0 #54b3d6;
color: white;
}


悬停链接文本交换效果


我们在悬停时将链接的文本与其他一些文本交换。将鼠标悬停在文本上,链接的文本会随着新文本的滑入而滑出。


 <p><a href="#" data-replace="给个三连,好不好嘛"><span>鼠标放到这里试一试</span></a></p>

让我们给链接一些基本样式。我们需要给它相对定位来固定伪元素,确保它的显示是inline-block为了获得盒子元素样式的可供性,并隐藏伪元素可能导致的任何溢出。


  a {
overflow: hidden;
position: relative;
display: inline-block;
}

::before,::after设置为链接的全宽,左侧位置为零,并且绝对定位。


a::before,
a::after {
content: '';
position: absolute;
width: 100%;
left: 0;
}

::after伪元素从 HTML 标记中的链接数据属性获取内容:


a::after {
content: attr(data-replace);
}

transform: translate3d()::after伪元素元素向右移动 200%,悬停再回到以前的位置。


a::after {
content: attr(data-replace);
top: 0;
transform-origin: 100% 50%;
transform: translate3d(200%, 0, 0);
}

a:hover::after,
a:focus::after {
transform: translate3d(0, 0, 0);
}

我们使用transform: scaleX(0)::before伪元素,因此默认情况下它是隐藏的。悬停后我们将使它显示出来,就像2px高度一样,并将其固定到 上bottom,使其看起来像文本上的下划线那种感觉,看一下代码就理解我说的意思了


a::before {
background-color: #54b3d6;
height: 2px;
bottom: 0;
transform-origin: 100% 50%;
transform: scaleX(0);
}

a:hover::before,
a:focus::before {
transform-origin: 0% 50%;
transform: scaleX(1);
}

随后加入了transform效果、一些颜色等等以获得完整的效果。

作者:前端高级工程师宋
来源:juejin.cn/post/7143596588579946503
an>

收起阅读 »

vue3 实现 chatgpt 的打字机效果

在做 chatgpt 镜像站的时候,发现有些镜像站是没做打字机的光标效果的,就只是文字输出,是他们不想做吗?反正我想做。于是我仔细研究了一下,实现了打字机效果加光标的效果,现在分享一下我的解决方案以及效果图 共识 首先要明确一点,chatgpt 返回的文本格...
继续阅读 »

在做 chatgpt 镜像站的时候,发现有些镜像站是没做打字机的光标效果的,就只是文字输出,是他们不想做吗?反正我想做。于是我仔细研究了一下,实现了打字机效果加光标的效果,现在分享一下我的解决方案以及效果图


Kapture 2023-04-14 at 14.02.32.gif


共识


首先要明确一点,chatgpt 返回的文本格式是 markdown 的,最基本的渲染方式就是把 markdown 文本转换为 HTML 文本,然后 v-html 渲染即可。这里的转换和代码高亮以及防 XSS 攻击用到了下面三个依赖库:



  • marked 将markdwon 转为 html

  • highlight 处理代码高亮

  • dompurify 防止 XSS 攻击


同时我们是可以在 markdown 中写 html 元素的,这意味着我们可以直接把光标元素放到最后!


将 markdown 转为 html 并处理代码高亮


先贴代码


MarkdownRender.vue


<script setup>
import {computed} from 'vue';
import DOMPurify from 'dompurify';
import {marked} from 'marked';
import hljs from '//cdn.staticfile.org/highlight.js/11.7.0/es/highlight.min.js';
import mdInCode from "@/utils/mdInCode"; // 用于判断是否显示光标

const props = defineProps({
// 输入的 markdown 文本
text: {
type: String,
default: ""
},
// 是否需要显示光标?比如在消息流结束后是不需要显示光标的
showCursor: {
type: Boolean,
default: false
}
})

// 配置高亮
marked.setOptions({
highlight: function (code, lang) {
try {
if (lang) {
return hljs.highlight(code, {language: lang}).value
} else {
return hljs.highlightAuto(code).value
}
} catch (error) {
return code
}
},
gfmtrue: true,
breaks: true
})

// 计算最终要显示的 html 文本
const html = computed(() => {
// 将 markdown 转为 html
function trans(text) {
return DOMPurify.sanitize(marked.parse(text));
}

// 光标元素,可以用 css 美化成你想要的样子
const cursor = '<span class="cursor"></span>';
if (props.showCursor) {
// 判断 AI 正在回的消息是否有未闭合的代码块。
const inCode = mdInCode(props.text)
if (inCode) {
// 有未闭合的代码块,不显示光标
return trans(props.text);
} else {
// 没有未闭合的代码块,将光标元素追加到最后。
return trans(props.text + cursor);
}
} else {
// 父组件明确不显示光标
return trans(props.text);
}
})

</script>

<template>
<!-- tailwindcss:leading-7 控制行高为1.75rem -->
<div v-html="html" class="markdown leading-7">
</div>
</template>

<style lang="postcss">
/** 设置代码块样式 **/
.markdown pre {
@apply bg-[#282c34] p-4 mt-4 rounded-md text-white w-full overflow-x-auto;
}
.markdown code {
width: 100%;
}

/** 控制段落间的上下边距 **/
.markdown p {
margin: 1.25rem 0;
}
.markdown p:first-child {
margin-top: 0;
}

/** 小代码块样式,对应 markdown 的 `code` **/
.markdown :not(pre) > code {
@apply bg-[#282c34] px-1 py-[2px] text-[#e06c75] rounded-md;
}

/** 列表样式 **/
.markdown ol {
list-style-type: decimal;
padding-left: 40px;
}
.markdown ul {
list-style-type: disc;
padding-left: 40px;
}

/** 光标样式 **/
.markdown .cursor {
display: inline-block;
width: 2px;
height: 20px;
@apply bg-gray-800 dark:bg-gray-100;
animation: blink 1.2s step-end infinite;
margin-left: 2px;
vertical-align: sub;
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>

可以发现最基本的 markdown 显示还是挺简单的,话就不多说了,都在注释里。


我想你也许对判断消息中的代码块是否未闭合更感兴趣,那么就继续看下去吧!


代码块是否未闭合


markdown 有两种代码块,一种是 `code` ,另一种是 ``` code ```,我叫他小代码块和大代码块。


一开始我是想用正则去判断的,但是奈何有点复杂,我实在想不出应该如何去编写正则,让 chatgpt 写的正则也会判断失败,而且还要考虑到转义符,就算写出了正则,估计也会很复杂和难以维护。


经过短暂的苦思冥想后,我想到了之前在 《Vue.js设计与实现》 中看到的用有限元状态机解析 html 文本的方案。


这里有请 chatgpt 简单介绍一下有限元状态机:



有限元状态机(finite element state machine)是一种基于状态机的建模技术,它可以将系统的行为描述为一组状态和状态之间的转换。每个状态可以对应系统内的一种状态,而状态之间的转换则表示系统内部的转换过程。有限元状态机还可以用于系统的可靠性分析和测试,同时也可以作为软件工程中的一种建模工具,帮助代码实现、测试和调试等方面。


在实际应用中,有限元状态机通常是通过自动化工具生成,并且可以用于控制系统、网络通信协议、软件接口等方面的设计和测试。由于有限元状态机具有自动化生成和完备性检查等优点,因此它已经成为了一种广泛应用的建模与测试工具。



请站在web前端的视角下进行介绍



从前端开发的角度来看,有限元状态机可以用于构建复杂的交互式Web应用程序。在Web应用程序中,有限元状态机可以描述用户界面的不同状态(例如,登录、注册、未登录等),以及状态之间的转换(例如,从登录状态转换到注册状态)。


通常情况下,前端工程师使用JavaScript实现有限元状态机,包括定义状态、定义状态之间的转换规则、处理状态转换时的行为等。使用有限元状态机可以帮助前端工程师更好地掌控应用程序的状态,从而减少代码中的复杂条件分支和无法预测的行为。同时,有限元状态机也可以帮助前端团队共同理解应用程序的状态和转换规则,从而更好地协作开发和维护Web应用程序。


总之,有限元状态机是一种非常有用的前端开发技术,可以帮助前端工程师更好地构建和管理Web应用程序的状态和行为,提高应用程序的可靠性和用户体验。



回到正题,我可以一点一点的从头开始去解析 markdown 文本。想象这么一个简单的状态转换流程:



  • 初始状态为文本状态。

  • 遇到代码块标记,文本状态转换到代码块开始状态。

  • 再次遇到代码块标记,从代码块开始状态转换到文本状态。


不过现实要更复杂一点,我们有小代码块和大代码块。有限元状态机的妙处就在这里,当处在小代码块状态的时候,我们不需要操心大代码块和正常文本的事,他的下一个状态只能是遇到小代码块的闭合标签,进入文本状态。


理解了这些,再来看我的源码,才会发现他的精妙。


const States = {
text: 0, // 文本状态
codeStartSm: 1, // 小代码块状态
codeStartBig: 2, // 大代码块状态
}

/**
* 判断 markdown 文本中是否有未闭合的代码块
* @param text
* @returns {boolean}
*/

function isInCode(text) {
let state = States.text
let source = text
let inStart = true // 是否处于文本开始状态,即还没有消费过文本
while (source) { // 当文本被解析消费完后,就是个空字符串了,就能跳出循环
let char = source.charAt(0) // 取第 0 个字
switch (state) {
case States.text:
if (/^\n?```/.test(source)) {
// 以 ``` 或者 \n``` 开头。表示大代码块开始。
// 一般情况下,代码块前面都需要换行。但是如果是在文本的开头,就不需要换行。
if (inStart || source.startsWith('\n')) {
state = States.codeStartBig
}
source = source.replace(/^\n?```/, '')
} else if (char === '\\') {
// 遇到转义符,跳过下一个字符
source = source.slice(2)
} else if (char === '`') {
// 以 ` 开头。表示小代码块开始。
state = States.codeStartSm
source = source.slice(1)
} else {
// 其他情况,直接消费当前字符
source = source.slice(1)
}
inStart = false
break
case States.codeStartSm:
if (char === '`') {
// 遇到第二个 `,表示代码块结束
state = States.text
source = source.slice(1)
} else if (char === '\\') {
// 遇到转义符,跳过下一个字符
source = source.slice(2)
} else {
// 其他情况,直接消费当前字符
source = source.slice(1)
}
break
case States.codeStartBig:
if (/^\n```/.test(source)) {
// 遇到第二个 ```,表示代码块结束
state = States.text
source = source.replace(/^\n```/, '')
} else {
// 其他情况,直接消费当前字符
source = source.slice(1)
}
break
}
}
return state !== States.text
}

export default isInCode

到这里,就已经实现了一个 chatgpt 消息渲染了。喜欢的话点个赞吧!谢谢!


作者:七分小熊猫
来源:juejin.cn/post/7221792648541356093
收起阅读 »

裸辞一个月备考事业编所历所思

3.7日正式离职离现在也一个月了,这一个月里人类该有的情绪或大都尝了一遍。浅聊一下这一个月里干的事及一些感受。 原计划与现实 原本计划三月中旬提离职,四月 深圳-〉重庆-〉云南 半月游,五月开始刷题、下旬回到武汉。 计划永远赶不上变化。 2.23像往常一样坐在...
继续阅读 »

3.7日正式离职离现在也一个月了,这一个月里人类该有的情绪或大都尝了一遍。浅聊一下这一个月里干的事及一些感受。


原计划与现实


原本计划三月中旬提离职,四月 深圳-〉重庆-〉云南 半月游,五月开始刷题、下旬回到武汉。


计划永远赶不上变化。


2.23像往常一样坐在工位吃早餐。打开手机微信,看到一条订阅关于湖北事业单位招考的。于是,点进去看了看报考单位,那时就开始在想要不要试试。好嘛,我和一些朋友说了这个打算,好几个也一起加入备考大军。


第二天周五我调休了。算上周末三天,我开始了解事业单位考试及确定报考的岗位(实则,啥也不知道,随便选了一个我们小县城的岗位试试看)。


周一去到公司就提了离职。最后走的那天晚上请了几个同事吃了散伙饭,道了告别。


离职.png



我:拜拜,下辈子再见~ (人生中很多人再见就是再也不见了)



IMG_1089.png


备考


报名.PNG



  • 前期


离职后,当然是要给自己放松一下。于是,给自己放纵了两天。


接着开始备考。朋友分享了一套粉笔课程,每天也就看1-3个职测基础视频。


开始还是感觉蛮新鲜的。特别是一些逻辑、图形推理。哇,原来有这些套路。还记得以前面试一家公司让我做的题和这些差不多,当时觉得这都是些啥啊,和我做前端有关系吗。


其实期间除了看视频学习,其他时间基本都是在刷手机。那段时间B站推给我的全是大龄找不到工作、工作不好找等等让人致郁的视频。每天除了不专心学习就是无止尽的焦虑感。



  • 中期


后面把理论攻坚里的职测看完之后,更加放飞自我,每天也就打开粉笔app做几道题,然后就去手机里吸收消极情绪。很好笑,每天都在继续做前端、转行、摆摊、回家种地无限循环,当然包括现在偶尔也会是这样。


第一天:


等我考完这个破试回去武汉还是先找前端工作看看。


第二天:


刷了下B站,刷到说程序员找不到工作了,大龄了更加没人要了。那就考完试回来把后期视频好好学习下,回去找个剪辑师工作试试。


第三天:


打开Boss,搜索剪辑师;很多都是招流媒体,然后要求:会剪辑、会策划、会写脚本、会拍摄、会运营,还需要有工作经验。


又是那个无解的问题。


公司:我们需要有经验的。


我:我就是没有经验才找工作积累经验啊。


总结:转行不易。


第四天:


发现现在摆摊很火,去B站刷别人摆摊分享。了解了很多,常见的烤肠,还有之前没听过的热奶宝。这里可以推荐一个up主蜻园,还蛮不错的。


第五天:


算了,先考完试再说吧,实在不行就回家种地。刷B站,哎,这个剧还不错搜索全集cut,花几天看完。


这段时间差不多一个多星期,情绪就在这些上面循环往复,每天凌晨1、2点开始睡,但是得翻来覆去,差不多4、5点才能睡着,第二天起来已经中午了,再做个饭,差不多下午了,一天也差不多了。每天都是致郁的一天。



  • 后期


要考试了,得突击一下,把考前冲刺视频看了看,一天做几题。裸考综应和公共基础,其他随缘吧。


备考.png


参加考试



  • 去到酒店
    IMG_1258.png
    酒店备考.png


IMG_1271.png



  • 考试当天


考试的人真多,看起来很多都不是很大。


考试当天.png


考试当天听到一个女生对另外一个说:考试前几天整晚整晚都睡不着。(现在的人都不容易啊,很多大学生毕业即失业,都在卷考研、考编;白天准备考研,晚上准备考编。)


去到考场,安检然后需要手机关机,真是尴尬,第一次用苹果手机不会关机,当时迟迟关不了机,监考老师一度怀疑我有问题。


说:哎,你怎么回事,关机关这么久。


最后我问了旁边考生怎么关机,然后老师教了我,接着说:不会你早点说啊,一点都不谦虚。


我尴尬而不失礼貌的微笑。


快要考试了,我去上了个厕所,毕竟连续考三个半小时,时间也紧张,压根做不完,上厕所时间都得把握的好(喂,能不能在学习上多花点功夫哈)。


离谱,去厕所路上经过一个教室,外面三个女生都还在看书复习,我上完厕所出来,还在看,然后监考老师对她们说,快进来安检了,不要看了。


进考场看过来,可以看出很多女生都是那种很爱学习的人,就感觉我一个是来碰运气的。



  • 考试中


职测真做不完,之前和朋友说语文我完全不行,只能靠数学,结果数学计算相关压根没时间做。做题顺序不是按考卷顺序来的,开始也直接跳过常识题,直接言语开始啥的。


记得等我计算完资料分析的第一题后,我一看时间,我的妈,只剩下半小时了,数学运算直接放弃,当然不止数学运算,还有几十道都没有做,都是选择题,最后都是闭眼涂。


最无语的是我的综应都没有做完。作文十年没有写过了,我TM全抄的给的素材,离大谱;字写的也丑,唯一记得是以前老师对我说:一笔一画写清楚就行,不要连笔。感觉不连笔我字都不会写了,反正做的特慢,作文还大篇幅抄,最后来个总结,离大谱。



  • 考完试


考完试我就感觉自己是废物,感觉可以另谋生路了,这辈子估计是指望不上了。
考试结束.png


然后回酒店收东西,准备回家。退完房出来,下着瓢泼大雨,就像我的心情一样。


打了个的,准备去南站坐车,司机说好像只有北站有车去我那个地方;好嘛,又绕了老远了。在车上和司机聊了好多。


司机说也是回来考试的啊,然后说了我是从深圳回去考。


司机:这么远回来要是考不...没说完,算了,不说了,要是考的不好,还花了这么多钱。


我:笑了下,考不上就算了呗,还能怎么办。


然后说了很多,说她女儿也是搞计算机这行的,当时要她找个稳定的工作,不带编也行,她不干,现在在广州...叭叭叭,一起说了一路。


回家,晚上老妈回来一起聊了好多。很多时候我们焦虑,需要一个情绪缺口去宣泄。


返程


考完这次试,再加和老妈聊的许多,完全没有玩玩休息下的心情,第二天就准备返回深圳。


到深圳后就把自己后期AE中级课程开启了,开始学习(AE好难啊)。


后面准备一边上课学习一边背前端八股文,偶尔出去拍拍照,积累些剪辑素材,之后回湖北后剪视频纪念用。


最后


最后想说:想的多了都是问题,做的多了都是答案。当你一直在消极情绪里时,可以找一些喜欢的事情去做,从不好的情绪里脱离出来,不必想的过多、过远,毕竟65岁退休都“太早了”。


作者:前端Y酱
来源:juejin.cn/post/7221131892427014205
收起阅读 »

五分钟实现一个chatGPT打字效果

web
由于chatGPT最近大火,甲方爸爸觉得这样的打字效果很酷,必须要在项目中安排一下,所以动手实现了这个效果 打字状态分析 loading - 在等待打字内容的时候光标会一直显示且闪烁 tyeing - 在打字中光标会显示但不闪烁 end - 在打字结束后光标...
继续阅读 »

由于chatGPT最近大火,甲方爸爸觉得这样的打字效果很酷,必须要在项目中安排一下,所以动手实现了这个效果


打字状态分析



  1. loading - 在等待打字内容的时候光标会一直显示且闪烁

  2. tyeing - 在打字中光标会显示但不闪烁

  3. end - 在打字结束后光标隐藏


样式


// 光标字符显示
.typing::after {
content: '▌';
}
// 光标闪烁动画
.blinker::after {
animation: blinker 1s step-end infinite;
}
@keyframes blinker {
0% {
visibility: visible;
}
50% {
visibility: hidden;
}
100% {
visibility: visible;
}
}

内容打印功能实现


结合定时器和光标样式设置


**
* @description:
* @param {HTMLElement} dom - 打印内容的dom
* @param {string} content - 打印文本内容
* @param {number} speed - 打印速度
* @return {void}
*/
function printText(dom, content, speed = 50) {
let index = 0
setCursorStatus(dom, 'typing')
let printInterval = setInterval(() => {
dom.innerText += content[index]
index++
if (index >= content.length) {
setCursorStatus(dom, 'end')
clearInterval(printInterval)
}
}, speed)
}

/**
* @description: 设置dom的光标状态
* @param {HTMLElement} dom - 打印内容的dom
* @param {"loading"|"typing"|"end"} status - 打印状态
* @return {void}
*/

function setCursorStatus(dom, status) {
const classList = {
loading: 'typing blinker',
typing: 'typing',
end: '',
}
dom.className = classList[status]
}

效果预览


作者:chansee97
来源:juejin.cn/post/7221368910139113531
an>

收起阅读 »

就在昨天,我也关闭了朋友圈 ... ...

关于“关闭朋友圈”这个话题的文章,若干年前无意中就看到过,当我给我自己的第一个感觉是——不痛不痒。 为什么要关闭呢?每天在班车上闲来无事,看看周围的朋友们都有什么有趣的事情发生,多好啊。不懂为什么要去可以的关闭它,真的不懂。 随着年纪慢慢变大(很逃避“变老”这...
继续阅读 »

关于“关闭朋友圈”这个话题的文章,若干年前无意中就看到过,当我给我自己的第一个感觉是——不痛不痒


为什么要关闭呢?每天在班车上闲来无事,看看周围的朋友们都有什么有趣的事情发生,多好啊。不懂为什么要去可以的关闭它,真的不懂。


随着年纪慢慢变大(很逃避“变老”这个词),自己也越来越逃避社交,每到闲暇的时候,总是喜欢自己一个人听一听音乐,然后做几道LeetCode算法题,写一写算法题图解(虽然没多少人看)。但是很舒服,很自在。


再也不像十多年前,一到周末,三五好友,推杯换盏,牛皮吹得连马云都会觉得自己啥也不是。足迹也遍布了王府井、天安门、故宫、鸟巢、水立方、恭王府……


我有时在想,为什么自己越来越脱离了社会的群体了呢?反而更沉浸于自己的精神世界中,甚至一度怀疑自己患上了深度抑郁症


直到我近期读完了李笑来写的**《财富自由之路》**,被无数次的触动和震惊,人和人在认知的差距真的可以相差了一个南极到北极的距离。回首过往,自己干过的太多傻憨憨的事情,也不由自主的汗颜得低下了头


当时还是在2016年的时候吧,自己买了一辆很喜欢的车子,兴奋、激动、恨不得一时间给所有亲戚朋友打电话,告诉他们这车有多么的好,配置有多么的丰富。好像再迟了一秒钟,车子就会融化消失一样。从订车、到提车、再到洗车、再到开车去公司的路上,无数的朋友圈都在拼命的告诉周围的人,“我买车了!”嗨,现在一想。蛮尴尬的。


就像知乎有一篇帖子写到,“**朋友圈总是陷入到“羡慕别人”和“处心积虑让别人羡慕”的荒谬境地,发票圈和看票圈变得越来越无趣了”,**这句话说的多真实。


再看抖音也是一样,人均劳斯莱斯,人均2,3000万的豪宅,满地的“成功学小丑”——“我职高毕业,但是!毕业第一年,我开了麻辣烫店,净利润500万,第二年我就开劳斯莱斯库,手下团队100多人……


现在的网络充斥着太多的一夜暴复,沉浸在这种氛围下的我们也越来越浮躁了。就像我很喜欢的一个主播叫“在石250”,他在直播的时候就说“我是80后,当时网络也没那么发达,当我毕业工作的时候,看到路上开过一辆宝马3系,我都觉得牛逼得很。而现在呢,闹市街头好多小年轻用手机拍车子,你开过去一辆宝马5系,他都会摇一摇头,说上一句“这车一般,凑活事儿吧,比奔驰E300差远了!”,然后每个月领着2000块钱的工资,去网吧啃泡面。


我们被充斥了这么多网络垃圾之后,自己会受影响吗?绝对会的,而且会被毒害很深。我们发现,自己做的事情也来越沉不住气了,自己越来越觉得不月入10万都赶不上国民平均收入了,自己不买辆保时捷都没法出去跟朋友聚会了,自己家房子小于150平米那基本“狗都摇头了”。


我们该停一停了。我们需要信息,但并非这种信息。让我们自己安静下来,沉下心,静静的冲一杯绿茶,坐在窗边吹着微风,读一本我们很早就想读,但是被刷抖音和看朋友圈替代的书。坚持下去,你会发现,原来世界如此美好。自己的精神世界那么的安宁,外面世界的浮躁气息突然的这么让你嗤之以鼻。


说了很多,是的。就在昨天,我也关闭了朋友圈,去开始迎接一个全新的世界。在那个世界里,只有安宁、祥和、暖风和青云~


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

AutoGPT太火了,无需人类插手自主完成任务,GitHub2.7万星

OpenAI 的 Andrej Karpathy 都大力宣传,认为 AutoGPT 是 prompt 工程的下一个前沿。 近日,AI 界貌似出现了一种新的趋势:自主人工智能。 这不是空穴来风,最近一个名为 AutoGPT 的研究开始走进大众视野。特斯拉前 AI...
继续阅读 »

OpenAI 的 Andrej Karpathy 都大力宣传,认为 AutoGPT 是 prompt 工程的下一个前沿。


近日,AI 界貌似出现了一种新的趋势:自主人工智能

这不是空穴来风,最近一个名为 AutoGPT 的研究开始走进大众视野。特斯拉前 AI 总监、刚刚回归 OpenAI 的 Andrej Karpathy 也为其大力宣传,并在推特赞扬:「AutoGPT 是 prompt 工程的下一个前沿。」



不仅如此,还有人声称 ChatGPT 已经过时了,AutoGPT 才是这个领域的新成员。



项目一经上线,短短几天狂揽 27K + 星,这也侧面验证了项目的火爆。



GitHub 地址:github.com/torantulino…

问题来了,AutoGPT 到底是什么?它是一个实验性的开源应用程序,展示了 GPT-4 语言模型的功能。该程序由 GPT-4 驱动,可以自主实现用户设定的任何目标。



具体来说,AutoGPT 相当于给基于 GPT 的模型一个内存和一个身体。有了它,你可以把一项任务交给 AI 智能体,让它自主地提出一个计划,然后执行计划。此外其还具有互联网访问、长期和短期内存管理、用于文本生成的 GPT-4 实例以及使用 GPT-3.5 进行文件存储和生成摘要等功能。AutoGPT 用处很多,可用来分析市场并提出交易策略、提供客户服务、进行营销等其他需要持续更新的任务。

正如网友所说 AutoGPT 正在互联网上掀起一场风暴,它无处不在。很快,已经有网友上手实验了,该用户让 AutoGPT 建立一个网站,不到 3 分钟 AutoGPT 就成功了。 期间 AutoGPT 使用了 React 和 Tailwind CSS,全凭自己,人类没有插手。看来程序员之后真就不再需要编码了。



之后该用户补充说,自己的目标很简单,就是用 React 创建一个网站。提出的要求是:创建一个表单,添加标题「Made with autogpt」,然后将背景更改为蓝色。AutoGPT 成功的构建了网站。该用户还表示,如果给 AutoGPT 的 prompt 更多,表现会更好。

图源:twitter.com/SullyOmarr/…

接下里我们再看一个例子。假装自己经营一家鞋公司,给 AutoGPT 下达的命令是对防水鞋进行市场调查,然后让其给出 top5 公司,并报告竞争对手的优缺点 :



首先,AutoGPT 直接去谷歌搜索,然后找防水鞋综合评估 top 5 的公司。一旦找到相关链接,AutoGPT 就会为自己提出一些问题,例如「每双鞋的优缺点是什么、每款排名前 5 的防水鞋的优缺点是什么、男士排名前 5 的防水鞋」等。

之后,AutoGPT 继续分析其他各类网站,并结合谷歌搜索,更新查询,直到对结果满意为止。期间,AutoGPT 能够判断哪些评论可能偏向于伪造,因此它必须验证评论者。



执行过程中,AutoGPT 甚至衍生出自己的子智能体来执行分析网站的任务,找出解决问题的方法,所有工作完全靠自己。

结果是,AutoGPT 给出了 top 5 防水鞋公司的一份非常详细的报告,报告包含各个公司的优缺点,此外还给出了一个简明扼要的结论。全程只用了 8 分钟,费用为 10 美分。期间也完全没有优化。



这个能够独立自主完成任务的 AutoGPT 是如何运行的呢?我们接着来看。

AutoGPT:30 分钟内构建你自己的 AI 助手

作为风靡互联网的 AI 智能体,AutoGPT 可以在 30 分钟内完成设置。 你就可以拥有自己的 AI,协助完成任务,提升工作效率。

这一强大的 AI 工具能够自主执行各种任务,设置和启动的简便性是一大特征。在开始之前,你需要设置 Git、安装 Python、下载 Docker 桌面、获得一个 OpenAI API 密钥。

克隆存储库

首先从 GitHub 中克隆 AutoGPT 存储库。



使用以下命令导航到新建文件夹 Auto-GPT。



配置环境

在 Auto-GPT 文件夹中,找到.env.template 文件并插入 OpenAI API 密钥。接着复制该文件并重命名为.env。



安装 Python 包

运行以下命令,安装需要的 Python 包。



运行 Docker

运行 Docker 桌面,不需要下载任何容器,只需保证程序处于激活状态。



运行 AutoGPT



执行以下命令,运行 AutoGPT。



设置目标**

AutoGPT 虽是一个强大的工具,但并不完美。为避免出现问题,最好从简单的目标开始,对输出进行测试,并根据自身需要调整目标,如上文中的 ResearchGPT。

不过,你如果想要释放 AutoGPT 的全部潜力,需要 GPT-4 API 访问权限。GPT-3.5 可能无法为智能体或响应提供所需的深度。

AgentGPT:浏览器中直接部署自主 AI 智能体

近日,又有开发者对 AutoGPT 展开了新的探索尝试,创建了一个
可以在浏览器中组装、配置和部署自主 AI 智能体的项目 ——AgentGPT。** 项目主要贡献者之一为亚马逊软件工程师 Asim Shrestha,已在 GitHub 上获得了 2.2k 的 Stars。



AgentGPT 允许你为自定义 AI 命名,让它执行任何想要达成的目标。自定义 AI 会思考要完成的任务、执行任务并从结果中学习,试图达成目标。如下为 demo 示例:HustleGPT,设置目标为创立一个只有 100 美元资金的初创公司。



再比如 PaperclipGPT,设置目标为制造尽可能多的回形针。



不过,用户在使用该工具时,同样需要输入自己的 OpenAI API 密钥。AgentGPT 目前处于 beta 阶段,并正致力于长期记忆、网页浏览、网站与用户之间的交互。

GPT 的想象力空间还有多大,我们继续拭目以待。

参考链接: medium.com/@tsaveratto…


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

GPT-4自我修复!国外小哥开发神级「金刚狼」,无限自我Debug

【新智元导读】继 GPT-4 超强进化后,现在还能自我修复。国外网友开发一个「金刚狼」项目,能够自动修复 Python 中的 bug,并运行代码。 要问程序员,一天中最烦的时候是什么? 那一定是给写好的程序 Debug 了。而现在,这种局面要得到改善了! 国...
继续阅读 »

【新智元导读】继 GPT-4 超强进化后,现在还能自我修复。国外网友开发一个「金刚狼」项目,能够自动修复 Python 中的 bug,并运行代码。


要问程序员,一天中最烦的时候是什么?


那一定是给写好的程序 Debug 了。而现在,这种局面要得到改善了!



国外一名叫 BioBootloader 的开发者基于 GPT-4 搞了一个叫「金刚狼」的项目,能够自我修复 Python 脚本。


从名字就能看出来,这项目主打一个「自我愈合」。通过 GPT 识别代码中的错误,并提供修改,直至程序顺利运行。


不过,「金刚狼」目前只能用在 Python 上。


这项目已经在 GitHub 上收揽了 1.2k 星,108 个 Fork。



金刚狼?金刚狼!


BioBootloader 表示,用「金刚狼」运行你的程序,只要一崩溃,GPT-4 就会自动编辑,然后给出出错的原因。


哪怕码农写了一大堆 Bug,也没事。「金刚狼」会反复运行,直到一切 Bug 都被 De 掉。



GPT-4 想必大家都不陌生。这是由 OpenAI 开发的多模态人工智能语言模型。


BioBootloader 在推特上的演示视频中,展示了「金刚狼」的具体使用方式。



视频中,开发者先写了个简单的四则运算代码,然后故意把其中一些部分写错。



(正确的)


比方说,把结尾的 return result 随便改成 return res,而 res 没有定义,于是就出错了。


小哥还把减法部分的代码删掉了,就是上方的 substract_numbers。这样一来,下面 calculate 那里就一定会报错。因为 subtract 没有定义了。



(错误的)


之后直接运行「金刚狼」即可,GPT 生成的部分会出现在右侧。



可以看到,「金刚狼」快速识别出了错误,并且附上了解释。


「脚本中没有定义 subtract_numbers.


res 这个变量没有定义,应该用 result 代替。」



不光给了建议,「金刚狼」还直接把改好的代码附上了。红色是应该删掉的部分,绿色是添加的部分。


实际上,「金刚狼」是一个封装器,它负责运行程序,捕捉错误信息,再把这些错误发送给 GPT-4,询问 GPT 代码出了什么问题。


像 GPT-4 这种 LLM(即大型语言模型),是用自然语言「编程」的,而这些指令被视为 prompts。


「金刚狼」所实现的功能很大一部分要归功于精心编写的提示,阅读这些提示就可以更好的理解整个过程。


目前该项目已经发布在了 GitHub 上。小哥也是贴心的给出了设置上的要求。



不止是 Python


在 GitHub 上,BioBootloader 发布了自己未来的计划,「金刚狼」的功能会越来越全面、强大。



「目前的版本只是我花了几个小时搞得一个原型产品。未来还会有很多可能的延展,同时欢迎大家一起来开发。」




  • 添加标志来定制使用方法,例如在运行改变的代码前要求用户确认。




  • 对 GPT 的编辑格式进行进一步的迭代。目前,GPT 在缩进方面有点困难,但我确信这一点可以得到改善。




  • 一套有问题的文件的例子,我们可以在上方的测试进行提示,以确保其可靠性,并衡量改进的如何。




  • 多个文件 / 代码库——向 GPT 发送堆栈跟踪中出现的所有文件




  • 对大文件更好地处理,即我们是否应该只向 GPT 发送相关的类 / 函数?




  • 扩展到 Python 以外的编程语言




从上面那个简单的例子可以看出来,这个脚本还是未来可期的。


毕竟,总不能让用别的语言工作的码农们看着智能 Debug Python 的「金刚狼」眼红呀。


参考资料:


twitter.com/bio_bootloa…


hackaday.com/2023/04/09/…


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

推荐几个可以免费使用的ChatGPT工具

在ChatGPT相关API推出之后,各种工具如雨后春笋一般层出不穷,这篇文章就列举一些日常使用到的工具。 工具列表 myreader.io myReader主页 这款工具的作者是@madawei2699,github主页地址为t.co/adJBYWbjkF,...
继续阅读 »

在ChatGPT相关API推出之后,各种工具如雨后春笋一般层出不穷,这篇文章就列举一些日常使用到的工具。


工具列表


myreader.io


myReader主页


myReader主页


这款工具的作者是@madawei2699,github主页地址为t.co/adJBYWbjkF,…



  • 在线读取任意网页内容包括视频(YouTube),并根据这些内容回答你提出的相关问题或总结相关内容

  • 支持读取电子书与文档(支持PDF、EPUB、DOCX、Markdown、TXT),并根据这些内容回答你提出的相关问题或总结相关内容

  • 定时发送每日热榜新闻,无论新闻是中文还是其他语言,它都能使用chatGPT用中文自动总结新闻的内容,方便快速获取热点新闻信息

  • 支持 prompt 模版,能根据消息历史记录的上下文回答你的问题,甚至能和你玩游戏

  • 支持多国语音交互(英文、中文、德语与日语),它会根据你的语言使用相关语言的声音来回答你的问题,从而帮助你训练外语能力,可以理解为它是你的私人外教


具体功能演示可以参考我的AI阅读助手


chatpdf


ChatPDF主页


ChatPDF主页


这个可以看作是一个PDF辅助阅读的工具,用户上传自己的PDF之后,可以以对话的方式与工具进行交互,快速获取PDF文件的内容。


ChatPaper


专注于“科研狗”的工具,通过ChatGPT实现对论文进行总结,帮助“科研人”进行论文初筛(目前不支持针对论文内容进行对话)。


ChatPaper主页


ChatPaper主页


另外相关的工具还有润色工具、审稿工具、审稿回复工具


最后,这篇文章——ChatGPT应用开发小记中提到的基于chatGPT的应用类型的分类也有借鉴意义。


原理


之前准备写一篇专门介绍上述工具类的原理介绍(其实ChatGPT的 插件——chatgpt-retrieval-plugin),但是后来查看了几个项目的源码之后发现,这类工具的主要原理其实比较直观:



  • 解析相关输入为文本

  • 将文本分句后获取句子的embedding(这一步目前处理的处理方式大都是根据长度截断)并存储至数据库

  • 用户输入转换为embedding,并在数据库中召回相关性最高的句子集合

  • 将召回的句子与用户输入句子组装为ChaptGPT的输入,获取输出


上述思路虽然直观,但要获取更好的结果,其实除了第三步,其余每一步都有优化的空间:



  • 文本解析可以针对不同类型的数据针对性解析

  • 文本分句方式可以采取特殊标点进行分句,同时句子embedding也有很多可选生成方法

  • 召回的句子与用户输入句子组装为ChaptGPT的输入,结合任务特定的prompt,获取更适合任务的输出


具体流程图可以参考gpt-langchain-pdf:


gpt-langchain-pdf


gpt-langchain-pdf


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

面试整理-kotlin与jetpack

面试可能会问到的问题 内联函数与高阶函数 对委托的理解 扩展方法以及其原理 协变与逆变 协程相关知识(创建方式、原理) jetpack使用过哪些库 LiveData和LifeCycle的原理 Viewmodel的原理 WorkManager的使用场景 Nav...
继续阅读 »

面试可能会问到的问题



  1. 内联函数与高阶函数

  2. 对委托的理解

  3. 扩展方法以及其原理

  4. 协变与逆变

  5. 协程相关知识(创建方式、原理)

  6. jetpack使用过哪些库

  7. LiveData和LifeCycle的原理

  8. Viewmodel的原理

  9. WorkManager的使用场景

  10. Navigation使用过程中有哪些坑


内联函数和高阶函数



关键不是问你什么概念,而是看你在实际使用中有没有注意这些细节



概念



  • 内联函数:编译时把调用代码插入到函数中,避免方法调用的开销。

  • 高阶函数:接受一个或多个函数类型的参数,并/或返回一个函数类型的值


概念就这两句话,实际使用的时候却有很大的用途。比如我们常用的apply、run、let这些其实就是一个内联高阶函数。


// apply 
public inline fun <T> T.apply(block: T.() -> Unit): T { block() return this }
// run
public inline fun <T, R> T.run(block: T.() -> R): R { return block() }
// let
public inline fun <T, R> T.let(block: (T) -> R): R { return block(this) }

使用心得



  1. 有时候为了代码整洁,我们不会让一个方法超过一屏幕,会把里面的方法抽成几个小的方法,但是方法会涉及到入栈出栈,而内联函数就可以保证代码的整洁又避免了方法进栈出栈的开销。这个是我们稍微注意一下很方便做的优化。

  2. 为了简化函数的调用我们可以使用高阶函数,除了系统提供的apply、run、let这些外,自己其实平时也会写一些高阶函数,比如下面的例子





    • 使用高阶函数增加代码可读性




// 使用高阶函数简化网络请求处理
fun <T> Call<T>.enqueue(
onSuccess: (response: Response<T>) -> Unit,
onError: (error: Throwable) -> Unit,
onCancel: () -> Unit
) {
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
onSuccess(response)
} else {
onError(Exception("Request failed with code ${response.code()}"))
}
}

override fun onFailure(call: Call<T>, t: Throwable) {
if (!call.isCanceled) {
onError(t)
} else {
onCancel()
}
}
})
}

---
// 使用的时候
call.enqueue(
onSuccess = { response ->
// 在这里处理网络请求成功的逻辑

},
onError = { error ->
// 在这里处理网络请求失败的逻辑
},
onCancel = {
// 在这里处理网络请求取消的逻辑
}
)





    • 使用高阶函数减少无用回调,方便使用




// 使用高阶函数简化回调函数
fun EditText.doOnTextChanged(action: (text: CharSequence?, start: Int, before: Int, count: Int) -> Unit) {
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
action(s, start, before, count)
}

override fun afterTextChanged(s: Editable?) {
}
})
}

// 使用的时候只需要关系一个回调就可以
editText.doOnTextChanged { text, _, _, _ ->
// 在这里处理输入框文本变化的逻辑
}


这两个例子很好的说明了高阶函数的作用,可以简化一些操作,也可以增强可读性。其实还有一些其他的作用比如用高阶函数实现RecycleView初始化时的函数式编程、对一些方法添加缓存等等。
只要涉及到对原有方法的增强或者简化或者添加多一层封装实现链式调用都可以考虑使用高阶函数。


对委托的理解



因为委托在开发中真的非常好用,问这个问题就想看看你有没有真的理解委托



首先委托的概念就是把一个对象的职责委托给另外一个对象,在kotlin中有属性的委托和类的委托。属性的委托比如by lazy,他的作用是使用到的时候才加载简化了判空代码也节省了性能。类的委托通常是一个接口委托一个对象interface by Class。目的是对一个类的解耦方便以后相同功能的代码复用。例子就不举例了,就是但凡开发中想到有些代码是可以复用的时候可以考虑能不能写成一个接口去交给委托类去实现。


问到by lazy可能还会问你与lateinit的区别。



  • lateinit:延时加载,只是告诉编译器不用检查这个变量的初始化,不能使用val修饰

  • by lazy:懒加载,lazy是一个内联高阶函数,通过传入自身来做一些初始化的判断。


扩展方法以及其原理



扩展函数也是使用kotlin时非常好用的一个特性,多多少少可能也会提一嘴。



实际开发中我们的点击事件、资源获取等都可以使用。好处就不多说了,比如加入防抖,或者获取资源时的捕获异常,都可以减少日后添加需求时的开发量


private var lastClickTime = 0L
fun View.setSingleClickListener(delay: Long = 500, onClick: () -> Unit) {
setOnClickListener {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime > delay) {
onClick()
lastClickTime = currentTime
}
}
}


  • 原理
    Kotlin 中的扩展方法其实是一种静态的语法糖,本质上是一个静态函数,不是实例函数。编译器会将扩展方法转化为静态函数的调用。
    比如



fun String.lastChar(): Char = this.get(this.length - 1)
---
val s = "hello"
val c = s.lastChar() // 转化为 StringKt.lastChar(s)

协变与逆变(out 和 in)



这个问的可能比较少,这个问题其实主要还是看你有没有写过一些大型架构,尤其是像rxjava这种设计到入参出参的。




  • 协变与逆变是数学中的概率,协变就是x跟y正相关图形是往上的,逆变就是x跟y负相关图形是往下的。

  • 协变往上的肯定有个最大的上限,java中的上限就是obj,所以你会看到很多这样的代码out Any或者?extentd Object

  • 逆变往下的肯定有个最小值,所以你会看到很多这样的代码out T或者? super T


这里面还会涉及到一个set和get的问题,协变只能get不能set。比如逆变只能set不能get。这个结论你可以记起来,也可以理解一下,这个是面向对象的基础。举个例子说明


爷爷辈(会玩手机)、爸爸辈(会玩手机会上网)、孙子辈(会玩手机会上网会打游戏)。 比如指定的上限(out、extends)是爷爷辈,如果只是作为返回值,直接返回T就可以,因为不管你返回什么类型,最后都可以用爷爷辈来接。而如果用于set,你可以传个爸爸辈或者孙子辈的进来,里面并不知道你确切的类型就出问题了。


反过来,如果逆变(in、super)指定的下限是孙子辈,用于set就可以,因为孙子已经包含了爷爷、爸爸辈的内容了。而返回就不行,因为你外面返回如果用t接,你不知道是孙子辈还是老一辈。如果返回的是老一辈你外面调用用的是孙子辈打游戏就崩了。


协程相关知识



  • 协程的基本概念:协程是一个轻量级线程。可以用同步的方式编写异步代码,避免了异步代码传参时所引发的回调地狱。核心概念是挂起跟恢复。即协程可以在执行过程中主动挂起,等待某些事件发生后再恢复执行。挂起可以开发者控制比如调用await或者直接用suspend修饰。恢复是编译器的活。我们只管用就好了。

  • 其他的概念其实跟线程差不多



      • 和协程构建器:launchasync创建一个协程





      • 调度器是切换线程的:Dispatchers.IO、Dispatchers.Main





      • 协程作用域:通常由 coroutineScope 或 supervisorScope 函数创建,协程作用域可以用于确保协程在退出时所有资源都被正确释放。





      • 异常处理和取消:异常处理可以使用try-cache也可以使用CoroutineExceptionHandler指定一个协程异常处理的函数。





  • Flow



使用协程肯定会使用的一个机制,可以代替rxJava做一些简单的操作。



使用例子


import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
val flow = flow {
for (i in 1..10) {
delay(100)
emit(i)
}
}

flow
.buffer() // 缓冲区大小
.onEach {
println("Emitting $it")
delay(200)
}
.collectLatest {
println("Collecting $it")
delay(300)
}
}




    • 原理




Flow 是一种基于懒加载的异步数据流,它可以异步产生多个元素,同时也可以异步消费这些元素。Flow 的每个元素都是通过 emit 函数产生的,而这些元素会被包装成一个包含了多个元素的数据流。Flow 还支持各种各样的操作符,如 map、filter、reduce 等等,可以方便地对数据流进行处理和转换。


jetpack使用过哪些库



下面的不一定都用过,说几个自己用过的就好,但是既然用了就要对原理很熟悉,不然别人一问就倒




  1. ViewModel:用于在屏幕旋转或其他配置更改时管理UI数据的生命周期。

  2. LiveData:用于将数据从ViewModel传递到UI组件的观察者模式库。

  3. Room:用于在SQLite数据库上进行类型安全的ORM操作的库。

  4. Navigation:用于管理应用程序导航的库。

  5. WorkManager:用于管理后台任务和作业的库。

  6. Paging:用于处理分页数据的库。

  7. Data Binding:用于将布局文件中的视图绑定到应用程序数据源的库。

  8. Hilt:用于实现依赖注入的库。

  9. Security:提供加密和数据存储的安全功能。

  10. Benchmark:用于测试应用程序性能的库。


LiveData和LifeCycle的原理


LiveData


使用上非常简单,就是上下游的通知,就是一个简化版的rxjava




  1. LiveData持有一个观察者列表,可以添加和删除观察者。

  2. 当LiveData数据发生变化时,会通知观察者列表中的所有观察者。

  3. LiveData可以感知Activity和Fragment的生命周期,当它们处于激活状态时才会通知观察者,避免了内存泄漏和空指针异常。

  4. LiveData还支持线程切换,可以在后台线程更新数据,然后在主线程中通知观察者更新UI。


LiveData提供了setValuepostValue两个方法来设置数据通知



  • setValue:方法只能在主线程调用,不依赖Handler机制来回调,

  • postValue:可以在任何线程调,同步到主线程依赖于Handler,需要等待主线程空闲时才会执行更新操作。


LifeCycle


用于监听生命周期,包含三个角色。LifecycleOwner、LifecycleObserver和Lifecycle




  • LifecycleObserver是Lifecycle的观察者。viewmodel默认就实现了这个接口

  • LifecycleOwner是具有生命周期的组件,如Activity、Fragment等,它持有一个Lifecycle对象

  • Lifecycle是LifecycleOwner的生命周期管理器,它定义了生命周期状态和转换关系,并负责通知LifecycleObserver状态变化的事件


了解这三个角色其实就很容易理解了,本质上LifeCycle也是一个观察者模式,管理数据的是LifeCycle,生命周期的状态都是通过它来完成的。而我们写代码的时候要写的一句是getLifecycle().addObserver(xxLifeCycleObserver());是添加一个观察者,这个观察者就能收到相应的通知了。


Viewmodel的原理



这个问题有可能会问你Viewmodel跟Activity哪个先销毁、Viewmodel跟Activity是怎么进行生命周期的绑定的。



Viewmodel的两个重要类:ViewModelProviderViewmodelStore。其实就是我们使用时用到的


// 这里this接收的其实是一个`ViewModelStoreOwner`是一个接口,我们的AppCompatActivity已经实现了
aViewModel = ViewModelProvider(this).get(AViewModel::class.java)


  • ViewModelStore 是一个存储 ViewModel 的容器,用于存储与某个特定的生命周期相关联的 ViewModel



是一个全局的容器,实际上就是一个HashMap。




  • ViewModelProvider用于管理ViewModel实例的创建和获取


其实这里设计的理念也比较好理解,比如旋转屏幕这个场景,我们会使用Viewmodel来保存数据,因为他数据不会被销毁,之所以不被销毁不用想也只是肯定是脱离Activity或者Fragment保存的。



知道了Viewmodel会全局保存这一点,应该会有一些疑问,就是这个Viewmodel是什么时候回收的。



在Activity或者Fragment销毁其实只是移除了他的引用,当内存不足时gc会回收或者手动调用clear方法回收。所以回答Activity和Viewmodel谁的生命周期比较长时就知道了,只要不是手动清除肯定是ViewModel的生命周期比Activity长。


因为ViewModel一直存在,所以如果太多需要做一些优化,原则很简单,就是把ViewModel细分,有些没必要保存的手动清除,有些需要全局的就使用单例。


WorkManager的使用场景



其实就是一个定时任务,人家问你使用场景是看你有没有真正用过。




  1. 需要在特定时间间隔内执行后台任务,例如每天的定时任务或周期性的数据同步。

  2. 执行大型操作,例如上传或下载文件,这些操作需要时间较长,需要在后台执行。

  3. 应用退出时需要保存数据,以便在下一次启动时可以使用。

  4. 执行重复性的任务,例如日志记录或数据清理。


Navigation使用过程中有哪些坑



这个问题首先要明确Navigation是干嘛的才知道有什么坑





  • Navigation翻译过来是导航,其实就是一个管理Fragment的栈类似与我们使用Activity一样,样提供的方法也是一样的比如动画、跳转模式,并且它还可以让我们不用担心Fragment是否被回收直接调用它的跳转,没有的话会帮我们做视图的恢复数据它已经内部处理好了,还支持一些跳转的动画传参等都有相应的api。简而言之,Navigation能做的FragmentManager都能做,只是相对麻烦而已。




  • Navigation优势就不多说了,合适的场景就是线性的跳转,比如A跳B跳C跳D这种,直接一行代码就可以跳转。返回到指定的页面也有方法,比如从D返回到navController.popBackStack(R.id.fragmentA, false)。这里的ture和false要注意,具体的细节就去看官网了。




  • 不太适合的场景就是相互的调用,比如A跳B跳A跳B这种反复的,需要你设置好跳转模式,如果模式不对会出现反复的创建和销毁,这里使用SingleTop跳转模式可以解决。但是要处理的可能是你是什么地方跳过来是,返回方法要处理一下。


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

Android 开发中需要了解的 Gradle 知识

Gradle 是一个基于 Groovy 的构建工具,用于构建 Android 应用程序。在 Android 开发中,了解 Gradle 是非常重要的,因为它是 Android Studio 默认的构建工具,可以帮助我们管理依赖项、构建应用程序、运行测试等。 本...
继续阅读 »

Gradle 是一个基于 Groovy 的构建工具,用于构建 Android 应用程序。在 Android 开发中,了解 Gradle 是非常重要的,因为它是 Android Studio 默认的构建工具,可以帮助我们管理依赖项、构建应用程序、运行测试等。


本文将介绍 Android 开发中需要了解的一些 Gradle 知识,包括 Gradle 的基本概念、Gradle 的构建脚本、Gradle 的任务和插件等。


Gradle 的基本概念


Gradle 是一个基于项目的构建工具,它允许我们通过编写构建脚本来定义构建过程。Gradle 的基本概念包括:



  • 项目(Project):Gradle 中的项目是指构建的基本单元,一个项目包含多个模块。

  • 模块(Module):Gradle 中的模块是指项目中的一个组件,可以是一个库模块或应用程序模块。

  • 任务(Task):Gradle 中的任务是指执行构建过程的基本单元,每个任务都有一个名称和一个动作(Action)。

  • 依赖项(Dependency):Gradle 中的依赖项是指项目中的一个模块或库,用于在构建过程中引用其他代码或资源。


Gradle 的构建脚本


Gradle 的构建脚本是基于 Groovy 语言的脚本文件,文件名为 build.gradle,位于项目的根目录和每个模块的目录中。构建脚本可以定义项目的依赖项、构建任务和发布应用程序等。


Gradle 的构建脚本由以下两个部分组成:




  1. buildscript 块:用于定义 Gradle 自身的依赖项和配置。




  2. 模块配置块:用于定义模块的依赖项和任务。




下面是一个示例构建脚本:


// 定义构建脚本使用的 Gradle 版本
buildscript {
repositories {
// 定义依赖项所在的仓库
google()
mavenCentral()
}
dependencies {
// 定义 Gradle 自身的依赖项
classpath 'com.android.tools.build:gradle:7.1.3'
}
}

// 定义模块的依赖项和任务
apply plugin: 'com.android.application'

android {
compileSdkVersion 31

defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
// 定义模块的依赖项
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
}

Gradle 的任务


Gradle 的任务是构建过程的基本单元,每个任务都有一个名称和一个动作。Gradle 内置了很多任务,例如编译代码、运行测试、打包应用程序等。我们也可以根据需要自定义任务。


Gradle 的任务由以下三个部分组成:




  1. 任务名称:任务的唯一标识符,通常由一个或多个单词组成,例如 build、assembleDebug 等。




  2. 任务依赖项:任务依赖于其他任务,可以使用 dependsOn() 方法指定任务依赖项,例如:




task myTask {
dependsOn otherTask
doLast {
println 'myTask executed'
}
}

上面的示例中,myTask 任务依赖于 otherTask 任务,即在执行 myTask 之前需要先执行 otherTask。




  1. 任务动作:任务要执行的操作,可以使用 doFirst() 和 doLast() 方法指定任务动作,例如:




task myTask {
doFirst {
println 'myTask starting'
}
doLast {
println 'myTask executed'
}
}

上面的示例中,myTask 任务在执行前会先打印一条消息,然后执行任务动作,执行完毕后再打印一条消息。


Gradle 的插件


Gradle 的插件是用于扩展 Gradle 功能的组件,每个插件都提供一组任务和依赖项,用于构建应用程序或库模块。Gradle 中有很多插件,例如 Android 应用程序插件、Java 库插件等。我们也可以根据需要自定义插件。


Gradle 的插件由以下两个部分组成:



  1. 插件声明:用于声明插件及其依赖项,例如:


plugins {
id 'com.android.application' version '7.1.3'
}

上面的示例中,声明了 Android 应用程序插件及其依赖项。



  1. 插件配置:用于配置插件的行为和属性,例如:


android {
compileSdkVersion 31
defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

上面的示例中,配置了 Android 应用程序插件的属性,例如编译版本、应用程序 ID、最小 SDK 版本等。


总结


本文介绍了 Android 开发中需要了解的一些 Gradle 知识,包括 Gradle 的基本概念、构建脚本、任务和插件等。


Gradle 是一个功能强大的构建工具,通过掌握 Gradle 的基本概念、构建脚本、任务和插件等知识,可以更好地理解和使用 Gradle,从而提高 Android 应用程序的开发效率和质量。


需要注意的是,Gradle 是一项非常庞大和复杂的技术,本文仅对其中一些基本概念和知识进行了介绍,对于更深入和复杂的问题,需要通过进一步的学习和实践来掌握和解决。


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

Kotlin | 使用vararg可变参数

背景 一般在项目开发中,我们经常会在关键节点上埋点,而且埋点中会增加一些额外参数,这些参数通常是成对出现且参数个数是不固定的。如下: //定义事件EVENT_ID const val EVENT_ID = "event_xmkp" //注意:这里传入的是va...
继续阅读 »

背景


一般在项目开发中,我们经常会在关键节点上埋点,而且埋点中会增加一些额外参数,这些参数通常是成对出现参数个数是不固定的。如下:


//定义事件EVENT_ID
const val EVENT_ID = "event_xmkp"

//注意:这里传入的是vararg可变参数
fun String.log(vararg args: String) {
if (args.size % 2 > 0) {
throw RuntimeException("传入的参数必须是偶数")
}
if (args.isEmpty()) {
buryPoint(this)
} else {
//注意这里:可变参数在作为数组传递时需要使用伸展(spread)操作符(在数组前面加 *)
buryPoint(this, *args)
}
}

private fun buryPoint(eventId: String, vararg args: String) {
if (args.isNotEmpty()) {
Log.e(TAG, "buryPoint: $eventId, args: ${args.toList()}")
} else {
Log.e(TAG, "buryPoint: $eventId")
}
}

调用方式如下:


EVENT_ID.log()
EVENT_ID.log("name", "小马快跑")
EVENT_ID.log("name", "小马快跑", "city", "北京")

示例中可变参数可以是0个、2个、4个,执行结果:


2022-11-22 19:00:54 E/TTT: eventID: event_xmkp
2022-11-22 19:00:54 E/TTT: eventID: event_xmkp, args: [name, 小马快跑]
2022-11-22 19:00:54 E/TTT: eventID: event_xmkp, args: [name, 小马快跑, city, 北京]

可以看到通过定义可变参数,在调用方可以灵活地传入0个多个参数,下面就分析下Kotlin方法中的可变参数。


注意:可变参数在作为数组传递时需要使用伸展操作符(在数组前面加 *),如果去掉 *号,编译器会报如下错:


请添加图片描述


Kotlin中使用可变参数


Java中可变参数规则:



  • 使用...表示可变参数

  • 可变参数只能在参数列表的最后

  • 可变参数在方法体中最终是以数组的形式访问


Kotlin中可变参数规则:



  • 不同于Java,在Kotlin中如果 vararg 可变参数不是列表中的最后一个参数, 可以使用具名参数语法传递其后的参数的值。

  • Java一样,在函数内,可以以数组的形式使用这个可变参数的形参变量,而如果需要传递可变参数,需要在前面加上伸展(spread)操作符(在数组前面加 *),第一节已给出示例。


对Kotlin可变参数反编译


对上一节中的String.log()代码反编译成Java代码:


//kt代码
fun String.log(vararg args: String) {
if (args.size % 2 > 0) {
throw RuntimeException("传入的参数必须是偶数")
}
if (args.isEmpty()) {
buryPoint(this)
} else {
//注意这里:可变参数在作为数组传递时需要使用伸展(spread)操作符(在数组前面加 *)
buryPoint(this, *args)
}
}

转换之后:


 // Java代码
public final void log(@NotNull String $this$log, @NotNull String... args) {
...
if (args.length % 2 > 0) {
throw (Throwable)(new RuntimeException("传入的参数必须是偶数"));
} else {
if (args.length == 0) {
this.buryPoint($this$log);
} else {
this.buryPoint($this$log, (String[])Arrays.copyOf(args, args.length));
}
}
}


  • Kotlinvararg args: String参数转换成Java的 @NotNull String... args

  • Kotlinspread伸展操作符*args转换成Java(String[])Arrays.copyOf(args, args.length),可见最终还是通过系统拷贝生成了数组。

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

5分钟带你学会MotionLayout

1、前言 最近在开发中,同事居然对MontionLayout一知半解,那怎么行!百里偷闲写出此文章,一起学习、一起进步。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢🙏 希望你在阅读这篇文章的时候,已经对下面的内容熟练掌握了 Animat...
继续阅读 »

1、前言


最近在开发中,同事居然对MontionLayout一知半解,那怎么行!百里偷闲写出此文章,一起学习、一起进步。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢🙏


希望你在阅读这篇文章的时候,已经对下面的内容熟练掌握了



对了还有ConstraintLayout务必熟练掌握


对了,如果可以,请跟随敲代码,毕竟你脑补的代码,没有编译器。


2、简介


1)根据功能将MontionLayout视为属性动画框架、TransitionManagerCoordinatorLayout 的混合体。允许描述两个布局之间的转换(如 TransitionManager),但也可以为任何属性设置动画(不仅仅是布局属性)。


2)支持可搜索的过渡,如 CoordinatorLayout(过渡可以完全由触摸驱动并立即过渡到的任何点)。支持触摸处理和关键帧,允许开发人员根据自己的需要轻松自定义过渡。


3)在这个范围之外,另一个关键区别是 MotionLayout 是完全声明式的——你可以用 XML 完整地描述一个复杂的转换——不需要代码(如果你需要通过代码来表达运动,现有的属性动画框架已经提供了一种很好的方式正在做)。


4)MotionLayout 只会为其直接子级提供其功能——与 TransitionManager 相反,TransitionManager 可以使用嵌套布局层次结构以及 Activity 转换。


3、何时使用


MotionLayout 设想的场景是当需要移动、调整实际 UI 元素(按钮、标题栏等)或为其设置动画时——用户需要与之交互的元素。


重要的是要认识到运动是有目的的——不应该只是你应用程序中一个无偿的特殊效果;应该用来帮助用户了解应用程序在做什么。Material Design 原则网站很好地介绍了这些概念。


有一类动画只需要处理播放预定义的内容,用户不会——或不需要——直接与内容交互。视频、GIF,或者以有限的方式,动画矢量可绘制对象或lottie文件通常属于此类。MotionLayout 并不专门尝试处理此类动画(但当然可以将们包含在 MotionLayout 中)。


4、依赖


确保constraintlayout版本>=2.0.0即可



build.gradle



dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
}
//or
dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
}

5、ConstraintSet


如果使用ConstraintLayout并不够多,那对ConstraintSets的认识可能不够完善,我们也展开说说


ConstraintSets包含了一个或多个约束关系,每个约束关系定义了一个视图与其父布局或其他视图之间的位置关系。通过使用ConstraintSets,开发者可以在运行时更改布局的约束关系,从而实现动画或动态变化的布局效果。


比如ConstraintSets包含了以下方法:



  1. clone():克隆一个ConstraintSet实例。

  2. clear():清除所有的约束关系。

  3. connect():连接一个视图与其父布局或其他视图之间的约束关系。

  4. center():将一个视图水平或垂直居中于其父布局或其他视图。

  5. create():创建一个新的ConstraintSet实例。

  6. constrain*():约束一个视图的位置、大小、宽高比、可见性等属性。

  7. applyTo():将约束关系应用到一个ConstraintLayout实例。


还有更多方法就不一一列举了


只使用 ConstraintSet 和 TransitionManager 来实现一个平移动画



fragment_motion_01_basic.xml




<androidx.constraintlayout.widget.ConstraintLayout   
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cl_container"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
android:background="@color/orange"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


// 定义起始状态的 ConstraintSet (clContainer顶层容器)
val startConstraintSet = ConstraintSet()
startConstraintSet.clone(clContainer)
// 定义结束状态的 ConstraintSet
val endConstraintSet = ConstraintSet()
endConstraintSet.clone(clContainer)
endConstraintSet.connect(
R.id.button,
ConstraintSet.END,
ConstraintSet.PARENT_ID,
ConstraintSet.END,
8.dp
)
endConstraintSet.setHorizontalBias(R.id.button,1f)
clContainer.postDelayed({
// 在需要执行动画的地方
TransitionManager.beginDelayedTransition(clContainer)
// 设置结束状态的 ConstraintSet
endConstraintSet.applyTo(clContainer)
}, 1000)

我们首先使用 ConstraintSet.clone() 方法来创建起始状态的 ConstraintSet。然后,我们通过 ConstraintSet.clone() 和 ConstraintSet.connect() 方法来创建结束状态的 ConstraintSet,其中 connect() 方法用于连接视图到另一个视图或父容器的指定位置。在这里,我们将按钮连接到父容器的右端(左端在布局中已经声明了),从而使其水平居中。接着我们使用setHorizontalBias使其水平居右。


在需要执行动画的地方,我们调用 TransitionManager.beginDelayedTransition() 方法告诉系统要开始执行动画。然后,我们将结束状态的 ConstraintSet 应用到 MotionLayout 中,从而实现平滑的过渡。


图片转存失败,建议将图片保存下来直接上传


ConstraintSet 的一般思想是它们封装了布局的所有定位规则;由于您可以使用多个 ConstraintSet,因此您可以即时决定将哪组规则应用于您的布局,而无需重新创建您的视图——只有它们的位置/尺寸会改变。


MotionLayout 基本上建立在这个想法之上,并进一步扩展了这个概念


6、引用现有布局


在第5点中,我们新建了一个xml,我们继续使用,不过需要将androidx.constraintlayout.widget.ConstraintLayout修改为androidx.constraintlayout.motion.widget.MotionLayout



fragment_motion_01_basic.xml



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
android:background="@color/orange"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>

你会得到一个错误


image-20230407090719919

 靠着强大的编辑器,生成一个


image-20230407090719919

你就会得到下面这个和一个新的xml文件


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/motion_layout_01_scene">

</androidx.constraintlayout.motion.widget.MotionLayout>

也就是一个MotionScene文件



motion_layout_01_scene.xml



<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
</MotionScene>

这里我们先用再新建两个xml,代表开始位置和结束位置



motion_01_cl_start.xml



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/button"
android:background="@color/orange"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


motion_01_cl_end.xml



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/button"
android:background="@color/orange"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

修改一下



motion_layout_01_scene.xml



<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto">
<!-- Transition 定义动画过程中的开始状态和结束状态 -->
<!-- constraintSetStart 动画开始状态的布局文件引用 -->
<!-- constraintSetEnd 动画结束状态的布局文件引用 -->
<Transition
motion:constraintSetEnd="@layout/motion_01_cl_end"
motion:constraintSetStart="@layout/motion_01_cl_start"
motion:duration="1000">
<!--OnClick 用于处理用户点击事件 -->
<!--targetId 设置触发点击事件的组件 -->
<!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
<OnClick
motion:clickAction="toggle"
motion:targetId="@+id/button" />
</Transition>
</MotionScene>

部分解释都在注释中啦。好了 ,运行吧。


图片转存失败,建议将图片保存下来直接上传


这里的TransitionOnClick我们先按下不表。


7、独立的 MotionScene


上面的例子中,我们使用了两个XML+一个原有的布局为基础,进行的修改。最终重用您可能已经拥有的布局。MotionLayout 还支持直接在目录中的 MotionScene 文件中描述 ConstraintSet res/xml


我们在res/xml目录中新建一个xml文件



motion_layout_02_scene.xml



<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end"
motion:duration="1000">
<!--OnClick 用于处理用户点击事件 -->
<!--targetId 设置触发点击事件的组件 -->
<!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
<OnClick
motion:clickAction="toggle"
motion:targetId="@+id/button" />
</Transition>

<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

</MotionScene>

首先,在 <MotionScene> 标签内定义了两个 <ConstraintSet>,分别代表动画的开始状态(start)和结束状态(end)。每个 <ConstraintSet> 内包含一个 <Constraint>,用于描述一个界面组件(如按钮或文本框)的属性。


<Transition> 标签中,我们通过 app:constraintSetStartapp:constraintSetEnd 属性指定了动画的起始和终止状态。在这个简单的示例中,我们没有插值器等属性,但可以通过添加相应的属性(如 android:durationapp:interpolator 等)来自定义动画效果。


运行一下,一样


图片转存失败,建议将图片保存下来直接上传


7、注意



  1. ConstraintSet 用于替换受影响View的所有现有约束。

  2. 每个 Constraint 元素应包含要应用于View的所有约束。

  3. ConstraintSet 不是应用增量约束,而是清除并仅应用指定的约束。

  4. 对于只有一个View需要动画的场景,MotionScene 中的 ConstraintSet 只需包含该View的 Constraint。

  5. 可以看出 MotionScene 定义和之前是相同的,但是我们将开始和结束 ConstraintSet 的定义直接放在文件中。与普通布局文件的主要区别在于我们不指定此处使用的View的类型,而是将约束作为元素的属性。


8、AndroidStudio预览工具


Android Studio 支持预览 MotionLayout,可以使用设计模式查看并编辑 MotionLayout


Snipaste_2023-04-11_14-25-19


标号含义如下



  1. 点击第一个你可以看到,当前页面的具有IDimage-20230411160718057

  2. 点击第二个,可以看到起始动画的位置 image-20230411160815353

  3. 点击第三个,可以看到终止动画的位置 image-20230411160808841

  4. 第四个,可以操作动画的预览,暂停,播放,加速,拖动,等等。

  5. 而你可以看到途中有一条线,可以使用tools:showPaths="true"开启


9、补充


今天回过来一看,示例还是少了,我稍微加几个


<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:motion="http://schemas.android.com/apk/res-auto">

   <Transition
       motion:constraintSetEnd="@+id/end"
       motion:constraintSetStart="@+id/start"
       motion:duration="1000">
       <!--OnClick 用于处理用户点击事件 -->
       <!--targetId 设置触发点击事件的组件 -->
       <!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
       <OnSwipe
           motion:dragDirection="dragEnd"
           motion:touchAnchorId="@+id/button1"
           motion:touchAnchorSide="end" />

   </Transition>

   <ConstraintSet android:id="@+id/start">

       <Constraint
           android:id="@+id/button1"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           motion:layout_constraintBottom_toTopOf="@id/button2"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />

       <Constraint
           android:id="@+id/button2"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:alpha="1"
           motion:layout_constraintBottom_toTopOf="@id/button3"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button1" />

       <Constraint
           android:id="@+id/button3"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:rotation="0"
           motion:layout_constraintBottom_toTopOf="@id/button4"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button2" />

       <Constraint
           android:id="@+id/button4"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:elevation="0dp"
           motion:layout_constraintBottom_toTopOf="@id/button5"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button3" />

       <Constraint
           android:id="@+id/button5"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:scaleX="1"
           android:scaleY="1"
           motion:layout_constraintBottom_toBottomOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button4" />
   </ConstraintSet>

   <ConstraintSet android:id="@+id/end">
       <Constraint
           android:id="@+id/button1"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginEnd="8dp"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintHorizontal_bias="1"
           motion:layout_constraintBottom_toTopOf="@id/button2"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />

       <Constraint
           android:id="@+id/button2"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:alpha="0.2"
           motion:layout_constraintBottom_toTopOf="@id/button3"
           motion:layout_constraintHorizontal_bias="1"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button1" />


       <Constraint
           android:id="@+id/button3"
           android:layout_width="64dp"
           android:layout_height="64dp"
           motion:layout_constraintHorizontal_bias="1"
           android:layout_marginStart="8dp"
           android:rotation="360"
           motion:layout_constraintBottom_toTopOf="@id/button4"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button2" />

       <Constraint
           android:id="@+id/button4"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:elevation="10dp"
           motion:layout_constraintBottom_toTopOf="@id/button5"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintHorizontal_bias="1"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button3" />

       <Constraint
           android:id="@+id/button5"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:scaleX="2"
           motion:layout_constraintHorizontal_bias="1"
           android:scaleY="2"
           motion:layout_constraintBottom_toBottomOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button4" />
   </ConstraintSet>

</MotionScene>

其余部分就不一一展示了,因为你们肯定都知道啦。效果如下


2023-04-12_11-49-35 (1)


10、下个篇章


因为篇幅原因,我们先到这,这篇文章,只是了解一下,下一篇我们将会深入了解各种没有详细讲解的情况。


如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏


11、感谢



  1. 校稿:ChatGpt

  2. 文笔优化:ChatGpt

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

同一页面多次调用图形验证码

缘由一个页面需要两个验证码,使用同一个验证码调用两次会导致有前一个失效。那么我们需要创建不同的两个验证码,分别做验证。截图展示具体实现同时引入多个KgCaptcha的js。引入多个JS时,请定义 plural 参数;通过该参数区分定义对象名...
继续阅读 »

缘由

一个页面需要两个验证码,使用同一个验证码调用两次会导致有前一个失效。那么我们需要创建不同的两个验证码,分别做验证。


截图展示



具体实现

  • 同时引入多个KgCaptcha的js。
  • 引入多个JS时,请定义 plural 参数;通过该参数区分定义对象名,如plural=1,则对象名为kg1,以此类推。
<script src="captcha.js?appid=XXX&plural=1" id="KgCaptcha1"></script>
<script src="captcha.js?appid=XXX&plural=2" id="KgCaptcha2"></script>
  • 初始化验证码
<script type="text/javascript">

// 第一个验证码
kg1.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox1",
// 验证成功事务处理
success: function(e) {
console.log(e);
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});

// 第二个验证码
kg2.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox2",
// 验证成功事务处理
success: function(e) {
console.log(e);
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});

</script>

  • 创建验证码框区域
<!-- 第一个验证码 -->
<div id="captchaBox1"></div>
<!-- 第二个验证码 -->
<div id="captchaBox2"></div>


总结

SDK开源地址:https://github.com/KgCaptcha,顺便做了一个演示:https://www.kgcaptcha.com/demo/

收起阅读 »

一个Node.js图形验证码的生成

效果图准备访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppId、AppSecret。提供后端SDK来校验token(即安全凭据)是否合法 ,目前支持PHP版、Python版、Java/JSP版、...
继续阅读 »

效果图


准备

  • 访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppId、AppSecret。
  • 提供后端SDK来校验token(即安全凭据)是否合法 ,目前支持PHP版、Python版、Java/JSP版、.Net C#版。
  • 访问Node.js官网,下载Node.js运行环境,访问Vue.js中文官网,安装下载Vue.js,创建一个Vue项目,具体操作请查看Vue.js中文官网。

项目目录


index.html

项目根目录index.html文件,头部引用KgCaptcha的js。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--引入凯格行为验证码js-->
<script id="KgCaptcha" src="captcha.js?appid=XXX"></script>
<!--引入凯格行为验证码js-->
</head>
<body>
<!--Vue主体-->
<div id="app"></div>
<!--Vue主体-->
</body>
</html>

main.js

src/main.js文件中,配置路由。

import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
// 配置全局路由、组件
new Vue({
el: '#app',
router,
components: { App },
template: ''
})

App.vue

src/App.vue文件中,定义html。

<template>
<div id="app">
<!--自定义组件、内容-->
<form id="form">
token: <input name="token" _cke_saved_name="token" _cke_saved_name="token" _cke_saved_name="token" id="token">
<!--凯格行为验证码组件-->
<div id="captchaBox"></div>
<!--凯格行为验证码组件-->
<button type="submit">提交</button>
</form>
<!--自定义组件、内容-->
</div>
</template>

<script>
export default {
name: 'App',
}
//初始化凯格行为验证码
kg.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox",
// 验证成功事务处理
success: function(e) {
console.log(e);
kg.$('#token').value = e['token']
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});
</script>


总结

SDK开源地址:https://github.com/KgCaptcha,顺便做了一个演示:https://www.kgcaptcha.com/demo/


收起阅读 »

Vue.js 滑动拼图验证码实现笔记

背景关于验证码的使用场景还是非常多的,很多网站上的验证码可谓是五花八门,下面是我使用Vue.js实现滑动拼图验证码做的一个笔记。效果展示准备工作访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppI...
继续阅读 »

背景

关于验证码的使用场景还是非常多的,很多网站上的验证码可谓是五花八门,下面是我使用Vue.js实现滑动拼图验证码做的一个笔记。

效果展示



准备工作

  • 访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppId、AppSecret。
  • 提供后端SDK来校验token(即安全凭据)是否合法 ,目前支持PHP版、Python版、Java/JSP版、.Net C#版。
  • 访问Vue.js中文官网,复制Vue.js插件链接。
  • 注意:先HTML头部初始化行为验证码,然后HTML底部初始化Vue.js,否则KgCaptcha的js部分函数与被Vue.js发生冲突,导致失效。

实现代码

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--头部引入Vue.js插件-->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!--头部引入Vue.js插件-->
<!--头部引入行为验证码js插件-->
<script id="KgCaptcha" src="captcha.js?appid=XXX"></script>
<script>
kg.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox",
// 验证成功事务处理
success: function(e) {
console.log(e);
kg.$('#token').value = e['token'];
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});
</script>
<!--头部引入行为验证码js插件-->
</head>

<body>
<div id="app">
<!--自定义内容、Vue组件-->
token: <input name="token" id="token" />
<!--行为验证码组件-->
<div id="captchaBox"></div>
<!--行为验证码组件-->
<button type="button">提交</button>
<!--自定义内容、Vue组件-->
</div>
</body>

<!--底部运行Vue.js代码-->
<script>
var app = new Vue({
el: '#app',
})
</script>
<!--底部运行Vue.js代码-->

</html>


最后

SDK开源地址:https://github.com/KgCaptcha,顺便做了一个演示:https://www.kgcaptcha.com/demo/

收起阅读 »

就在昨天,我也关闭了朋友圈 ... ...

关于“关闭朋友圈”这个话题的文章,若干年前无意中就看到过,当我给我自己的第一个感觉是——不痛不痒。 为什么要关闭呢?每天在班车上闲来无事,看看周围的朋友们都有什么有趣的事情发生,多好啊。不懂为什么要去可以的关闭它,真的不懂。 随着年纪慢慢变大(很逃避“变老”这...
继续阅读 »


关于“关闭朋友圈”这个话题的文章,若干年前无意中就看到过,当我给我自己的第一个感觉是——不痛不痒


为什么要关闭呢?每天在班车上闲来无事,看看周围的朋友们都有什么有趣的事情发生,多好啊。不懂为什么要去可以的关闭它,真的不懂。


随着年纪慢慢变大(很逃避“变老”这个词),自己也越来越逃避社交,每到闲暇的时候,总是喜欢自己一个人听一听音乐,然后做几道LeetCode算法题,写一写算法题图解(虽然没多少人看)。但是很舒服,很自在。


再也不像十多年前,一到周末,三五好友,推杯换盏,牛皮吹得连马云都会觉得自己啥也不是。足迹也遍布了王府井、天安门、故宫、鸟巢、水立方、恭王府……


我有时在想,为什么自己越来越脱离了社会的群体了呢?反而更沉浸于自己的精神世界中,甚至一度怀疑自己患上了深度抑郁症


直到我近期读完了李笑来写的**《财富自由之路》**,被无数次的触动和震惊,人和人在认知的差距真的可以相差了一个南极到北极的距离。回首过往,自己干过的太多傻憨憨的事情,也不由自主的汗颜得低下了头


当时还是在2016年的时候吧,自己买了一辆很喜欢的车子,兴奋、激动、恨不得一时间给所有亲戚朋友打电话,告诉他们这车有多么的好,配置有多么的丰富。好像再迟了一秒钟,车子就会融化消失一样。从订车、到提车、再到洗车、再到开车去公司的路上,无数的朋友圈都在拼命的告诉周围的人,“我买车了!”嗨,现在一想。蛮尴尬的。


就像知乎有一篇帖子写到,“**朋友圈总是陷入到“羡慕别人”和“处心积虑让别人羡慕”的荒谬境地,发票圈和看票圈变得越来越无趣了”,**这句话说的多真实。


再看抖音也是一样,人均劳斯莱斯,人均2,3000万的豪宅,满地的“成功学小丑”——“我职高毕业,但是!毕业第一年,我开了麻辣烫店,净利润500万,第二年我就开劳斯莱斯库,手下团队100多人……


现在的网络充斥着太多的一夜暴复,沉浸在这种氛围下的我们也越来越浮躁了。就像我很喜欢的一个主播叫“在石250”,他在直播的时候就说“我是80后,当时网络也没那么发达,当我毕业工作的时候,看到路上开过一辆宝马3系,我都觉得牛逼得很。而现在呢,闹市街头好多小年轻用手机拍车子,你开过去一辆宝马5系,他都会摇一摇头,说上一句“这车一般,凑活事儿吧,比奔驰E300差远了!”,然后每个月领着2000块钱的工资,去网吧啃泡面。


我们被充斥了这么多网络垃圾之后,自己会受影响吗?绝对会的,而且会被毒害很深。我们发现,自己做的事情也来越沉不住气了,自己越来越觉得不月入10万都赶不上国民平均收入了,自己不买辆保时捷都没法出去跟朋友聚会了,自己家房子小于150平米那基本“狗都摇头了”。


我们该停一停了。我们需要信息,但并非这种信息。让我们自己安静下来,沉下心,静静的冲一杯绿茶,坐在窗边吹着微风,读一本我们很早就想读,但是被刷抖音和看朋友圈替代的书。坚持下去,你会发现,原来世界如此美好。自己的精神世界那么的安宁,外面世界的浮躁气息突然的这么让你嗤之以鼻。


说了很多,是的。就在昨天,我也关闭了朋友圈,去开始迎接一个全新的世界。在那个世界里,只有安宁、祥和、暖风和青云~



作者:爪哇缪斯
来源:juejin.cn/post/7221418382108622906

收起阅读 »

uniapp 手机号码一键登录保姆级教程

背景 通过uniapp来开发App,目前内部上架的App产品现有的登录方式有「账号/密码」 和 「手机号/验证码」两种登录方式;但这两种方式还是不够便捷,目前「手机号一键登录」是替代短信验证登录的下一代登录验证方式,能消除现有短信验证模式等待时间长、操作繁琐和...
继续阅读 »

背景


通过uniapp来开发App,目前内部上架的App产品现有的登录方式有「账号/密码」 和 「手机号/验证码」两种登录方式;但这两种方式还是不够便捷,目前「手机号一键登录」是替代短信验证登录的下一代登录验证方式,能消除现有短信验证模式等待时间长、操作繁琐和容易泄露的痛点。


因此,结合市面上的主流App应用,以及业务方的需求,我们的App产品也需要增加「手机号一键登录」功能。 DCloud联合个推公司整合了三大运营商网关认证的服务,通过运营商的底层SDK,实现App端无需短信验证码直接获取手机号。


uni官方提供了对接的方案文档,可自行查阅,也可继续阅读本文


准备工作


1 目前支持的版本及运营商



  • 支持版本:HBuilderX 3.0+

  • 支持项目类型:uni-app的App端,5+ App,Wap2App

  • 支持系统平台: Android,iOS

  • 支持运营商: 中国移动,中国联通,中国电信


2 费用


2.1 运营商费用

目前一键登录收费规则为每次登录成功请求0.02元,登录失败则不计费。


2.2 云空间费用

开通uniCloud是免费的,其中阿里云是全免费,腾讯云是提供一个免费服务空间。


阿里云

选择阿里云作为服务商时,服务空间资源完全免费,每个账号最多允许创建50个服务空间。阿里云目前处于公测阶段,如有正式业务对稳定性有较高要求建议使用腾讯云。


image.png


阿里云的服务空间是纯免费的。但为避免资源滥用,有一些限制,见下:


image.png



除上面的描述外,阿里云没有其他限制。
因为阿里云免费向DCloud提供了硬件资源,所以DCloud也没有向开发者收费。如果阿里云后续明确了收费计划,DCloud也会第一时间公布。



腾讯云

选择腾讯云作为服务商时,可以创建一个免费的服务空间,资源详情参考腾讯云免费额度;如想提升免费空间资源配额,或创建更多服务空间,则需付费购买。


image.png


2.3 云函数费用

如果你的一键登录业务平均每天获取手机号次数为10000次,使用阿里云正式版云服务空间后,对应云函数每天大概消耗0.139元


接入


1 重要前置条件



  • 手机安装有sim卡

  • 手机开启数据流量(与wifi无关,不要求关闭wifi,但数据流量不能禁用。)

  • 开通uniCloud服务(但不要求所有后台代码都使用uniCloud)

  • 开发者需要登录 DCloud开发者中心,申请开通一键登录服务。


2 开发者中心-开通一键登录服务


此官方文档详细步骤开通一键登录服务,开通后将当前项目加入一键登录内,审核2-3天;


3 开通uniCloud


一键登录在客户端获取 access_token 后,必须通过调用uniCloud中云函数换取手机号码,
所以需要开通uniCould;


登录uniCloud中web控制台里,新建服务空间,开通uniCloud


在uniCloud的云函数中拿到手机号后,可以直接使用,也可以再转给传统服务器处理,也可以通过云函数url化方式生成普通的http接口给5+ App使用。


4 客户端-一键登录


当前项目关联云空间

项目名称点击右键,创建云环境,创建的云环境应与之前开通的云空间类型保持一致,我这里选择腾讯云;


image.png


创建好后当前项目下会多个文件夹「uniCloud」,点击右键关联创建好的云空间


image.png


image.png


关联成功


image.png


获取可用的服务提供商(暂时作用不大)

一键登录对应的 provider ID为 'univerify',当获取provider列表时发现包含 'univerify' ,则说明当前环境打包了一键登录的sdk;


uni.getProvider({
service: 'oauth',
success: function (res) {
console.log(res.provider)// ['qq', 'univerify']
}
});

参考文档


预登录(可选)

预登录操作可以判断当前设备环境是否支持一键登录,如果能支持一键登录,此时可以显示一键登录选项;


uni.preLogin({
provider: 'univerify',
success(){ //预登录成功
// 显示一键登录选项
},
fail(res){ // 预登录失败
// 不显示一键登录选项(或置灰)
// 根据错误信息判断失败原因,如有需要可将错误提交给统计服务器
console.log(res.errCode)
console.log(res.errMsg)
}
})

参考文档


请求登录授权

弹出用户授权界面。根据用户操作及授权结果返回对应的回调,拿到 access_token,此时客户端登录认证完成;设置自定义按钮等;后续「需要将此数据提交到服务器获取手机号码」


uni.login({
provider: 'univerify',
univerifyStyle: { // 自定义登录框样式
//参考`univerifyStyle 数据结构`
},
success(res){ // 登录成功 在该回调中请求后端接口,将access_token传给后端
console.log(res.authResult); // {openid:'登录授权唯一标识',access_token:'接口返回的 token'}
},
fail(res){ // 登录失败
console.log(res.errCode)
console.log(res.errMsg)
}
})

参考文档


获取用户是否选中了勾选框

新增判断是否勾选一键登录相关协议函数;


uni.getCheckBoxState({
success(res){
console.log(res.state) // Boolean 用户是否勾选了选框
console.log(res.errMsg)
},
fail(res){
console.log(res.errCode)
console.log(res.errMsg)
}
})

参考文档


用access_token换手机号

客户端获取到 access_token 后,传递给uniCloud云函数,云函数中通过uniCloud.getPhoneNumber方法获取真正的手机号。


换取手机号有三种方式:




  1. 在前端直接写 uniCloud.callFunction ,将 access_token 传给指定的云函数。但需要在「云函数内部」请求服务端接口并将电话号码传到服务器;




  2. 使用普通ajax请求提交 access_token 给uniCloud的云函数(不考虑);




  3. 使用普通ajax请求提交 access_token 给自己的传统服务器,通过自己的传统服务器再转发给 uniCloud 云函数。但uniCloud上的「云函数需要做URL化」;




我们目前使用的是第三种,防止电话号码暴露到前端,通过java小伙伴去请求uniCloud云函数,返回电话号码给后端;


// 云函数验证签名,此示例中以接受GET请求为例作演示
const crypto = require('crypto')
exports.main = async(event) => {

const secret = 'your-secret-string' // 自己的密钥不要直接使用示例值,且注意不要泄露
const hmac = crypto.createHmac('sha256', secret);

let params = event.queryStringParameters
const sign = params.sign
delete params.sign
const signStr = Object.keys(params).sort().map(key => {
return `${key}=${params[key]}`
}).join('&')

hmac.update(signStr);

if(sign!==hmac.digest('hex')){
throw new Error('非法访问')
}

const {
access_token,
openid
} = params
const res = await uniCloud.getPhoneNumber({
provider: 'univerify',
appid: 'xxx', // DCloud appid,不同于callFunction方式调用,使用云函数Url化需要传递DCloud appid参数
apiKey: 'xxx', // 在开发者中心开通服务并获取apiKey
apiSecret: 'xxx', // 在开发者中心开通服务并获取apiSecret
access_token: access_token,
openid: openid
})
// 返回手机号给自己服务器
return res
}

res结果


{
"data": {
"code": 0,
"success": true,
"phoneNumber": "166xxxx6666"
},
"statusCode": 200,
"header": {
"Content-Type": "application/json; charset=utf-8",
"Connection": "keep-alive",
"Content-Length": "53",
"Date": "Fri, 06 Nov 2020 08:57:21 GMT",
"X-CloudBase-Request-Id": "xxxxxxxxxxx",
"ETag": "xxxxxx"
},
"errMsg": "request:ok"
}

参考文档


客户端关闭一键登录授权界面

请求登录认证操作完成后,不管成功或失败都不会关闭一键登录界面,需要主动调用closeAuthView方法关闭。完成业务服务登录逻辑后通知客户端关闭登录界面。


uni.closeAuthView()

参考文档


错误码

一键登录相关的错误码


但其中状态码30006,官方未给出相关的说明,但与相关技术沟通得知,该状态码是运营商返回的,大概率是网络信号不好,或者其它等原因造成的,没办法修复,只能是想办法兼容改错误;


目前我们的兼容处理方案是:程序检测判断如果出现该状态码,则关闭一键登录授权页面,并跳转到原有的「手机号验证码」登录页面


参考文档


5 云函数-一键登录


自HBuilderX 3.4.0起云函数需启用uni-cloud-verify之后才可以调用getPhoneNumber接口,扩展库uni-cloud-verify


需要在云函数的package.json内添加uni-cloud-verify的引用即可为云函数启用此扩展,无需做其他调整,因为HbuilderX内部已经集成了该扩展库,只需引入即可,不用安装,代码如下:


{
"name": "univerify",
"extensions": {
"uni-cloud-verify": {} // 启用一键登录扩展,值为空对象即可
}
}

参考文档


6 运行基座和打包


使用uni一键登录,不需要制作自定义基座,使用HBuilder标准真机运行基座即可。在云函数中配置好apiKey、apiSecret后,只要一键登录成功,就会从你的账户充值中扣费。


在菜单中配置模块权限


image.png


参考文档


需要注意的问题


1. 开通手机号一键登录是否同时需要开通苹果登录?


目前只开通手机号一键登录,未开通苹果登录,在我们项目里是可以的,但是App云打包时是会弹框提示的,但是并不影响项目在App Store中发布;


2. 如果同一个token多次反复获取手机号会重复扣费么?


不会,这种场景应该仅限于联调测试使用,正式上线每次都应该获取最新token,避免过期报错;


3. access_token过期时间



  • token过期时间是10分钟

  • 每次请求获取手机号接口时,都应该从客户端获取最新的token

  • 在取号成功时进行扣费,获取token不计费


4. 预登录有效期


预登录有效期为10分钟,超过10分钟后预登录失效,此时调用login授权登录相当于之前没有调用过预登录,大概需要等待1-2秒才能弹出授权界面。 预登录只能使用一次,调用login弹出授权界面后,如果用户操作取消登录授权,再次使用一键登录时需要重新调用预登录。


作者:Wendy的小帕克
来源:juejin.cn/post/7221422131857506359
收起阅读 »

整个活儿~永远加载不满的进度条

web
前言各位开发大佬,平时肯定见到过这种进度条吧,一直在加载,但等了好久都是在99% 如下所示: 有没有好奇这个玩意儿咋做的呢? 细听分说 (需要看使用:直接看实践即可)fake-progress如果需要实现上面的这个需求,其实会涉及到fake-progre...
继续阅读 »

前言

各位开发大佬,平时肯定见到过这种进度条吧,一直在加载,但等了好久都是在99%

如下所示:

有没有好奇这个玩意儿咋做的呢?
细听分说 (需要看使用:直接看实践即可)

fake-progress

如果需要实现上面的这个需求,其实会涉及到fake-progress这个库,具体是干嘛的呢?
这个库会提供一个构造函数,创建一个实例对象后,里面的属性会给我们进度条需要的数据等信息。
如图所示:


fake-progress库的源码如下:

/**
* Represents a fakeProgress
* @constructor
* @param {object} options - options of the contructor
* @param {object} [options.timeConstant=1000] - the timeConstant in milliseconds (see https://en.wikipedia.org/wiki/Time_constant)
* @param {object} [options.autoStart=false] - if true then the progress auto start
*/

const FakeProgress = function (opts) {
 if (!opts) {
   opts = {};
}
 // 时间快慢
 this.timeConstant = opts.timeConstant || 1000;
 // 自动开始
 this.autoStart = opts.autoStart || false;
 this.parent = opts.parent;
 this.parentStart = opts.parentStart;
 this.parentEnd = opts.parentEnd;
 this.progress = 0;
 this._intervalFrequency = 100;
 this._running = false;
 if (this.autoStart) {
   this.start();
}
};

/**
* Start fakeProgress instance
* @method
*/

FakeProgress.prototype.start = function () {
 this._time = 0;
 this._intervalId = setInterval(
   this._onInterval.bind(this),
   this._intervalFrequency
);
};

FakeProgress.prototype._onInterval = function () {
 this._time += this._intervalFrequency;
 this.setProgress(1 - Math.exp((-1 * this._time) / this.timeConstant));
};

/**
* Stop fakeProgress instance and set progress to 1
* @method
*/

FakeProgress.prototype.end = function () {
 this.stop();
 this.setProgress(1);
};

/**
* Stop fakeProgress instance
* @method
*/

FakeProgress.prototype.stop = function () {
 clearInterval(this._intervalId);
 this._intervalId = null;
};

/**
* Create a sub progress bar under the first progres
* @method
* @param {object} options - options of the FakeProgress contructor
* @param {object} [options.end=1] - the progress in the parent that correspond of 100% of the child
* @param {object} [options.start=fakeprogress.progress] - the progress in the parent that correspond of 0% of the child
*/

FakeProgress.prototype.createSubProgress = function (opts) {
 const parentStart = opts.start || this.progress;
 const parentEnd = opts.end || 1;
 const options = Object.assign({}, opts, {
   parent: this,
   parentStart: parentStart,
   parentEnd: parentEnd,
   start: null,
   end: null,
});

 const subProgress = new FakeProgress(options);
 return subProgress;
};

/**
* SetProgress of the fakeProgress instance and updtae the parent
* @method
* @param {number} progress - the progress
*/

FakeProgress.prototype.setProgress = function (progress) {
 this.progress = progress;
 if (this.parent) {
   this.parent.setProgress(
    (this.parentEnd - this.parentStart) * this.progress + this.parentStart
  );
}
};

我们需要核心关注的参数只有timeConstant,autoStart这两个参数,通过阅读源码可以知道timeConstant相当于分母,分母越大则加的越少,而autoStart则是一个开关,如果开启了直接执行start方法,开启累计的定时器。
通过这个库,我们实现一个虚拟的进度条,永远到达不了100%的进度条。
但是如果这时候像接口数据或其他什么资源加载完了,要到100%了怎么办呢?可以看到代码中有end()方法,因此显示的调用下实例的end()方法即可。

实践

上面讲了这么多下面结合圆形进度条(后面再出个手写圆形进度条)来实操一下,效果如下:


代码如下所示:

<template>
 <div ref="main" class="home">
   </br>
   <div>{{ fake.progress }}</div>
   </br>
   <Progress type="circle" :percentage="parseInt(fake.progress*100)"/>
   </br></br>
   <el-button @click="stop">停止</el-button>
   </br></br>
   <el-button @click="close">关闭</el-button>
 </div>
</template>

<script>
import FakeProgress from "fake-progress";

export default {
 data() {
   return {
     fake: new FakeProgress({
       timeConstant : 6000,
       autoStart : true
    })
  };
},
 methods:{
   close() {
     this.fake.end()
  },
   stop() {
     this.fake.stop()
  }
},
};
</script>

总结

如果需要实现一个永远不满的进度条,那么你可以借助fake-progress
核心是1 - Math.exp((-1 * this._time) / this.timeConstant) 这个公式
涉及到一个数据公式: e的负无穷次方 趋近于0。所以1-e^-x永远到不了1,但趋近于1

核心原理就是:用时间做分子,传入的timeConstant做分母,通过Math.exp((-1 * this._time) / this.timeConstant) 可知,如果时间不断累积且为负值,那么Math.exp((-1 * this._time) / this.timeConstant) 就无限趋近于0。所以1 - Math.exp((-1 * this._time) / this.timeConstant) 就可以得到无限趋近于1 的值

总结,如果需要使用的话,在使用的地方创建一个实例即可(配置autoStart之后就会自动累加):

new FakeProgress({
   timeConstant : 6000,
   autoStart : true
})

如果需要操作停止或介绍使用其实例下的对应方法即可

this.fake.end()
this.fake.stop()

作者:前端xs
来源:juejin.cn/post/7219195850539057212

收起阅读 »

低代码开发,是稳扎稳打还是饮鸩止渴?

web
2023年,从业者对低代码的发展充满了想象,人们认为,未来低代码它的商业价值不可估量。据Gartner的最新报告显示,到2023年,全球低代码开发技术市场规模预计将达到269亿美元,比2022年增长19.6%。 随着数字化进入深水区,企业碎片化、个性化、临时...
继续阅读 »

2023年,从业者对低代码的发展充满了想象,人们认为,未来低代码它的商业价值不可估量。据Gartner的最新报告显示,到2023年,全球低代码开发技术市场规模预计将达到269亿美元,比2022年增长19.6%。



随着数字化进入深水区,企业碎片化、个性化、临时化的需求不断涌现,而无论传统应用还是SaaS服务,都无法满足企业的全部需求,企业组织越来越多地转向低代码开发技术,以满足对快速应用交付和高度定制的自动化工作流程不断增长的需求。


image.png


中小企业的IT基础薄弱,人才有限,自研难度很大;中大型企业虽然有专门的IT部门,但审核流程长,业务部门的需求也无法立马满足。而低代码开发,只需编写少量代码或无需代码,就可以快速生成应用程序,在理论上刚好是解决这类问题的钥匙。


全民开发


低代码确实可以满足企业大部分IT需求,普通的业务人员也能进行应用搭建,成为平台的最终用户,写更少的代码,花更少的钱,干更多的事。就算是拥有独立IT部门的中大型企业,也会存在大量临时性边缘的业务需求,低代码可以很好的应对。


image.png


目前市场上有三种类型的低代码厂家:原生厂商、应用软件厂商、云厂商。随着低代码玩家越来越多,整个赛道的竞争将越来越激烈,有从业者发出呐喊:低代码产品未来到底是继续加功能,让更多开发者进来,以此满足客户普遍需求?还是通过一些其他模块或者应用市场的方式来解决客户专业需求?


一些厂商认为应该细分领域,比如深耕CRM、进销存、OKR、人事管理等热门应用模板;还有一部分厂商认为低代码的发展应该要走一条农村包围城市的路,从小处着眼,走普遍路线,主协作,帮助产研内部进行更高效的协同和项目管理,帮助IT部门更好地与业务部门建立起协作关系即可。


image.png


所以,在低代码赛道上,未来的“分流”趋势或将越来越明显。以JNPF为代表的“轻应用”派,由表单所驱动,重视数据处理能力、快速开发能力、低门槛等。


JNPF,立足于低代码开发技术,采用主流的两大技术Java/.Net开发,专注低代码开发,有拖拽式的代码生成器,灵活的权限配置、SaaS服务,强大的接口对接,随心可变的工作流引擎。支持多端协同操作,100%提供源码,支持多种云环境部署、本地部署。


image.png


基于代码生成器,可一站式开发多端使用Web、Android、IOS、微信小程序。代码自动生成后可以下载本地,进行二次开发,有效提高整体开发效率。


开源入口:http://www.yinmaisoft.com/?from=jeuji…


已经覆盖零售、医疗、制造、银行、建筑、教育、社会治理等主流行业,一站式搭建:生产管理系统、项目管理系统、进销存管理系统、OA办公系统、人事财务等等。可以节省开发人员80%时间成本,并且有以构建业务流程、逻辑和数据模型等所需的功能。



这是看得见的价值,但也有看不见的顾虑


有人认为,低代码应用是一种“饮鸩止渴”的行为,会让部分企业觉得,数字化转型就那样,哪些业务需要,就采用低代码应用“缝缝补补”即可,最终浅尝辄止,公司的整个数字化转型停在半道,欠缺完备性、统一性以及系统性。类似的问题,或许在未来会出现,也可能会在低代码应用的迭代过程中被解决。



2023,行至水深处,低代码的路会越来越难走,但这也是黎明前必经的黑暗。稻盛和夫曾说,人生如粥,熬出至味,相信在穿过重重迷雾后,2023年低代

作者:jnpfsoft
来源:juejin.cn/post/7220696541308436541
码也将迎来新的发展。

收起阅读 »

AutoGPT太火了,无需人类插手自主完成任务,GitHub2.7万星

OpenAI 的 Andrej Karpathy 都大力宣传,认为 AutoGPT 是 prompt 工程的下一个前沿。 近日,AI 界貌似出现了一种新的趋势:自主人工智能。 这不是空穴来风,最近一个名为 AutoGPT 的研究开始走进大众视野。特斯拉前 AI...
继续阅读 »

OpenAI 的 Andrej Karpathy 都大力宣传,认为 AutoGPT 是 prompt 工程的下一个前沿。


近日,AI 界貌似出现了一种新的趋势:自主人工智能

这不是空穴来风,最近一个名为 AutoGPT 的研究开始走进大众视野。特斯拉前 AI 总监、刚刚回归 OpenAI 的 Andrej Karpathy 也为其大力宣传,并在推特赞扬:「AutoGPT 是 prompt 工程的下一个前沿。」



不仅如此,还有人声称 ChatGPT 已经过时了,AutoGPT 才是这个领域的新成员。



项目一经上线,短短几天狂揽 27K + 星,这也侧面验证了项目的火爆。



GitHub 地址:github.com/torantulino…

问题来了,AutoGPT 到底是什么?它是一个实验性的开源应用程序,展示了 GPT-4 语言模型的功能。该程序由 GPT-4 驱动,可以自主实现用户设定的任何目标。



具体来说,AutoGPT 相当于给基于 GPT 的模型一个内存和一个身体。有了它,你可以把一项任务交给 AI 智能体,让它自主地提出一个计划,然后执行计划。此外其还具有互联网访问、长期和短期内存管理、用于文本生成的 GPT-4 实例以及使用 GPT-3.5 进行文件存储和生成摘要等功能。AutoGPT 用处很多,可用来分析市场并提出交易策略、提供客户服务、进行营销等其他需要持续更新的任务。

正如网友所说 AutoGPT 正在互联网上掀起一场风暴,它无处不在。很快,已经有网友上手实验了,该用户让 AutoGPT 建立一个网站,不到 3 分钟 AutoGPT 就成功了。 期间 AutoGPT 使用了 React 和 Tailwind CSS,全凭自己,人类没有插手。看来程序员之后真就不再需要编码了。



之后该用户补充说,自己的目标很简单,就是用 React 创建一个网站。提出的要求是:创建一个表单,添加标题「Made with autogpt」,然后将背景更改为蓝色。AutoGPT 成功的构建了网站。该用户还表示,如果给 AutoGPT 的 prompt 更多,表现会更好。

图源:twitter.com/SullyOmarr/…

接下里我们再看一个例子。假装自己经营一家鞋公司,给 AutoGPT 下达的命令是对防水鞋进行市场调查,然后让其给出 top5 公司,并报告竞争对手的优缺点 :



首先,AutoGPT 直接去谷歌搜索,然后找防水鞋综合评估 top 5 的公司。一旦找到相关链接,AutoGPT 就会为自己提出一些问题,例如「每双鞋的优缺点是什么、每款排名前 5 的防水鞋的优缺点是什么、男士排名前 5 的防水鞋」等。

之后,AutoGPT 继续分析其他各类网站,并结合谷歌搜索,更新查询,直到对结果满意为止。期间,AutoGPT 能够判断哪些评论可能偏向于伪造,因此它必须验证评论者。



执行过程中,AutoGPT 甚至衍生出自己的子智能体来执行分析网站的任务,找出解决问题的方法,所有工作完全靠自己。

结果是,AutoGPT 给出了 top 5 防水鞋公司的一份非常详细的报告,报告包含各个公司的优缺点,此外还给出了一个简明扼要的结论。全程只用了 8 分钟,费用为 10 美分。期间也完全没有优化。



这个能够独立自主完成任务的 AutoGPT 是如何运行的呢?我们接着来看。

AutoGPT:30 分钟内构建你自己的 AI 助手

作为风靡互联网的 AI 智能体,AutoGPT 可以在 30 分钟内完成设置。 你就可以拥有自己的 AI,协助完成任务,提升工作效率。

这一强大的 AI 工具能够自主执行各种任务,设置和启动的简便性是一大特征。在开始之前,你需要设置 Git、安装 Python、下载 Docker 桌面、获得一个 OpenAI API 密钥。

克隆存储库

首先从 GitHub 中克隆 AutoGPT 存储库。



使用以下命令导航到新建文件夹 Auto-GPT。



配置环境

在 Auto-GPT 文件夹中,找到.env.template 文件并插入 OpenAI API 密钥。接着复制该文件并重命名为.env。



安装 Python 包

运行以下命令,安装需要的 Python 包。



运行 Docker

运行 Docker 桌面,不需要下载任何容器,只需保证程序处于激活状态。



运行 AutoGPT



执行以下命令,运行 AutoGPT。



设置目标**

AutoGPT 虽是一个强大的工具,但并不完美。为避免出现问题,最好从简单的目标开始,对输出进行测试,并根据自身需要调整目标,如上文中的 ResearchGPT。

不过,你如果想要释放 AutoGPT 的全部潜力,需要 GPT-4 API 访问权限。GPT-3.5 可能无法为智能体或响应提供所需的深度。

AgentGPT:浏览器中直接部署自主 AI 智能体

近日,又有开发者对 AutoGPT 展开了新的探索尝试,创建了一个
可以在浏览器中组装、配置和部署自主 AI 智能体的项目 ——AgentGPT。** 项目主要贡献者之一为亚马逊软件工程师 Asim Shrestha,已在 GitHub 上获得了 2.2k 的 Stars。



AgentGPT 允许你为自定义 AI 命名,让它执行任何想要达成的目标。自定义 AI 会思考要完成的任务、执行任务并从结果中学习,试图达成目标。如下为 demo 示例:HustleGPT,设置目标为创立一个只有 100 美元资金的初创公司。



再比如 PaperclipGPT,设置目标为制造尽可能多的回形针。



不过,用户在使用该工具时,同样需要输入自己的 OpenAI API 密钥。AgentGPT 目前处于 beta 阶段,并正致力于长期记忆、网页浏览、网站与用户之间的交互。

GPT 的想象力空间还有多大,我们继续拭目以待。

参考链接: medium.com/@tsaveratto…


作者:机器之心
来源:juejin.cn/post/7221089899281580091
收起阅读 »

产品说要让excel在线编辑,我是这样做的。

web
背景 最近公司项目有需求, 某导入功能, 想让客户选完excel文件, 直接将加载到web的excel编辑器中, 修改、确认, 之后上传导入。 效果查看 选择 Luckysheet(dream-num.github.io/LuckysheetD…) ,一款...
继续阅读 »

背景


最近公司项目有需求, 某导入功能, 想让客户选完excel文件, 直接将加载到web的excel编辑器中, 修改、确认, 之后上传导入。


效果查看


Kapture 2023-04-13 at 13.37.05.gif


选择



就看到了这两个, 最后选择了Luckysheet, 看他的star比较多, 哈哈。


需求实现分析


分析一下整个流程。


其实大体就两步, 搞进去,抽离出来。


一、加载本地excel到web编辑器中


1、拿到本地excel文件流


2、转换为 Luckysheet 要的格式


3、new 一个 Luckysheet 实例, 挂在到对应标签上


完成以上就把excel加载进去了, 显示出来了。


在线编辑的事就是这个库帮咱们搞定了.


二、 从web编辑器导出文件流 上传


等客户在线编辑完成, 就需要点击一个按钮, 导出文件流, 确认并调接口上传


1、获取 Luckysheet里工作表的数据


image.png


luckysheet.getAllSheets()

2、将数据加工并使用xlsx或者exceljs导出文件流


导出为为arrayBuffer, 再将arrayBuffer转为Blob


3、调后端接口上传


开发实践


一、引入 lucky-sheet


有两种方式


1、官方文档里的cdn


这种加载有点慢


<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css' />
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js"></script>

2、自己打包, 传到oss, 引入(推荐)



第一种第三方的cdn不稳定, 有时候很慢,还是建议,拉他的仓库,然后打个包,传到自己静态资源库, 来使用



npm run builddist 传上去使用


二、指定容器


<div id="luckysheet"></div>

三、导入本地文件


1、 用elment的上传文件组件 选择文件


但是这里不上传,仅仅是用它选择文件拿到文件对象File


<div class="import-okr">
<!-- ,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet -->
<el-upload
v-model:file-list="fileList"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
class="upload-demo"
:before-upload="beforeUpload"
action=""
:show-file-list="false"
>
<button @click="uploadFile">上传数据</button>
</el-upload>
</div>

2、beforeUpload 方法拿到文件


const beforeUpload = (file) => {
console.log(file)
}

image.png


3、将文件流转换为lucky要的格式


github.com/dream-num/L…


安装转换工具


npm install luckyexcel

使用


// After getting the xlsx file
LuckyExcel.transformExcelToLucky(file,
function(exportJson, luckysheetfile){
// exportJson就是转换后的数据
},
function(error){
// handle error if any thrown
}

4、将转换后的数据创建表格


// 将拿到的数据创建表格
luckysheet.create({
container: 'luckysheet', // luckysheet is the container id
data:exportJson.sheets,
title:exportJson.info.name,
userInfo:exportJson.info.creator,
lang: 'zh', // 设定表格语言
myFolderUrl: window.location.href,
showtoolbarConfig: {
pivotTable: false, //'数据透视表'
// protection: false, // '工作表保护'
print:false, // '打印'
image: false, // 插入图片
},
showinfobar: false,
options: {
// 其他配置
userImage:'http://qzz-static.forwe.store/public-assets/pgy_kj_pic_logo.png?x-oss-process=image/resize,m_fill,w_72,h_72', // 头像url
userName:'Lucky', // 用户名
}
});

完整代码


const beforeUpload = (file) => {
console.log(file)
// 转换工具, 将文件流转换为lucky要的格式
LuckyExcel2.transformExcelToLucky(
file,
function(exportJson, luckysheetfile){
isShowExcel.value = true
console.log(exportJson)
nextTick(() => {
window.luckysheet.destroy();
// 将拿到的数据创建表格
luckysheet.create({
container: 'luckysheet', // luckysheet is the container id
data:exportJson.sheets,
title:exportJson.info.name,
userInfo:exportJson.info.creator,
lang: 'zh', // 设定表格语言
myFolderUrl: window.location.href,
showtoolbarConfig: {
pivotTable: false, //'数据透视表'
// protection: false, // '工作表保护'
print:false, // '打印'
image: false, // 插入图片
},
showinfobar: false,
options: {
// 其他配置
userImage:'http://qzz-static.forwe.store/public-assets/pgy_kj_pic_logo.png?x-oss-process=image/resize,m_fill,w_72,h_72', // 头像url
userName:'Lucky', // 用户名
}
});
})
},
function(err){
logger.error('Import failed. Is your fail a valid xlsx?');
});
}

四、导出


1、利用 luckysheet.getAllSheets() 获取表数据


console.log(luckysheet.getAllSheets())

image.png


2、exceljs将上述对象转换为excel文件流


import Excel  from 'exceljs'
// 导出excel
const exportExcel = async function (luckysheet) { // 参数为luckysheet.getluckysheetfile()获取的对象
// 1.创建工作簿,可以为工作簿添加属性
const workbook = new Excel.Workbook()
// 2.创建表格,第二个参数可以配置创建什么样的工作表
luckysheet.every(function (table) {
if (table.data.length === 0) return true
const worksheet = workbook.addWorksheet(table.name)
// 3.设置单元格合并,设置单元格边框,设置单元格样式,设置值
setStyleAndValue(table.data, worksheet)
setMerge(table.config.merge, worksheet)
setBorder(table.config.borderInfo, worksheet)
return true
})
// 4.写入 buffer
const buffer = await workbook.xlsx.writeBuffer()
return buffer
}

3、 写个方法,执行上述两步


// 保存文件
const onClickSaveFile = async ( ) => {
console.log(luckysheet.getAllSheets())
const buf = await exportExcel(luckysheet.getAllSheets())
const blob = new Blob([buf]);
// $emit('file', blob)
handleUpload(blob)
}

4、上传方法


利用formData, 将生成的文件二进制流发给后端


const handleUpload = async(file) => {
// isShowExcel.value = false
const loading = ElLoading.service({
fullscreen: true,
text: '上传中,请稍等',
background: 'rgba(0,0,0,0.1)'
});
try {
const formData = new FormData()
formData.append('file', file)
const {code, data, message } = await IMPORT_OKR(formData)
if(code === 1) {
//...
}
loading.close()
} catch (error) {
console.log(error)
loading.close()
}
}

遇到问题


1、iconfont冲突


lucky-sheet这个项目里的iconfont类名和我项目里一样,导致有些被覆盖了.
image.png


解决: 将他项目里 iconfont 换成 lucky-sheet, 相关类名也全部替换, 然后重新打包,再引入,即可解决


2、lucky-sheet层级不够高,无法编辑


image.png


elmentui和antd的一些组件层级比较高,所以, 让kucky的层级更高即可


解决: 增加下述css即可


.luckysheet-input-box { z-index: 2000; } .luckysheet-cols-menu { z-index: 2001; }

最后


妥妥的都是站在巨人的肩膀上


求赞


作者:浏览器API调用工程师
来源:juejin.cn/post/7221368910139342907
收起阅读 »

从前端变化看终身就业

接触前端已10年有余,从菜鸟到现在的老鸟;肚皮围度增长了,发量却减少了;头开始往下低了,发际线却往上走了!感叹时光易逝之际,不免有些伤感;风云突变的年代,总是少不了焦虑和无奈; 而立之年开始背负了各种债务,操很多心,最担心的莫过于父母一年年老去,小孩成长路上各...
继续阅读 »

接触前端已10年有余,从菜鸟到现在的老鸟;肚皮围度增长了,发量却减少了;头开始往下低了,发际线却往上走了!感叹时光易逝之际,不免有些伤感;风云突变的年代,总是少不了焦虑和无奈; 而立之年开始背负了各种债务,操很多心,最担心的莫过于父母一年年老去,小孩成长路上各种担心、顾虑;当然还有工作,一份安稳的、相对不错的收入是一家人生活的保障,也让希望增加了不少,今天跟大家一起聊聊工作吧!



终身就业还是终身职业



作为社会人、搬砖人,已经错过了最好的机会;也许是从来就不曾有过机会,已经失去了终生职业的可能(公务员、事业单位....);在国内很少有人能在一家企业供职到领社保,听说国外(比如德国、日本)有,我想说的是: 而今我们(在能力不是超强、又没有Backgroud的情况下)应该努力追求终生就业; 



企业想要你终生职业,但实际呢? 不管在任何一家单位工作,作为企业总是希望员工是忠诚度极高的,可以胜任公司安排的各种任务,还一心一意的工作,把公司当自己家,有奉献精神;错吗? ---当然是对的; 如果我是老板我也会这么想;


很不幸,现实和理想总是惊为天人,面对糟糕的经济形式,活下去成为企业第一要务,而精简人员总是成为企业断臂求生的一种惯用方式,现实就是残酷如此;于个人而言,在不乐观的环境中能有一份相对稳定的工作,有个可观的收入就显得极为重要,这就需要追求终生就业的能力(不论什么行业);


互联网的发展与前端



说说自己这10多年的心路历程,可能是反面教材,如果能为你带来一些参考或借鉴或一些帮助我是很高兴的;



进入编程世界,与PHP的初恋

进入编程世界,源于羡慕!(2010年)看到同学用HTML写了一个表单,当时觉得觉得很高级,很厉害; 当时他学的是PHP(号称最好的语言),所以也就不自然的被影响、认可了PHP;


转行

从高中到大学,想从事的一直是健身相关的行业和工作,但是真正接触了发现似乎跟自己设想的那么好;当然这不是真正导致转行的原因;真正原因是朋友找我(大学专业:软件工程,但是从来没有学过)做一个网站,我却屁都不会,跟亲戚、朋友说自己还是软件相关专业毕业的;所以,为了装X,也为了对得起软件工程专业那个本本(成人自考),放弃了自己研究多年的健身,毅然报了培训班学起了PHP; 诸位有没有跟我一样的呢?


痛苦的学习

大学几年浑浑噩噩的过了,去报PHP培训时候,老师说:“学过C很容易学的,PHP先从HTML开始,很容易上手的”;交钱的时候自以为学过(上课虽然在睡觉)也或多或少听了一些,没吃过猪肉还没见过猪跑啊(实际上啥都不会),应该问题不大;想起同学说(10年刚毕业)在深圳(拿6-8K),就开始幻想上了;


最难的Table + CSS布局

学习PHP的路上,最让我难堪的竟然是HTML和CSS;保守起见,老师选择了(相比DIV + CSS)更为简单的Table(用Dreamweaver拖拽) + CSS;然而,半个月过去了,竟然连写个百度首页都写不出来;呜呼哀哉,布局难难于上青天!恰逢十一国庆节,老师留的任务就是写个百度首页,如果连百度首页都写不出来,那说明不适合走这一行;结果呢? 一周过后还是写不出来,唉,每天上课时候会想到一万个放弃,回家后每一分钟都会想到N多个放弃;后来想着,钱也交了,就多坚持一下吧,就稀里糊涂的把课程学完了(HTML CSS Javascript PHP)


找到适合自己的方向

课程结束的时候,老师给个建议: PHP感觉有难度,就好好把div + css + jQuery学好,做前端、做前端、做前端!然而,入门的我还是选择了做PHP,一年多的时间学会了从切图、写页面、写PHP、写SQL语句、搭建服务器,天呐,完全飘了(实际上还是个小白),直到偶然机会(2013年)做了前端,突然找到了码页面的灵感,这种所见即所得的搬砖工作很有感觉,哈哈哈;


其实,这里想说的是:1是坚持;2是老师的层次比我高很多,他在很早就给我指明了道路,而执着于自己的愚见(当然也不全是错,也有收获),最后还是走上了老师指导的方向!


诸位,如果你们有个好的老师、高人指导,那是极为幸福的事情,一定要珍惜!


PS: (2011年)《编写高质量代码--web前端修炼之道》这本书对我前端方面的能力提升帮助非常大; 同时也感谢作者: 阿当,在我成长道路上的一些指导和帮助;


PS: 现在互联网平台很发达,在学习视频课程、阅读技术类书籍、技术资料的时候,建议可以尝试类型一下作者(译者);很多技术大牛还是很乐意给一些建议和指导的<致敬>;


学会听取建议、做出自己的判断

3年后,厚着脸皮请教老师接下来该学点啥能让薪资再增长一些,对未来有帮助; 老师给了一个方向: “Web GIS”,这一次照做了,掌握了一些Gis相关的基础,了解了Arcgis for javascript的常用方法等,结合近期的招聘,我觉得这算是很好的扩展了自己的选择;


PS: 建议菜鸟多向行业内的大牛请教,向身边段位高的朋友、同事多请教;


拥抱变化



互联网变化之快,技术更新之快,已经让很多人发出"学不动"的呼喊,但是我想说的是,只要你还想吃这碗饭,学不动还是要学;



yu6.png


学习&&提升

记得入行时前端面试:

- 会不会处理IE6、IE8兼容,有没有写过hack

- DIV + CSS 怎么实现div的垂直居中和水平居中,有几种实现方式

- 块级标签和行内标签的区别

- jQuery的prop方法和attr方法的区别

- Ajax有没有用过

- 会不会PS切图?gif和png的区别

- 什么是闭包?举个栗子


再后来学习了: 


- Bootstrap (不用了)

- AngularJs \ BackboneJS (不用了)

- requireJs \ seajs (不用了)

- grunt \ gulp \ bower(不用了)

- 响应式布局 (几乎不用了)


现在用的Vue \ React 也写了有好多年了,我想很快也会被新的所替代吧;

18年花了接近一万大洋购买了珠峰架构的课程,系统的学习了几个月,算是第二次技术比较大的提升吧,当然收入也相应的提高了一些;


PS: 想分享的是,很多技能可能生命周期很短,但是,身处当下我们还是要去积极学习,哪怕后来不用了,但是里面的一些思想会给我们未来某个时候带来很多帮助(懂得Bootstrap的设计思想就容易理解less\sass的使用,看到ElementUI、AntD等就一看就懂);


PS: 决定工作岗位、薪资的技术只是一部分,切勿过于迷恋于某个技术,跟随时代、拥抱变化,市场才是决定二者的最重要的因素!


运动&&养生

说点轻松又严肃的,各位看官,身体才是革命的本钱! 10年的老菜鸟目前除了颈椎不舒服(怪手机不怪写代码)外,其他的还好,论加班还能跟年轻人一战,哈哈哈! 这当然得益于过去多年的习惯:


- 经常跑步、爬山、健身

- 很少胡吃海喝,水果吃的多,烧烤啤酒几乎不碰;

- 每天吃饭不吃饱,原则上是不饿就行;


PS: 建议大家适当的增加运动; 如果歇了很久,要启动你的小马达,要慢慢来,勿操之过急; 最重要的是坚持;


"舍"&&"得"


舍得之间品味人生,舍得之间自有方寸;然而,舍 && 得又何其的难;



- 菜鸟期间的我是舍不得花钱买课程学习的,心疼钱啊; 后来受朋友影响开始花钱去买课程,花钱找老师学习(有的技能人家凭什么告诉你呢?),发现自己的进步突然就快了很多、收获也很大(为什么工作后就不舍得花钱学习了?);


- 知识就是金钱,如今我们知道听歌、追剧都要买VIP,为什么找工作的时候不知道购买VIP呢(我好多朋友、同事上BOSS刷招聘说每天都是那几个,殊不知买了VIP后消息就多了很多,你都没购买服务,招聘APP凭什么给你最新的资讯呢?)


- 工作、学习之余一定要花点时间去陪陪家人、运动、多走一走(哪怕是带小孩玩、哪怕去公园晒晒太阳、去商场逛逛看看美女),工作、技术很重要,人生的全部还有很多;工作是个弹力球,掉下去还有机会弹起来,而身体、家庭是玻璃球,要是碎了那就。。。


踏平坎坷成大道,路就在脚下

- 说了那么多,此刻会想什么呢? 代码要一行一行的写,日子还得一天一天的过,我曾因为负债累累(每个月却只发一次工资)而着急,然急又能如何,倒不如平静以对,正如《论语》中有云: "吾终尝日不食 终夜不寝,以思,无益,不如学也"!


- 环境不友好,是不是就没有机会了? ----当然有机会,当然有路可走! 

- 路在哪? ---- 路在脚下


前端的路该怎么走


各位,我们看到招聘APP上前端岗的需求量比往年同期少很多,这个是事实;与此同时企业还是有各种各样的需求的; 2023年了,还是以过去的思维去看(劳资会Vue 、 React),无异于缘木求鱼,那一定会让你感动悲观;何不换个思路、换个角度呢?



- 大前端方向还有很大空间: Vue\React + Flutter(或类似) + 小程序,正所谓:“山重水复疑无路 柳暗花明又一村”


- 前端 + GIS(或3D),观察BOSS上关于Webgis的招聘就知道了,如果能先于大多数人掌握了GIS、3D方面的知识,那选择是不是广阔了很多,正所谓: "有心栽花花不开 无心插柳柳成荫",何必要拘泥于某一种形态呢


- 前端架构师也是一些技术深度追求者的方向


(个人在二线城市,结合自己的经历和对Boss上岗位、薪资变化的观察,提出的拙见,欢迎批评、指导)


结语


- 强哥说了:“风浪越大鱼越贵”,挑战与机遇共存,我们应当在大变化的浪潮中调整自己的帆,拥抱惊涛骇浪和变化,磨砺出终身就业的能力!



  • 不要给自己贴标签(强哥:“我就是个卖鱼的”),现在的处境不代表未来没有机会、希望(到强盛集团);


- 编码之路上是: 路漫漫其修远兮 吾将上下而求索


- 人生道路上需要另一种气度《定风波·莫听穿林打叶声》---苏轼 : "莫听穿林打叶声 何妨吟啸且徐行; 竹杖芒鞋轻胜马,谁怕? 一蓑烟雨任平生; 料峭春风吹酒醒,微冷,山头斜照却相迎; 回首向来萧瑟处,归去, 也无风雨也无晴" 。


作者:风雪中的兔子
来源:juejin.cn/post/7220800667589197885
收起阅读 »

何不食肉糜?

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。 视频见这里:http://www.bilibili.com/video/BV1Bb… 当时我印象非常...
继续阅读 »

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。


视频见这里:http://www.bilibili.com/video/BV1Bb…



当时我印象非常深刻,微博评论是清一色的 “何不食肉糜”,或者说“房租你付?”


可能是因为这件事情的刺激,管清友后来才就有了“我特别知道年轻人建议专家不要建议”的言论。


对还是错?


在我看来,管清友的这个建议可以说是掏心掏肺,非常真诚,他在视频里也说了,他是基于很多实际案例才说的这些话,不是说教。


为什么我这么肯定呢?


很简单,我就是代表案例。


我第一家公司在浦东陆家嘴,四号线浦东大道地铁站旁边,我当时来上海的时候身无分文,借的家里的钱过来的,我是贫困家庭。


但,为了节约时间,我就在公司附近居住,步行五分钟,洗头洗澡都是中午回住的地方完成,晚上几乎都是11:00之后回去,倒头就睡,因为时间可以充分利用。


节约的时间,我就来研究前端技术,写代码,写文章,做项目,做业务,之前的小册(免费章节,可直接访问)我也提过,有兴趣的可以去看看。


现在回过头来看那段岁月,那是充满了感激和庆幸,自己绝对做了一个非常正确的决定,让自己的职业发展后劲十足。


所以,当看到管清友建议就近租房的建议,我是非常有共鸣的,可惜世界是参差的,管清友忽略了一个事实,那就是优秀的人毕竟是少数,知道如何主动投资自己的人也是凤毛麟角,他们根本就无法理解。


又或者,有些人知道应该要投资自己,但是就是做不到,毕竟辛苦劳累,何苦呢,做人,不就是应该开心快乐吗?


说句不好听的,有些人的时间注定就是不值钱的。


工作积极,时间长是种优势?


一周前,我写了篇文章,谈对“前端已死”的看法,其中提到了“团队下班最晚,工作最积极”可以作为亮点写在简历里。


结果有人笑出了声。



好巧的是,管清友的租房建议也有人笑了,出没出声并不知道。



也有人回复“何不食肉糜”。


这有些出乎我的意料,我只是陈述一个简单的事实,却触动了很多人的敏感神经。


我突然意识到,有些人可能有一个巨大的认知误区,就是认为工作时长和工作效率是负相关的,也就是那些按时下班的是效率高,下班晚的反而是能力不足,因为代码不熟,bug太多。



雷军说你说的很有道理,我称为“劳模”是因为我工作能力不行。


你的leader也认为你说的对,之前就是因为我每天准时下班,证明了自己的能力,所以自己才晋升的。


另外一个认知误区在于,把事实陈述当作目标指引。


如果你工作积极,是那种为自己而工作的人,你就在简历中体现,多么正常的建议,就好比,如果你是北大毕业的,那你就在简历中体现,没任何问题吧。


我可没有说让你去拼工作时长,装作工作积极,就好比我没有让你考北大一样。


你就不是这种类型的人,对吧,你连感同身受都做不到,激动个什么呢,还一大波人跟着喊666。


当然,我也理解大家的情绪,我还没毕业的时候,也在黑心企业待过,钱少事多尽煞笔,区别在于,我相对自驱力和自学能力强一些,通过自己的努力跳出了这个循环。


但大多数人还是被工作和生活推着走,所以对加班和内卷深恶痛绝,让本就辛苦的人生愈发艰难,而这种加班和内卷并没有带来收入的提升。


那问题来了,有人通过努力奋斗蒸蒸日上,有人的辛苦努力原地踏步,同样的,有的人看到建议觉得非常有用,有的人看到建议觉得何不食肉糜,区别在哪里呢?


究竟是资本作恶呢?还是自己能力不足呢?


那还要建议吗?


管清友不再打算给年轻人建议了,我觉得没必要。


虽然,大多数时候,那些听得进去建议的人大多不需要建议,而真正需要建议的又听不进,但是,那么多年轻人,总有一部分潜力股,有一些真正需要帮助的人。


他们可能因为环境等原因,有短暂的迷茫与不安,但是,来自前人发自真心的建议或许可以让他们坚定自己前进方向,从而走出不一样的人生。


就像当年我被乔布斯的那些话语激励过那般。


所以,嘲笑之人任其笑之,只要能帮助到部分人,那就有了价值。


因此,我不会停止给出那些我认为对于成长非常有帮助的建议。


(完)


作者:张鑫旭
来源:juejin.cn/post/7221487809789182008
收起阅读 »

KgCaptcha滑动拼图验证码在搜索中的作用

开头验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。验证码展示具体实现前端代码// 引入js<script src="captcha.js?appid=XX...
继续阅读 »

开头

验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。

验证码展示



具体实现

前端代码
// 引入js
<script src="captcha.js?appid=XXX"></script>
<script>
kg.captcha({
// 绑定弹窗按钮
button: "#captchaButton",

// 验证成功事务处理
success: function (e) {
// 验证成功,直接提交表单
// form1.submit();
console.log(e);
},

// 验证失败事务处理
failure: function (e) {
console.log(e);
},

// 点击刷新按钮时触发
refresh: function (e) {
console.log(e);
}
});
</script>

<a id="captchaButton"></a>



验证结果说明

 

字段名
数据类型描述
 

code
 

number
 

返回code信息
 

msg
 

string
 

验证结果信息
 

rid
 

number
 

用户的验证码应用id
 

sense
 

number
 

是否开启无感验证,0-关闭,1-开启
 

token
 

string
 

验证成功才有:token
 

weight
 

number
 

错误严重性,0正常错误,可以继续操作,1一般错误,刷新/重新加载拼图,2严重错误,错误次数过多拒绝访问


Python代码

from wsgiref.simple_server import make_server
from KgCaptchaSDK import KgCaptcha
def start(environ, response):
# 填写你的 AppId,在应用管理中获取
AppID = "AppId"
# 填写你的 AppSecret,在应用管理中获取
AppSecret = "AppSecret"
request = KgCaptcha(AppID, AppSecret)
# 填写应用服务域名,在应用管理中获取
request.appCdn = "https://cdn.kgcaptcha.com"
# 请求超时时间,秒
request.connectTimeout = 10
# 用户id/登录名/手机号等信息,当安全策略中的防控等级为3时必须填写
request.userId = "kgCaptchaDemo"
# 使用其它 WEB 框架时请删除 request.parse,使用框架提供的方法获取以下相关参数
parseEnviron = request.parse(environ)
# 前端验证成功后颁发的 token,有效期为两分钟
request.token = parseEnviron["post"].get("kgCaptchaToken", "") # 前端 _POST["kgCaptchaToken"]
# 客户端IP地址
request.clientIp = parseEnviron["ip"]
# 客户端浏览器信息
request.clientBrowser = parseEnviron["browser"]
# 来路域名
request.domain = parseEnviron["domain"]
# 发送请求
requestResult = request.sendRequest()
if requestResult.code == 0:
# 验证通过逻辑处理
html = "验证通过"
else:
# 验证失败逻辑处理
html = f"{requestResult.msg} - {requestResult.code}"
response("200 OK", [("Content-type", "text/html; charset=utf-8")])
return [bytes(str(html), encoding="utf-8")]
httpd = make_server("0.0.0.0", 8088, start) # 设置调试端口 http://localhost:8088/
httpd.serve_forever()


最后

SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

收起阅读 »