注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

原来微信小游戏用的技术就是web, 有想法的直接可以做

web
12月玩了2个微信小游戏, 发现都是在玩数值, 其实就是同一个游戏场景, 于是想自己写一个试试.然后看了微信小游戏文档, 推荐 cocos creator, 学了下发现 web 开发者那是根本不用学.自己写了2个demo, 于是分享给大家.cocos crea...
继续阅读 »

12月玩了2个微信小游戏, 发现都是在玩数值, 其实就是同一个游戏场景, 于是想自己写一个试试.

然后看了微信小游戏文档, 推荐 cocos creator, 学了下发现 web 开发者那是根本不用学.

自己写了2个demo, 于是分享给大家.

cocos creator

cocos creator 是个游戏引擎, 他推荐使用 vscode 和 chrome, 并且 ts 是唯一支持的脚本语言.

他的预览就是打开chrome的一个网页, 主体是个canvas, 这个场景下cc可能就是一系列资源的执行器.

重要的是他可以打包到微信小游戏, 也是微信小游戏推荐的框架.

也就是我可以用 ts 写小程序了.

其实也就是个 html5 的小游戏, 而 cc 包装了h5小游戏要手动写的requestAnimationFrame执行器, 提供了更方便的编辑器, 包装了一些游戏开发要用到的概念.

网页开发和游戏开发的区别

显然网页开发和游戏开发是不同的, 来稍作分析.

游戏元素

网页元素一般由div布局, 终端的节点一般是文字, 或者输入框.

游戏元素看起来容易一些, 因为没有输入. 手机小游戏只有通过点击来传达一些指令.

游戏元素也有布局, 但没网页 bfc, flex 这些复杂的东西, 全部绝对定位, 也有z轴.

再细看游戏元素, 其实每个元素就是个图片.

简单总结, 游戏的所有元素就是图片, 通过设置x, y, z的数值来定位. 比网页开发容易得多.

游戏交互

网页的功能主要是2个部分: 输入和展示.

所以网页的交互也就是改变参数后刷新列表.

我们来分析游戏的交互, 也分为2个部分: 改变位置与结算.

随着游戏的开始和玩家的点击, 其实就是元素的位置发生改变而已.

我们只要通过脚本控制元素的位置. 这些位置和具体游戏场景相关, cc 也会提供常用工具库.

另外一个是结算, 判断分数高低, 或者数组比较, 最多通过位置计算碰撞, 来判断游戏结果.

可以看到这些计算都是在脚本中进行的, 也都是比较简单的数据结构或者数学公式.

在游戏场景外, 一些菜单, 设置的界面就和网页差不多了.

cc 系统介绍

我看了一个视频, 自己写了2个demo, 简单总结下 cc 的系统.

总的来说, cc 像是个低代码平台.

编辑器界面

编辑器就是典型的低代码.

  • 场景界面. 就是把元素拖拖拽拽的地方.
  • 资源列表. 放代码和图片的地方, 就是网页开发的src目录. 资源的类型值得下文展开.
  • 节点层级. 在编辑场景的时候, 场景通常是有多个节点的, 节点之间有层级关系便于维护, 所以有个界面展示.
  • 节点属性. 在场景界面里选中节点, 肯定是可以编辑这个节点的属性的, 大小/位置什么的.

这些元素一看就是低代码了, 应该是低代码借鉴了这些游戏引擎的.

这些面板都是可以拖动位置, 或者合并成tab的, 很方便.

资源类型介绍

上面说到资源, 资源类型还挺多的. 这里介绍一点我用到的.

  • ts文件, 图片文件.

脚本文件和图片文件都是用来拖到节点里, 和节点绑定的.

  • 场景.

应该是 cc 的核心了. 从文件看来, 就是个 json. 所以拖拖拽拽的结果就是修改 json. 然后通过 json schema 执行渲染或打包.

场景是由节点组成的. 在场景里新建节点并嵌套, 来构建游戏场景.

节点的种类是很多的, 可以插入图片变为元素, 也可以绑定脚本, 作为一个"虚拟节点", 只是为了维护方便.

场景有必须的节点是 canvas 和 camera.

  • prefab.

可以理解为"组件". 在场景中编辑了一些节点, 如果觉得可以复用, 直接把整个节点拖到资源列表里, 就会产生一个 prefab. 使用的时候拖动这个 prefab 到场景, 就会产生一个实例了.

更多的应该是用脚本批量创建.

  • 动画.

其实和ts文件与图片文件一样, 是关联到节点上的. 但他是 cc 特有的, 可以在 cc 里编辑动画内容, 可以对各个属性做帧动画, 也可以导入动画软件做的动画.

开发流程

我写了2个算能跑的项目, 来说说开发的过程.

  1. 资源目录下新建一些文件夹: scripts, imgs, animation, scene.
  2. 主要开发就是编辑场景. 在场景里添加节点, 然后给节点贴图, 从"资源列表"把资源拖到"节点属性面板"就好了, 容易.

我的节点很简单, 就是玩家角色, 和背景.

  1. 建立个空节点, 写游戏逻辑. 具体操作是新建个 ts 文件, 然后拖到这个节点属性上.
  2. 游戏逻辑需要操作的内容, 包括动画, 都以"拖动"的方式关联到"节点属性面板"上.

这样就写好一个游戏了.

游戏逻辑开发是和 html5 游戏一样的, 最后一小节我再赘述下吧.

游戏逻辑编写

游戏逻辑在 ts 的脚本文件中编写.

所有新建的 ts 文件都会有一个初始模板. 内容是export class XXX extends cc.Component {}.

这个类有2个生命周期方法. start()update().

update()方法的参数deltatime是离上一帧的时间, 不了解的去看下 h5 游戏的执行就好了.

游戏逻辑一定涉及到元素, 只要在脚本文件里声明一个属性, 就能在节点属性面板上看到一个属性.

把这个属性需要控制的元素拖过去就行.

然后元素节点也可以绑定脚本. 这个脚本可以通过this.node提供的 api 来操作元素的位置.

元素节点一般会绑定动画, 也需要把动画声明在属性里, 然后从资源列表把动画拖动到自己的节点属性面板上, 就可以在脚本里调用动画了.

我现在理解的层级是这样的:

  • 总脚本gameControl写在单独节点里. 写游戏逻辑与结算判断.
  • 会动的元素, 自己绑定节点, 写一些方法供总脚本调用.
  • 编辑一些动画, 供上一步"会动的元素"调用. 一般是和元素位置的移动同时调用的.

贴一些代码

这里分享个具体的demo代码. demo内容很简单, 按方向键角色就会在地图上走路.

走路的时候会播放一个帧动画, 是从微信表情里导出的20个png.

脚本文件只有2个. 一个是gameControl游戏控制, 只做了监听键盘事件, 并调用player脚本的对应方法.

另一个player脚本写了对应的方法, 改变一些参数, 在update()方法根据参数来设置角色的位置.

gameControl.ts

import { _decorator, Component, Node, input, Input, EventKeyboard } from 'cc'
const { ccclass, property } = _decorator

import { player } from './player'

@ccclass('gameControl')
export class gameControl extends Component {
@property(player)
public player: player = null

start() {
input.on(Input.EventType.KEY_DOWN, (event) => {
switch (event.keyCode) {
case 37:
this.player.left()
break
case 38:
this.player.up()
break
case 39:
this.player.right()
break
case 40:
this.player.down()
break

}
})
}

update(deltaTime: number) {

}
}

player.ts

import { _decorator, Component, Node, Animation, tween, Vec3, math } from 'cc'
const { ccclass, property } = _decorator

@ccclass('player')
export class player extends Component {

@property(Animation)
anim: Animation = null

@property(Node)
lulu: Node = null

private direction = new Vec3(1, 0, 0)
private isMoving = false
private movePeriod = 0

start() {

}

update(deltaTime: number) {
if (this.isMoving) {
if (this.movePeriod < 1) {
let target = this.node.position
Vec3.add(target, this.node.position, this.direction)
this.node.setPosition(target)
this.movePeriod += deltaTime
} else {
this.isMoving = false
}
}
}

left() {
if (!this.isMoving) {
this.lulu.setRotationFromEuler(0, 0, 180)
this.direction = new Vec3(-1, 0, 0)
this.startMove()
}
}

right() {
if (!this.isMoving) {
this.lulu.setRotationFromEuler(0, 0, 0)
this.direction = new Vec3(1, 0, 0)
this.startMove()
}
}

up() {
if (!this.isMoving) {
this.lulu.setRotationFromEuler(0, 0, 90)
this.direction = new Vec3(0, 1, 0)
this.startMove()
}
}

down() {
if (!this.isMoving) {
this.lulu.setRotationFromEuler(0, 0, 270)
this.direction = new Vec3(0, -1, 0)
this.startMove()
}
}

startMove() {
this.anim.play()
this.isMoving = true
this.movePeriod = 0
}
}

另外做的demo是跟着cocos creator 文档的2d游戏做的. 有兴趣的也可以跟我一样, 先照着这个做一遍, 再自己新建个空项目自己操作.

最后

我认为 cocos creator 对 web 开发者来说真的是非常好上手了.

我认为小游戏的设计分为2个吧. 核心游戏场景, 与, 游戏运营.

其实核心游戏场景都不复杂的, 那怎么能让玩家一直玩呢.

其实就是策划运营, 操作一些数据, 让每次玩同一个场景, 看到不同的数字, 和不同的皮肤.

用户就会为了这些数字(pay for ability), 和皮肤(pay for love)来付费了.

我认为游戏脚本不难. 难在2点:

  1. 游戏的完整度, 需要美术和动画, 程序只能控制角色的位置, 加上动画才让人有操作角色的感觉. 精美的游戏场景也能让玩家觉得真实.
  2. 策划: 数值系统, 货币系统, 奖励系统, 活动这些. 让玩家重复玩同一个场景几百遍还觉得自己在成长, 真是牛逼.


作者:nujnewnehc
来源:juejin.cn/post/7456805812045725734
收起阅读 »

海康摄像头 web 对接

web
真的烦躁,一个月拿着死工资,每天写着增删改查,不知道以后能做什么,有时候真的想离职,进广东电子厂.... 这段时间,XXXX 要加一个海康监控,哎。 苦命开发 从官网下载下来的web包 \webs\codebase 目录中有,第一个是插件,必须安装的, 后面...
继续阅读 »

真的烦躁,一个月拿着死工资,每天写着增删改查,不知道以后能做什么,有时候真的想离职,进广东电子厂....


这段时间,XXXX 要加一个海康监控,哎。


苦命开发


从官网下载下来的web包


image.png


\webs\codebase 目录中有,第一个是插件,必须安装的, 后面两个JS文件是开发必要的。还要一个 JQ的,它内部使用了jq


image.png


初始化插件


引入了提供的jS后,就可以开始牛马了。。。。


首先注册插件,并检查更新


因为我这是4个摄像头,所以窗口是 2 * 2


WebVideoCtrl.I_InitPlugin 是用于初始化插件,成功回调是cbInitPluginComplete


WebVideoCtrl.I_CheckPluginVersion 用于检查更新。


在自动登录这里,我准备了数组,包含登录端口密码等信息,建议每一个之后都要等1秒。多个账号登录,插件只加载一次即可。


init() {
// 这里的代码会在文档完全加载后执行
WebVideoCtrl.I_InitPlugin({
iWndowType: 2, // 设置分屏类型为 2*2,显示 4 个窗口
bWndFull: true, // 支持单窗口双击全屏
bDebugMode: true, // 关闭调试模式
cbInitPluginComplete: async () => {
console.log("插件初始化完成")
try {
// 加载插件
await WebVideoCtrl.I_InsertOBJECTPlugin("divPlugin")
// 检查插件是否最新
const bFlag = await WebVideoCtrl.I_CheckPluginVersion()
if (bFlag) {
alert("检测到新的插件版本,双击开发包目录里的HCWebSDKPlugin.exe升级!")
}

for (const item of this.channel) {
// 自动登陆
this.clickLogin(item)
await new Promise(resolve => setTimeout(resolve, 1000))
}
} catch {
alert("插件初始化失败,请确认是否已安装插件;如果未安装,请双击开发包目录里的HCWebSDKPlugin.exe安装!")
}
},
iTopHeight: 0 // 插件窗口的最高高度
})
}

实现登录


WebVideoCtrl.I_Login 是登录接口



  • 参数1:ip地址

  • 参数2:1 是http,2 是https

  • 参数3:端口

  • 参数4:平台账户

  • 参数5:平台密码


// 登陆
clickLogin(item) {
WebVideoCtrl.I_Login(item.ip, 1, item.port, 'admin', 'admin123', {
timeout: 3000,
success: () => {
console.log('登陆成功')
setTimeout(() => {
setTimeout(() => {
this.getChannelInfo(item)
}, 1000)
}, 10)
},
error: (oError) => {
if (this.ERROR_CODE_LOGIN_REPEATLOGIN === oError.errorCode) {
console.log('已登录过!')
} else {
console.log(" 登录失败!", oError.errorCode, oError.errorMsg)

}
}
})
}

获取通道信息


getChannelInfo 函数需要传递一个当前控制摄像头的信息对象。


模拟通道接口:WebVideoCtrl.I_GetAnalogChannelInfo


这里会使用 jq的一些方法,会对获取的xml元素进行遍历,并将获取的信息,加入到数组集合中,进行预览视频。



  • id:获取的通道号是预览的必要字段。

  • 数字通道:支持高清甚至超高清分辨率,如 1080P、2K、4K 等,但是对网络要求较高

  • 零通道:无法播放,坏掉了。

  • 模拟通道:成本小,实时性高。


// 初始化通道
getChannelInfo(item) {
// 模拟通道
WebVideoCtrl.I_GetAnalogChannelInfo(item.ip, {
success: (xmlDoc) => {
const oChannels = $(xmlDoc).find('VideoInputChannel')
$.each(oChannels, (i, channelObj) => {
let id = $(channelObj).find('id').eq(0).text(),
name = $(channelObj).find('name').eq(0).text()
if ("" === name) {
name = "Camera " + (i < 9 ? "0" + (i + 1) : (i + 1))
}
const ch = this.channel.find(arr => arr.ip === item.ip)
ch.channelId = id
ch.name = name
})
console.log(item.ip + '获取模拟通道成功!')

},
error: function (oError) {
console.log(ip + '获取模拟通道失败!', oError.errorCode, oError.errorMsg)

}
})
// 数字通道
WebVideoCtrl.I_GetDigitalChannelInfo(item.ip, {
success: function () {
// console.log(item.ip + '获取数字通道成功!')
},
error: function (oError) {
// console.log(item.ip + '获取数字通道失败!', oError.errorCode, oError.errorMsg)
}
})
// 零通道
WebVideoCtrl.I_GetZeroChannelInfo(item.ip, {
success: function () {
// console.log(item.ip + '获取零通道成功!')
},
error: function (oError) {
// console.log(item.ip + '获取零通道失败!', oError.errorCode, oError.errorMsg)
}
})
// 直接预览
this.clickStartRealPlay(item)
}

预览窗口


clickStartRealPlay 函数需要传递一个当前控制摄像头的信息对象。


WebVideoCtrl.I_GetWindowStatus 可以获取窗口的状态,比如传递 0 ,可以查看 第一个窗口的状态。返回值如果不是null,表示在播放了。


WebVideoCtrl.I_Stop 用于关闭当前播放的窗口,参数 iWndIndex 用于控制关闭的那个窗口,默认会根据当前选中的窗口。


WebVideoCtrl.I_StartRealPlay 预览视频



  • 参数一:ip地址 + 下划线 + 端口,拼接的字符串,比如:'192.168.1.101_80'

  • 参数二:是码流,1 主码流,2 子码流

  • 参数三:是前面通过通道获取的通道ID

  • 参数四:默认是false,表示是否播放零通道

  • 参数五:RTSP端口号


// 预览窗口
clickStartRealPlay(item) {
const ips = item.ip + '_' + item.port
// 获取窗口的状态
const oWndInfo = WebVideoCtrl.I_GetWindowStatus(item.g_iWndIndex)
const iRtspPort = ''
const iChannelID = item.channelId
const bZeroChannel = item.zeroType
const szInfo = ''

const startRealPlay = function () {
WebVideoCtrl.I_StartRealPlay(ips, {
iWndIndex: item.g_iWndIndex,
iStreamType: 1,
iChannelID: iChannelID,
bZeroChannel: bZeroChannel,
iPort: iRtspPort,
success: function () {
console.log(ips + '开始预览成功!')
},
error: function (oError) {
console.log(ips + " 开始预览失败!", oError.errorCode, oError.errorMsg)
}
})
}

if (oWndInfo != null) { // 已经在播放了,先停止
WebVideoCtrl.I_Stop({
success: function () {
startRealPlay()
}
})
} else {
startRealPlay()
}
}

摄像头功能控制


接口:WebVideoCtrl.I_PTZControl



  • 参数一:操作类型(1-上,2-下,3-左,4-右,5-左上,6-左下,7-右上,8-右下,9-自转,10-调焦+, 11-调焦-, 12-F聚焦+, 13-聚焦-, 14-光圈+, 15-光圈-

  • 参数二:true 停止,false 启动

  • 参数三:对象:iWndIndex 窗口号,默认为当前选中窗口,iPTZSpeed 云台速度,默认为4


<div class="jiu" :style="{display: isOpen ? 'flex': 'none'}">
<div class="remote-control">
<el-tooltip content="向左上转动" placement="top-start" effect="light">
<div class="button top-left" @mousedown="mouseDownPTZControl(5, false)"
@mouseup="mouseDownPTZControl(1, true)">
</div>
</el-tooltip>
<el-tooltip content="向上转动" placement="top-start" effect="light">
<div class="button" @mousedown="mouseDownPTZControl(1, false)"
@mouseup="mouseDownPTZControl(1, true)">

<i class="iconfont icon-shangjiantou1"></i>
</div>
</el-tooltip>
<el-tooltip content="向右上转动" placement="top-start" effect="light">
<div class="button top-right" @mousedown="mouseDownPTZControl(7, false)"
@mouseup="mouseDownPTZControl(1, true)">
</div>
</el-tooltip>
<el-tooltip content="向左转动" effect="light">
<div class="button" @mousedown="mouseDownPTZControl(3, false)"
@mouseup="mouseDownPTZControl(1, true)">

<i class="iconfont icon-zuojiantou"></i>
</div>
</el-tooltip>
<el-tooltip content="开启自动旋转" effect="light">
<div class="button center" @click="mouseDownPTZControl(9, false)">
<i class="iconfont icon-zidongxuanzhuan"></i>
</div>
</el-tooltip>
<el-tooltip content="向右转动" effect="light">
<div class="button" @mousedown="mouseDownPTZControl(4, false)"
@mouseup="mouseDownPTZControl(1, true)">

<i class="iconfont icon-youjiantou"></i>
</div>
</el-tooltip>
<el-tooltip content="向左下转动" effect="light">
<div class="button bottom-left" @mousedown="mouseDownPTZControl(6, false)"
@mouseup="mouseDownPTZControl(1, true)">
</div>
</el-tooltip>
<el-tooltip content="向下转动" effect="light">
<div class="button" @mousedown="mouseDownPTZControl(2, false)"
@mouseup="mouseDownPTZControl(1, true)">

<i class="iconfont icon-xiajiantou1"></i>
</div>
</el-tooltip>
<el-tooltip content="向右下转动" effect="light">
<div class="button bottom-right" @mousedown="mouseDownPTZControl(8, false)"
@mouseup="mouseDownPTZControl(1, true)">
</div>
</el-tooltip>
</div>
</div>
<!-- 下方操作按钮 -->
<div class="div-group" :style="{display: isOpen ? 'block': 'none'}">
<div style="display: flex; justify-content:space-around;">
<el-button-group>
<el-tooltip content="焦距变大" placement="top" effect="light">
<div class="btn-groups" @mousedown="mouseDownPTZControl(10, false)" @mouseup="mouseDownPTZControl(11, true)">
<i class="iconfont icon-fangdajing-jia"></i>
</div>
</el-tooltip>
<el-tooltip content="焦距变小" placement="top" effect="light">
<div class="btn-groups" @mousedown="mouseDownPTZControl(11, false)" @mouseup="mouseDownPTZControl(11, true)">
<i class="iconfont icon-fangdajing-jian"></i>
</div>
</el-tooltip>
</el-button-group>
<el-button-group>
<el-tooltip content="焦点前调" placement="top" effect="light">
<div class="btn-groups" @mousedown="mouseDownPTZControl(12, false)" @mouseup="mouseDownPTZControl(12, true)">
<i class="iconfont icon-jiaodianqiantiao"></i>
</div>
</el-tooltip>
<el-tooltip content="焦点后调" placement="top" effect="light">
<div class="btn-groups" @mousedown="mouseDownPTZControl(13, false)" @mouseup="mouseDownPTZControl(12, true)">
<i class="iconfont icon-jiaodianhoutiao"></i>
</div>
</el-tooltip>
</el-button-group>
<!-- <el-button-group>
<el-tooltip content="光圈扩大" placement="top" effect="light">
<el-button>
<i class="iconfont icon-guangquankuoda"></i>
</el-button>
</el-tooltip>
<el-tooltip content="光圈缩小" placement="top" effect="light">
<el-button>
<i class="iconfont icon-guangquansuoxiao"></i>
</el-button>
</el-tooltip> -->

</el-button-group>
</div>
</div>

mouseDownPTZControl(iPTZIndex, selection) {
// 获取窗口状态
const oWndInfo = WebVideoCtrl.I_GetWindowStatus(this.item.g_iWndIndex)
if (oWndInfo == null) {
return
}

// 如果是零通道,直接返回
if (this.item.zeroType) {
return
}
let iPTZSpeed = selection ? 0 : 4
// 表示开启了自动
if (9 === iPTZIndex && this.g_bPTZAuto) {
// 将速度置为 0
iPTZSpeed = 0

} else {
this.g_bPTZAuto = false
}

// 控制云平台
WebVideoCtrl.I_PTZControl(iPTZIndex, selection, {
iWndIndex: this.item.g_iWndIndex, iPTZSpeed,
success: (xmlDoc) => {
if (9 == iPTZIndex) {
this.g_bPTZAuto = !this.g_bPTZAuto
}
},
error: function (oError) {
console.log(oWndInfo.szDeviceIdentify + " 开启云台失败!", oError.errorCode, oError.errorMsg)
}
})

}

到此就结束了,海康这个还不错,就是没有vue webpack的包,在webpack 的环境下,是会报错的。


作者:哪里的破水瓶
来源:juejin.cn/post/7449644683330240549
收起阅读 »

我:偷偷告诉你,我们项目里的进度条,全都是假的!🤣 产品:???😲

web
扯皮 最近接到了一个需求:前端点击按钮触发某个任务并开启轮询获取任务进度,直至 100% 任务完成后给予用户提示 这个业务场景还挺常见的,但是突然上周后端联系到我说现在的效果有点差,之前都是小任务那进度条展示还挺不错的,现在有了一些大任务且会存在排队阻塞的情况...
继续阅读 »

扯皮


最近接到了一个需求:前端点击按钮触发某个任务并开启轮询获取任务进度,直至 100% 任务完成后给予用户提示


这个业务场景还挺常见的,但是突然上周后端联系到我说现在的效果有点差,之前都是小任务那进度条展示还挺不错的,现在有了一些大任务且会存在排队阻塞的情况,就导致视图上经常卡 0% 排队,用户体验太差了,问能不能在刚开始的时候做个假进度先让进度条跑起来😮


因此就有了这篇文章,简单做一下技术调研以及在项目中的应用


正文


其实假进度条也不难做,无非是轮询的时候我们自己做一个随机的自增,让它卡到 99% 等待后端真实进度完成后再结束


只不过还是想调研一下看看市面上有没有一些成熟的方案并去扒一下它们的源码🤓


NProgress


首先当我听到这里的需求后第一时间想到的就是它:rstacruz/nprogress: For slim progress bars like on YouTube, Medium, etc


记得大学期间做的一些中后台系统基本都少不了路由跳转时的顶部进度条加载,那时候就有了解到 NProgress,它的使用方式也很简单,完全手控:NProgress: slim progress bars in JavaScript,去文档里玩一下就知道了


视图呈现的效果就是如果你不手动结束那它就会一直缓慢前进卡死 99% ,挺符合我们这里的需求,可以去扒一下它内部进度计算相关的逻辑


NProgress 的内容实际上比较少,源码拉下来可以看到主要都在这一个 JS 文件里了:


image.png


需要注意的是我们看的是这个版本:rstacruz/nprogress at v0.2.0,master 分支与 npm 安装的 0.2.0 内部实现还是有些差别的


我们这里不关注它的样式相关计算,主要来看看对进度的控制,直奔 start 方法:


image.png


还是比较清晰的,这里的 status 就是内部维护的进度值,默认为 null,所以会执行 NProgress.set,我们再来看看 set 方法:


image.png


set 方法里有一大堆设置动画样式逻辑都被我剪掉了,关于进度相关的只有这些。相当于利用 clamp 来做一个夹层,因为初始进来的 n 为 null,所以经过处理后进度变为 0.08


再回到 start 的逻辑,其中 work 就是内部轮询控制进度自增的方法了,初始配置 trickle 为 true 代表自动开启进度自增,由于进度条在 set 方法中已经设置为 0.08,所以走到后面的 NProgress.trickle 逻辑


image.png


看来这里就是进度控制的核心逻辑了, trickle 里主要调用了 inc,在 trickle 中给 inc 传递了一个参数:Math.random() * Settings.trickleRate,显然这里范围是:0 <= n < 0.02


而在 inc 中,如果传递的 amount 有值的话那就每次以该值进行自增,同时又使用 clamp 将最大进度卡在 0.994


最后再调用 set 方法,set 里才是更新进度和视图进度条的方法,涉及到进度更新时都需要回到这里


当然 NProgress.inc 也可以手动调用,还对未传参做了兼容处理:



amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95)



即根据当前进度 n 计算剩余进度,再随机生成自增值


再来看 done 方法,它就比较诡异了:


image.png


按理来说直接将进度设置为 1 就行,但它以链式调用 inc 再调用 set,相当于调用了两次 set


而这里 inc 传参又没什么规律性,推测是为了 set 中的样式处理,感兴趣的可以去看看那部分逻辑,还挺多的...😶


一句话总结一下 NProgress 的进度计算逻辑:随机值自增,最大值限制


但是因为 NProgress 与进度条样式强绑定,我们肯定是没法直接用的


fake-progress


至于 fake-progress 则是我在调研期间直接搜关键词搜出来的😶:piercus/fake-progress: Fake a progress bar using an exponential progress function


很明显看介绍就是干这个事的,而且还十分专业,引入数学函数展示假进度条效果,具有说服力:


image.png


所以我们项目中其实就是用的这个包,只不过和 NProgress 类似,两个包都比较老了,瞟一眼源码发现都是老 ES5 了🤐


因为我们项目中用的是 React,这里给出 React 的 demo 吧,为了编写方便使用了几个 ahooks 里的 hook:


image.png


其实使用方法上与 NProgress 都类似,不过两者都属于通用的工具库不依赖其他框架,所以像视图渲染都需要自己手动来做


注意实例化中的传参 timeConstant,某种意义上来讲这个值就相当于“进度增长的速率”,但也不完全等价,我们来看看源码


因为不涉及到样式,fake-progress 源码更简单,核心就在这里:


image.png


下方的数学公式就是介绍图中展示的,只能说刚看到这部分内容是真的是死去的数学只是突然又开始攻击我😅,写了那么多函数,数学函数是啥都快忘了


我们来简单分析一下这个函数 1 - Math.exp(-1 * x),exp(x)= exe^xexe^x 的图像长这样,高中的时候见的太多了:


image.png


那假如这里改成 exp(-x) 呢?有那味了,以前应该是有一个类似的公式 f(x)f(x)f(x)f(-x) 图像效果是关于 y 轴对称,好像是有些特殊的不符合这个规律?🤔反正大部分都是满足的


image.png


OK,那我们继续进行转换,看看 -exp(-x) 的效果


同样有个公式 f(x)f(x)f(x)-f(x) 图像效果是关于 x 轴对称:


image.png


初见端倪,不知道你们有没有注意 -exp(-x) 最终呈现的图像是无限接近于 x 轴的,也就是 0:


image.png


那有了🤓,假如我再给它加个 1 呢?它不就无限接近于 1 了,即 -exp(-x) + 1,这其实就是 fake-progress 里公式的由来:
image.png


但你会发现如果 x 按 1 递增就很快进度就接近于 1 了,所以有了 timeConstant 配置项来控制 x 的增长,回看这个公式:1 - Math.exp(-1 * this._time / this.timeConstant)


this._time 是一直在增长的,而 this.timeConstant 作为分母如果被设置为一个较大的值,那可想而知进度增长会巨慢


所以 fake-progress 的核心原理是借助数学函数,以函数值无限接近于 1 来实现假进度条,但是这种实现有一个 bug,可以看我提的这个 issues,不过看这个包的更新时间感觉作者也不会管了😅:


bug: progress may reach 100% · Issue #7 · piercus/fake-progress


image.png


useFakeProgress


虽然我们现在项目中使用的是 fake-progress,但是个人感觉用起来十分鸡肋,而且上面的 bug 也需要自己手动兼容,因此萌生出自己封装一个 hook 的想法,让它更符合业务场景


首先我们确定一下进度计算方案,这里我毫不犹豫选择的是 NProgress 随机值增长方案,为什么?因为方便用户自定义


而且 NProgress 相比于 fake-progress 有一个巨大优势:手动 set 进度后仍然保持进度正常自动递增


这点在 fake-progress 中实现是比较困难的,因为你无法保证手动 set 的进度是在这个函数曲线上,相当于给出函数 y 值反推 x 值,根据反推的 x 值再进行递增,想想都麻烦


确定好方案后我们来看下入参吧,参考 NProgress 我定义了这几个配置项:


image.png


这里我简单解释一下 rerender 和 amount 配置:


实际上在封装这个 hook 的时候我一直在纠结这里的 progress 到底是 state 还是 ref,因为大多数场景下 hook 内部通过轮询定时器更新进度,而真实业务代码中也会开启定时器去轮询监听业务接口的


所以如果写死为 state,那这个场景 hook 内部的每次更新 render 是没必要的,但是假如用户又想只是使用假进度展示,没有后端业务接口呢?


思来想去其实完全可以放权给用户进行配置,因为 state = ref + update,统一使用 ref,用户配置 rerender 时我们在每次更新时 update 即可


至于 amount 我是希望放权给用户进行自定义递增值,你可以配置成一个固定值也可以配置成随机值,更可以像 NProgress master 分支下这样根据当前进度来控制自增,反正以函数参数的形式能够拿到当前的 progress:


image.png


至于实现细节就不再讲述了,实际上就是轮询定时器没什么复杂的东西,直接上源码了:


import { useRef, useState } from "react";

interface Options {
minimun?: number;
maximum?: number;
speed?: number;
rerender?: boolean;
amount?: (progress: number) => number;
formatter?: (progress: number) => string;
onProgress?: (progress: number) => void;
onFinish?: () => void;
}

export function useFakeProgress(options?: Options): [
{ current: string },
{
inc: (amount?: number) => void;
set: (progress: number) => void;
start: () => void;
stop: () => void;
done: () => void;
reset: () => void;
get: () => number;
}
] {
const {
minimun = 0.08,
maximum = 0.99,
speed = 800,
rerender = false,
amount = (p: number) => (1 - p) * clamp(Math.random() * p, minimun, maximum),
formatter = (p: number) => `${p}`,
onProgress,
onFinish,
} = options || {};

const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const progressRef = useRef(0);
const progressDataRef = useRef(""); // formatter 后结果
const [, update] = useState({});

const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);

const setProgress = (p: number) => {
progressRef.current = p;
progressDataRef.current = formatter(p);
onProgress?.(p);
if (rerender) update({});
};

const work = () => {
const p = clamp(progressRef.current + amount(progressRef.current), minimun, maximum);
setProgress(p);
};

const start = () => {
function pollingWork() {
work();
timerRef.current = setTimeout(pollingWork, speed);
}

if (!timerRef.current) pollingWork();
};

const set = (p: number) => {
setProgress(clamp(p, minimun, maximum));
};

const inc = (add?: number) => {
set(progressRef.current + (add || amount(progressRef.current)));
};

const stop = () => {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = null;
};

const reset = () => {
stop();
setProgress(0);
};

const done = () => {
stop();
setProgress(1);
onFinish?.();
};

return [
progressDataRef,
{
start,
stop,
set,
inc,
done,
reset,
get: () => progressRef.current,
},
];
}

这里需要补充一个细节,在返回值里使用的是 progressDataRef 是 formatter 后的结果为 string 类型,如果用户想要获取原 number 的 progress,可以使用最下面提供的 get 方法拿 progressRef 值


一个 demo 看看效果,感觉还可以:
image.png


fake-progress.gif


当然由于直接返回了 ref,为了防止用户篡改可以再上一层代理劫持,我们就省略了


这也算一个工具偏业务的 hook,可以根据自己的业务来进行定制,这里很多细节都没有补充只是一个示例罢了🤪


End


以上就是这篇文章的内容,记得上班之前还在想哪有那么多业务场景需要封装自定义 hook,现在发现真的是各种奇葩需求都可以封装,也算是丰富自己武器库了...


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

差生文具多,这么些年,为了写代码我花了多少钱?

背景 转眼写代码有10多年了,林林总总花费了很多的钱,现在主要按照4大件来盘点下我都买了啥。 电脑 acer 4741g ¥4500+ 这是我入门时的一款电脑,整体配置在当时还是属于中等的。 当时用的编辑器还是notepad++,在这个配置下,还是可以愉...
继续阅读 »

背景


转眼写代码有10多年了,林林总总花费了很多的钱,现在主要按照4大件来盘点下我都买了啥。


电脑


image.png


acer 4741g ¥4500+


ceE4n8in48Ac.jpg

这是我入门时的一款电脑,整体配置在当时还是属于中等的。

image.png

当时用的编辑器还是notepad++,在这个配置下,还是可以愉快的编码的。


mac air 2013 ¥8800+


image.png

image.png

image.png


当时被苹果的放进信封的广告创意所折服,这也是我的第一台apple,在之后就一直用苹果了。到手后的感觉是,薄,确实薄,大概只有我宏基的1/3-1/4厚。


当时apple的简洁,快速,很少的配置,让我在环境变量上苦苦挣扎的心酸得以释放。以后也不用比较各种笔记本参数了


mac book pro13 2015 ¥9000+


image.png

image.png


当时买这台的原因是因为air进水了,经常死机,修了2次后,又坏了。一怒之下,直接买了一台。


换了新的retina屏之后,色彩质量和效果都提升了不少,对比原来的air性能也是拉升了超级多。但是因为是16上半年买的,所以没体验到toch bar,到现在都没体验过。。。


这是我真正意义上的第一台十分满意的电脑,大概是当时的理想型了。


公司电脑


2016年下半年进了一家创业公司,公司配置了mac book pro,比我的配置还高,所以之后一直就是用公司的。


2021年换新公司,公司配了thinkpad,又一次开始用win。然后又被win各种打败,有时又有了换回mac的想法。


当前–mac book pro14 2021 ¥21999


image.png


主要入手的原因是公司的电脑我觉得太慢了。当时开发小程序,脚手架是公司自己的,每次打包都是全量的,没有缓存。所以每次打包短则7,8分钟,长则10多分钟。加上切分支/安装依赖(如果两个分支依赖版本不同,需要强制更新),导致我每天花费大量的时间等待上。


同事早于我入手了M1,反馈巨好,于是我也买了,想着配置拉的满点,但是还是高估了自己的钱包,低估了苹果的价格,只能退而求其次的选择了中档配置。


image.png

每次看着低低的负载,都是满满的安全感。

image.png

另外m1是支持stable diffusion的,所以偶尔我也会炼丹


显示器


显示器.png


dell U2424H ¥1384


image.png

其实在写代码之前也买过几台显示器,但是以程序员视角来说,第一台是这台。原因是当时公司也是这个型号,主要是能旋转,谁能拒绝一台自由旋转的显示器呢?

image.png


而且dell的质量和做工都不错,在当时是十分喜欢的。


小米 Redmi 27 ¥789.9


image.png


dell那台显示器是放在家里的,公司也需要显示器,而且自带设备每个月可以补贴100,所以就入手了这款,原因无他:便宜,也够大。


但是用久了,发现也有些问题。例如失真等,但是真的便宜,


厂家送寄,但因为合作内容没谈拢,本周寄回


键盘


当前-cherry G80-3000 ¥689


image.png


一把真正可以用到包浆的键盘,大多数看到这个键盘的感觉应该都是黄色,而不是原本的白色,不知道是不是材质的问题,极其容易变黄。同时由于键帽又不变黄,所以呈现了诡异的脏脏的颜色。


因为本身机械键盘的高度,所以建议加个手托比较好。各种轴也齐全,任君选择。

目前这个键盘在家里游戏了,毕竟是个全键盘


当前–京造C2 ¥253


image.png

选择这个键盘的原因嘛,同事有了,并且是一个带灯的键盘。手感比cherry硬一些,但还属于是能接受的程度,整体延迟比较低(也可能是因为有线的原因)。目前是在办公室使用的一款,当前这篇文章就是用这个敲出来的。


鼠标


image.png


总览


鼠标其实留在手边的不太多,大多数都是消耗品了,这么些年,各种有用过。大概用了不下10个鼠标,我只挑2个重点的说吧。


微软ie 3.0 ¥359


image.png


这是我用过最好的鼠标,没有之一。握感极佳,用久了也不累,比其他的鼠标都舒服万分。


当前–apple magic trapad ¥899


image.png

mac用户的最终归属就是板子,如果你刚开始用mac,那么建议直接用板子吧。支持原生手势操作,各种mac本身触控板的事情都完美适用,真正的跟你的电脑和为一体。


欢迎评论区留言你的设备


如上所述,我这年的大头是电脑,消耗品是鼠标、,那么你都花了多少钱呢?


作者:天元reborn
来源:juejin.cn/post/7395473411651682343
收起阅读 »

2025年,前端开发为什么一定要学习Rust?

web
引言Rust语言是一门现代系统编程语言,由Mozilla Research于2009年开始开发,Mozilla Research 是 Mozilla 基金会旗下的一个研究部门,专注于推动开放网络和创新技术的发展,Rust语言正是在 Mozilla Resear...
继续阅读 »

引言

Rust语言是一门现代系统编程语言,由Mozilla Research于2009年开始开发,Mozilla Research 是 Mozilla 基金会旗下的一个研究部门,专注于推动开放网络和创新技术的发展,Rust语言正是在 Mozilla Research 中孕育并发展的。

Rust 最早是 Mozilla 雇员 Graydon Hoare 的个人项目,在2006年开始了Rust语言的初步设计,Mozilla 随后投入资源,支持Rust的发展,并最终于2010年公开这个项目,2015年发布1.0版本。

以下引用自Rust 语言圣经

大家可能疑惑 Rust 为啥用了这么久才到 1.0 版本?与之相比,Go 语言 2009 年发布,却在 2012 年仅用 3 年就发布了 1.0 版本[^1]。

● 首先,Rust 语言特性较为复杂,所以需要全盘考虑的问题非常多;

● 其次,Rust 当时的参与者太多,七嘴八舌的声音很多,众口难调,而 Rust 开发团队又非常重视社区的意见;

● 最后,一旦 1.0 快速发布,那绝大部分语言特性就无法再被修改,对于有完美强迫症的 Rust 开发者团队来说,某种程度上的不完美是不可接受的。

因此,Rust 语言用了足足 6 年时间,才发布了尽善尽美的 1.0 版本。

大家知道 Rust 的作者到底因为何事才痛下决心开发一门新的语言吗?

说来挺有趣,在 2006 年的某天,作者工作到精疲力尽后,本想回公寓享受下生活,结果发现电梯的程序出 Bug 崩溃了,要知道在国外,修理工可不像在中国那样随时待岗,还要知道,他家在 20 多楼!

最后,他选择了妥协,去酒店待几天等待电梯的修理。

当然,一般人可能就这样算了,毕竟忍几天就过去了嘛。但是这名伟大的程序员显然也不是一般人,他面对害他流离失所的电梯拿起了屠龙宝刀 - Rust。

自此,劈开一个全新的编程世界。

深入了解Rust

为什么要创建Rust这门语言?

在 Rust 出现之前,系统级编程领域主要由 C 和 C++ 统治。虽然这两种语言在性能方面表现出色,但它们也存在一些固有的缺陷,促使了 Rust 的诞生。

什么是系统级编程语言?

简单来说,系统级编程语言用于开发操作系统、驱动程序、嵌入式系统、游戏引擎、数据库等对性能和硬件控制要求极高的软件。

有以下特性:

  • 硬件访问: 系统级语言需要能够直接访问硬件资源,直接操作硬件
  • 高性能: 系统级程序通常需要直接操作硬件,对性能要求非常高。因此,系统级语言通常具有高效的内存管理机制和优化的编译器,以生成高效的机器码。
  • 较强的类型系统和编译时检查:为了尽早发现潜在的错误,系统级语言通常具有较强的类型系统和编译时检查机制,以提高代码的可靠性和安全性。
  • 并发和并行: 现代计算机系统通常具有多核处理器,系统级程序需要能够有效地利用多核资源,实现并发和并行执行,以提高性能。
  • 内存控制: 系统级编程需要对内存进行精细的控制,包括内存分配、释放、布局等。一些系统级语言允许开发者直接操作内存地址,以实现更高的灵活性和效率

有哪些系统级编程语言?

  • C/C++

    无GC,性能高,内存不安全

  • Rust

    无GC,性能高,内存安全

  • Go

    有GC,性能不如Rust,安全性不如Rust。

  • Assembly Language(汇编语言)

    性能高,开发效率低

  • zig

    无GC,性能高,安全性不如Rust,发展初期

C/C++ 的缺陷

  • 内存安全问题: C/C++ 允许开发者手动管理内存,这虽然提供了灵活性,但也容易导致各种内存安全问题,如:
    • 空指针(Null Pointers): 访问未初始化的指针或空指针会导致程序崩溃。
    • 野指针(Wild Pointers): 指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。。
    • 悬垂指针(Dangling Pointers): 指针指向曾经存在的对象,但该对象已经被释放,再次访问该指针会导致未定义行为,悬垂指针是野指针的一种。
    • 双重释放(Double Free): 释放同一块内存两次,导致崩溃或不可预测的行为。
    • 内存泄漏(Memory Leaks): 分配的内存没有被及时释放,导致内存占用不断增加,最终可能导致系统崩溃。
    • 缓冲区溢出(Buffer Overflows): 向缓冲区写入超出其容量的数据,可能覆盖相邻的内存区域,导致程序崩溃或安全漏洞。
  • 并发安全问题: C/C++ 的并发编程容易引入数据竞争(Data Races)等问题,导致程序行为不确定,难以调试和维护。
  • 缺乏现代化的语言特性: C/C++ 的语法相对陈旧,缺乏一些现代化的语言特性,如模式匹配、类型推断等,使得代码编写和维护相对繁琐。

Rust 的创建正是为了解决 C/C++ 等语言的这些不足,同时保留其高性能的优点。具体来说,Rust 的设计目标是:

  • 解决内存安全问题: Rust 通过所有权系统、借用检查器等机制,在编译时就杜绝了空指针、野指针、数据竞争等内存安全问题。
  • 提供安全的并发编程: Rust 的所有权系统和类型系统也对并发安全提供了保障,使得开发者可以更容易地编写安全的并发程序。
  • 提供现代化的语言特性: Rust 引入了模式匹配、类型推断、trait 等现代化的语言特性,提高了代码的简洁性、可读性和可维护性。
  • 保持高性能: Rust 的设计理念是“零成本抽象”,即提供高级的抽象能力,但不会带来额外的运行时开销,且无需垃圾回收器等运行时机制,从而避免了额外的性能开销,媲美 C/C++。

简而言之,因为还缺一门无 GC 且无需手动内存管理、性能高、工程性强、语言级安全性、广泛适用性的语言,而 Rust 就是这样的语言。

为什么选择Rust语言?

  • 保证安全、内存占用小的同时能提供和C/C++ 一样的性能
  • 广泛的适用性,系统编程、网络服务、命令行工具、WebAssembly等场景都能应用
  • 生态渐渐完善,有大量的库和框架,完整的工程化开发工具链,强大的包管理
  • 社区非常活跃和友好,文档全面

Rust不是闭门造车的语言,能看出来设计者是做过大量不同语言的项目的人,Rust从解决实际问题出发,借鉴和融合其他语言的优点,又能够创新地提出所有权和生命周期,这个强大的能力带来了0开销的内存安全和线程安全。

凡事有利必有弊,Rust是站在前人的肩膀上,它集百家之长,借鉴了其他语言的许多优秀特性,如npm的包管理、go的channel来进行并发通信、Haskell的trait(类似java的接口),还有元组、泛型、枚举,闭包、智能指针这些特性并非rust原创,但Rust确实把这些优点全部吸收了进来,而没有做过度的设计和发散,让有一些其他语言基础的人还能够减轻一些上手成本。

即使这样,Rust依然是在一众语言里学习曲线最陡峭的语言之一,此外,Rust为了提高运行时性能必然是会牺牲一些编译时的效率的。

但是,这丝毫不会影响Rust成为一门伟大的语言,如果有一门语言可以改变你的编程思维方式,倒逼你进行更好的代码设计,让你在初学过程中连连发出“原来是这样啊”的感叹,那么它一定是Rust。

Rust 核心设计理念对前端有哪些启示

Rust的很多设计理念都可以在前端领域中或多或少地找到影子,让你明白前端某些技术为什么要这么设计,以及为什么不那么设计。

安全性设计

1. 类型安全

  • 静态类型检查:

    Rust 提供了一个非常强大的类型系统,确保了类型安全。在编译时,Rust 会强制要求所有变量和函数都有明确的类型声明。这使得很多潜在的错误能够在编译时被捕获,避免了运行时出现类型错误。

    反观JavaScript,作为js开发者,那是太有发言权了,由于js是动态类型语言,所以很多错误只能在运行时被发现,跑着跑着可能就出一个线上bug,这是多少前端开发者的痛啊。

    当在写了一段时间Rust后,我们就会明白TypeScript为什么会火了,以及TS为什么是必要的,TS不能解决所有问题,但能解决大部分低级问题。

    如果你不用TS,提高代码健壮性也是有方法的,只不过心智负担更重。参照文章接口一异常你的页面就直接崩溃了?

  • 不可变性和可变性:

    在 Rust 中,变量默认是不可变的,只有显式声明为可变 (mut) 才能修改。这种设计减少了错误发生的概率,因为不可变数据是线程安全的,不会在多个地方被修改。

    在JS中也有类似的设计,联想到ES6的const和let,const 只能保证变量引用不可变,但如果引用的是对象或数组,内容依然可以改变。可谓是相当鸡肋。

2. 内存安全

Rust 提供了一种独特的所有权系统来自动管理内存,避免了许多传统语言中常见的内存错误,如内存泄漏、悬垂指针和双重释放。

Rust 的内存安全设计包括以下几个方面:

  • 所有权系统:Rust 中的所有权系统确保每个资源只有一个所有者,而所有权可以转移。一旦所有权转移,原所有者无法再访问或修改该资源,资源离开作用域时会自动释放,这就避免了双重释放和内存泄漏的问题。
  • 借用检查器:Rust 的借用检查器确保在同一时间只能存在不可变借用或一个可变借用。这避免了并发情况下的内存冲突。
  • 生命周期:Rust 的生命周期系统确保引用的有效性,在编译时检查引用的生命周期与持有它的资源的生命周期是否匹配,从而防止悬空引用和野指针。

反观JavaScript 中的内存问题:

  • 内存泄漏(Memory Leak): 内存泄漏是指程序无法释放不再使用的内存,导致内存资源被浪费。在 JavaScript 中,由于垃圾回收机制,内存泄漏通常发生在以下几种情况:

    • 全局变量:

      全局变量是在全局作用域中声明的,它们在程序执行期间存在,直到程序结束时才会被销毁。因此,无论这些全局变量是否仍在使用,它们都将保持存在,无法被垃圾回收器回收。

        // 全局变量
      var globalVar = { name: 'example' };

      // 该对象即使没有被引用,仍然会存在,直到页面关闭

      在浏览器中,全局变量被视为 window 对象的属性,在 Node.js 中,则是 global 对象。

      垃圾回收器一般不会回收全局变量,原因之一是全局变量的清理通常意味着整个应用程序的关闭或重载。如果要强制回收全局变量,会导致额外的复杂性和性能开销。因此,大多数 JavaScript 引擎(如 V8)选择让全局变量一直存活。

      全局变量不仅会导致内存泄漏还有容易被意外覆盖的风险,尽量使用模块化、闭包等方式来避免将变量暴露到全局作用域中。

      同理,全局变量也会导致无法准确的进行treeshaking优化,因为全局变量是有副作用的。

    • 闭包(Closures)

      闭包可能会保持对外部函数作用域变量的引用,从而防止这些变量被回收。

      function createClosure() {
      let largeObject = new Array(1000000).fill('Memory leak');

      // 返回一个函数,访问 largeObject
      return function() {
      console.log(largeObject[0]);
      };
      }

      const closure = createClosure();

      // 使用完闭包后,显式清除引用
      closure = null; // 删除对闭包的引用,垃圾回收器可以回收 largeObject

      largeObject 是一个占用大量内存的对象。当我们调用 createClosure 时,它返回一个内部函数 closure,这个内部函数会引用 largeObject。尽管 largeObject 的生命周期在 createClosure 执行完之后结束,但由于 closure 仍然持有对 largeObject 的引用,这个对象就无法被垃圾回收器回收,从而导致内存泄漏。

      闭包本身不会引起内存泄漏,但如果闭包捕获了外部函数的引用,且这些引用长时间未清除,就可能导致内存泄漏。

  • 事件监听器

    如果没有正确移除事件监听器,可能导致无法释放关联的内存。

    如果我们为 DOM 元素注册了事件监听器,但没有在适当的时候移除它们,尤其是在元素被删除或不再需要时,事件监听器会一直保持对 DOM 元素的引用,从而防止垃圾回收。

    "my-element">Click me!


    <script>
    let element = document.getElementById('my-element');

    // 给 DOM 元素添加事件监听器
    function handleClick() {
    console.log('Element clicked');
    }

    element.
    addEventListener('click', handleClick);

    // 假设我们从 DOM 中移除了该元素
    document.body.removeChild(element);

    // 但是我们没有移除事件监听器,事件监听器仍然持有对该元素的引用
    // 因此该元素无法被垃圾回收
    script>

    需要手动清除事件监听器

    element.removeEventListener('click', handleClick);  // 移除事件监听器
    element = null; // 清除对 DOM 元素的引用
  • DOM 元素引用

    如果 DOM 元素的引用在不再需要时没有清除,垃圾回收机制也无法回收它们。

    当我们通过 DOM 操作获取并引用一个 DOM 元素时,如果该元素的引用没有及时清除,即使该元素已经被移除或不再需要,它也不会被垃圾回收,从而导致内存泄漏。

    "my-element">Hello, World!


    <script>
    // 获取 DOM 元素并保存引用
    let element = document.getElementById('my-element');

    // 动态移除该元素
    document.body.removeChild(element);

    // 但是我们没有清除 element 引用
    // 这个引用仍然指向已经从 DOM 树中移除的元素
    // 此时垃圾回收器无法回收这个元素,因为引用仍然存在
    script>

    可以使用 element = null 来清除引用,但这个操作需要手动执行,容易忘记。

v8的垃圾回收器

V8 中的GC采用标记清除法进行垃圾回收。主要流程如下:

  • 标记:从根对象开始,遍历所有的对象引用,被引用的对象标记为活动对象,没有被引用的对象(待清理)标记为垃圾数据。
  • 垃圾清理:将所有垃圾数据清理掉

在我们的开发过程中,如果我们想要让垃圾回收器回收某一对象,就将对象的引用直接设置为 null

let a = {}; // {} 可访问,a 是其引用

a = null; // 引用设置为 null
// {} 将会被从内存里清理出去

但如果一个对象被多次引用时,例如作为另一对象的键、值或子元素时,将该对象引用设置为 null 时,该对象是不会被回收的

let a = {};
let arr = [a];

a = null;
console.log(arr)
// [{}]

因为a被arr引用,即使a不被使用了,也不会被释放,除非arr也被设置为null。

JS也考虑到了这一点,在ES6中推出了: WeakMap和WeakSet 。它对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用(对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为)。

let a = {};
let arr = new WeakSet();
arr.add(a);

a = null;
console.log(arr.has(a))
// false

即 arr 对a的引用是弱引用,如果a不用了,不会阻止垃圾回收。

以上代码可以在控制台自行尝试一下

即便JS给出了可以避免特定场景的内存泄漏的方案,但依然无法避免所有场景的内存泄漏,而且就算你熟谙内存泄漏的各种场景以及对应解决方案,百密也终有一疏,更何况实际开发中代码能跑起来我们就几乎不会考虑啥内存问题,而Rust则强制你一定要考虑内存安全,否则编译都不过。

3. 并发安全

Rust 的并发模型通过其所有权和借用规则确保了并发编程中的安全性。Rust 中的并发安全设计包括:

  • 数据竞争防止:Rust 中,数据竞争在编译时就能被发现。Rust 通过所有权规则确保要么有多个不可变借用,要么有一个可变借用,从而避免了并发时对共享数据的非法访问。
  • 线程安全:Rust 使用 Send 和 Sync 特性来标识哪些类型可以在线程之间传递或共享。Send 允许数据在不同线程之间传递,Sync 允许多个线程共享数据。
  • 锁机制:Rust 提供了 Mutex 和 RwLock 等机制来确保在多线程环境下对共享资源的安全访问。

JS是单线程,但是JS的单线程是基于事件循环的非阻塞的,所以可以通过异步来实现伪并发,竞态条件、数据竞争、数据共享等问题在JS中是很难发现的,甚至别人的代码修改了你的数据你都不知道,没有一定的开发经验积累,去排查由此产生地莫名其妙的bug是相当折磨人的。

当学习了Rust之后,你就会下意识地去考虑你所定义的数据的安全性,有没有不确定的执行顺序引发的问题?有没有可能被非预期的共享和修改?我改了这个对象会不会影响到其他部分的功能表现等等?从而去想办法将可能会发生的问题扼杀在摇篮里。例如在处理组件状态时,采用不可变数据结构和函数式编程模式可以减少出错的机会。实际上对于前端开发者来说,这类bug是相当常见的。

JS其实也是在不断努力解决其本身存在的各种问题的,例如不断升级的ES新特性,使用 use stric 开启严格模式,还有函数式编程范式的流行,以及各类框架都支持的状态管理等等,这些措施都是为了让JS代码能够更加健壮,弥补JS本身的一些不足。

不可变数据结构: 在React中,使用useState和useReducer来管理状态,避免直接修改状态对象。以及redux等状态管理库,还有像Immer这样的库来简化不可变数据的操作。 函数式编程: 在JavaScript中,封装有明确输入和输出的函数,或使用高阶函数(如map、filter、reduce)来处理数据,避免修改原始数据,从而保持代码的清晰性和可测试性。

无论是函数式编程,还是状态管理,都是为了减少每个动作的副作用,有明确的数据流,让代码更安全更加可维护,低耦合高内聚不是一句空话,是业界大佬们真正在不断去实践的。只是我们自己没有感知,而实际上JS这门语言自身的缺陷真的很多,用JS去开发很容易,但是用JS去开发出健壮又高性能的代码是很难的,这可能也是为什么前端框架和库百花齐放而又前仆后继的原因。

4. 错误处理

Rust没有传统意义上的异常机制。在许多编程语言中,错误通常会通过运行时抛出异常来传递,而Rust采用了一种完全不同的方式来处理错误。

Rust通过Result类型和Option类型来明确地处理错误和空值,Rust的错误处理是编译时检查的,必须显式处理Result或Option,如果忽略了错误处理,编译器会报错,确保错误处理不被遗漏。这种做法可以避免程序出现未处理的异常,增强程序的健壮性。

Result 类型:Rust使用Result类型来显式表示一个函数可能返回的两种状态:成功(Ok(T))或失败(Err(E))。这种方式要求函数调用者在编译时就明确考虑到错误的处理,而不是依赖于运行时的异常机制。

Result是一个枚举类型,定义如下:

enum Result {
Ok(T),
Err(E),
}

Option 类型:在处理可能的空值时,Rust使用Option类型,它表示一个值可能存在(Some(T))或不存在(),避免了空指针异常的问题。

enum Option {
Some(T),
,
}

在前端开发中,JavaScript和TypeScript也可以借鉴Rust的错误处理机制,明确地处理每一种错误情况,尤其是空值问题,没有一个前端开发能躲过 undefined 的摧残。

高性能设计

1. 零成本抽象

零成本抽象是指使用高级编程语言的抽象(如函数式编程的高阶函数、泛型、闭包等)时,不会引入额外的性能开销或运行时成本。换句话说,编写高抽象层的代码并不会影响程序的性能,编译器能够将抽象代码转化为与低级代码相同的高效机器码。

在 Rust 中,“零成本抽象”特别重要,因为 Rust 旨在提供与 C 和 C++ 等低级语言相似的性能,同时保持高层次的代码抽象和安全性。通过静态分析和优化,Rust 能够在编译时消除大多数抽象层的开销。

零成本抽象的三个原则:

  • 没有全局成本(No global cost): 一个零成本抽象不应该对不使用该功能的程序的性能产生负面影响。

    换句话说,零成本抽象应该只在使用时产生影响,在未使用时不会引入任何额外的开销。

  • 最佳性能(Optimal performance): 一个零成本的抽象应该编译成相当于底层指令编写的最佳实现。意味着它在使用时会以尽可能接近底层代码的方式运行,即它的性能应当与手写的低级实现相当。

    可以理解为,如果你想要用rust抽象某个高级能力,那么抽象完成的性能不能比用更原始写法实现的性能差,如果你想要抽象前端框架,那么就不能比直接操作DOM的JS原生写法性能差。

  • 改善开发者体验(Improves developer experience): 抽象的意义在于提供新的工具,由底层组件组装而成,让开发者更容易写出他们想要的代码,提高开发效率和代码可读性。

举几个例子

  1. Rust 的所有权系统(ownership system)和生命周期(lifetimes)。

当你写一个简单的程序,没有使用所有权系统的特性时,编译器会对这些特性进行优化,使得它们对程序的性能没有任何影响。只有当你使用这些特性时,编译器才会引入相关的检查和优化。

  1. 迭代器(Iterators) Rust 的迭代器是一种高效的抽象
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().map(|x| x * 2).sum();
println!("Sum: {}", sum);
}

在这个例子中,iter 返回一个迭代器,map 是一个高阶函数,它将一个闭包应用到每个元素。尽管你使用了高阶函数和迭代器,Rust 会在编译时优化这段代码,确保它与手写的 for 循环代码在性能上等效。

Rust 的迭代器通过惰性求值来避免不必要的中间计算。当你调用 .map() 和 .sum() 时,Rust 会合并这些操作为一个高效的迭代过程,不会在内存中创建不必要的临时集合,最终生成与手动实现同样高效的机器码。

而在JS中,几种高阶函数的使用则是有成本的,如map、foreach 等,在进行一些极端的数据量测试时,性能差异相比 for 循环就比较明显了,但是js引擎实际上也是做了很多的优化的,这点不用太过于担心,放心用就好了。

  1. 泛型(Generics) Rust 中的泛型是一个典型的零成本抽象。

使用泛型时,Rust 在编译时会根据类型替换(monomorphization单态化)生成具体的代码,在运行时没有任何额外的开销。

fn add(a: T, b: T) -> T
where
T: std::ops::Add,
{
a + b
}

在这个例子中,add 函数是一个泛型函数。Rust 编译器在编译时会为每种特定类型(例如 i32、f64 等)生成专门的机器码。

如果你调用 add(1, 2),Rust 会为 i32 类型生成专门的代码。同样,如果你调用 add(1.5, 2.5),Rust 会为 f64 类型生成专门的代码。泛型不会引入运行时的性能损失。编译后的代码与手写的具体类型版本在性能上是等价的。

  1. 错误处理

包括Rust的错误处理,由于Rust不使用异常机制,程序的控制流更加清晰,避免了异常捕获时的运行时性能开销。

对前端的启示

  • 抽象高级能力提升开发效率和体验: 前端框架(如 React 和 Vue)、TypeScript等都是属于抽象的高级能力。例如,React 的优化算法通过虚拟 DOM 的比对、最小化 DOM 更新,已实现类似的零成本抽象。除此之外还有很多事可以做。
  • 优化代码:减少抽象代码中不必要的计算,必要时惰性引入(前端叫懒加载)或计算,减少对不相关部分的隐形影响(前端叫副作用),利用好模块化开发和treeshaking等。
  • WebAssembly (Wasm): Rust 本身可以编译为 WebAssembly,这为前端性能优化开辟了新天地。借鉴 Rust 的这一特点,前端可以通过将性能瓶颈部分的代码(如图像处理、数据加密等)使用 Rust 编写并编译为 WebAssembly 运行,从而提升性能。

总结

社区很多人并不看好Rust甚至很激进地开喷,人确实是会有自己的舒适区的,当用熟了一样语言后,便不那么容易接受某一个自己不熟悉的语言更好,但是,尝试走出舒适区,真正地去接触Rust,一定会也能够感受到Rust的设计光辉。

要学习Rust,你需要先深入理解内存、堆栈、引用、变量作用域等这些其它高级语言往往不会深入接触的内容,Rust 会通过语法、编译器和 clippy 这些静态检查工具半帮助半强迫的让你成为更优秀的程序员,写出更好的代码。

Rust 程序只要能跑起来,那代码质量其实就是相当不错的,甚至不需要调试,能编译基本就没bug(当然是避免不了逻辑bug的)。正因为较高的质量下限,我们维护别人的代码时心智负担也比较小,能编译通过基本能保证不新增bug,把精力完全放在业务逻辑上就可以了。

而如果用javascript写程序,我们必须一边写一边调试,虽然写出能跑的程序极为简单,但要想没有bug,心智负担极高,js的程序员上限不封顶,但下限极低。而且review代码成本也很高,1000个人有1000种写法,review完改一改可能又不小心改出bug了。

JS对开发者要求较低,也是时代变了,条件好了,搁以前几百兆内存、单核cpu的时候,那时候的JS开发该有多痛苦啊。

社区流传着一个很奇怪的论调,“通过学习Rust,你能写出更好的xx语言的代码”。

学习Rust后,会潜移默化地影响你写其他语言代码时的思维方式,最直观的变化就是,对javascript中各类容易造成不安全不稳定的情况会更加敏感,所以,某种程度来看,Rust的价值可能并不在于用它写出多么优秀的代码,更重要的是它带给你的全面的方法论层面的提升。

我使用Rust做了比较丰富的尝试,包括用Rust写命令行工具、用Rust写 postcss 插件、用Rust写vite 插件、用Rust写WebAssembly在前端页面中使用,整体体验和效果还是非常棒的,WebAssembly的尝试可以查看文章Rust + wasm-pack + WebAssembly 实现Gitlab 代码统计,比JS快太多了,其他实践后面会陆续和大家分享,感兴趣的小伙伴可以关注收藏插个眼~

附:

Rust在前端领域的应用

  • SWC: 基于 Rust 的前端构建工具,可以理解为 Rust 版本的 Babel,但是性能有 10 倍提升。目前被 Next.js、Deno , Rspack等使用。
  • Tauri:Tauri 是目前最流行的 Electron 替代方案,通过使用 Rust 和 Webview2 成功解决了 Electron 的包体积大和内存占用高的问题。Atom 团队也是看到了 Tauri 的成功,才决定基于 Rust 去做 Zed 编辑器。
  • Parcel2:零配置构建工具,特点是快速编译和不需要配置,和 Vite、Webpack等打包比起来更加简单,而且是基于 Rust 开发
  • Biome: 旨在取代许多现有的 JavaScript 工具,集代码检测、打包、编译、测试等功能于一身。
  • Rspack: 基于 Rust 的高性能 Web 构建工具, 对标 Webpack, 兼容大部分Webpack api
  • Rocket: 可以帮助开发人员轻松编写安全的Web应用程序, 对标 Expressjs,性能卓越,具体参考 Web Frameworks Benchmark
  • Yew : 使用 Rust 开发 h5 页面,支持类 jsx 的语法,和 React 类似开发前端网页,打包产物是 wasm,挺有趣。
  • Napi-rs: 用 Rust 和 N-API 开发高性能 Node.js 扩展,可以替代之前用 C++ 开发的 Node.js 扩展,许多基于 Rust 语言开发的前端应用都结合这个库进行使用。
  • Rolldown: 基于 Rust 的 Rollup 的替代品。
  • 美国国防部准备征求一个把所有C代码翻译成Rust的软件。

作者:Pursue_LLL
来源:juejin.cn/post/7450021642377199643

收起阅读 »

我的2024:裁员,退市,协和,副业,剑来;全赶上了!!

一点都不夸张,就是这么惊悚!这么刺激! 裁员,退市 公司在2023年10月份,来了一次全球大裁员。整个过程从官宣,到结束,也就一周时间,不长。但后续影响的阴影,笼罩了2024年,整整一年。 对于被裁员的人,是残忍的。收拾一下,告别一段经历,一群好友,慢慢开启下...
继续阅读 »

一点都不夸张,就是这么惊悚!这么刺激!


裁员,退市


公司在2023年10月份,来了一次全球大裁员。整个过程从官宣,到结束,也就一周时间,不长。但后续影响的阴影,笼罩了2024年,整整一年。

对于被裁员的人,是残忍的。收拾一下,告别一段经历,一群好友,慢慢开启下一段旅程。当找到下一份工作后,往往很快就告别了这一段低落的情绪。很多人再联系时,问他们,当时的感觉如何,很有一些人给出的答复是:塞翁失马,焉知非福!因为公司给的补偿方案,都还是不错的,N+2, N+3的人,也是有的。

对于我们这些暂时还在的人,反而更加折磨。被阴影笼罩,看不到希望。熟悉的人不在了,组织结构也变了,未来充满了不确定性,焦虑一下子包围了过来,把你的世界塞得满满当当。内心世界一下子就沉寂,阴暗了起来。对于像我这样已经35+的人来说,实在是有些窒息。

不出所料,2024年又来了一波。不过和之前不一样的是,大家似乎淡定了许多。也许是柔软的心也长出了厚厚的茧,同样的打击,反而来得更轻松了些。

裁员的压力,其实来自于上市公司的财报要求。全球经济下行,市场不景气,作为上市公司,需要对全球投资人负责。而裁员是短期见效最快的方案。因为科技公司,最贵的其实就是人力成本。但很明显,这并不是长期最有效的方法。最终,公司做出了退市的决定。其实,综合来看,并不算坏。


协和


医院是离生死最近的地方,再次面对时,本觉得可以洒脱些,然而,不然!真的真的是再也不想去医院了!

35。是一个普通的数字。上有老,下有小。是对幸福的一种描述。

可把它们放在一起:35+,上有老,下有小。就变成了巨大的压力,还是喘不过气的那种。


这无形的压力,很容易让你自我怀疑,焦虑万分:



  • 我真是一点用都没有,找工作都怕找不到。

  • 我真是一点用都没有,不能给家人最好的医疗环境。在生命面前,尊严是那么的奢侈,活着就已经不错了。



不想再列举更多,也不想制造焦虑。因为这玩意儿,现在到处都是,廉价的很。


副业


因为一直喜欢读源码,并且喜欢写一些源码阅读笔记 - 大家感兴趣也可以翻翻我之前的文章,多少和源码有一些关联。

前些年读到Hugo源码的时候,就有一个想法:反正源码也读了,笔记也记了,索性整理成一本书,名字我都想好了,就叫《深入理解Hugo之源码精读》。

读着读着,就有了一些困惑和想法,就跑去Hugo官方论坛上,寻求帮助,和验证想法。在2023年9月份的一天,发了一篇Hugo Headless CMS的贴子,问官方有没有计划,开发这样一个版本,就能把Hugo的服务,通过API暴露出来,那应用的场景一下子就能开阔起来。我也就可以用Hugo来帮我生成我书的站点,还能帮我生成书的电子版,我就可以发布到各大在线书商平台了。当然最后得到的答复是,并没有这样的计划。就都还好,坏就坏在,我一时抽抽,夸下海口。说我也是软件工程师,不行我来写一个吧...

后面找了半天,怎么删帖... 没找着...

想着咱也不能丢咱们中国程序员的脸啊,这要是丢起来,可丢到国外了。别人丢我不管,咱可不能够丢这个脸!一咬牙,一跺脚,还有什么好说的,那就撸一个吧。就这样,在阴暗的2024里,有一出,没一出的,慢慢的,还真给我实现了这么个PoC(验证了可行性)。

也是因为这个项目,后来演变成了我现在的SaaS服务 - Friday。一个可以将Markdown笔记,转换成Hugo站点的服务。目前还在持续稳步迭代中。自己也给自己立了个Flag,先干个20年再说,要走,就走个完整的商业闭环。

可能,这就是大家说的,当上帝给你关了一扇门的时候,也会给你开一扇窗。正因为每天有坚持去看看,去写写,做一些具体的事情,不让自己有太多时间胡思乱想,反而也就没那么焦虑了!# 剑来

最近,国漫《剑来》特别火,火得理所当然,火得恰逢其时!

特别能理解,大家为什么能对齐先生,敬爱有佳,热泪盈眶。在强大的生活面前,谁还不是陈平安,谁还不想有齐先生这样的引路人!


2025


最后,回过头来看:


还好,我还有工作。
还好,大家都好好的。
还好,能做自己喜欢的事情。


2025,欢迎你,再一起加把劲吧。


最后,把我喜欢的剑来台词送给大家:


愿大家,永远不要对生活失去希望!
岁岁平,岁岁安,岁岁平安!




写在圣诞


2024年12月25号


作者:韦德说
来源:juejin.cn/post/7452280790791258162
收起阅读 »

我这🤡般的7年开发生涯

前两天线上出了个漏洞,导致线上业务被薅了 2w 多块钱。几天晚上没咋睡,问 ChatGPT,查了几晚资料,复盘工作这么久来犯下的错误。 我在公司做的大部分是探索性、创新性的需求,行内人都知道这些活都是那种脏活累活,需求变化大,经常一句话;需求功能多,看着简单...
继续阅读 »

前两天线上出了个漏洞,导致线上业务被薅了 2w 多块钱。几天晚上没咋睡,问 ChatGPT,查了几晚资料,复盘工作这么久来犯下的错误。



我在公司做的大部分是探索性、创新性的需求,行内人都知道这些活都是那种脏活累活,需求变化大,经常一句话;需求功能多,看着简单一细想全是漏洞;需求又紧急,今天不上线业务就要没。


所以第一个建议就是大家远离这些需求,否则你会和我一样变得不幸。


但是👴🐂🍺啊,接下来也就算了,还全干完了。正常评估一个月的需求,我 tm 半个月干完上线;你给我一句话,我干完一整条链路上的事;你说必须今天上线,那就加班加点干上线。


就这样干了几年,黄了很多,也有做起来的。但是不管业务怎么发展,这样做时间长了会出现很多致命问题。


开发忙成狗


一句话需求太多,到最后只有开发最了解业务,所有人所有事都来找开发,开发也是人,开发还要写代码呢。最先遇到的问题就是时间严重不够,产品跟个摆设一样,什么忙都帮不上,我成了产品开发结合体。


bug 来了


开发一忙,节奏就乱了,乱则生 bug,再加上原本需求上逻辑不完整的深坑,坑上叠坑,出 bug 是迟早的事。


形象崩塌


一旦出现 bug,人设就毁了。记住一句话,没人会感谢你把原本一个月的需求只用半个月上线,大家都觉得这玩意本来就半个月工时。慢慢的开始以半个月的工时要求你。


那些 bug 自己回头,慢慢做都是可以避免的,就像考试的时候做完了卷子复查一遍,很多问题回头看一下都能发现,结果因为前期赶工,没时间回看,而且有很多图快的写法,后期都是容易出问题的。


形象崩塌在职场中是最恐怖的,正所谓好事不出门,坏事传千里。


一旦出了问题,团队、领导、所有人对你的体感,那都是直线下降,你之前做的所有好事,就跟消失了一样,别人对你的印象,一提起来说的都是,这不是当时写出 xxx bug 的人吗?这还怎么在职场生存?脸都没了,项目好处也跟自己没关系了。


我 tm 真是愣头青啊蠢的💊💩,从入职开始都想的是多学点多干点,结果干的越多错的越多,现在心态干崩了,身体干垮了,钱还没混子多,还背了一身骂名和黑锅。


之前我看同事写代码贼慢,鼠标点来点去,打字也慢一拍,我忍不住说他你这写代码速度太慢了,可以用 xxx 快捷键等等,现在回想起来,我说他不懂代码,其实是我不懂职场。


我真是个纯纯的可悲🤡。


提桶跑路


bug 积累到一定程度,尤其是像我这样出现点资金的问题,那也差不多离走人不远了,我感觉我快到这个阶段了,即使不走,扣钱扣绩效也是在所难免的,综合算下来,还没那些混子赚的多。


我亲自接触的联调一哥们儿,一杯茶,一包烟,一个 bug 修一天。是真真正正的修了一天,从早到晚。那天我要上线那个需求,我不停的催他,后来指着代码说着逻辑让他写,最终半夜转点上线。我累的半死不活,我工资和他差不多,出了问题我还要背锅。


我现在听到 bug 都 PTSD 了,尤其是资金相关的,整个人就那种呆住,大脑空白,心脏像被揪住,我怀疑我有点心理问题了都。


为什么别人可以那么安心的摸鱼?为什么我要如此累死累活还不讨好?我分析出几点我的性格问题。


责任心过强


什么事都觉得跟自己有关系,看着别人做的不好,我就自己上手。


到后期产品真 tm 一句话啊,逻辑也不想,全等着我出开发方案,产品流程图,我再告诉她哪里要改动。不是哥们?合着我自己给出需求文档再自己写代码?


为人老实


不懂拒绝,不懂叫板。


运营的需求,来什么做什么,说什么时候上线就什么时候上线。不是哥们?我都还不知道要做什么,你们把上线时间都定了?就 tm 两字,卑微。


用力过猛


十分力恨不得使出十一分,再加一分吃奶的劲儿。一开始就领导很高的期望,后面活越来越多,而且也没什么晋升机会了,一来的门槛就太高了知道吧,再想提升就很难了。


先总结这么多吧,我现在心情激荡的很,希望给各位和我性格差不多一点提醒,别像我这样愣头青,吃力不讨好,还要遭人骂。后面再写写改进办法。


作者:小兵张健
来源:juejin.cn/post/7450047052804161576
收起阅读 »

从前端的角度出发,目前最具性价比的全栈路线是啥❓❓❓

web
今年大部分时间都是在编码上和写文章上,但是也不知道自己都学到了啥,那就写篇文章来盘点一下目前的技术栈吧,也作为下一年的参考目标,方便知道每一年都学了些啥。 我的技术栈 首先我先来对整体的技术做一个简单的介绍吧,然后后面再对当前的一些技术进行细分吧。 Reac...
继续阅读 »

今年大部分时间都是在编码上和写文章上,但是也不知道自己都学到了啥,那就写篇文章来盘点一下目前的技术栈吧,也作为下一年的参考目标,方便知道每一年都学了些啥。


20241223154451


我的技术栈


首先我先来对整体的技术做一个简单的介绍吧,然后后面再对当前的一些技术进行细分吧。


React、Typescript、React Native、mysql、prisma、NestJs、Redis、前端工程化。


React


React 这个框架我花的时间应该是比较多的了,在校期间已经读了一遍源码了,对这些原理已经基本了解了。在随着技术的继续深入,今年毕业后又重新开始阅读了一遍源码,对之前的认知有了更深一步的了解。


也写了比较多跟 React 相关的文章,包括设计模式,原理,配套生态的使用等等都有一些涉及。


在状态管理方面,redux,zustand 我都用过,尤其在 Zustand 的使用上,我特别喜欢 Zustand,它使得我能够快速实现全局状态管理,同时避免了传统 Redux 中繁琐的样板代码,且性能更优。也对 Zustand 有比较深入的了解,也对其源码有过研究。


NextJs


Next.js 是一个基于 React 的现代 Web 开发框架,它为开发者提供了一系列强大的功能和工具,旨在优化应用的性能、提高开发效率,并简化部署流程。Next.js 支持多种渲染模式,包括服务器端渲染(SSR)、静态生成(SSG)和增量静态生成(ISR),使得开发者可以根据不同的需求选择合适的渲染方式,从而在提升页面加载速度的同时优化 SEO。


在路由管理方面,Next.js 采用了基于文件系统的路由机制,这意味着开发者只需通过创建文件和文件夹来自动生成页面路由,无需手动配置。这种约定优于配置的方式让路由管理变得直观且高效。此外,Next.js 提供了动态路由支持,使得开发者可以轻松实现复杂的 URL 结构和参数化路径。


Next.js 还内置了 API 路由,允许开发者在同一个项目中编写后端 API,而无需独立配置服务器。通过这种方式,前后端开发可以在同一个代码库中协作,大大简化了全栈开发流程。同时,Next.js 对 TypeScript 提供了原生支持,帮助开发者提高代码的可维护性和可靠性。


Typescript


今年所有的项目都是在用 ts 写了,真的要频繁修改的项目就知道用 ts 好处了,有时候用 js 写的函数修改了都不知道怎么回事,而用了 ts 之后,哪里引用到的都报红了,修改真的非常方便。


今年花了一点时间深入学习了一下 Ts 类型,对一些高级类型以及其实现原理也基本知道了,明年还是多花点时间在类型体操上,除了算法之外,感觉类型体操也可以算得上是前端程序员的内功心法了。


React Native



不得不说,React Native 不愧是接活神器啊,刚学完之后就来了个安卓和 ios 的私活,虽然没有谈成。



React Native 和 Expo 是构建跨平台移动应用的两大热门工具,它们都基于 React,但在功能、开发体验和配置方式上存在一些差异。React Native 是一个开放源代码的框架,允许开发者使用 JavaScript 和 React 来构建 iOS 和 Android 原生应用。Expo 则是一个构建在 React Native 之上的开发平台,它提供了一套工具和服务,旨在简化 React Native 开发过程。


React Native 的核心优势在于其高效的跨平台开发能力。通过使用 React 语法和组件,开发者能够一次编写应用的 UI 和逻辑,然后部署到 iOS 和 Android 平台。React Native 提供了对原生模块的访问,使开发者能够使用原生 API 来扩展应用的功能,确保性能和用户体验能够接近原生应用。


Expo 在此基础上进一步简化了开发流程。作为一个开发工具,Expo 提供了许多内置的 API 和组件,使得开发者无需在项目中进行繁琐的原生模块配置,就能够快速实现设备的硬件访问功能(如摄像头、位置、推送通知等)。Expo 还内置了一个开发客户端,使得开发者可以实时预览应用,无需每次都进行完整的构建和部署。


另外,Expo 提供了一个完全托管的构建服务,开发者只需将应用推送到 Expo 服务器,Expo 就会自动处理 iOS 和 Android 应用的构建和发布。这大大简化了应用的构建和发布流程,尤其适合不想处理复杂原生配置的开发者。


然而,React Native 和 Expo 也有各自的局限性。React Native 提供更大的灵活性和自由度,开发者可以更自由地集成原生代码或使用第三方原生库,但这也意味着需要更多的配置和维护。Expo 则封装了很多功能,简化了开发,但在需要使用某些特定原生功能时,开发者可能需要“弹出”Expo 的托管环境,进行额外的原生开发。


样式方案的话我使用的是 twrnc,大部分组件都是手撸,因为有 cursor 和 chatgpt 的加持,开发效果还是杠杠的。


rn 原理也争取明年能多花点时间去研究研究,不然对着盲盒开发还是不好玩。


Nestjs


NestJs 的话没啥好说的,之前也都写过很多篇文章了,感兴趣的可以直接观看:



对 Nodejs 的底层也有了比较深的理解了:



Prisma & mysql


Prisma 是一个现代化的 ORM(对象关系映射)工具,旨在简化数据库操作并提高开发效率。它支持 MySQL 等关系型数据库,并为 Node.js 提供了类型安全的数据库客户端。在 NestJS 中使用 Prisma,可以让开发者轻松定义数据库模型,并通过自动生成的 Prisma Client 执行类型安全的查询操作。与 MySQL 配合时,Prisma 提供了一种简单、直观的方式来操作数据库,而无需手动编写复杂的 SQL 查询。


Prisma 的核心优势在于其强大的类型安全功能,所有的数据库操作都能通过 Prisma Client 提供的自动生成的类型来进行,这大大减少了代码中的错误,提升了开发的效率。它还包含数据库迁移工具 Prisma Migrate,能够帮助开发者方便地管理数据库结构的变化。此外,Prisma Client 的查询 API 具有很好的性能,能够高效地执行复杂的数据库查询,支持包括关系查询、聚合查询等高级功能。


与传统的 ORM 相比,Prisma 使得数据库交互更加简洁且高效,减少了配置和手动操作的复杂性,特别适合在 NestJS 项目中使用,能够与 NestJS 提供的依赖注入和模块化架构很好地结合,提升整体开发体验。


Redis


Redis 和 mysql 都仅仅是会用的阶段,目前都是直接在 NestJs 项目中使用,都是已经封装好了的,直接传参调用就好了:


import { Injectable, Inject, OnModuleDestroy, Logger } from "@nestjs/common";
import Redis, { ClientContext, Result } from "ioredis";

import { ObjectType } from "../types";

import { isObject } from "@/utils";

@Injectable()
export class RedisService implements OnModuleDestroy {
private readonly logger = new Logger(RedisService.name);

constructor(@Inject("REDIS_CLIENT") private readonly redisClient: Redis) {}

onModuleDestroy(): void {
this.redisClient.disconnect();
}

/**
*
@Description: 设置值到redis中
*
@param {string} key
*
@param {any} value
*
@return {*}
*/

public async set(
key: string,
value: unknown,
second?: number
): Promise<Result<"OK", ClientContext> | null> {
try {
const formattedValue = isObject(value)
? JSON.stringify(value)
: String(value);

if (!second) {
return await this.redisClient.set(key, formattedValue);
} else {
return await this.redisClient.set(key, formattedValue, "EX", second);
}
} catch (error) {
this.logger.error(`Error setting key ${key} in Redis`, error);

return null;
}
}

/**
*
@Description: 获取redis缓存中的值
*
@param key {String}
*/

public async get(key: string): Promise<string | null> {
try {
const data = await this.redisClient.get(key);

return data ? data : null;
} catch (error) {
this.logger.error(`Error getting key ${key} from Redis`, error);

return null;
}
}

/**
*
@Description: 设置自动 +1
*
@param {string} key
*
@return {*}
*/

public async incr(
key: string
): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.incr(key);
} catch (error) {
this.logger.error(`Error incrementing key ${key} in Redis`, error);

return null;
}
}

/**
*
@Description: 删除redis缓存数据
*
@param {string} key
*
@return {*}
*/

public async del(key: string): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.del(key);
} catch (error) {
this.logger.error(`Error deleting key ${key} from Redis`, error);

return null;
}
}

/**
*
@Description: 设置hash结构
*
@param {string} key
*
@param {ObjectType} field
*
@return {*}
*/

public async hset(
key: string,
field: ObjectType
): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.hset(key, field);
} catch (error) {
this.logger.error(`Error setting hash for key ${key} in Redis`, error);

return null;
}
}

/**
*
@Description: 获取单个hash值
*
@param {string} key
*
@param {string} field
*
@return {*}
*/

public async hget(key: string, field: string): Promise<string | null> {
try {
return await this.redisClient.hget(key, field);
} catch (error) {
this.logger.error(
`Error getting hash field ${field} from key ${key} in Redis`,
error
);

return null;
}
}

/**
*
@Description: 获取所有hash值
*
@param {string} key
*
@return {*}
*/

public async hgetall(key: string): Promise<Record<string, string> | null> {
try {
return await this.redisClient.hgetall(key);
} catch (error) {
this.logger.error(
`Error getting all hash fields from key ${key} in Redis`,
error
);

return null;
}
}

/**
*
@Description: 清空redis缓存
*
@return {*}
*/

public async flushall(): Promise<Result<"OK", ClientContext> | null> {
try {
return await this.redisClient.flushall();
} catch (error) {
this.logger.error("Error flushing all Redis data", error);

return null;
}
}

/**
*
@Description: 保存离线通知
*
@param {string} userId
*
@param {any} notification
*/

public async saveOfflineNotification(
userId: string,
notification: any
): Promise<void> {
try {
await this.redisClient.lpush(
`offline_notifications:${userId}`,
JSON.stringify(notification)
);
} catch (error) {
this.logger.error(
`Error saving offline notification for user ${userId}`,
error
);
}
}

/**
*
@Description: 获取离线通知
*
@param {string} userId
*
@return {*}
*/

public async getOfflineNotifications(userId: string): Promise<any[]> {
try {
const notifications = await this.redisClient.lrange(
`offline_notifications:${userId}`,
0,
-1
);
await this.redisClient.del(`offline_notifications:${userId}`);

return notifications.map((notification) => JSON.parse(notification));
} catch (error) {
this.logger.error(
`Error getting offline notifications for user ${userId}`,
error
);

return [];
}
}

/**
* 获取指定 key 的剩余生存时间
*
@param key Redis key
*
@returns 剩余生存时间(秒)
*/

public async getTTL(key: string): Promise<number> {
return await this.redisClient.ttl(key);
}
}

前端工程化


前端工程化这块花了很多信息在 eslint、prettier、husky、commitlint、github action 上,现在很多项目都是直接复制之前写好的过来就直接用。


后续应该是投入更多的时间在性能优化、埋点、自动化部署上了,如果有机会的也去研究一下 k8s 了。


全栈性价比最高的一套技术


最近刷到一个帖子,讲到了


20241223165138


我目前也算是一个小全栈了吧,我也来分享一下我的技术吧:



  1. NextJs

  2. React Native

  3. prisma

  4. NestJs

  5. taro (目前还不会,如果有需求就会去学)


剩下的描述也是和他下面那句话一样了(毕业后对技术态度的转变就是什么能让我投入最小,让我最快赚到钱的就是好技术)


总结


学无止境,任重道远。


作者:Moment
来源:juejin.cn/post/7451483063568154639
收起阅读 »

一半员工净资产过亿,英伟达中国员工自爆工资单

英伟达 据风险投资人 Ruben D 透露:芯片巨头英伟达 78% 的员工已成为百万美元富翁,有一半人的净资产甚至达到 2500 万美元(约合人民币 1.83 亿元)。 英伟达的薪资很高,但光靠现金薪资不足让一半人资产过亿,这里面的主要原因,是英伟达 🚀 一般...
继续阅读 »

英伟达


据风险投资人 Ruben D 透露:芯片巨头英伟达 78% 的员工已成为百万美元富翁,有一半人的净资产甚至达到 2500 万美元(约合人民币 1.83 亿元)。


英伟达的薪资很高,但光靠现金薪资不足让一半人资产过亿,这里面的主要原因,是英伟达 🚀 一般的涨幅,在过去五年翻了 20 倍。



英伟达作为"卖铲子"的人,总能精准踩中每个风口。


工业设计软件、3A游戏、虚拟货币、AI,带来了海量订单的同时,也把"濒死"的英伟达一度拉到「全球市值第一」的位置。


要知道,如今让全球科技公司都"高攀不起"的英伟达 CEO 黄仁勋,十年前还只能蹭刚起步的小米发布会来推销芯片。



当年,雷军还不是现在的"雷神",小米手机也才出到第三代。但即使是这般初创品牌的客户,也足以让黄仁勋毕恭毕敬。


十年河东十年河西,如今英伟达的市值,接近 50 个小米,不少有着股权激励的员工,身家也得以水涨船高。


这几年行情不景气,网上晒工资的人少了许多(或被限流),但仍然找到了一份 2021 年英伟达员工收入的资料:



该员工在 2021 年底共有 3 笔收入:股权激励 900W+、全年一次性奖金 16W+,月工资 17W+。


知道英伟达员工收入高,但却是我想象不到的高 🤣🤣🤣


而且,这还是 2021 年的英伟达,当时一股英伟达 30 不到,如今 130+(涨幅 433%),这两年的股权激励会去到多少,我不敢想 🤣🤣🤣


但巨额财富背后是高强度的工作,据英伟达员工爆料,他们基本每周工作 7 天,加班到凌晨 2 点也是常态,还要面临密集的会议安排和严格的时间管理要求,大家几乎没有多少时间陪伴家人,甚至开始考虑"半退休"状态,以缓解工作带来的负面影响。


对此,你怎么看?说实话,你是不是也想体验几年加班到 2 点的日子?欢迎评论区交流。


...


回归主题。


来一道「HOT 100」级别算法题。


题目描述


平台:LeetCode


题号:450


给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。


一般来说,删除节点可分为两个步骤:



  • 首先找到需要删除的节点;

  • 如果找到了,删除它。


示例 1:



输入:root = [5,3,6,2,4,null,7], key = 3

输出:[5,4,6,2,null,null,7]

解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。


另一个正确答案是 [5,2,6,null,4,null,7]。

示例 2:


输入: root = [5,3,6,2,4,null,7], key = 0

输出: [5,3,6,2,4,null,7]

解释: 二叉树不包含值为 0 的节点

示例 3:


输入: root = [], key = 0

输出: []

提示:



  • 节点数的范围 [0,104][0, 10^4]

  • 105 <=Node.val<=105-10^5 <= Node.val <= 10^5

  • 节点值唯一

  • root 是合法的二叉搜索树

  • 105 <=key<=105-10^5 <= key <= 10^5


进阶: 要求算法时间复杂度为 O(h)O(h)hh 为树的高度。


递归


利用题目本身的函数签名的含义,也就是「在以 root 为根的子树中,删除值为 key 的节点,并返回删除节点后的树的根节点」,我们可以用「递归」来做。


起始先对边界情况进行处理,当 root 为空(可能起始传入的 root 为空,也可能是递归过程中没有找到值为 key 的节点时,导致的 root 为空),我们无须进行任何删除,直接返回 null 即可。


根据当前 root.valkey 的大小关系,进行分情况讨论:



  1. 若有 root.val<keyroot.val < key,说明待删除的节点必然不是当前节点,以及不在当前节点的左子树中,我们将删除动作「递归」到当前节点的右子树,并将删除(可能进行)之后的新的右子树根节点,重新赋值给 root.right,即有 root.right = deleteNode(root.right, key)

  2. 若有 root.val>keyroot.val > key,说明待删除的节点必然不是当前节点,以及不在当前节点的右子树,我们将删除节点「递归」到当前节点的左子树,并将删除(可能进行)之后的新的左子树根节点,重新赋值给 root.left,即有 root.left = deleteNode(root.left, key)

  3. 若有 root.val=keyroot.val = key,此时找到了待删除的节点,我们根据左右子树的情况,进行进一步分情况讨论:



    • 若左/右子树为空,我们直接返回右/左子树节点即可(含义为直接将右/左子树节点搬到当前节点的位置)如图所示:

    • 若左右子树均不为空,我们有两种选择:



      • 从「当前节点的左子树」中选择「值最大」的节点替代 root 的位置,确保替代后仍满足 BST 特性;

      • 从「当前节点的右子树」中选择「值最小」的节点替代 root 的位置,确保替代后仍满足 BST 特性;


      我们以「从当前节点的左子树中选择值最大的节点」为例子,我们通过树的遍历,找到其位于「最右边」的节点,记为 tttt 作为最右节点,必然有 t.right = null),利用原本的 root 也是合法 BST,原本的 root.right 子树的所有及节点,必然满足大于 t.val,我们可以直接将 root.right 接在 t.right 上,并返回我们重接后的根节点,也就是 root.left



      而「从当前节点的右子树中选择值最小的节点」,同理(代码见 P2P2)。





Java 代码(P1):


class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return null;
if (root.val == key) {
if (root.left == null) return root.right;
if (root.right == null) return root.left;
TreeNode t = root.left;
while (t.right != null) t = t.right;
t.right = root.right;
return root.left;
} else if (root.val < key) root.right = deleteNode(root.right, key);
else root.left = deleteNode(root.left, key);
return root;
}
}

Java 代码(P2):


class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return null;
if (root.val == key) {
if (root.left == null) return root.right;
if (root.right == null) return root.left;
TreeNode t = root.right;
while (t.left != null) t = t.left;
t.left = root.left;
return root.right;
} else if (root.val < key) root.right = deleteNode(root.right, key);
else root.left = deleteNode(root.left, key);
return root;
}
}

C++ 代码(P1):


class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (!root) return nullptr;
if (root->val == key) {
if (!root->left) return root->right;
if (!root->right) return root->left;
TreeNode* t = root->left;
while (t->right) t = t->right;
t->right = root->right;
return root->left;
} else if (root->val < key) {
root->right = deleteNode(root->right, key);
} else {
root->left = deleteNode(root->left, key);
}
return root;
}
};

Python 代码(P1):


class Solution:
def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
if not root:
return
if root.val == key:
if not root.left:
return root.right
if not root.right:
return root.left
t = root.left
while t.right:
t = t.right
t.right = root.right
return root.left
elif root.val < key:
root.right = self.deleteNode(root.right, key)
else:
root.left = self.deleteNode(root.left, key)
return root

TypeScript 代码(P1):


function deleteNode(root: TreeNode | null, key: number): TreeNode | null {
if (!root) return null;
if (root.val === key) {
if (!root.left) return root.right;
if (!root.right) return root.left;
let t: TreeNode | null = root.left;
while (t!.right) t = t!.right;
t!.right = root.right;
return root.left;
} else if (root.val < key) {
root.right = deleteNode(root.right, key);
} else {
root.left = deleteNode(root.left, key);
}
return root;
};


  • 时间复杂度:O(h)O(h),其中 hh 为树的深度

  • 空间复杂度:忽略递归带来的额外空间消耗,复杂度为 O(1)O(1)


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7459953815325327412
收起阅读 »

支付宝事故这事儿,凭什么又是程序员背锅?有没有可能是这样的...

你好呀,我是歪歪。 昨天支付宝那事儿你听说了吧? 网传支付宝 14:40-14:45 所有的支付订单都按国补减免了 20%。 从网上铺天盖地的截图来看,非常多类型的交易都被“减免了 20%”。 说实话,歪师傅纵横互联网多年,什么千奇百怪的事情没见过? 比如这...
继续阅读 »

你好呀,我是歪歪。


昨天支付宝那事儿你听说了吧?


网传支付宝 14:40-14:45 所有的支付订单都按国补减免了 20%。



从网上铺天盖地的截图来看,非常多类型的交易都被“减免了 20%”。


说实话,歪师傅纵横互联网多年,什么千奇百怪的事情没见过?


比如这种还带款有政府补贴的,我觉得还说得过去,毕竟有时候确实有政策扶持,你要强行往这个上面圆谎,遇到外行也是能糊弄过去的:



但是个人转账都能有政府补贴的,这个还真没见过:



还真是猪八戒吃人参果,第一遭。


又好比大姑娘坐花轿,头一回。


我第一反应甚至是:我靠,现在的诈骗的套路都玩得这么深吗?我甚至都一眼看不穿它。


针对支付宝这个“真百亿补贴”行为,很多网友纷纷猜测这波又是程序员干的。


有这种可能,但是我觉得还有另外一种可能,在这种可能性中,这个问题和程序员毫无关系。


我个人猜测这可能是一次运营配置事件。


我觉得是这样的。


根据官方信息,过两天,就是 1 月 20 日,国补就要正式开始了:



支付宝的程序员在这之前接到了一个国补相关的需求,需求可能是要求在创建支付订单的时候,根据订单对应的产品来判断该产品是否符合国补条件,从而决定是否进行对应的金额减免。


那么哪些产品符合国补条件,这个逻辑肯定不是写死在代码里面的。


应该是作为一个运营配置项,可做实时配置、灵活调整,而这个配置项又可能相对复杂。


比如同时可以配置支持 A 大类下的 B 小类产品、不支持 C 大类的 D 小类产品,类似这种条件组合吧。


具体的运营人员在做参数配置时,不知道出于什么原因,配置出了一个条件组合之后,这个组合的最终效果按照程序逻辑解析是所有订单都可以参与国补。


这种配置肯定是要一审二审,层层审批的。


但是巧了,审核人员就是没看出来这个配置是有问题的,给通过了。


然后就出现了这个“真百亿补贴”行为。


这种行为,换个场景就更好理解了。


就类似于营销发优惠券。


本来设计的营销活动是给指定客群的用户发一个满减的无门槛优惠券。


结果运营人员在选客群的时候操作失误,给所有的用户都发了优惠券。


你说这个场景,和程序员有什么关系?


本来就存在给所有的用户发优惠券的需求和场景,使用的人用错了,你怪我开发的时候为什么不想着拦截一下?


还有天理吗?


但是这个真实的场景又比优惠券惨烈的多,毕竟优惠券客户不一定真的去用,但是这次“补贴”是真金白银的给出去了呀。


而且还是直接给到了无数个 C 端用户,追都不好追。


哎,惨啊,真的惨。


我再强调一次啊,以上是我个人的猜测,没有任何依据。


我作为一个程序员,屁股当然得歪一下,当然是希望这个问题和程序员无关了。


只有苦一苦运营的兄弟了。(手动狗头


如果真的是因为程序员编码问题导致的,朋友,不管你看不看得到,挺住,没啥大不了的,我入行第一天就查过了,程序员因为非主观 BUG 导致公司重大损失的,不需要承担法律责任。


但是,话又说回来,这里面就没有程序员的事儿了吗?


肯定有呀!


分析数据范围,捞问题数据,做数据修复等等这些后续的事情,肯定还是程序员来做的。


昨天晚上一定有一波相关人员没睡觉,整晚整晚的都在想这个事情到底是什么了。


另外,我昨天还看到了这个短信:



我猜这个短信是假的,因为短信内容太过随意了。


简单来说,“BUG”这个词,就不应该出现在短信里面,不可能是官方用语。


你能确保收到短信的每个人都知道“BUG”是什么意思吗?


注意信息茧房的存在,你认为的常识不一定是每个人的常识。


我就在现实生活中遇到过不知道“BUG”是什么意思的,我还要解释一番。


另外,都说到短信了,顺便给大家分享一个小技巧。


这个截图暴露了短信发送方的前 8 位数字。


这 8 位数字就是短信号段,这里面是大有文章。


简单来说,106 短信码号码是由工信部或通信管理局颁发,接入移动、联通、电信三大基础运营商并进行统一管理,共分为1062、1063、1065、1066、1068 和 1069,用于不同的服务目的。


其中,1066、1068/1069 号段的短信需要企业在工信部进行申请,1062、1063 号段则是向各省通信管理局进行申请。


国家也有一个号段查询网站,前八位一输就知道这个短信是哪一家公司发的了。


比如上面截图中的 10680503:



nac.miit.gov.cn/#/notice/gx…




这个公司看起来和支付宝没有任何关系,这个也正常。你用真的支付宝发送的短信去查,查到的也不是支付宝,因为这只是对接的众多短信通道中的一个而已。


但是如果以后收到骚扰短信,可以按照这个去投诉,最终这个投诉会附带着一点点惩罚,到最初的短信发送方的。


一个小技巧,送给大家,祝大家春节前的倒数第二个周末愉快。


最后,不管和程序是否相关,保命箴言再背一次总是没错的:可监控、可灰度、可回滚。


---- 2025.01.17 01:22 更新 ----


上面的内容 16 号晚上就写好了,想着早上起来就发。 17 号凌晨被起来上厕所的时候看到官方声明了:



看起来确实是运营活动配置问题,看来歪师傅的猜测还是比较靠谱的。


看起来没有一个程序员在本次事件中受伤。


但是看起来又像是一个边界值的问题,可能存在程序校验不到位的情况。


然而这些都不重要了,重要的是我怎么开始起夜了???



一定是白天水喝多了,嗯,一定是这样的。


最后,欢迎关注公众号:why技术。好玩儿,来玩儿。


作者:why技术
来源:juejin.cn/post/7460449861403951113
收起阅读 »

外行转码农,焦虑到躺平

介绍自己 本人女,16年本科毕业,学的机械自动化专业,和大部分人一样,选专业的时候是拍大腿决定的。 恍恍惚惚度过大学四年,考研时心比天高选了本专业top5学校,考研失败,又不愿调剂,然后就参加校招大军。可能外貌+绩点优势,很顺利拿到了很多工厂offer,然后欢...
继续阅读 »



介绍自己


本人女,16年本科毕业,学的机械自动化专业,和大部分人一样,选专业的时候是拍大腿决定的。


恍恍惚惚度过大学四年,考研时心比天高选了本专业top5学校,考研失败,又不愿调剂,然后就参加校招大军。可能外貌+绩点优势,很顺利拿到了很多工厂offer,然后欢欢喜喜拖箱带桶进厂。


每天两点一线生活,住宿吃饭娱乐全在厂区,工资很低但是也没啥消费,住宿吃饭免费、四套厂服覆盖春夏秋冬。


我的岗位是 inplan软件维护 岗位,属于生产资料处理部门,在我来之前6年该岗位一直只有我师傅一个人,岗位主要是二次开发一款外购的软件,软件提供的api是基于perl语言,现在很少有人听过这个perl吧。该岗位可能是无数人眼里的神仙岗位吧,我在这呆了快两年,硬是没写过一段代码...


inplan软件维护 岗位的诞生就是我的师傅开创的,他原本只是负责生产资料处理,当大家只顾着用软件时,他翻到了说明书上的API一栏,然后写了一段代码,将大家每日手工一顿操作的事情用一个脚本解决了,此后更是停不下来,将部门各种excel数据处理也写成了脚本,引起了部门经理的注意,然后就设定了该岗位。


然而,将我一个对部门工作都不了解的新人丢在这个岗位,可想我的迷茫。开始半年师傅给我一本厚厚的《perl入门到精通》英文书籍,让我先学会 perl 语言。(ps:当时公司网络不连外网,而我也没有上网查资料的习惯,甚至那时候对电脑操作都不熟练...泪目)


师傅还是心地很善良很单纯的人,他隔一段时间会检查我的学习进度,然而当他激情澎拜给我讲着代码时,我竟控制不住打起了瞌睡,然后他就不管我了~~此后我便成了部门透明人物,要是一直透明下去就好了。我懒散的工作态度引起了部门主管的关注,于是我成了他重点关注的对象,我的工位更是移到了他身后~~这便是我的噩梦,一不小心神游时,主管的脸不知啥时凑到了我的电脑屏幕上~~~😱


偶然发现我的师傅在学习 php+html+css+js,他打算给部门构建一个网站,传统的脚本语言还是太简陋了。我在网上翻到了 w3scool离线文档 ,这一下子打开了我的 代码人生。后面我的师傅跳槽了,我在厂里呆了两年觉得什么都没学到,也考虑跳槽了。


后面的经历也很魔幻,误打误撞成为了一名前端开发工程师。此时是2018年,算是前端的鼎盛之年吧,各种新框架 vue/react/angular 都火起来了,各种网站/手机端应用如雨后春笋。我的前端之路还算顺利吧,下面讲讲我的经验吧


如何入门


对于外行转码农还是有一定成本的,省心的方式就是报班吧,但是个人觉得不省钱呀。培训班快则3个月,多的几年,不仅要交上万的培训费用,这段时间0收入,对于家境一般的同学,个人不建议报班。


但是现在市场环境不好,企业对你的容忍度不像之前那么高。之前几年行业缺人,身边很多只懂皮毛的人都可以进入,很多人在岗位半年也只能写出简单的页面,逻辑复杂一点就搞不定~~即使被裁了,也可以快速找到下家。这样的日子应该一去不复返了,所以我们还是要具备的实力,企业不是做慈善的,我们入职后还是要对的起自己的一份工资。


讲讲具体怎么入门吧


看视频:


b站上有很多很多免费的视频,空闲之余少刷点段子,去看看这些视频。不要问我看哪个,点击量大的就进去看看,看看过来人的经验,看看对这个行业的介绍。提高你的信息量,普通人的差距最大就在信息量的多少


还是看视频:


找一个系统的课程,系统的学习 html+css+js+vue/react,我们要动手写一些demo出来。可以找一些优秀的项目,自己先根据它的效果自己实现,但后对着源码看看自己的局限,去提升。


做笔记:


对于新人来说,就是看了视频感觉自己会了,但是写起来很是费力。为啥呢?因为你不知道也记不住有哪些api,所以我们在看视频学习中,有不知道的语法就记下来。

我之前的经验就是手动抄写,最初几年抄了8个笔记本,但是后面觉得不是很方便,因为笔记没有归纳,后续整理笔记困难,所以我们完全可以用电子档的形式,这方便后面的归纳修改。

嘿嘿,这里给大家推荐一下我的笔记 前端自检清单,这是我对我的笔记的总结,现在看来含金量不是很大,这些文章基本就copy总结别人的文章,很少有自己的思想,我更多是将它当成一个手册吧,我自己也经常遗忘一些API,所以时不时会去翻翻。


回顾:


我们的笔记做了就要经常的翻阅,温故而知新,经常翻阅我们的笔记,经常去总结,突然有一天你的思维就上升了一个高度。



  • 慢慢你发现写代码就是不停调用api的过程

  • 慢慢你会发现程序里的美感,一个设计模式、一种新思维。我身边很多人都曾经深深沉迷过写代码,那种成就感带来的心流,这是物质享受带来不了的


输出:


就是写文章啦,写文章让我们总结回顾知识点,发现知识的盲区,在这个过程中进行了深度思考。更重要的是,对于不严谨的同学来说,研究一个知识点很容易浅尝则止,写文章驱动自己去更深层系统挖掘。不管对于刚入行的还是资深人士,我觉得输出都是很重要的。


推荐大家去看神说要有光大神的文章 为什么我能坚持?因为写技术文章给我的太多了呀!,这时我最近很喜欢的一个大神,他的文章我觉得很有深度广度(ps:不是打广告呀,真心觉得受益了)。


持续提升


先谈谈学历歧视吧,现在很多大厂招聘基本条件就是211、985,对此很是无奈,但是我内心还是认可这种要求的,我对身边的本科985是由衷的佩服的。我觉得他们高考能考上985,身上都是有过人之处的,学习能力差不了。


见过很多工作多年的程序员,但是他们的编码能力无法描述,不管是逻辑能力、代码习惯、责任感都是很差的,写代码完全是应付式的,他们开发的代码如同屎山。额,但是我们也不要一味贬低他人,后面我也学会了尊重每一个人,每个人擅长的东西不一样,他可能不擅长写代码,但是可能他乐观的心态是很多人不及的、可能他十分擅长交际...


但是可能的话,我们还是要不断提高代码素养



  • 广度:我们实践中,很多场景没遇到,但是我们要提前去了解,不要等需要用、出了问题才去研究。我们要具备一定的知识面覆盖,机会是给有准备的人的。

  • 深度:对于现在面试动不动问源码的情况,很多人是深恶痛绝的,曾经我也是,但是当我沉下心去研究的时候,才发现这是有道理的。阅读源码不仅挺高知识的广度,更多让我们了解代码的美感


具体咋做呢,我觉得几下几点吧。(ps:我自己也做的不好,道理都懂,很难做到优秀呀~~~)



  • 扩展广度:抽空多看看别人的文章,留意行业前沿技术。对于我们前端同学,我觉得对整个web开发的架构都要了解,后端同学的mvc/高并发/数据库调优啥的,运维同学的服务器/容器/流水线啥的都要有一定的了解,这样可以方便的与他们协作

  • 提升深度:首先半路出家的同学,前几年不要松懈,计算机相关知识《操作系统》《计算机网络》《计算机组成原理》《数据结构》《编译原理》还是要恶补一下,这是最基础的。然后我们列出自己想要深入研究的知识点,比如vue/react源码、编译器、低代码、前端调试啥啥的,然后就沉下心去研究吧。


职业规划


现在整个大环境不好了,程序员行业亦是如此,身边很多人曾经的模式就是不停的卷,卷去大厂,跳一跳年薪涨50%不是梦,然而现在不同了。寒风凌凌,大家只想保住自己的饭碗(ps:不同层次情况不同呀,很多大厂的同学身边的同事还是整天打了鸡血一般)


曾经我满心只有工作,不停的卷,背面经刷算法。22年下半年市场明显冷下来,大厂面试机会都没有了,年过30,对大厂的执念慢慢放下。


我慢慢承认并接受了自己的平庸,然后慢慢意识到,工作只是生活的一部分。不一定要担任ceo,才算走上人生巅峰。最近几年,我爱上了读书,以前只觉得学理工科还是实用的,后面慢慢发现每个行业有它的美感~


最后引用最近的读书笔记结尾吧,大家好好体会一下论语的“知天命”一词,想通了就不容易焦虑了~~~



自由就是 坦然面对生活,看清了世界的真相依然热爱生活。宠辱不惊,闲看庭前花开花落。去留无意,漫随天外云卷云舒。



image.png


作者:liucheng58
来源:juejin.cn/post/7343138429860347945
收起阅读 »

小毛驴 40km 通勤上班:不一样的工作日!

从到公司上班之后因为距离变远了,也不能像之前一样小毛驴上下班了。 所以通勤方案就变成了: 上班: 小毛驴 15min ----- 地铁 40min ----- 公交OR共享单车 12min...
继续阅读 »

从到公司上班之后因为距离变远了,也不能像之前一样小毛驴上下班了。


所以通勤方案就变成了:


上班:

小毛驴 15min ----- 地铁 40min ----- 公交OR共享单车 12min + 步行 5min

下班:

公交 12min ----- 地铁 40min ----- 小毛驴 15min

通勤费用: 小毛驴一块钱充电可以开两天。地铁 + 公交 来回 12块。


这半年下来地铁已经坐够够了。🤦‍♂️ 有的时候实在是不想坐了。就动了开小毛驴的心思。


但是百度地图看从家到公司的距离是 34km。之前公司到家的百度距离是 18km,其实等于翻翻了。


而且之前的路况很好么有什么红绿灯而且路上的人也很少。所以基本没有什么时间浪费18km大概半个小时左右就到了。


本来是想直接买一个新电瓶车来通勤用的,但是碰到那个什么新国标要去考摩托车驾-照就耽搁了。


然后正好这两天天气还行不冷不热。我就想要买今天就开小毛驴去公司得了。正好熟悉下路况。


早上还是按照正常出门的时间 7.25 出门。然后按照百度导航直接走。因为第一次开,路况不熟悉。按照百度走的路线全是走的人多的地方。早上正好又是上班高峰期。非机动车道上全部都是人。而且路上的红绿灯贼多。基本遇到一个红绿灯就要停下来。


前半程车的电量充足速度可以很快,但是路况太差了。路上人太多,而且有占着超车道一直慢悠悠的。开的血压飙升。所以就导致速度起不来。然后到了后半程的时候全是大路。而且没有什么红绿灯也没啥人,但是电量下去了,速度又上不来。脑壳痛!


最后到公司楼下的时候是 8.42。百度地图显示 34km 需要 2 小时零五分。实际电瓶车里程显示 40km ,耗时一小时 20 分。


其实 1 小时开车的时间是感知不到的。前半程因为都是人所以精神高度集中。


另外路上的风景也是不错的。可以走之前没有走到的地方。可以愉快的画图。


下面早上的时候拍的,因为第一次。怕时间不够。就随便瞎拍了两张记录了一下。


IMG_20240428_105957.jpg


IMG_20240428_105852.jpg


IMG_20240428_110048.jpg


IMG_20240428_104954.jpg


IMG_20240428_110017.jpg


等会晚上回去的时候看看能不能走另外一条路会不会快点。


IMG_20240427_221436.jpg


IMG_20240427_221603.jpg


MVIMG_20240426_192534.jpg


IMG_20240427_205345.jpg


IMG_20240427_222136.jpg


IMG_20240427_221712.jpg


IMG_20240427_222732.jpg


IMG_20240427_221628.jpg


IMG_20240427_221326.jpg


IMG_20240427_221537.jpg


作者:执行上下文
来源:juejin.cn/post/7362729128476524563
收起阅读 »

小米正式官宣开源!杀疯了!

最近,和往常一样在刷 GitHub Trending 热榜时,突然看到又一个开源项目冲上了 Trending 榜单。 一天之内就狂揽数千 star,仅仅用两三天时间,star 数就迅速破万,增长曲线都快干垂直了! 出于好奇,点进去看了看。 好家伙,这居然还是...
继续阅读 »

最近,和往常一样在刷 GitHub Trending 热榜时,突然看到又一个开源项目冲上了 Trending 榜单。


一天之内就狂揽数千 star,仅仅用两三天时间,star 数就迅速破万,增长曲线都快干垂直了!



出于好奇,点进去看了看。


好家伙,这居然还是小米开源的项目,相信不少小伙伴也刷到了。



这个项目名为:ha_xiaomi_home


全称:Xiaomi Home Integration for Home Assistant


原来这就是小米开源的 Home Assistant 米家集成,一个由小米官方提供支持的 Home Assistant 集成组件,它可以让用户在 Home Assistant 平台中使用和管理小米 IoT 智能设备。


Home Assistant 大家知道,这是一款开源的家庭自动化智能家居平台,以其开放性和兼容性著称,其允许用户将家中的智能设备集成到一个统一的系统中进行管理和控制,同时支持多种协议和平台。



通过 Home Assistant,用户可以轻松地实现智能家居的自动化控制,如智能灯光、智能安防、智能温控等,所以是不少智能家居爱好者的选择。


另外通过安装集成(Integration),用户可以在 Home Assistant 上实现家居设备的自动化场景创建,并且还提供了丰富的自定义功能,所以一直比较受 DIY 爱好者们的喜爱。



大家知道,小米在智能家居领域的战略布局一直还挺大的,IoT 平台的连接设备更是数以亿记,大到各种家电、电器,小到各种摄像头、灯光、开关、传感器,产品面铺得非常广。



那这次小米开源的这个所谓的米家集成组件,讲白了就是给 Home Assistant 提供官方角度的支持


而这对于很多喜欢折腾智能家居或者 IoT 物联网设备的小伙伴来说,无疑也算是一个不错的消息。


ha_xiaomi_home 的安装方法有好几种,包括直接 clone 安装,借助 HACS 安装,或者通过 Samba 或 FTPS 来手动安装等。


但是官方是推荐直接使用 git clone 命令来下载并安装。


cd config
git clone https://github.com/XiaoMi/ha_xiaomi_home.git
cd ha_xiaomi_home
./install.sh /config

原因是,这样一来当用户想要更新至特定版本时,只需要切换相应 Tag 即可,这样会比较方便。


比如,想要更新米家集成版本至 v1.0.0,只需要如下操作即可。


cd config/ha_xiaomi_home
git checkout v1.0.0
./install.sh /config

安装完成之后就可以去 Home Assistant 的设置里面去添加集成了,然后使用小米账号登录即可。



其实在这次小米官方推出 Home Assistant 米家集成之前,市面上也有一些第三方的米家设备集成,但是多多少少会有一些不完美的地方,典型的比如设备状态响应延时,所以导致体验并不是最佳。


与这些第三方集成相比,小米这次新推出的官方米家集成无论是性能还是安全性都可以更期待一下。


如官方所言,Home Assistant 米家集成提供了官方的 OAuth 2.0 登录方式,并不会在 Home Assistant 中保存用户的账号密码,同时账号密码也不再需提供给第三方,因此也就避免了账号密码泄露的风险。


但是这里面仍然有一个问题需要注意,项目官方也说得很明确:虽说 Home Assistant 米家集成提供了 OAuth 的登录方式,但由于 Home Assistant 平台的限制,登录成功后,用户的小米用户信息(包括设备信息、证书、 token 等)会明文保存在 Home Assistant 的配置文件中。因此用户需要保管好自己的 Home Assistant 配置文件,确保不要泄露。


这个项目开源之后,在网上还是相当受欢迎的,当然讨论的声音也有很多。
小米作为一家商业公司,既然专门搞了这样一个开源项目来做 HA 米家集成,这对于他们来说不管是商业还是产品,肯定都是有利的。


不过话说回来,有了这样一个由官方推出的开源集成组件,不论是用户体验还是可玩性都会有所提升,这对于用户来说也未尝不是一件好事。


那关于这次小米官方开源的 Home Assistant 米家集成项目,大家怎么看呢?


作者:CodeSheep
来源:juejin.cn/post/7454170332712386572
收起阅读 »

2025:白手起家,两娃的爸准备创业

前言 2024年陆陆续续听到老东家几个同事被裁的消息,倒有些后悔2023年自己主动提出离职,结束北漂回老家。算了下,损失个小几十万。 2024年6月份来了一场彻彻底底的自我反思,找不到明确目标,于是稀里糊涂定了两个计划: 每天读书,围绕技术、文学、创业类。 ...
继续阅读 »

前言


2024年陆陆续续听到老东家几个同事被裁的消息,倒有些后悔2023年自己主动提出离职,结束北漂回老家。算了下,损失个小几十万。


2024年6月份来了一场彻彻底底的自我反思,找不到明确目标,于是稀里糊涂定了两个计划:



  • 每天读书,围绕技术、文学、创业类。

  • 粉丝数涨到150。


工作方面,实话说呆的有些憋屈,但也算尽职尽责,年底拿了个优秀员工,满足了个人虚荣心。


创业必定是成功率低、过程艰巨的事,有这方面想法的小伙伴可+V微信进群相互取暖:wxiaomimap


2024年总结


掘金涨粉情况


说到定涨粉目标,有些低估自己的实力。当时只有30个粉丝,对标了下掘金的优秀创作者,感觉自己和他们差距挺大,于是定了个保守的目标:达到150个粉。


image.png


中间调整了两次目标,分别是300、500,最终赶在12月前把年度目标给完成了。写作过程中也得到官方、掘友、编辑社、三方的认可。


官方的:
image.png


掘友的:


image.png


三方的:


image.png


关于读书


为什么读书,看文学类书主要和老板吹牛用,看技术、创业类书算是个人储备。


截止目前已连续阅读209天,读完33本,做了3432条笔记。读书时间集中在上下班路上以及周末。


image.png


如果像读小说一样把书读完,作用真心不大,所以读完的书强迫自己写读后感,到目前已为27本书写过读后感。


image.png


2025年:两娃的爸,创业筹备


2024年12月初完成了年度目标,但也没闲着,开始着手考虑2025年我能干什么。为什么是创业筹备?基于两个原因。


按目前的大环境,工作的尽头就是被裁。前两天看到"前端欧阳"也面临失业(就他的实力,值得一个好工作),当公司遇到困难,技术牛人也仅是牛马。


image.png


卷孩子不如卷自己!现在的小孩压力太大,1月11日刚放寒假,1月11日-1月17日早上8:30-12:00,下午13:30-15:30已经排好了兴趣班、补习。有人说小孩的压力还不是大人给的,我只能说懂得都懂。所以,为了孩子轻松些,还是卷自己吧。


基于以上两个原因,经过12月份的深思熟虑,决定2025年开始个人产品筹备,把职业生涯紧紧把握在自己手里。目前有哪些进展?


产品定位、市场调研


作为技术人员,一定要分清开源项目和商业产品的区别。开源项目大多是自我价值体现,一个优秀的开源项目能在同圈提升个人影响力。一个商业产品的目的是盈利,当没考虑清楚产品最终能落地的受益群体,不能盲目启动。十个创业九个跨,有人会说即使失败,也会从失败中学习,但你付出的时间成本、金钱成本远大于你从失败中收获的经验。


目前,一边读创业类书籍学习前人经验,一边调研竞品市场。


已读完的创业类书籍:《财富自由从0到1》、《幕后产品》、《新手开公司》、《精益创业》。


之前有考虑做一款地图产品,当了解了市场同类产品exping,无法商业变现,2024年11月刚停止服务,我也就放弃做同类产品。


image.png


当前是个人IP的互联网时代,强如企业家雷军、周鸿祎、余承东、何小鹏等都得亲自下场运营各大视频号。甚至部分个人IP都能耗动一个公司的存亡。 微信公众号、微信视频号、抖音、小红书、快手、B站都是最小成本的产品运营渠道。


个人产品进展


目前已确认产品面向的差异化市场还存在比较大的空间。产品前期主要 to C,后期可根据影响力扩展to B。


团队组建过程也是检验产品能否落地的手段之一,如果没人愿意和你一起实施,那你得思考产品是否还值得继续做下去!差不多每个人都聊了2小时起步, 目前已有了可实施产品的初期团队,共5个人,包含前端、后端、UI、产品。


image.png


申请个人商标,当产品名称确认清楚了就得尽早申请商标。这不前段时间小米的Yu 7商标闹了乌龙,被别人占用。商标申请得有个人营业执照或企业执照。


创建了运营产品的公众号、视频号,虽然还没发什么内容,但得先想到运营手段。全网同名:绘个球


一个人走的快,一群人走的更远


2025年还得靠公司养活,在公司工作得干好。余下的时间并不多,产品、运营、技术、计划一个不落地都得考虑,一个产品能面向市场并开花结果,得有一群志同道合的伙伴一起使劲,2025在路上。。。。。。


产品开发在技术方面也会面临很多挑战,2025年文章输出集中在产品实现的技术挑战方面。


创业必定是成功率低、过程艰巨的事,有这方面想法的小伙伴可加我微信进群聊:wxiaomimap



我是前端下饭菜,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评论!



作者:前端下饭菜
来源:juejin.cn/post/7458931012854562842
收起阅读 »

不要让认知困死自己

7 年前,我被培训机构 8k 高薪的幌子,骗着带款了 2w 块钱,签完合同的后,我才觉察到自己被骗了。 但是 Java 好像挺有意思的,我学得不错,班里有 40 多个人,最后只有 3 个人找到了工作,我就是其中之一。 于是我入行了 Java,我的第一份工作在去...
继续阅读 »

7 年前,我被培训机构 8k 高薪的幌子,骗着带款了 2w 块钱,签完合同的后,我才觉察到自己被骗了。


但是 Java 好像挺有意思的,我学得不错,班里有 40 多个人,最后只有 3 个人找到了工作,我就是其中之一。


于是我入行了 Java,我的第一份工作在去哪儿,很感谢当年的领导给我面试机会,要知道当时我只有高中文凭,HR 反复确认要我来面试吗?我的领导说来试试吧。


为了那次面试,我推掉了所有其他面试,我对自己说一定要拿下这个 offer,在去面试的地铁上,我还在准备,我甚至准备了万一别人不要我,我可以做些什么来挽回。


后来面试通过了。


我珍惜这来之不易的机会,努力奋斗,晚上 10 点多下班是常态,有次冬天加到 12 点,我走在路上看着满大街的雪,反射出白光照向天空,亮亮的一点也不像晚上,我也一点都不冷。


那真是段美好的回忆,但是一年半后,压力太大,我主动提出了离职。


我跳槽到一家私企,工资翻了一倍,而且工作内容相比较之前,轻松的要死。除了基础代码开发,我顺手把服务器、Jenkins 部署、发布脚本、gitlab、redis、测试和线上环境全搭了。


但公司业务没做起来,部门解散了。


我接着找工作,年底通过了 thoughworks 的面试,和 HR 约定好了开年就发 offer 去上班,接到电话的时候我在图书馆,真是开心死了,现在回忆起来也不经咧开了嘴,爷也是能去外企的人了哈哈。


但是造化弄人,过年期间疫情来了,我的 offer 没了。


后来找到了一家创业公司,我和 boss 聊的很合拍,入职后工作了一年多,他和福禄建立了深度合作,把我带了过去。


截止至今,我在福禄工作了四年多,现在我面临着和 6 年前一样的问题,我对工作有了不一样的看法。


人们把工作当成一个赚钱的方式,出卖自己的时间换取经济价值。当然这没有问题,但我相信我的 boss,我感谢他知遇之恩,这些年我一直尽力多做一些事情。


只不过我的能力和认知,没有到那个境界,做的是很多,但也做错了很多,方法不对,成长也不够。


直到最近一次线上事故,我被击垮了。这是我五年来,第一次滋生离职的念头。


放下了对 boss “报恩” 的想法,我开始再次思考,工作的意义、生活的意义、人生的意义。


这不是我第一次思考,虽然我到现在也没找到答案。


曾经我的生活一眼望得到头,事业上,35 岁前努力工作存钱,35 后有了一定风险,但应该还能再工作几年,只要任劳任怨,万一真没公司要,再想别的出路,什么出路,我也不知道,生命总会找到出路。


生活中,20 多岁结婚,过几年生个娃,然后赚钱养家,天天为孩子奔波,把孩子养大,尽量给他好的环境。


身边的同事、朋友都是这样的,他们也劝我这样,不要想那些有的没的。


但是我忍不住会想,那我呢?我把时间给了工作、家庭、孩子,我在哪里?


有多少人真的了解自己?我不了解,我不知道自己喜欢什么,擅长什么,未来想做什么,想成为什么样的人。


我只是随大流,别人做什么,我也跟着做。买房、买车、结婚、生娃,这些人生重要节点,我做的那么随意。


痛苦让我成长,让我反思。如果回到 10 年前,我会对那时的自己这么说。


人生的重大决策,一定要仔细思考


买房


不要买房,不要背 30 年带款。这会把人压死,让人不敢尝试,不敢探索,失去勇气。


有个房贷压在头顶,那种窒息感和压力,无时无刻不在消耗自己。


做任何经济上有关的决策,都会忍不住想到我还有房贷呢。


会错失很多机会,也会让操作变形。


买车


车也是个消耗品,买车要钱,养车也要钱。停车费、过路费、油费、保养、车险,每年怎么也要大几千上万。


如果车带来的价值不如车的支出,我建议直接把房子租在公司附近,走路上下班,平时有事打车。


结婚


我之前从未思考过结婚意味着什么,也是随大流的和一个女孩子谈恋爱,谈了几年差不多了,就结婚了。


结婚意味着,和一个人共度余生。


这种影响未来几十年的决定,我甚至没认真思考过一天。


所以请一定要认真思考,可能即使我怎么说也不能理解,第一次结婚都没什么经验。


具体点说就是不要因为父母催婚去结婚;想清楚两个人之间的大方向上能不能统一;想清楚自己想从婚姻中获得什么,能提供什么价值;花点钱去找专业的人咨询,别找自己身边的案例,和自身一个 level 的人不会有很深的思考;不要因为牛牛充血一时冲动。


好在我运气不错,老婆挺好的,我们没什么摩擦,除了她不是很理解我的一些想法,剩下就是我跟个唐僧一样喜欢逼逼叨。


生娃


生娃那简直是比结婚更要命的存在。


以我现在的认知,我真的不理解为什么要生娃。


知乎上各种各样的答案,没一个能说服我,什么觉得生活没意思造个娃;夫妻生活不和谐靠娃调节等。


如果觉得生活没意思,夫妻没感情,那是自己的问题,生娃只是转移了问题,而不是解决问题。


所以我的态度是在没有想清楚为什么生娃之前,不生娃。


买车、买房、生娃,就算决定要做,我也建议晚点做,趁年轻先把事业打顺。


事业


刚进社会,懵懵懂懂啥都不知道,哪家公司给的钱多就去哪家。这是对的。


然后努力工作,不要躺平,多赚点钱,多存点钱。


工作了几年以后,兜里有点积蓄了,能覆盖两三年的支出,就要开始思考了。


这份工作有前途吗?我喜欢吗?对我个人有什么帮助?能学到什么知识?可不可以试试别的工作?


选择什么工作,入什么行业。这也是影响未来几十年的决策。


我觉得不要把工作看成出卖时间换取收入,那样会觉得自己在给别人打工,在给别人做事,心很累,做的事也不咋地。


应该把工作看成能力训练场、大型实验基地、资源交换中心;通过工作提升自己能力,让自己更值钱,通过公司验证个人的方法,学习经验,整理方法论。


把技能和经验学到一定程度,就可以结合手头积累的资源,自己创业了。


以学会赚钱为目的,为自己打工。


现在新能源和 AI 是公认的有前景的行业,往这两个方向靠。


现在我处在有点积蓄,准备尝试新的方向,去和钱比较近的岗位,学习赚钱能力,为以后创业赚钱打打基础。


寻找人生意义


我想有钱了再思考这个问题,被生活压的喘不过气的人,天天为生活奔波,哪有时间想这些呢?


在没有找到答案前,认真生活,对自己负责。


空闲时间刷抖音,打游戏,到处玩,偶尔放松可以理解,一直这样不行,这不叫认真生活。


当然这不怪当事人,我之前也是那样的,下班和周末看直播,刷短视频,搞学习什么,不存在的。


直到近期的变故,我深入思考这些问题,到处找课找人学习,认知有了提升,做这些事变得理所当然。


这种转变就像之前是强迫自己每天必须写篇文章,做复盘写总结,用意志力坚持,很痛苦,坚持不了多久。


现在是就是想写了,有感悟,想找个地方记录,主动的写,认真的写。


希望我的经历可以给各位参考,尽快提升认知,趁年轻,还有机会。


不要等有了车贷房贷,还有娃,但被裁了,或还没被裁但被当牛做马使劲压榨,而自身却没了任何反抗资本,才幡然醒悟,那样太残忍了些。


加油,共勉。


作者:jianzhangg
来源:juejin.cn/post/7458954918590988328
收起阅读 »

50个月年终奖?看完内部贴,我释怀的笑了

xhs 这两天,一篇爆料《xhs 最高 50 个月年终奖》的帖子火了。 从帖子的内容来看:绩效 3.75 可以拿到 8 个月年终奖;绩效 4 可以拿到 20 个月的年终奖;绩效 5 可以拿到 20 个月年终奖,外加 30 个月的期权。 综合下来,今年 xhs...
继续阅读 »

xhs


这两天,一篇爆料《xhs 最高 50 个月年终奖》的帖子火了。



从帖子的内容来看:绩效 3.75 可以拿到 8 个月年终奖;绩效 4 可以拿到 20 个月的年终奖;绩效 5 可以拿到 20 个月年终奖,外加 30 个月的期权。


综合下来,今年 xhs 最高有 50 个月的年终奖,就这帖子,还是带有「xhs 职业认证」(至少通过了企业邮箱认证)的网友发的 🤣🤣🤣


看完后,我直接沉默了。


前几天才讲了 蔚来最高 1 个月年终奖,还划分出多个等级,今天就有「xhs 年终奖 8 个月起步,最高 50 个月」的新故事。


现在简中网的黑话越来越多,要不是资深冲浪选手,有时候都真分不清楚网友发的内容,是在描述事实,还是在反串黑。


一般对于这些"惊天"好消息,如果是真,那么各大 App 早就预定热搜,但事实上并没有。


再进一步,深入到「仅 xhs 员工可见的同事圈」里,发现并不是"普天同庆,一片热烈"的内部氛围,反而更像是维权集中地。


有真诚反问,上半年的高绩效奖励到底还有没有,什么时候通知:



还有距离期权归属(真正到手)还剩两个月,就被"关账号"走人的吐槽贴:



一圈看下来,离职的在吐槽、低绩效的在吐槽、高绩效的也在吐槽 🤣🤣🤣



然后再回想刚开始的「50 个月年终奖」,我释怀的笑了,感觉还是太保守了,毕竟通知了也不一定什么时候发放,发放了也不一定能熬到期权归属,应该直接喊它 100 个月。


对此,你怎么看?你司的年终奖多少个月起步?


...


回归主题。


来一道和「校招」相关的算法题。


题目描述


平台:LeetCode


题号:926


如果一个二进制字符串,是以一些 00(可能没有 00)后面跟着一些 11(也可能没有 11)的形式组成的,那么该字符串是单调递增的。


给你一个二进制字符串 s,你可以将任何 00 翻转为 11 或者将 11 翻转为 00


返回使 s 单调递增的最小翻转次数。


示例 1:


输入:s = "00110"

输出:1

解释:翻转最后一位得到 00111.

示例 2:


输入:s = "010110"

输出:2

解释:翻转得到 011111,或者是 000111

示例 3:


输入:s = "00011000"

输出:2

解释:翻转得到 00000000

提示:



  • 1<=s.length<=1051 <= s.length <= 10^5

  • s[i]'0''1'


LIS 问题贪心解


根据题意,不难想到将原题进行等价转换:s 长度为 nn,原问题等价于在 s 中找到最长不下降子序列,设其长度为 ansans,那么对应的 nansn - ans 即是答案。


由于数据范围为 1e51e5,因此我们需要使用 LIS 问题的贪心求解方式:使用 g 数组记录每个长度的最小结尾元素,即 g[len] = x 含义为长度为 lenlen 的最长不下降子序列的结尾元素为 xx,然后在从前往后处理每个 t=s[i]t = s[i] 时,由于是求解「最长不下降子序列」,等价于找「满足大于 tt 的最小下标」,这可以运用「二分」进行求解。



不了解 LIS 问题或者不清楚 LIS 问题贪心解法的同学可以看前置 🧀 : LCS 问题与 LIS 问题的相互关系,以及 LIS 问题的最优解证明,里面详细讲解了 LIS 贪心解的正确性证明,以及 LCSLIS 在特定条件下存在的内在联系。



Java 代码:


class Solution {
public int minFlipsMonoIncr(String s) {
char[] cs = s.toCharArray();
int n = cs.length, ans = 0;
int[] g = new int[n + 10];
Arrays.fill(g, n + 10);
for (int i = 0; i < n; i++) {
int t = s.charAt(i) - '0';
int l = 1, r = i + 1;
while (l < r) {
int mid = l + r >> 1;
if (g[mid] > t) r = mid;
else l = mid + 1;
}
g[r] = t;
ans = Math.max(ans, r);
}
return n - ans;
}
}

C++ 代码:


class Solution {
public:
int minFlipsMonoIncr(string s) {
int n = s.length(), ans = 0;
vector<int> g(n + 10, n + 10);
for (int i = 0; i < n; i++) {
int t = s[i] - '0';
int l = 1, r = i + 1;
while (l < r) {
int mid = l + r >> 1;
if (g[mid] > t) r = mid;
else l = mid + 1;
}
g[r] = t;
ans = max(ans, r);
}
return n - ans;
}
};

Python 代码:


class Solution:
def minFlipsMonoIncr(self, s: str) -> int:
n, ans = len(s), 0
g = [n + 10] * (n + 10)
for i in range(n):
t = int(s[i])
l, r = 1, i + 1
while l < r:
mid = l + r >> 1
if g[mid] > t:
r = mid
else:
l = mid + 1
g[r] = t
ans = max(ans, r)
return n - ans

TypeScript 代码:


function minFlipsMonoIncr(s: string): number {
let n = s.length, ans = 0;
const g = new Array(n + 10).fill(n + 10);
for (let i = 0; i < n; i++) {
const t = parseInt(s[i]);
let l = 1, r = i + 1;
while (l < r) {
const mid = l + r >> 1;
if (g[mid] > t) r = mid;
else l = mid + 1;
}
g[r] = t;
ans = Math.max(ans, r);
}
return n - ans;
};


  • 时间复杂度:O(nlogn)O(n\log{n})

  • 空间复杂度:O(n)O(n)


前缀和 + 枚举


更进一步,利用 s 只存在 0011 两种数值,我们知道最后的目标序列形如 000...000000...111111...111 的形式。


因此我们可以枚举目标序列的 0011 分割点位置 idxidx(分割点是 0011 都可以,不消耗改变次数)。


于是问题转换为:分割点 idxidx 左边有多少个 11(目标序列中分割点左边均为 00,因此 11 的个数为左边的改变次数),分割点 idxidx 的右边有多少个 00(目标序列中分割点右边均为 11,因此 00 的个数为右边的改变次数),两者之和即是分割点为 idxidx 时的总变化次数,所有 idxidx 的总变化次数最小值即是答案。


而求解某个点左边或者右边有多少 1100 可通过「前缀和」进行优化。


Java 代码:


class Solution {
public int minFlipsMonoIncr(String s) {
char[] cs = s.toCharArray();
int n = cs.length, ans = n;
int[] sum = new int[n + 10];
for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (cs[i - 1] - '0');
for (int i = 1; i <= n; i++) {
int l = sum[i - 1], r = (n - i) - (sum[n] - sum[i]);
ans = Math.min(ans, l + r);
}
return ans;
}
}

C++ 代码:


class Solution {
public:
int minFlipsMonoIncr(string s) {
int n = s.length(), ans = n;
vector<int> sumv(n + 10, 0);
for (int i = 1; i <= n; i++) sumv[i] = sumv[i - 1] + (s[i - 1] - '0');
for (int i = 1; i <= n; i++) {
int l = sumv[i - 1], r = (n - i) - (sumv[n] - sumv[i]);
ans = min(ans, l + r);
}
return ans;
}
};

Python 代码:


class Solution:
def minFlipsMonoIncr(self, s: str) -> int:
n, ans = len(s), len(s)
sumv = [0] * (n + 10)
for i in range(1, n + 1):
sumv[i] = sumv[i - 1] + int(s[i - 1])
for i in range(1, n + 1):
l, r = sumv[i - 1], (n - i) - (sumv[n] - sumv[i])
ans = min(ans, l + r)
return ans

TypeScript 代码:


function minFlipsMonoIncr(s: string): number {
let n = s.length, ans = n;
const sumv = new Array(n + 10).fill(0);
for (let i = 1; i <= n; i++) sumv[i] = sumv[i - 1] + parseInt(s[i - 1]);
for (let i = 1; i <= n; i++) {
const l = sumv[i - 1], r = (n - i) - (sumv[n] - sumv[i]);
ans = Math.min(ans, l + r);
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(n)O(n)


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7458226274029797402
收起阅读 »

人总得裸辞一次(🎉裸辞|离沪|进京|入职|恋爱|买房|2024)

2024马上就要过去了,本命年呀,这一年真的太快太快了,发生了很多事情,记录一下,多年以后或许能够看到有一点点记录就足够了。 还记得2023年跨年的时候来北京跨年的,2024就真的到了北京工作,这一年算是集中发生了很多事情,比23年22年都精彩一些。 有一些错...
继续阅读 »

2024马上就要过去了,本命年呀,这一年真的太快太快了,发生了很多事情,记录一下,多年以后或许能够看到有一点点记录就足够了。


还记得2023年跨年的时候来北京跨年的,2024就真的到了北京工作,这一年算是集中发生了很多事情,比23年22年都精彩一些。


有一些错误的决定,也有一些正确的决定。总得来说差强人意,但是依旧悲观。


裸辞


上家公司是在上海,从本科毕业后就在这家公司工作,工作了两年吧,在这家公司认识了很多玩的很好很好的朋友,现在也时不时的会聚一下,群里总是会有各种话题
大家一起抱怨生活,一起玩乐,能在工作后认识这么要好的朋友实属幸运。


我们在上海的时候经常去唱歌,钓鱼,公园,晚上睡不着了在群里吆喝一声 喝点? 下一分钟就到了酒桌上,大家很合得来,都是性情中人,有时候会喝的多一点,然后第二天都请假了哈哈哈。


大家经常约出去玩,我们去过宁波、苏州、乌镇、无锡、扬州等上海周边的城市玩了个遍,现在想想那时候确实挺开心的,大家单身的居多,都是没有什么羁绊,玩得很高兴,压力大了偶尔打打麻将,去酒吧喝喝酒,然后接着周一上班,妥妥打工人生活实录,就靠周末过日子呐


但是公司效益不好,开始陆续裁员,除了我之外都回去了老家所在地工作,之后在上海就不好玩了,没有朋友,也没有钱,在这家公司两年了没有涨过工资,工作也是没有什么乐趣,那时候离职的心到达了顶峰,得知公司今年也不会涨薪后,我选择了裸辞不要裸辞!辞职后压力很大


离开


提了离职后,和项目上的同事们一起吃了一个散伙饭,然后在上海最后3个玩的比较好的朋友吃了顿饭,差不多我就留了一周时间在上海再待一周,看看上海,以后估计来的就比较少了。


那一周是真的舒服,没有工作压力,那时候也不想找工作,一周就是放松,不想任何事情,辞职的事情没有告诉父母,所以没有任何羁绊,天天吃了玩,玩了睡,差不多持续了一周后


我收拾行李开始北漂了,收拾行李的时候回想到刚毕业到上海漂泊,孤身一人,谁也不认识,搬家3次,发烧生病不及其次,其实就是加班太多了,压力太大导致的


当时和我非常要好的哥们苏总,我俩都阳了好几次,发烧好几次,什么病流行,我俩就得什么病,你看我们多时髦,在上海的两年还是收获了很多很多,但是已经不适合现阶段的我了,那就走!下一站北京!


进京


还好有一个非常要好的好哥们在北京实习,租的房子有一个月的空窗期,没有人住,所以我就可以白嫖一个月的住宿了,这一个月的白嫖住宿对我来讲还是很重要的,给我提供了一个月的找工作面试学习的时间。


非常感谢我这位朋友的照顾,才使得我在偌大的北京有一个可以休息的地方。感谢飞哥


从上海坐高铁到北京,自己拖着行李,正好那天晚上还在下雨,我见到了飞哥,飞哥请我吃了一顿饭后,我到了他的出租屋,那天晚上迟迟睡不着觉,想了很多事情,也担心找不到工作之后怎么办,回老家?回老家干什么工作呐?这或许是当代年轻人的痛吧


第二天睡醒后,我就开始找工作之旅了,没有接着休息了


现在的工作是真的不好找,主要还是自己核心竞争力不够导致的,经济形势不容乐观。到北京后也没有怎么玩,直接开始复习找工作,疯狂面试投简历,那段时间还是很规律的每天都在面试学习。


image.png


经过小一个月的备战,拿到了几个offer,最终选择了现在的这家公司


入职


拿到offer后很快就入职了,准备入职体检,体检tmd尿酸高点,我一般体检指标都是正常的,这次是第一次有指标不正常,不禁感慨,工作两年后确实伤身体


约好入职时间后,就休息了一两天,然后入职了现在这家公司,挺喜欢现在这家公司的,氛围很好,是一个创业公司,也不加班


在这家公司学习了很多,未来打算学习一下外语 这家公司有美国员工,正好练习一下口语


恋爱


啊哈哈哈,来北京一段时间后,就谈恋爱了,这里就简单说一下,留一点个人隐私


买房


在北京半个月稳定了后,我告诉了父母,我换工作到了北京,父母很开心,老家是邯郸的,距离北京不算远,基本上可以每周末都回家,经过考虑后,决定在邯郸买一个房子


在北京是不太可能了,未来大概率是要离开北京的,或早或晚吧,具体时间随缘吧


我拿了30%的首付钱,爸妈拿了70%的首付钱,我在邯郸买了一个三居室,在大学对面,我很喜欢这个地段,以后没事了大学里面看看腿也挺好的


这里也不细讲了,留点隐私


你呐?


你们呐,过的好吗?过的开心吗?


祝大家2025,天天开心








2024-12-12 于北京出租屋


作者:吃饺子不吃馅
来源:juejin.cn/post/7450878328036982819
收起阅读 »

不容易,35岁的我还在小公司苟且偷生

前言 前几天和前同事闲时聚餐,约了两个月的小聚终于达成了,程序员行业聚少离多,所幸大家的发量还坚挺着。 期间不可避免地聊到了自己的公司、行业状况以及对未来的看法,几杯老酒之后,大家畅所欲言,其中一位老哥侃起了他的职业生涯,既坎坷又无奈,饭后想起来挺有代表性的,...
继续阅读 »

前言


前几天和前同事闲时聚餐,约了两个月的小聚终于达成了,程序员行业聚少离多,所幸大家的发量还坚挺着。

期间不可避免地聊到了自己的公司、行业状况以及对未来的看法,几杯老酒之后,大家畅所欲言,其中一位老哥侃起了他的职业生涯,既坎坷又无奈,饭后想起来挺有代表性的,征得他同意故记录在此。

以下是老哥的历程。



cold.jpg


程序员的前半生


我今年35岁,有房有贷有妻女有老父母。


出生在90年代的农村,从小中规中矩,不惹事不喧哗不突出,三好学生没有我,德智体美没有全面发展。学习也算努力,不算小题做题家,因为只考了个本科。


大学学费全靠助学带款,勤工俭学补贴日用,埋头苦干成绩也只在年级中等偏下水平。有些同学早早就定下了大学的目标,比如考研、比如出国、比如考公,到了大三的时候大家基本都有了自己的目标。而我的目标就是尽早工作,争取早日还完带款,因此早早就开始准备找工作。

也许是上天眷顾,不知道怎么就被华为看重了(那会华为还没现在的如日中天,彼时是BAT的天下),稀里糊涂的接受了offer,没想到却是改变了后面十年的决定。


2013年,深圳的夏天阳光明媚,热气扑鼻,提着一个简单的箱子进入了坂田基地。

刚开始,工作上的一切都很新鲜,每个人都在忙碌,虽然不知道他们在忙什么,但感觉很高级的样子。同期入职的同事都比较厉害,很快就适应了工作,而自己还是没完全应对工作内容,于是下班之后继续留在公司学习,顺便蹭饭。

就这样,很快就一年过去了,自己也慢慢熟悉了工作节奏,但是加班也越来越多了。对于自己来说,为了过节点,6点是晚饭时间,9点是下班时间,12点正式下班。

平凡的日子没什么值得留恋,过一天、一个月、一年、四年都没什么两样,四年里学习到了不少的知识,也数了很多次深圳凌晨的路灯数。


作为深漂,没有遇到深圳爱情故事,也对高昂的房价绝望,于是决定回到二线城市,成为一名蓉漂。
2017年,还是和四年前一样的行李箱,出现在了老家的省会城市,只是那时的我没有了助学打款,怀里也攒下了一些血汗钱。

那时互联网行业发展还是如火如荼,前端的需求量也很大,也得益于华为公司发展越来越好,自己的华为经历很快就拿到了几个offer,选了一家初创公司,幻想着能有一番成就。


2018年底,眼看着房价越长越高,某链中介不断地灌输再不买明天就是另一个价了,错过这个村就没这个店了,也许是想有个家,也许是想着父母能到省会里一起住,拿出自己做牛马几年的积蓄加上父母一辈子辛苦攒的小十万的养老钱购买了城区里的新房,那会儿的价格已经比前两年涨了一倍多,妥妥的高位站岗,不过想着自己是刚需也不会卖,因此咬咬牙掏出了全部的积蓄怒而背上了三十年的房贷。


房子的事暂时落定了,全身心的投入到工作中,没想到老板只想骗投资人的钱,产品没弄好投资人不愿跟进了,坚持了三年,期间各种断臂求生,最终还是落了个司破人走的境地。


2020年,30岁的我第一次被动失业了,幸运的是也找到了另一半。为了尽可能节省支出,房子装修的事我们都是亲力亲为,最后花了十多万终于将房子装好了,虽然很简单但毕竟是自己在大城市里的第一套房子,那一刻,感觉十年的付出都是值得的。

背着沉重的房贷,期望能找到一份薪资稍微过得去的工作,于是在简历上优势那行写了:“可加班”。依稀记得有些HR对我进行了灵魂拷问:结婚了吗?有小孩了吗?你都30岁了还能加班吗?。我斩钉截铁地说:只要公司有需要,我定会全力以赴!


2022年,我们的孩子出世了,队友辞去了工作全心全意带小孩,而我更加努力了,毕竟有了四脚吞金兽,不得不肝。

虽然工作很努力,但成果一般,不是公司的技术担当,也不会是技术洼地。


2023年的某一天,和之前的364天一样的平淡,在座位上解Bug的我突然感觉到一阵心悸,呼吸不畅,实在不行了呼唤同事叫了120,去医院一套检查下来没发现什么大问题。医生询问是不是工作压力太大,平时加班很多?我说还好,平时也就加班到9点。医生笑了笑说你这种年轻人我见多了,都是压力大的毛病,平时工作不要久坐盯着屏幕多站起来走走。他让我回家多休息,回去后观察了几天还是偶尔会有心悸,再去了另一个医院进行检查,也是没有明确的诊断结果,只是说可能是这个问题,又可能是另一个问题。

过了1个月后,身体上的问题不见好转,我辞去了工作。


2023年末,找了一家小公司,也就是我现在的公司,工资没有涨,仔细算起来还变相下降了。

还是做的业务需求,也没有领导什么人,管好自己就行,直属上级还是个工作几年的小伙。这家公司主要的特点是不加班,技术难度不高,能做多少就是多少,前提是要报风险,领导也不会强迫加班。


就这样到了2024,神奇的是我已经很久没有心悸的感觉了,不知道是不加班还是心态转变的原因。
家里的小朋友也长大了,会说话了。我现在每天下班最温馨的的是她开着门期待我回家的那一刻,她的期盼的眼神就是我回家的动力。


公司在2024年也裁了不少人,领导也找我谈过问问我的想法,我说:我还是能胜任这份工作的。领导说:公司觉得你年级大了一些,工资虽然不是最高,但不太符合行情,你懂的。我说:我懂,可以接受适当的降薪。
就这样,我挺过了2024,然而过了一周领导走了。


2025年,我35周岁了。
现在的我已经彻底接受自己的平庸的事实了。在学生时代,从来都不出色,也不会垫底,就是那类最容易被忽略的人。在工作时代,不是技术大牛,也不是完全的水货,就是普普通通的程序员。


如果说上半生吃到了什么红利,只能说入坑了计算机这行业,技术给我带了收入,有了糊口的基础。没进股市,却被房价狠狠割了一道。


35岁的我,没有彻底躺平摆烂,也没有足够奋发进取。

35岁的我,有着24年的房贷,还好61岁的时候我还在工作,应该还能还房贷。

35岁的我,不吃海鲜不喝酒,尿酸500+。

35岁的我,人体工学椅也挽救不了腰椎间盘突出。

35岁的我,头发依然浓密,只是白发越来越多。

35岁的我,已经不打游戏,只是会看这各种小说聊以慰藉。

35岁的我,两点一线,每天挤着地铁,看众生百态。

35岁的我,早睡早起,放空自己。

35岁的我,暂时还没有领取毕业大礼包,希望今年还能苟过。

35岁的我,希望经济能够好起来,让如我一般平凡的人能够有活下去的勇气。


诸君,下一年再会~祝你平安喜乐,万事顺遂!


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

最新 GitHub 骗局!千万别中招!

今天一早,焚香沐浴更衣,打开全球最大同性交友网站,准备好好摸鱼;突然方向通知多了一条: 正疑惑是触发是 GitHub 的什么隐藏关卡呢,点进去一看: 一个 21 年的创建 issue?但是有个新的评论: 这个评论的大意是: 喂!x毛! GitHub 瞎...
继续阅读 »

今天一早,焚香沐浴更衣,打开全球最大同性交友网站,准备好好摸鱼;突然方向通知多了一条:


image.png


正疑惑是触发是 GitHub 的什么隐藏关卡呢,点进去一看:


image.png


一个 21 年的创建 issue?但是有个新的评论:


image.png


这个评论的大意是:



喂!x毛!
GitHub 瞎了眼相中你了,有个很适合你的职位,年薪高达 18w 刀乐!
赶紧来申请啊,各种福利各种巴适!
但是有记得在 24 小时内点击这个链接来申请哦!过时不候!
后面芭啦芭啦@了一大堆人,其中我的用户名赫然在列!



他真的!我哭死!原来天上真的会掉馅阱 😢...


但是仔细一看,评论的这个人,是默认头像。不对劲!非常不对劲!遂点进其主页一看:


image.png


啥也没有...


这时候事情就很明显了,然后我就去 GitHub 社区找了一下相关的反馈,果不其然,两天前开始有人在反馈相关问题:


image.png



原讨论传送门:github.com/orgs/commun…



于是笔者在隐私模式下打开@我的那个评论附上的链接:


image.png


这个页面会请求你使用 GitHub 授权登录,并且要求你授权各种高级权限;而一旦你授权了,大概率会发生的第一件事,就是你的帐号会在各种 issue 中发布上面那条“GitHub 求职骗局”的评论,以导致更多的人受骗...



截至笔者写下这篇水文时,该钓鱼网站链接已经无法打开。



而在早上笔者在 github.com/orgs/commun… 中留下评论后,陆陆续续又有上百个全球各地的开发者进行了反馈。甚至有领先一步的好哥们已经直接出手向域名注册商、域名托管服务商进行了举报,并收到了反馈:


image.png


而这,仅仅是在笔者写下这篇水文前的 23 分钟(一切发生得太快...


不仅如此,在笔者截完本文第三张图后,提及我的那条评论已经删除了 🤪 (一切发生得实在太快...


不不仅如此,在笔者敲完上一句话后打算再次确认一下发出评论那个用户(第四张截图),发现他已经被封禁了...


image.png


好家伙,这发生得也太快了吧!赶上直播了???


估计这一波,有不少帐号也受到波及,最好确认一下自己的帐号是否有被影响(吓得笔者又刷新了一下页面确认自己有没有被封禁)。


这也让笔者想起最近 GitHub、NPM 等各种平台都在极力地推动用户启用双因素身份验证(2FA),以提高用户帐号的安全;这样看来,确实是一个明智之举。


最后还是提醒一下各位:


不清楚来源的链接不要点!不清楚来源的链接不要点!不清楚来源的链接不要点!


就这样。


作者:Nauxscript
来源:juejin.cn/post/7337666469903122472
收起阅读 »

用AI做了个「微信红包封面」,还能卖钱?

点赞 + 关注 + 收藏 = 学会了 🧨快过年了,该发红包了🧧 使用默认红包封面彰显不了个性,去「微信红包封面开放平台」定制一个吧。 👉 cover.weixin.qq.com 👈 我是用自己公众号注册的。没有公众号的话也可以用自己的视频号去注册一个。 登录...
继续阅读 »

点赞 + 关注 + 收藏 = 学会了


🧨快过年了,该发红包了🧧


使用默认红包封面彰显不了个性,去「微信红包封面开放平台」定制一个吧。


👉 cover.weixin.qq.com 👈


01.png


我是用自己公众号注册的。没有公众号的话也可以用自己的视频号去注册一个。


登录后,点击页面右侧的“定制封面”就会跳转到创建红包封面的界面。


02.png


接下来就是填资料,上传你做好的封面图即可。


需要注意的是,「封面简称」建议填你的公众号或者视频号的名字,这样可以降低审核要求。如果你填其他内容就需要提交相关的材料「证明材料」给微信审核。


03.png


但!但!但!


现在没有免费的「微信红包封面」了,1元1个。


04.png


不懂设计又想弄个好看的红包封面怎么办?


交给AI吧😝


我已经写过好多篇「AI绘画」相关的教程,有条件(电脑配置足够高)的工友可以在自己电脑安装好 Stable Diffusion,在本地创作。


比如我这个封面(英雄联盟的金克丝)就是在自己电脑用 SD 做出来的。


05.png


电脑配置一般的工友可以用线上平台,比如哩布哩布(liblib.art/)。我这个封面就是用哩…


06.png


用哩布哩布的好处就是案例特别多,自己不懂怎么写提示词,可以先逛逛哩布哩布,看到喜欢的就选择“做同款”。也可以在“做同款”的基础上适当的修改一下提示词,按你的想法去创作。


07.png


在写本文时,哩布哩布每天会刷新300点算力值给你创作。简单来说,可以白嫖!


除了哩布哩布外,我之前还分享过数款AI绘画的平台,有需要的工友可以去看看 👉 mp.weixin.qq.com/s/nMZMfErDv…


「红包封面」微信收1元1个,那是不是可以帮别人定制封面赚点小钱呢🤔


08.PNG




金克丝的含义就是金克丝


IMG_6296.PNG


作者:德育处主任
来源:juejin.cn/post/7455880105451323404
收起阅读 »

以我两年多前端的血泪😭经验,给大家一点警示

工作两年多了,踩过了许多坑,希望大家不要踩,常常想如果我刚毕业就知道这些东西就好了,但是没有如果 一个人现在做了他多年以后认为正确的事情,他是很幸运的 永远不要期待领导主动加薪 不会还有人期待着领导某一天主动找你,小张,你来一下办公室,我有点事情给你说 到...
继续阅读 »

工作两年多了,踩过了许多坑,希望大家不要踩,常常想如果我刚毕业就知道这些东西就好了,但是没有如果



一个人现在做了他多年以后认为正确的事情,他是很幸运的



永远不要期待领导主动加薪


不会还有人期待着领导某一天主动找你,小张,你来一下办公室,我有点事情给你说

到办公室后,领导:你最近表现不错,公司决定给你涨薪20%,下个月开始执行

你一脸春梦样子


大哥醒醒吧,梦里才有!


现在大部分公司不会主动给员工加薪,能不降薪就算不错了,没有领导无缘无故能给员工加薪,绝大部分都是这样的,老板和员工本身就是利益冲突的,你挣的钱多了,老板怎么买法拉利呐


所以永远不要期待领导给你主动加薪资,你需要有筹码,有底气,当然筹码与底气很重要,但是更重要的是:你要主动争取


主动权要始终掌握在自己手中


永远不要裸辞


少看网上的一句梦想仗剑走天涯,就裸辞冲到了318


裸辞只会导致你找工作的时候更加被动,和hr聊薪资的时候更加被动,徒增你的焦虑


从某种角度来说,裸辞百害无一利,除非你在这家公司非常非常非常不爽了,再裸辞,当然你有足够多的钱另当别论,但是你大概率没有,都tm干程序员了,你能有多有钱?


不要裸辞,动了辞职的念头,那就着手准备,公司的活干的说得过去就行


慢慢找,一定要找到比现在待遇好的再辞职,不要降低标准,除非一直拿不到理想的薪资,不然不要将就,怕就怕这一将就,后面都得将就了。


一开始干程序员你就得明白,你得走


这个走有两层意思

1、你得跳槽,刚开始的时候千万不能觉得安逸,不想走,不想跳槽, 懂得都懂,这行得跳槽涨薪
你可能说安逸,不累,不想走,行,你20多k,不累,不走,很好,你很聪明


怕就怕有哥们11-12k的贪图安逸不走,你说你这薪资安逸个吊毛啊,再安逸就废了


该走就走


2、你能干到40?
大部分都够呛吧,如果40还是一线大头兵程序员,嗯,,,,,,很难,

刚开始干程序员就得明白,得在短期内快速攒钱,年纪大了得谋求后路,别以为现在挣的还可以,就嘎嘎花钱,到你年纪大一点有的后悔,得攒钱留后路


要敢于要价格


这个世界从来都撑死胆大的,饿死胆小的

从某种角度来说,你值多少钱取决于你自己敢要多少钱,你说老板会觉得要10k的程序员有多大价值吗?


大胆点,敢于争取自己想要的价格才是正道


自信点,大家都那个b样


世界是一个巨大的草台班子,你以为别人牛逼的很,其实他也以为你牛逼的很,都一样,自信点,都挺傻逼的


现在这家公司的老板说过一句话,使我受益无穷



人一定要有自信,大家都那样,你以为他牛逼,其实也就那样,时刻问问自己,凭啥他行,我不行



问问自己有核心竞争力吗


大部分人都没有,程序员的竞争力无非这几种

1、名校学历

2、github 500star+项目作者

3、长期积累的博客

4、社区有知名度、影响力(掘金等)

5、项目有亮点、难度

6、大厂实习工作经历

7、竞赛奖牌

没事多更新简历,多投一下,知道自己在市场上还能混的下去不


踏踏实实卷一段时间


很多人都在说不要卷。开玩笑,市场资源有限,不卷怎么行

但是不要焦虑,踏踏实实的,认认真真的卷一段时间,自己有了提升后,在谋求发展。

自身没有价值之前,说再多都是瞎掰扯,没屌用


俗称,耐得住寂寞,踏踏实实的做学问,这一点我得深刻反思



以上纯属瞎扯,如有不赞同,那就是你对



作者:吃饺子不吃馅
来源:juejin.cn/post/7457417025930117154
收起阅读 »

App出现技术问题,这样的中国电信让用户糟心了

web
前言 最近在中国电信app上销户一张中国电信山西区的电话卡,一打开销户界面我就惊了 点开一看,写入的本地变量、cookie一览无遗。 查看数据 存在采集用户手机型号、来源等数据行为产生的cookie 最引人注目的就是 zhizhendata2015jss...
继续阅读 »

前言


image.png


最近在中国电信app上销户一张中国电信山西区的电话卡,一打开销户界面我就惊了


image-20250105103537705


点开一看,写入的本地变量、cookie一览无遗。


image.png


查看数据


存在采集用户手机型号、来源等数据行为产生的cookie


最引人注目的就是


zhizhendata2015jssdkcross={
"distinct_id": "MTkxOTZhYzk3YTAyMDItMDgxMjMwYmRlOTFhOWU4LTQ3NzE2ZTBmLTM2NzkyOC0xOTE5NmFjOTdhMWE5NQ==",
"
first_id": "",
"
props": {
  "
$latest_traffic_source_type": "直接流量",
  "
$latest_search_keyword": "未取到值_直接打开",
  "
$latest_referrer": "",
  "
_latest_utm_scha": "utm_ch-010001002009.utm_sch-hg_sy_pdkp-2-125971000001-10519100001.utm_af-1000000037.utm_as-0043300037.utm_sd1-default",
  "
_latest_utm_sd1": "app-充流量-本地推荐",
  "
_latest_utm_sd2": "",
  "
_latest_shopid": "189.WAP.llrb-2079",
  "
_latest_utm_ch": "hg_app",
  "
_latest_utm_sch": "hg_sy_pdkp_kw02",
  "
_latest_utm_as": "hg_19y15GBwxllb"
},
"
login_type": "",
"
utms": {
  "
shopid": "189.WAP.llrb-2079"
},
"
$device_id": "19196ac97a0202-081230bde91a9e8-47716e0f-367928-19196ac97a1a95"
}


简单解释一下各种参数


image.png
很显然 ,这是一个用户行为追踪工具,记录你从那个平台点进来(广告投放)、你手机是啥样的(用户画像)、你搜索了什么。可能还会有你住哪里之类的数据



  • 博主一下懵了,之前常听人说大数据时代没有什么秘密,还不以为然。今日一遇还真是阿


登录状态与服务取消cookie


SXH5_CANCEL_SERVICE_LOGINSTATUS=SXH5_CANCEL_SERVICE_a2d95a5a764d4744b1eb1e468b583287

SXH5_CANCEL_SERVICE_LOGINTYPE=fmknjikneolbnhclejikfnbggkmnookc



  • 这两没什么好说的,看不出来啥


查看持久化数据


image-20250105111122637


userInfo={"type":"object","data":{"userName":"","userId":"","userAddress":"","facePhoto":"","frontImage":"","phone":""}}

vConsole_switch_y=335

loginType=sjhocr

xhAccount={"type":"object","data":{"xhzkAccount":"15383404397","xhfkPhone":"","xhkdAccount":""}}

authorPlate=xh

vConsole_switch_x=92

orderId=SXSMRZH5XH202501042248308427503

__DC_STAT_UUID=17360018747207051970


  • 这段数据也没啥好看的,有一些手机号、订单编号、和证明是微信小程序的__DC_STAT_UUID 项


image-20250105111458145



  • 更多就不继续探索了,这里的填写号码获取验证码控制台正常输出。。。。


问题可能产生原因



  • 首先,得知道中国电信app现在的模式是怎样的。

  • 据博主个人观察,电信app页面虽然都一样,但每个地区各自为营。如果你使用福州的电信手机卡登录,那么是跳转到福州电信负责的页面,如下图


s



  • 所以本次app出现问题,是山西电信没有处理好app端


image.png



  • 山西电信微信小程序是没什么问题的,而他的app没有做好对接,要么版本不一样……


吐槽



  • 我有个朋友之前注销电信流量卡时,被告知要去号码归属地才能销户。。这归属地离他十万八千里,过去就为了销卡显然不划算。于是他去工信部12300(微信公众号 现改名为 电信用户投诉 )投诉才成功线上销卡。

  • 电信现在按省来处理业务,如果你电信卡丢了且忘记卡号、归属地,那只能通过线上投诉才能得知自己卡号、归属地,不然各省是无权查别省号码。

  • 我线上销户时,客服A要求先交40元月租才能销户,但这张卡我从未使用,为何会产生月租?联系客服B后,他让我提供身-份-证照片、委托书及手持委托书照片,最终未交钱完成注销。但不同客服的说法不一,且身份信息完全暴露给客服,让人不安。


作者:Qiuner
来源:juejin.cn/post/7456898384352362522
收起阅读 »

这五年,我学这么多东西再没有高学历背景下,有没有意义

今年的10月初我被辞退了,公司为盈利,部门整体裁掉,感慨挺多的。也是我工作以来待的最久的一家单位了,从原来的菜鸟到现在能算是合格的前端工程师,不管怎么说,我很感谢这个平台。被辞退以现在的环境制定要面临降薪,或者换个没有福利待遇那么好的单位。这五年我得到的太多了...
继续阅读 »

今年的10月初我被辞退了,公司为盈利,部门整体裁掉,感慨挺多的。也是我工作以来待的最久的一家单位了,从原来的菜鸟到现在能算是合格的前端工程师,不管怎么说,我很感谢这个平台。

被辞退以现在的环境制定要面临降薪,或者换个没有福利待遇那么好的单位。

这五年我得到的太多了,可能有人说如果你在别的单位应该也会得到你应得的,这个说法确实没毛病。我只不过是一个打工仔,平台不过是带我见识了一些上限,如果我不愿意去学,可能也收获不了这么多。那么就从生活、技术等等方面展开来说下我得到了什么。

生活

刚入职这家公司没多久(2019年),我就跟我老婆处对象了。是的我们在2023年10月4号结婚了,也很顺利我们在12月份接着接到喜讯我们有宝宝了。宝宝出生在2024年10月3日(女宝),很漂亮,大眼萌娃。照片我放下面给各位叔叔姨姨们看看:

4601734486091_.pic_hd.jpg

4611734486094_.pic_hd.jpg

4631734486100_.pic_hd.jpg

买房:没逃得了当房奴(被逼的),在2023年8月份我们在南京市浦口桥北买了个小三室,总价150W。首付了70W,带款80W,20年,每个月要还5700左右。不好的是今年房子了好多钱。我们小区同户型的已经有挂120+的了,成交价更低

之前有个同事大哥说过,如果你买了房子,能娶妻生子,那么房子的价值就够了,它跌就跌吧。落了个媳妇+孩子了。

这里给还没有买房的朋友一些建议:带款要贷30年(前期压力小,后面可以提前还款), 买毛坯的话一定要预留好装修资金,其实我不建议买毛坯,那个时候你就会要负担房贷+房租+装修(我就是个例子:后悔si了😭),还要晾着一年半载的才能入住,最好能买个二手房直接能入住的。

我这几年在这家公司待遇也涨幅了几次:

第一次是2020年10月份,觉得自己还凑合,想去外面看看,当时确实环境大好,也拿到了不错的涨幅。后来就是我当时部门的前端领导全力留我,觉得我很负责,提出给我涨幅3.5k

第二次是2022年3月份,也是我这个前端领导要换平台,不在这个部门带领我们了。我也想出去看看😁,结果是部门领导留我了涨幅4k

第三次是2023年12月份,我把我们的一个用户后台管理(老旧难用)在一次迭代中,用时7天把14个模块整体重构(公司框架+公司UI组件),就是一天写两个模块,然后交给测试同学同步测试,第二天把bug和两个新模块同步修改好继续提测。7天后上线,新的web页很好用(都是老的web衬托的),得到了部门领导的主动涨薪1.5k

可以说我运气好,也可以说我遇到了好领导,我可能是有点价值的,但是我确实是幸运的。

技术

  1. (2019)、校招生好厉害

高校的校招生竟然这么厉害,这是我在这家单位接触校招生或者说应届生的真实感受。 也是他们本身基础就很扎实,有编程思想,有领导带领规划成长路线,又很积极爱学确实成长很快。

我开始了重新学了下ES6,买了一本阮一峰老师的《ES6标准入门》,来回看了3遍,深入理解了解构,数组的some、every字符串的:startsWith、endsWith、padStart、padEndSet、MapPromise等等。我觉得它帮助我特别大,如果没有这本书我或许会看到一些同事写的语法,我不认识的语法。为我的js奠定了一定的基础。

  1. (2020)、同事竟然手写正则
  • 再一次跟同事协作中需要检验一个ip段,他竟然给我直接手写手写,大佬真真厉害。然后我就去重新学习了正则表达式基本能达到写一些简单的表示式。
  1. (2020)、前端领导让我看看eggjs,了解怎么的用法,后面需要跟我做一个东西
  • 在这个里面学到了很多关于node的相关知识。
  1. (2020)、前端大屏大佬,我们有一个专门写大屏的前端大佬
  • 跟着他学到了怎么做适配rem、em、vw、postCss,使用echarts,并认识了d3js(后面刚好会用到,会更深入了解)
  1. (2020)、前端领导让我学习nginx,起个服务3000端口的时候能访问到百度的页面
  • 这里用到了转发服务以及代理资源,让我在后面对前端资源处理以及处理代理问题时有了很大的帮助,我搭建博客等好多地方都用到了。
  1. (2020)、前端领导让我用谷歌插件开发一个chrome插件,主要是 读取数据(标签页中的某个窗口)到周报生成渲染。

其实到这里我都没有学习编码规范,对组件设计的思想也了解不多,基本上就是野蛮开荒。本来一直计划给我做代码codereview,看看怎么能帮我做些规范的提升,一直没太多机会。后来就是他自身发展走了去了别的单位,然后换了个空降领导,他开始组织我们创建各种规范拦截,eslintgit提交拦截。

  1. (2021)、定义相关规范拦截 eslint、git
  • eslint: 用的airbnb的规范。还是蛮严格的帮助我纠正了不少代码缺点。
  • gitcommit 规范,分支规范等等。
  1. (2021)、阅读库源码提升自己的代码规范。我觉得对我帮助最大的是element-ui的源码

2021 年我觉得我最大的进步就是代码质量有了大幅度提升,懂的怎么设计组件了,怎么能写让代码更壮健,懂的了规范带来的价值。

  1. (2022)、我们开始放弃了jenkins做打包构建,换成了gitLab CI CD, 我深入了解并学习了下。
  2. (2022)、前【前端领导】让我协助他开发个微信小程序,有了解到了小程序的相关知识。
  3. (2022)、我们开始做一个图谱产品,拓扑图可视化分析,使用了d3js以及canvas
  • 为了支持大数据的统计以及绘制,在这两年中一直在做数据结构的优化以及接触web Worker、Wasm等,只在可视区域渲染等等优化策略。 算是学到了性能的优化手段,也了解到了图形算法的魅力。
  1. (2022)、学习TS,写了写react Hooks。也了解到了跟vue之前的区别
  2. (2022)、我开始研究怎么生成脚手架,发布NPM包。
  3. (2023)、这年我开始对我们公司平台组的产物做源码学习,学到了babel ASTwebpack插件编写等等,还写了个vscode的插件主要是处理AST的达到函数插桩。
  4. (2023)、这年我开始对之前用到的知识做了总结发表一系列文章(得到了掘金Lv5优秀创作者)。
  5. (2023)、我持续查阅那些好的插件包的源码具体怎么实现的,对编码思想又了更深层次的理解。懂得了怎么做能更好的建立一个可持续发展的方向,技术选型,风险评估等等。
  6. (2024)、这年精力主要是在工作上(大环境太不好了,不想被裁,也算是坚持到了最后),开发大屏,重构了好多老的代码,对产品的用户体验做到了细致的优化。

我学这么多东西有在没有高学历背景下,有没有意义?

实话说我在这次找工作中,对自己的一直努力的方向做了怀疑,还不如好好享受,学习这么多干啥呢? 约面试会被卡在学历上,我很烦恼,有些怀疑自己。我之前在环境大好的背景下,我始终认为多学习,一定能涨工资。

我也会向我老婆吐槽说学这么多又有啥用呢,第一步就被卡死了。是不是统招本科!!!抱歉我不是

她说你比你同学朋友们可能要好多了,他们一年、两年可能就会换个工作,在者说口罩那几年你一直也算很稳定,别人都换了好几家单位了,你知道为啥一直再给你加薪吗?其实都是你努力得来的! 工作嘛,咱们换个就行了,现在大环境不好,不好找是常态,她说没关系的,她还有存款(彩礼钱+自己之前攒下的),你就是一两年不上班都没问题。

是啊!我一直在稳步加薪,待遇在朋友中也还算不错的

没上班怎么可能不焦虑,我是刚好我家孩子出生那个月被裁的,我还等到了小孩20天后才开始找工作,那个时候父母还都在这边,我对双方父母说我是在休产假+年假,在出去面试那几天也是说出去给我闺女办理正件

后来因为孩子要喝奶粉 + 房贷 + 房租 + 装修,其实我很大的压力,老婆虽然没催我给我压力,但是每天还是很焦虑的,有的时候会算我家娃一个月花费多少... 唉!!

一切都是有意义

我有掘金社区优秀创作者,加我的个人博客网站,再加上我会的技术,在加上我还有发表的各类插件,写在简历上都是我的优势,我会刻意去找技术面试官去投简历,我会抓住每次面试机会,努力的去展示自己。

好消息我用时14天收到offer了,一家小单位,双休,待遇降了20%。我知道现在的行情,就是我在多花一个月精力去面试,去找,我可能会更奔溃(最近压力很大),最后的待遇应该也会跟现在这个的大差不差的。 现在都是一个萝卜一个坑,我果断选择入职。

回头看其实我由于我的那些优势,我算约到了不少面试,我总共面试6家,二面了2家,我跟我同时被裁的前端同事聊,他说用时两个月才约到2家面试。我运气真的好的太多了

还有个好消息就是前【领导】11月初找我做一个项目,持续7个月,让我兼职每天干2-4个小时,每个月给我1W。由于我现在通勤+加班,可能没太多精力去做,然后就介绍给我的同事阿祖全职,给了他1.8W

5111735353355_.pic.jpg

5081735352115_.pic.jpg

其实前领导能找到我也是认可我的工作能力,我其实也后悔推荐给朋友了,我应该找他一起兼职这份工作,我俩平分这个1w,毕竟我刚降薪加实习期80%,落差不少。

11月中的时候又有前单位的UI设计师(他很认可我,每次都说我写页面还原度是最高的)找上我说要不要考虑私活报价1.2W,做一个网站+h5,其实1.2w我俩平分不算特别高,但是最后没能接下来。

image.png

12月下我前前【领导】来找我说他们单位可能要裁员,然后呢他搞了这么多年不想在打工了,想着做做产品。他好像看中小程序的市场,他也有些人脉。我其实在第一份兼职1w的没接的时候我也考虑做什么产品化的东西能卖钱。小程序的市场看到某宝好多个卖模版的,还老便宜了,加上我也没有什么人脉就放弃了。这次跟着它们用业余时间搞搞试试。用雷布斯的一句话:我们悄悄搞,没搞成,就当我们没有搞过!!。雄起!!!!

5101735352790_.pic.jpg

接下来的计划

  • 产品化的东西让他们计划,只是定了个初步目标,还暂未执行!
  • 攒钱买个小车(10w),有孩子了没车的话出行不是很方便
  • 我老婆在做xiao红书,做母婴方面的,最近粉丝也达到了600个🔥,继续加油
  • 我在斗音上拍我闺女视频,但是没太多流量,刚刚投了150块钱,得到了40多个粉丝,😭,继续加油吧!!!
  • 我在学习Java,主要是要是做点什么写后端能方便点
  • 搞钱!!!!!!!!


作者:三原
来源:juejin.cn/post/7453120781771341859
收起阅读 »

和后端大战三百回合后,卑微前端还是选择了自己写excel导出

web
前言 对于一个sass项目,或者一个中后台项目来说,导出excel表格应该是家常便饭,本来最简单的实现方式是后端去做表格,前端请求接口拿到一个地址下载就行了。但是,因为我们这个项目之前就是前端做表格,加上这个表格相对比较复杂需要合并行和列,后端不会加上又有别的...
继续阅读 »

前言


对于一个sass项目,或者一个中后台项目来说,导出excel表格应该是家常便饭,本来最简单的实现方式是后端去做表格,前端请求接口拿到一个地址下载就行了。但是,因为我们这个项目之前就是前端做表格,加上这个表格相对比较复杂需要合并行和列,后端不会加上又有别的项目堆积没有时间研究,所以就是后端提供数据,前端来做表格。


那里复杂


image-20241209095449488.png


可以看到,有二级标题,还有行的合并,如果仅仅是二级标题,倒是可以直接写死,但是行的合并是根据数据去计算该合并那些行的,再比如后面如果有三级标题,四级标题的需求呢?那不是又寄了,所以我选择将这个封装成一个方法,当然也是在网上找大佬们的解决方案实现的,东抄抄西抄抄就实现了我想要的功能。


传参


既然封装成一个方法,最好就是传入数据,表头,文件名后,就能自动下载一个excel表格,这才是封装的意义。代码并不是人,只能根据你设定好的路去走,所以数据的结构就显得很重要了,这个函数想要接收什么样的数据结构,要怎么去处理这些数据结构。


表头 header


表头接收一个数组,每一项有title,prop,children(如果有子级标题),title即为列名,prop为数据属性绑定名,children为子标题。


const header = [
{
'title': '券商(轉出方)',
'prop': 'orgName',
       'width': '100px'
  },
  {
       'title': '存入股票',
        'children': [
                {
                   'title': '存入股票名稱/代碼',
                   'prop': 'stockNameCode',
      'width': '100'
                },
                {
                   'title': '股票數量(股)',
                   'prop': 'stockNum',
                    'width': '100'
                },
                {
                   'title': '成本價(HKD)',
                   'prop': 'stockPrice',
                   'width': '100'
                }
          ]

  }
]

数据 dataSource


数据也是接收一个数组,但是这里需要做一个处理,因为每一项的children是一个数组,可能会有多个值,换句话来说,下面只有两条数据,分别是id为1和id为2,但实际上在excel表格中需要显示3行,所以需要处理一下。


const dataSource = [
  {
      id:1
      orgName:‘a’,
      children:[
      {
               stockNameCode:'A1',
               stockNum:'A2',
               stockPrice:'A3'
  },
          {
               stockNameCode:'B1',
               stockNum:'B2',
               stockPrice:'B3'
  },
]
  },
  {
      id:2
      orgName:'b',
      children:[
      {
               stockNameCode:'A1',
               stockNum:'A2',
               stockPrice:'A3'
  }
]  
  },
]

处理后的数据(也就是将children解构了,变成3条)


[
  {
      id:1
      orgName:‘a’,
      stockNameCode:'A1',
      stockNum:'A2',
      stockPrice:'A3'
  },
  {
      stockNameCode:'B1',
      stockNum:'B2',
      stockPrice:'B3'

  },
  {
      id:2
      orgName:‘b’,
  stockNameCode:'A1',
      stockNum:'A2',
      stockPrice:'A3'  
  }
]

sheetjs前置知识


对于我们前端生成excel,基本都是使用基于sheetjs封装的第三包,最经常使用的是xlsx,我这里因为对表格做了一些样式所以使用的xlsx-js-style,xlsx-js-style是提供了很多样式的,比如字体,居中,填充,具体大家可以去看官网。因为可能有些人是没做过excel的需求的,所以这里简单说一下生成excel的一种主流程。


import XLSX from 'xlsx-js-style'
// 需要一个二维数组
var aoa = [
  ["S", "h", "e", "e", "t", "J", "S"],
  [  1,   2,   ,   ,   5,   6,   7],
  [  2,   3,   ,   ,   6,   7,   8],
  [  3,   4,   ,   ,   7,   8,   9],
  [  4,   5,   6,   7,   8,   9,   0]
];
// 将二维数组转成工作表
var ws = XLSX.utils.aoa_to_sheet(aoa);
// 创建一个工作簿
var wb = XLSX.utils.book_new();
// 将工作表添加到工作簿
XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
// 生成excel
XLSX.writeFile(wb, "SheetJSExportAOA.xlsx");

导出的表格,这是官网的demo: xlsx.nodejs.cn/docs/api/ut…


image-20241210090608300.png


所以封装这个函数,主要流程也是和这个一样的,只不过我们要做的时候,将传入的参数处理成我们想要的二维数组,以及在这基础做一些合并,样式的操作,下面介绍了一些属性的作用,具体大家还是需要去官网查看的。


ws['!merges']


ws['!merges'] 是工作表对象 ws 的一个属性,用于存储工作表中的合并单元格信息,该属性的值是一个数组,其中每个元素都是一个对象,描述了一个合并单元格区域


// s是start e是end合并单元格区域的起始位置和结束位置,
// r是行 c是列
ws['!merges'] = [
{ s: { r: startRow, c: startCol }, e: { r: endRow, c: endCol } }
];

比如{ s: { r: 0, c: 0 }, e: { r: 0, c: 1 } } 表示合并从 A1(第 1 行第 1 列)到 B1(第 1 行第 2 列)的单元格。


ws['!ref']


ws['!ref'] 是工作表对象 ws 的一个属性,用于表示该工作表中数据的范围引用。这个范围引用是一个字符串,遵循 Excel 的单元格范围表示法,格式通常为 A1:B10,其中 A1 是范围的左上角单元格,B10 是范围的右下角单元格


ws['!cols']


ws['!cols'] 是工作表对象 ws 的一个属性,它用于存储工作表中列的相关信息,比如列的宽度、隐藏状态等


主函数


有了这些前置知识,相信你肯定是能看懂这个主函数的,我们先从主线上来看,不去研究这个函数做了什么,只需要看他得到了什么,某一个函数的细节我们后面会有介绍。



header 表头


dataSource 数据


fileName 文件名



import XLSX from 'xlsx-js-style'
function exportExcel (header, dataSource, fileName) {
 // 根据表头数组去计算行数和列数
 const {row: ROW, col: COL} = excelRoWCol(header)
 const aoa = []
 const mergeArr = []
 
 // 根据表头初始化aoa 二维数组
 for (let rowNum = 0; rowNum < ROW; rowNum++) {
   aoa[rowNum] = []
   for (let colNum = 0; colNum < COL; colNum++) {
     aoa[rowNum][colNum] = ''
  }
}
   
 // 根据表头以及数据生成,去合并列和行,会处理mergeArr
 mergeArrFn(mergeArr, header, aoa, dataSource, ROW, COL)
   
 // 最后往aoa中 添加表格数据
 aoa.push(...jsonDataToArray(header, dataSource))

 const ws = XLSX.utils.aoa_to_sheet(aoa)
 // 添加样式
 ExcelStyle(ws, header, ROW)
 // 合并
 ws['!merges'] = mergeArr
 // 创建一个工作簿
 const wb = XLSX.utils.book_new()
 // // 将工作表添加到工作簿
 XLSX.utils.book_append_sheet(wb, ws, 'sheet1')
 // 生成excel
 XLSX.writeFile(wb, fileName + '.xlsx')
}
export default exportExcel

相对前面那个下载excel的demo来说,无非就多了根据传入的header和dataSource去初始化生成aoa以及mergeArr,aoa就是前面demo的二维数组,mergeArr表示我们需要合并的单元格,也就是前面提到的ws['!merges'],我们得到这个mergeArr也是为了赋值给它,还有就是给它添加样式了。


excelRoWCol


这个函数是根据表头去确认这个excel的表头有多少行,有多少列,因为我们传入的column,有children,children里可能还有chidren,是一个的结构,所以我们想要知道有多少行和多少列,无非就是去求这颗树的深度和宽度,所以就是两个算法题了。


// 深度递归函数
function treeDeep (root) {
 if (root) {
   if (root.children && root.children.length !== 0) {
     let maxChildrenLen = 0
     for (const child of root.children) {
       maxChildrenLen = Math.max(maxChildrenLen, treeDeep(child))
    }
     return 1 + maxChildrenLen
  } else {
     return 1
  }
} else {
   return 0
}
}
// 宽度递归函数
function treeWidth (root) {
 if (!root) return 0
 if (!root.children || root.children.length === 0) return 1
 let width = 0
 for (const child of root.children) {
   width += treeWidth(child)
}
 return width
}

function excelRoWCol(header) {
 let row = 0
 let col = 0
 for (const item of header) {
   row = Math.max(treeDeep(item), row)
   col += treeWidth(item)
}
 return {
   row,
   col
}
}

mergeArrFn



mergeArr 这个函数就是在修改这个值


header 表头


aoa 二维数组数


dataSource 数据


headerRowLen 表头行数


headerColLen 表头列数



这个函数有两个作用,第一就是将我们初始化的二维数组,用header进行赋值。第二,就是根据表头以及数据去生成mergeArr(赋值给ws['!merges'])。首先,对于header去遍历每一个表头去生成当前这一列的合并信息。假设一个只有二级表头的表头,如果当前这一列有二级标题,便根据子标题去合并主标题那一行所有的列,如果当前这一列没有子标题,便将这一列的第一行和第二行都和合并了。三级表头,四五级表头也是这样的思路。


function mergeArrFn(mergeArr, header, aoa, dataSource, headerRowLen) {
 // 根据header去生成一部分的 mergeArr
 let temCol = 0
 for (const item of header) {
   generateExcelColumn(aoa, 0, temCol, item, mergeArr)
   temCol += treeWidth(item)
}

 // 根据dataSource去生成一部分的 mergeArr
 let rowStartIndex = headerRowLen
 for (const item of dataSource) {
   generateExcelRow(rowStartIndex, item, mergeArr, header)
   rowStartIndex += treeWidth(item)
}
}

generateExcelColumn


这个函数简单来说就是前面所说的,假设一个只有二级表头的表头,如果当前这一列有二级标题,便根据子标题去合并主标题那一行所有的列,如果当前这一列没有子标题,便将这一列的第一行合第二行都和合并了。三级表头,四五级表头也是这样的思路。具体还是得自己理解代码,都有写注释。



aoa 就是那个aoa


row 就是行数


col 就是列数


curHeader 就是当前那一列


mergeArr 就是那个mergeArr



function generateExcelColumn(aoa, row, col, curHeader, mergeArr) {
 // 当前列的宽度
 const curHeaderWidth = treeWidth(curHeader)
 // 赋值
 aoa[row][col] = curHeader.title
 // 如果有子标题也就是说当前这一行就需要合并了
 if (curHeader.children) {
   // 举个例子,假设有一个表头两行两列,需要把他变成第一行只有一列,第二行依然是两列
   // 就需要变成 {s : { r:0,c:0 }, e : { r:0, c: 0+2-1 }}
   mergeArr.push({s: {r: row, c: col}, e: {r: row, c: col + curHeaderWidth - 1}})

   // 如果子标题还有子标题,就是递归了,要注意更新列数就行
   let tempCol = col
   for (const child of curHeader.children) {
     generateExcelColumn(aoa, row + 1, tempCol, child, mergeArr)
     tempCol += treeWidth(child)
  }
} else {
   // 这里的逻辑就是 如果没有子标题,就正常显示
   // 举个例子,假设整个表头是有三级表头,三级表头也就是有3行,如果第5列是没有任何子级表头的那应该是
   // {s:{r:0,c:5},e:{r:2,c:5}}
   if (row !== aoa.length - 1) {
     mergeArr.push({s: {r: row, c: col}, e: {r: aoa.length - 1, c: col}})
  }
}
}

generateExcelRow


这个函数是根据datasource去生成mergeArr,从mergeArrFn看我们去遍历datasource的每一项,在外层维护rowStartIndex这个变量,我们假设某一项数据的children是一个长度为3的数组,那么通过treeWidth方法(寻找树的宽度)得到的数据就是3,也就是说这一项数据应该占表格3行,但是并不是所有列都是需要3行数据的,所以我们需要去获取到一个不用合并的列prop数组,我们通过这项数据的children的key值去获取,所以这就需要对数据格式有要求了!然后再通过header和getgetLeafProp去获取所有prop,最后遍历判断是否需要去合并行。合并的逻辑是这样的,还是以那个children是一个长度为3的数组为例,如果要合并肯定是3行合并成一行。以第一列为例子,就是 { s : { r : 0, c : 0 }, e : { r : 2 , c : 0 }},下面去遍历props时,下标刚好就是当前的列数。



rowStartIndex 就是从表头的下一行开始


curitem 就是遍历dataSource当前的行


mergeArr 就是mergeArr


header 表头数组



// 合并行
function generateExcelRow(rowStartIndex, curitem, mergeArr, header) {
 // 当前行的高度
 const curHeaderWidth = treeWidth(curitem)
 // 不需要合并的列prop
 const noMerge = (curitem.children && curitem.children.length > 0) ? Object.keys(curitem.children[0]) : []
 // 找到所有prop
 const props = []
 for (const item of header) {
   props.push(...getLeafProp(item))
}
 // 遍历props
 props.forEach((item, index) => {
   // 不是子元素就要合并
   if (!noMerge.includes(item)) {
     mergeArr.push({s: {r: rowStartIndex, c: index}, e: {r: rowStartIndex + curHeaderWidth - 1, c: index}})
  }
})
}

jsonDataToArray


这个函数就是为了生成一个二维数组,因为有子标题,所以可能需要递归。逻辑上也比较简单,假设表头是header,数据源是data,header经过处理后变成了props数组,而data根据props处理后就得到了我们想要的数据。


const header = [
  {
       title: 'a'
       prop: 'aprop'
  },
  {
       title: 'b',
       children:[
          {
               title:'c',
               prop:'cprop'
          },
          {
               title:'d',
               prop:'dprop'
          }
      ]
  },
  {
       title:'e',
       prop:'eprop'
  }
]
const data = [
  {
       aprop:'a1',
       b:{
      cprop:'c1',
      dprop:'d1'
  },
       e:'e1'
  },
  {
       aprop:'a2',
       b:{
      cprop:'c2',
      dprop:'d2'
  },
       eprop:'e2'
  },
]
// 得到的porps
['aprop','cprop','dprop','eprop']

// 最后得到的是这个
[
  ['a1','c1','d1','e1']
  ['a2','c2','d2','e2']
]

getLeafProp其实就是去找所有叶子节点的算法题,recursiveChildrenData就是根据我们得到的props去从data中拿到对应的值,然后如果遇到children就递归去拿,要注意的是就是children要第一条是不要的,children第一条是和这一项数据是一样的。


function jsonDataToArray (header, data) {
 const props = []
 for (const item of header) {
   props.push(...getLeafProp(item))
}
 return recursiveChildrenData(props, data)
}
// 获取叶子节点所有的prop,也就是excel表格每一列的prop
function getLeafProp(root) {
 const result = []
 if (root.children) {
   for (const child of root.children) {
     result.push(...getLeafProp(child))
  }
} else {
   result.push(root.prop)
}
 return result
}
// 从数据中获取对应porps的值
function recursiveChildrenData(props, data) {
 const result = []
 for (const rowData of data) {
   const row = []
   for (const index of props) {
     row.push(rowData[index])
  }
   result.push(row)
   if (rowData.children) {
     result.push(...recursiveChildrenData(props, rowData.children).slice(1))
  }
}
 return result
}

ExcelStyle


这个方法倒是简单,这里其实还可以将表头以及单元格样式抽离出去成为主函数exportExcel的配置项。这个函数干了啥呢,首先就是从columns中拿到每一列的宽度,处理成 ws['!cols']想要的格式,ws['!cols']这个就是sheetJS的配置表格列宽的一个属性。然后就是一些单元格样式,具体去看xslx-js-style的官网。decode_range和encode_cell这两个方法有简单介绍,具体大家去看sheetJS官网吧。



ws 就是 那个表格数据实例


columns 是表头数组


ROW 是表头有多少行


XLSX.utils.decode_range: 用于解析 Excel 工作表中的范围字符串并将其转换为结构化的对象


XLSX.utils.encode_cell:是将一个包含行号和列号的对象编码为 Excel 中常见的单元格地址表示形式



function ExcelStyle (ws, header, ROW) {
 // 列宽
 const widthes = []
 for (const item of header) {
   widthes.push(...getLeafwidth(item))
}
 // 处理成 ws['!cols'] 想要的格式
 const wsCOLS = widthes.map(item => {
   return {
     wpx: item || 100
  }
})
 ws['!cols'] = wsCOLS
 // 定义所需的单元格格式
 const cellStyle = {
   font: { name: '宋体', sz: 11, color: { auto: 1 } },
   // 单元格对齐方式
   alignment: {
     // / 自动换行
     wrapText: 1,
     // 水平居中
     horizontal: 'center',
     // 垂直居中
     vertical: 'center'
  }
}
 // 定义表头
 const headerStyle = {
   border: {
     top: { style: 'thin', color: { rgb: '000000' } },
     left: { style: 'thin', color: { rgb: '000000' } },
     bottom: { style: 'thin', color: { rgb: '000000' } },
     right: { style: 'thin', color: { rgb: '000000' } }
  },
   fill: {
     patternType: 'solid',
     fgColor: { theme: 3, 'tint': 0.3999755851924192, rgb: 'DDD9C4' },
     bgColor: { theme: 7, 'tint': 0.3999755851924192, rgb: '8064A2' }
  }
}
 // 添加样式
 const range = XLSX.utils.decode_range(ws['!ref'])
 for (let row = range.s.r; row <= range.e.r; row++) {
   for (let col = range.s.c; col <= range.e.c; col++) {
     // 找到属性名
     const cellAddress = XLSX.utils.encode_cell({ c: col, r: row })
     if (ws[cellAddress]) {
       // 前几行是表头,添加表头样式
       if (row < ROW) {
         ws[cellAddress].s = headerStyle
      }
       ws[cellAddress].s = {
         ...ws[cellAddress].s,
         ...cellStyle
      }
    }
  }
}
}

// 和getLeafProp类似,只是找的字段不一样
function getLeafwidth(root) {
 const result = []
 if (root.children) {
   for (const child of root.children) {
     result.push(...getLeafwidth(child))
  }
} else {
   result.push(root.width)
}
 return result
}

总结


其实这次也是我第一次自己前端导出excel的需求,之前基本都是后端干的,给个地址直接模拟a标签下载就行了。本来呢,我看项目中也是有封装导出excel的方法的,但是有点晦涩难懂啊,看了下导出的效果,也并不能实现需求。我一直觉得在原有基础的去添加一些相似的功能逻辑,真不如直接重新封装一个方法。然后我测试过了将所有代码赋值到同一个js文件,正常引入传对应的数据结构是能跑通的。其实是有点问题的,就是在根据数据行合并的时候,如果是children里面还children,也就是也要递归,我有点不好拿捏判断递归的时机,加上本来对递归就是一知半解,搞得有点混乱,大家感兴趣的可以试试。


作者:落课
来源:juejin.cn/post/7447368539936587776
收起阅读 »

我二姨卖猪为什么不能自己决定价格

我二姨既养猪也养牛,收益是赔的时候更多。 如果你有投资猪肉股经验,一定知道猪周期。要是在猪肉下行周期中囤猪,那就等着赔吧,赔多少而已。 我年轻的时候就想,为啥二姨自己养的猪,自己却不能决定卖多少价格?多年过去这个问题总算是有点眉目。 本质是二姨在利用市场销售自...
继续阅读 »

我二姨既养猪也养牛,收益是赔的时候更多。


如果你有投资猪肉股经验,一定知道猪周期。要是在猪肉下行周期中囤猪,那就等着赔吧,赔多少而已。


我年轻的时候就想,为啥二姨自己养的猪,自己却不能决定卖多少价格?多年过去这个问题总算是有点眉目。


本质是二姨在利用市场销售自己养的猪,而市场有其自身的规则,单一家庭养猪户是没有办法决定市场猪价的。


一 市场


市场准确的说是市场经济,自我国宋代就已诞生。


然而现代市场经济理论的奠基人是一位西方经济学家——亚当·斯密,就是写了《国富论》的作者。


在《国富论》中其详细阐述了自由市场经济的原理。他提出了“看不见的手”理论,认为在自由竞争的市场中,每个人都在追求自己的利益,这种追求会像一只“看不见的手”一样,引导市场资源向最有利于社会的方向分配。


在姚洋的《经济学的意义》中提到福利经济学第一定律:如果由市场自己去生产和交换,最后经济总会达到帕累托最优。提到福利经济学第二定律:任何的帕累托最优状态,通过调整初始的禀赋分配,最后都能在市场机制下实现。帕累托最优指的是不可能在不牺牲任何人利益的情况下改善其他人的福利的状态。


所以市场经济被认为是配置资源最好的方式,至少目前还没找到比她更好的方式。


曾经一位伟大的国人说,他所做的事情不过是对我们的国家做了一次市场化改革。


现在我们建设的是具有中国特色的市场经济。


二 边际与均衡价格


边际是一种思维方式,就是永远看市场中最后一个人的行为或者最后一个产品的情况。比如在劳动力市场上,工资不是市场中的平均水平的劳动者决定的,而是最后一个参加劳动的人决定的,要看给他多高的工资他才愿意去做这份工作,同时也要看他有多大的贡献工厂才雇用他,两者相等的时候才是市场里的均衡工资。


边际能够解释一些实际问题。比如高速费的收取,如果不收取高速费,会导致高速拥堵,收取高速费导致对高速使用价格敏感者退出,所以说高速价格不是由第一个人决定的,而是最后一些人决定的。


边际也是新商品上市后价格的演化,直到形成均衡价格。就生猪市场而言,其是成熟市场,均衡价格已经形成。在均衡价格下,价格决定于供求关系,决定于价值链,决定于生猪出厂价格和猪肉消费价格。


我的老家在河北,我从我妈那里了解到我们老家农村的猪肉价格是10/斤元上下;而我在北京小区超市看到的是13/斤元上下。这个价格我认为肯定不是大家口袋里没钱造成的。


我又看了下A股几家上规模的生猪养殖集团:牧原股份、温氏股份、正邦科技。三者在2023年都是大幅亏损,其中正邦科技更是st了。而2020年牧原股份大幅度盈利200多个亿,我还查到2020年河北9月份的平均生猪价格,为33.73元/公斤,这都赶上今年的猪肉价了。


这样的数据结果表明今年的猪肉或者生猪价格,主要是供给导致,是生猪太多,生猪养殖太卷,不得不低价销售导致。


总结一下,生猪养殖市场均衡价格由供求关系决定,供求关系就像是天平,只有其上的砝码发生较大变化时才会影响平衡。就生猪市场来说,供求关系可以被牧原股份这种千万生猪体量的养殖集团影响,可以被一场范围特别大的猪瘟影响。


单一家庭养猪户因为生猪体量非常非常小,影响力微乎其微,不可能影响供求关系,也就不可能决定生猪价格,这也就是我二姨不能决定卖猪价格的原因。


三 周期


这部分属于题外话。不仅猪市场存在周期;文明也有周期,表现为王朝的兴衰更替;经济本身也存在周期,比如加息周期和降息周期;现在更有万物皆周期一说。


一种解释是,周期的产生源于人的贪婪。有一句话著名的话:人们从历史中吸取的教训就是从不吸取教训


文明周期源于王朝的后期统治者普遍开始奢侈,导致统治力衰弱,最终王朝灭亡,比如烽火戏诸侯。


经济周期源于债务,也是贪婪。债务越借越大,越借越不想还,就比如现在的美利坚,你看他的国债多大了,一年利息都1w多亿。


或许周期本源于人性,源于这个世界本身,且看那天地有四季,有日月更替。


尾声


现在二姨已经不养猪了。如果还养猪,我会建议要当有大猪场倒闭时再进入,这个时候市场上能卖猪的少了,而想买猪的没变,均衡价格该起来了。


作者:从码农开始
来源:juejin.cn/post/7352100456334639114
收起阅读 »

独立开发:害怕产品被抄袭怎么办?

抄袭是无法避免的,担心被抄袭是很正常的。 大部分情况下,我们要尽量将产品 藏 起来。 其实所谓的 藏,就是 尽量让产品被用户看到,尽量让产品不被同行看到。 那么这个时候呢,我们需要用不为人所知的账号,甚至可以伪装一些个人信息,去产品用户所在的平台进行推广。 藏...
继续阅读 »

抄袭是无法避免的,担心被抄袭是很正常的。


大部分情况下,我们要尽量将产品 起来。


其实所谓的 ,就是 尽量让产品被用户看到,尽量让产品不被同行看到


那么这个时候呢,我们需要用不为人所知的账号,甚至可以伪装一些个人信息,去产品用户所在的平台进行推广。


得好,推广 得好,那这个基本就成了,闷声赚钱吧。


如果没藏住怎么办呢?那就分为 及时雨马后炮 了。


所谓 及时雨,就是不要把鸡蛋放在一个篮子里,狡兔三窟,不要在一根树上吊死,等等。


做多个产品,产生多份收益,把风险分摊。


那么 马后炮 呢,就是看产品的 护城河 有多深了。


也就是说,在产品被同行发现之后,你在这个产品的方向上,跑马圈地,扩展了多大的领土。


具体点来说的话就是,用户交流群有多少,产品社区,产品粉丝有多少。


其中最直观的就是用户交流群,这是你可以用光速传达给用户的最方便,最直接的一个方式。


你可以在群里发挥包括宣传,打压,贬低,超越等方式,来让自己的产品表现得技高一筹,来持续获得用户的青睐。


这是其他竞品短期无法达到的,也是我们自己产品的先发优势和使用惯性。


当然,最终呢,肯定要人无我有,人有我优,才能真正立于不败之地的。


这是适应于大部分产品的策略,记住,是大部分,不是所有。


那么还有另外一部分情况,把产品 住反而不好。


比如你的产品本来就是给同行用的,还藏着掖着怎么行,同行知道得越多越好。


fnMap函数地图


举个例子,我的 VSCode 编辑器插件,fnMap (函数地图),目前已经有 1 万多次安装量,都是我同行,我巴不得知道的人更多一点,安装量越多越好。


问题来了,你不怕同行抄袭吗?


不怕。


为什么呢?这是一个概率问题。


抄袭你作品的人,一定是看过或者用过你作品的人。


这些人里面,对独立开发,对 VSCode 插件开发感兴趣的,不多。


刚兴趣的,有能力开发的,又要少一点。


有能力开发的,有时间,有精力,能坚持开发出来的,又要少一大部分。


能开发出来,能推广运营,能卖出去,能让人付费的,又再少一点。


盈利了,能在微薄的产品收入下,长期维护,更新,完善的人呢,还要再少一点。


总而言之呢,产品的抄袭不是一个 0 到 1 的过程就完事了,它是一个 0-1-100 的过程。


有 99%的人会结束于 99%的进度之前,你的爱好,你的兴趣,你的坚持,就是你最好的防具。


这就好像,你看别人做一个东西赚钱,如果你也抄一个,很有可能别人依旧在赚钱,你就是赚不到钱。


所以,抄袭问题可以担心,但不要过渡担心,用心做产品,就能产生抄袭屏障。


最后呢,产品的公开构建,其实也有好处,可以在项目从开发,到上线,到推广,到运营的过程中持续曝光。


有曝光就有流量,有流量就能产生成交。


很多时候,流量问题,远大于被抄袭的问题。


所以,不用太过于担心产品被抄袭,反而要持续地,不断地,换着花样地,去向全世界分享你的产品。


作者:前端之虎陈随易
来源:juejin.cn/post/7441009356105760803
收起阅读 »

我们领证啦

是的,我们领证了。在跟她经历2年时间的相处后,我们在今天2024年1月5日正式办理了结婚登记。# 我是如何找到老婆的 其实这次还是有那么一点点波折的,因为外地人无法在上海直接领结婚证,但是这个日子是我爷爷请算命先生帮我们看好的,所以我们决定回到我的老家湖北十堰...
继续阅读 »

是的,我们领证了。在跟她经历2年时间的相处后,我们在今天2024年1月5日正式办理了结婚登记。# 我是如何找到老婆的


其实这次还是有那么一点点波折的,因为外地人无法在上海直接领结婚证,但是这个日子是我爷爷请算命先生帮我们看好的,所以我们决定回到我的老家湖北十堰办理结婚登记。


今天请了一天假,考虑到怕一些突发事件,因为我们同省不同市,我怕还要什么证明,我们选择了坐飞机,预留一些时间,比如资料不齐要补资料什么的。因为6点20的飞机,我们定了4.的闹钟,但是凌晨一点半我就醒了,然后一直睡不着,可能是有点小激动的缘故吧。没等闹钟响,我们3点50分起床,煮了2个鸡蛋,带了2盒酸奶,烧了一壶开水装了一杯就匆匆出发了,昨晚预定的出租车4点20也准时到了。到了机场安检才发现不能自带水,酸奶也得喝掉,因为好几年没有坐过飞机了,竟然连这都不知道😂。6点20的飞机,因为晚点,等了一会,大概6点30就起飞了,还好还好,早晨9点就到了武当山机场,晚出发,提前达,这也是可以了。


然后我们打车到民政局,这里有一点小波折,地图一搜随便挑了个,到那发现门口竖了一个牌子,民政局换址了。


图片


我们没办法,只能坐公交去牌子上面民政局的新地址:蓝山郡。到了那里,发现那里是市政府一带,找了好一会才得知,在一个大排档旁边上去的二楼,终于找到了张湾区民政局,忘记拍了,反正非常小的一个门面,仿佛生怕别人找到似的😂。


进了大厅我们发现此时里面只有我们办理,我本来还怕排队。办理的小姐姐人很好,很细心,业务也很熟练,我们提供身-份-证、沪口本、3张照片,期间我们填了2张表,签名,按了6个手印,大概10分钟就办好了。


图片


办理期间我们全程没有表露出很兴奋的表情,以至于出民政局时,我在想当时应该面露开心一点,我甚至觉得自己没有表现好。不过这些都不重要了,此时我们很开心,我们一起走出大厅,我们觉得我们俩此刻是最幸福的人。


总的来看,此次回老家办理结婚登记,整个过程还是挺顺利的。


最后,祝天下有情人终成眷属,希望大家龙年行大运!


作者:大数据技术派
来源:juejin.cn/post/7322355350921461800
收起阅读 »

pnpm v10正式发布,重磅更新,历时3个月,12个版本

web
犹抱琵琶半遮面,千呼万唤始出来,pnpm v10 终于正式发布了。 众所周知,笔者有关注行业技术最新进展的爱好,这次的 pnpm v10 版本,也已经跟踪了好几个月了。 而这次,v10 正式版终于发布了。 版本时间pnpm 10.02025年01月08日pnp...
继续阅读 »


犹抱琵琶半遮面,千呼万唤始出来,pnpm v10 终于正式发布了。


众所周知,笔者有关注行业技术最新进展的爱好,这次的 pnpm v10 版本,也已经跟踪了好几个月了。


而这次,v10 正式版终于发布了。


版本时间
pnpm 10.02025年01月08日
pnpm 10.0 RC 32025年01月05日
pnpm 10.0 RC 22024年12月29日
pnpm 10.0 RC 12024年12月27日
pnpm 10.0 RC 02024年12月16日
pnpm 10.0 Beta 32024年12月12日
pnpm 10.0 Beta 22024年12月09日
pnpm 10.0 Beta 12024年11月29日
pnpm 10.0 Alpha 42024年11月25日
pnpm 10.0 Alpha 32024年11月25日
pnpm 10.0 Alpha 22024年11月15日
pnpm 10.0 Alpha 12024年11月15日
pnpm 10.0 Alpha 02024年10月08日

以上是笔者整理的 pnpm v10 发布过程,从 草案,到 测试版,到 候选版,再到最后的 正式版,可谓是花了不少功夫啊。


也从侧面说明了,pnpm 团队对这次 v10 版本的重视程度,必然是有大事发生,那么话不多说,我们看看本次的更新内容吧。


依赖项的生命周期脚本不会在安装期间执行


这是一个重要变化,依赖包的 生命周期脚本 不会自动执行了。


那么问题来了,可能有些读者还不知道什么是 生命周期脚本生命周期脚本 英文名叫做 Lifecycle scripts


包括以下几种:



  1. 安装相关脚本

    • preinstall:在安装软件包之前执行。

    • install:在安装软件包时执行。

    • postinstall:在安装软件包之后执行。



  2. 发布相关脚本

    • prepare:在发布软件包之前执行。

    • prepublishOnly:只在 npm publish 时执行。

    • prepack:在打包软件包之前执行。

    • postpack:在打包软件包之后执行。



  3. 运行相关脚本

    • prestart/start/poststart:在运行 npm start 时执行。

    • prerestart/restart/postrestart:在运行 npm restart 时执。

    • prestop/stop/poststop:在运行 npm stop 时执行。

    • pretest/test/posttest:在运行 npm test 时执行。




pnpnm v10 开始,这些依赖包中的生命周期脚本都不会自动执行了,这样可以进一步提高安全性。


投票


官方也发起了一个投票:pnpm 可以在安装期间阻止依赖项的生命周期脚本。但这是一个可选功能。我们应该默认阻止它们吗?


最终赞成禁用生命周期脚本的占大多数。


那么我们要让某些依赖包的脚本可以自动执行的话,应怎么做呢?


{
"pnpm": {
"onlyBuiltDependencies": ["fsevents"]
}
}

如上示例,pnpm 提供了一个 onlyBuiltDependencies 参数,所有可以自动执行生命周期脚本的包,都要手动写到里面。


这么一来呢,确实提高了安全性,但是对于开发者来说,也提高了不少复杂性。


因为,可能有些依赖包,或者说依赖包的依赖包,需要自动执行脚本才能生效。


如果采用手动模式,那就很可能很难找到,到底要执行哪个包的生命周期脚本,提高了安全性的同时,也降低了开发的便捷性。


pnpm link 行为更新


这个可能有很多人还没用过,主要用途有 2 个:



  1. 替换已安装的软件包

    • 当你正在开发一个依赖包,想在另一个项目中测试它时,可以使用 pnpm link 将本地版本链接到目标项目。

    • 这样可以避免频繁地发布和安装依赖包,提高开发效率。



  2. 添加全局可用的二进制文件

    • 如果你开发了一个包含命令行工具的软件包,可以使用 pnpm link 将其二进制文件注册到全局,以便在任何地方都可以执行。

    • 这对于开发 CLI 工具非常有用。




那么这次的主要变化有 2 个。



  1. 通过 pnpm link 默认创建的是全局包,在之前,则需要 pnpm link -g 才可以创建全局包。

  2. workspace 的多包项目中,override 被添加到工作区的根目录,将依赖项链接到工作区中的所有项目。


总而言之,就是能全局的就全局,把影响范围扩大化,免得抠抠搜搜的。


可能有读者不知道 override 是啥,这里也科普一下:


假设项目中有两个依赖 A 和 B,它们都依赖于同一个包 lodash,但是需要使用不同的版本。


那么可以使用 overrides 来指定使用 lodash 的特定版本:


{
"dependencies": {
"A": "^1.0.0",
"B": "^2.0.0"
},
"pnpm": {
"overrides": {
"lodash": "^4.17.21"
}
}
}

这样就可以确保项目中使用的 lodash 版本是 4.17.21,而不管 A 和 B 各自需要的版本是什么。


如果某个依赖包存在问题,也可以使用 overrides 来替换它:


{
"dependencies": {
"problem-package": "^1.0.0"
},
"pnpm": {
"overrides": {
"problem-package": "my-forked-package@^1.0.1"
}
}
}

在这个例子中,我们将 problem-package 替换为 my-forked-package 的 1.0.1 版本。


可能我写的文章稍微啰嗦了点,主要是考虑到读者可能存在不同的经验水平,所以一些概念也扩展科普一下。


使用 SHA256 进行安全哈希处理


各种哈希算法已更新为 SHA256,以增强安全性和一致性:



  • node_modules/.pnpm 内的长路径现在使用 SHA256 进行哈希处理。

  • 锁定文件中的长对等依赖关系哈希现在使用 SHA256 而不是 MD5。

  • pnpm-lock.yamlpackageExtensionsChecksum 字段中存储的哈希现在为 SHA256。

  • 副作用缓存密钥现在使用 SHA256。

  • 锁定文件中的 pnpmfile 校验和现在使用 SHA256。


配置更新



  1. manage-package-manager-versions:默认启用。pnpm 现在默认根据 package.json 中的 packageManager 字段管理自己的版本。

  2. public-hoist-pattern:默认情况下不会提升任何内容。名称中包含 eslintprettier 的包不再提升到 node_modules 的根目录。

  3. 已将 @yarnpkg/extensions 升级至 v2.0.3,这可能会改变您的 pnpm-lock 文件。

  4. virtual-store-dir-max-length:Windows 上的默认值已减少到 60 个字符。

  5. 减少脚本的环境变量:在脚本执行期间,会设置较少的 npm_package_* 环境变量。仅保留 nameversionbinenginesconfig

  6. 即使 NODE_ENV=production,所有依赖项现在都会安装。


从现在开始,NODE_ENV=production 也会安装所有依赖,包括开发依赖,这对于像我这样的强迫症来说,有点难以接受,没有用到的依赖我为啥要安装?


查看官方文档,可以通过 pnpm add --prod 来只安装 dependencies 依赖。


全局存储更新



  1. 全局 store 升级到 v10

  2. 一些注册表允许使用不同的软件包名称或版本发布相同的内容。为了适应这种情况,商店中的索引文件现在使用内容哈希和软件包标识符来存储。

    1. 验证锁文件中的完整性是否与正确的包相对应,在 Git 冲突解决不佳后可能并非如此。

    2. 允许相同的内容被不同的包或者同一个包的不同版本引用。



  3. 更高效的副作用索引。存储中的索引文件结构已更改。现在通过仅列出文件差异而不是所有文件,可以更有效地跟踪副作用。

  4. 新的索引目录存储了包内容映射。以前,这些文件位于文件中。


其他重大变化



  • # 字符现在在 node_modules/.pnpm 内的目录名称中被转义。

  • 运行 pnpm add --global pnpmpnpm add --global @pnpm/exe 现在会失败并出现错误消息,指导您改用 pnpm 自我更新。

  • 通过 URL 添加的依赖项现在在锁文件中记录最终解析的 URL,确保完全捕获任何重定向。

  • pnpm deploy 命令现在仅适用于具有 inject-workspace-packages=true 的工作区。引入此限制是为了让我们能够使用工作区锁定文件为已部署的项目创建适当的锁定文件。

  • 删除了从 lockfile v6v9 的转换。如果您需要 v6v9 的转换,请使用 pnpm CLI v9

  • pnpm test 现在将 test 关键字后的所有参数直接传递给底层脚本。这与 pnpm run test 的行为一致。以前您需要使用 -- 前缀。

  • pnpm deploy 现在尝试从共享锁文件创建专用锁文件以进行部署。如果没有共享锁文件或 force-legacy-deploy 设置为 true,它将回退到没有锁文件的部署。


次要变化


添加了对一种名为 configurational dependencie 的新依赖项类型的支持


这些依赖项在所有其他类型的依赖项之前安装 (在 dependenciesdevDependenciesoptionalDependencies 之前)。


配置依赖项不能具有其自身或生命周期脚本的依赖项,应使用精确版本和完整性校验和添加它们。


示例:


{
"pnpm": {
"configDependencies": {
"my-configs": "1.0.0+sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="
}
}
}

新的 verify-deps-before-run 设置


此设置控制 pnpm 在运行脚本之前如何检查 node_modules,有以下值:



  • install:如果 node_modules 已过时,则自动运行 pnpm install。

  • warn:如果 node_modules 已过时,则打印警告。

  • prompt:如果 node_modules 已过时,则提示用户确认运行 pnpm install。

  • error:如果 node_modules 已过时,则抛出错误。

  • false:禁用依赖性检查。


新的 inject-workspace-packages 设置允许对所有本地工作区依赖项进行硬链接,而不是对其进行符号链接。


以前,这可以使用 dependencyMeta[].injected 来实现,现在仍然受支持。


更快的重复安装


在重复安装时,pnpm 会执行快速检查以确保 node_modules 是最新的。


pnpm add 与默认工作区目录集成


添加依赖项时,pnpm add 会检查默认工作区目录。


如果依赖项和版本要求与目录匹配,pnpm add 将使用 catalog: 协议。


如果没有指定版本,它将匹配目录的版本。


如果不匹配,它将恢复为标准行为。


pnpm dlx 解析调整


pnpm dlx 现在将软件包解析为其确切版本,并将这些确切版本用作缓存键。


这可确保 pnpm dlx 始终安装最新请求的软件包。


node_modules 验证


某些命令没有 node_modules 验证,不应修改 node_modules 的命令 (例如 pnpm install --lockfile-only) 不再验证或清除 node_modules。


以上就是本次 pnpm v10 的更新内容,感谢阅读,欢迎点赞,评论和转发。


作者:前端之虎陈随易
来源:juejin.cn/post/7457307617129496614
收起阅读 »

我不允许还有人不知道前端实现时刻洪水模拟的方法!🔥

web
二维水动力 HydroDynamic2D 二维水动力介绍 二维水动力模型对象 HydroDynamic2D,基于真实数据驱动生成水动力模型(根据不同时刻下每个网格的流向、流速、高程、水位) 二维水动力模型考虑了水流在平面上的变化,适用于河道弯曲、水流方向多变的...
继续阅读 »

二维水动力 HydroDynamic2D


二维水动力介绍


二维水动力模型对象 HydroDynamic2D,基于真实数据驱动生成水动力模型(根据不同时刻下每个网格的流向、流速、高程、水位)


二维水动力模型考虑了水流在平面上的变化,适用于河道弯曲、水流方向多变的情况。这种模型能够更准确地反映水流在平面上的分布情况,适用于需要精确模拟水流动态的场景,如城市排水系统设计、洪水模拟。二维模型的优势在于能够提供更详细的水流信息,但计算复杂度较高,需要更多的计算资源和时间。


二维水动力效果2.gif

本篇文章主要介绍在DTS 数字孪生引擎中实现二维水动力效果。在DTS SDK中开放了 HydroDynamic2D对象 添加二维水动力,并可以通过多种数据源进行添加,如 Bin、Sdb、Shp、Tif 的方式。


本文章主要介绍shp加载的方式,这种方式相对其他方式会更简单通用。


shp数据源添加方式


所需数据源


二维水动力是用数据驱动生成渲染效果的接口,所以数据源及其重要。


要利用shp为数据源进行添加,使用的是addByShp()方法,其与数据源相关的参数有两个:shpFilePath shpDataFilePath


shpFilePath其实就是水动力模型中水面网格的范围与高程,shpDataFilePath则代表每个网格的水深以及流速、流向



  • shpFilePath: 添加二维水动力模型整体范围的shp文件路径,取值示例:"C:/shpFile/xxx.shp"。



    • 此shp文件包含水动力模型所有网格的范围

    • shp类型为Polygon

    • 坐标系必须与工程坐标系保持一致

    • 必须包含 ID和Elev 两个字段:ID是网格ID;Elev是网格的高程值,单位是米




image-20241216172254779.png

  • shpDataFilePath: 可选参数,仅在update()方法执行生效。更新二维水动力模型时包含水面网格的dat类型文件路径,取值示例:"C:/datFile/xxx.dat"。

  • dat文件是一种二进制文件,它提取了某一时刻包含的所有水面网格的信息,并把这些信息依次写入了二进制文件dat。

  • 一个水面网格信息包含如下一组四个值:id (int),h (double),u(double),v(double),必须完全符合顺序以及数据类型。

  • id对应shp属性表ID字段,h是网格对应的水深(单位是米),uv是流速和流向(单位米/秒,u朝东,v朝北)。

  • 更新效果需要准备多个时刻的.dat文件,如下图所示

    image-20241216172314255.png


添加方法


1、准备测试数据

这里给大家准备好了一些数据资源,包括了实现的数据源、代码以及dat数据转换的程序,大家可以自行下载测试



百度网盘数据资源连接:pan.baidu.com/s/1XS3UDkrB…




  • 【文件资源】@path : 放到cloud文件资源路径

  • 【示例代码】code : demo源代码,直接用demo工程场景运行即可

  • 【dat数据转换】jsonToDat : json转dat代码,分别含有node.js、java、python示例代码


准备好两份数据分别是shpFilePath填写的shp文件,以及shpDataFilePath填写的dat文件集。文件可以直接用本地路径读取,建议放置到Cloud文件资源路径下,用@path的方式引用


这里可以用孪创启航营给大家准备的数据进行测试,在提供的文件夹的【文件资源】@path\【孪创启航营】HydroDynamic2D


2、通过shp网格数据初始化水动力模型

通过add()初始化水动力模型,并使用focus()定位到网格位置,但没有具体内容,还需要调用update添加.dat数据驱动效果。


add()参数文章末尾有详解


//添加shp数据源
fdapi.hydrodynamic2d.clear()

let hydrodynamic2d_add = {
id: 'hdm_shp', // HydroDynamic2D对象ID
collision: false, //开启碰撞sd
displayMode: 0, // 显示样式,取值范围:[0,1],0水体样式(默认值),1热力样式
waterMode: 0, // 水面显示模式,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式
arrowColor: [1, 1, 1, 0], // 箭头颜色和透明度
speedFactor: 0.1, // 速度因子
rippleDensity: 1, // 水波纹辐射强度
rippleTiling: 3, // 水波纹辐射平铺系数
shpFilePath: '@path:【孪创启航营】HydroDynamic2D/shp/grid.shp' // 添加二维水动力模型整体范围的shp文件路径
}
await fdapi.hydrodynamic2d.add(hydrodynamic2d_add)

await fdapi.hydrodynamic2d.focus('hdm_shp', 200)

3、根据.dat更新水动力模型

写一个定时器,根据不同时刻,调用hydrodynamic2d.update()更新shpDataFilePath路径,达到水动力更新的效果。



  • 参数updateTime 是更新动画的插值时间,单位为秒,一般与更新定时器的时间一致即可。


let index = 0
let hydrodynamicModel_for_update = {
id: 'hdm_shp', // HydroDynamic2D对象ID
updateTime: 1, // 更新动画的插值时间
shpDataFilePath: ''// 更新二维水动力模型时包含水面网格的dat类型文件路径
}

// 使用dat数据填充shp网格
let updateTimer = setInterval(async () => {
hydrodynamicModel_for_update.shpDataFilePath = '@path:【孪创启航营】HydroDynamic2D/dat/hydrodynamic_' + index + '.dat'

if (index > 9) {
clearInterval(updateTimer)
} else {
await __g.hydrodynamic2d.update(hydrodynamicModel_for_update) // 水动力更新
index = index + 1
}
}, 1000)

通过以上就可以达成二维水动力的创建以及更新了。


4、实现二维水动力热力效果

二维水动力支持热力效果,可以根据.dat文件中的水深字段进行配色


二维水动力热力效果.gif

仅需要把add()中的displayMode参数设置为1热力样式,再通过valueRangecolors进行热力样式的调整



  • valueRange (array) ,二维水动力模型颜色插值对应的数值区间

  • colors (object) 二维水动力模型自定义调色板对象,包含颜色渐变控制、无效像素颜色和调色板区间数组



    • gradient (boolean) 是否渐变

    • invalidColor (Color) 无效像素点的默认颜色,默认白色

    • colorStops (array) 调色板对象数组,每一个对象包含热力值和对应颜色值,结构示例:[{"value":0, "color":[0,0,1,1]}],每一个调色板对象支持以下属性:

      • color (Color) 值对应的调色板颜色

      • value (number) 值




    const addHeat = async () => {
    fdapi.hydrodynamic2d.clear()

    let hydrodynamic2d_add = {
    id: 'hdm_shp_heat', // HydroDynamic2D对象ID
    offset: [0, 0, 0], // 二维水动力模型的整体偏移,默认值:[0, 0, 0]
    displayMode: 1, // 显示样式,取值范围:[0,1],0水体样式(默认值),1热力样式
    waterMode: 2, // 水面显示模式,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式
    arrowColor: [1, 1, 1, 0.5], // 箭头颜色和透明度
    collision: false, //开启碰撞sd
    arrowTiling: 3, // 箭头平铺系数
    speedFactor: 0.1, // 速度因子
    rippleDensity: 1, // 水波纹辐射强度
    rippleTiling: 2, // 水波纹辐射平铺系数
    shpFilePath: '@path:【孪创启航营】HydroDynamic2D/shp/grid_heat.shp',

    valueRange: [1, 1.3], // 二维水动力模型颜色插值对应的数值区间
    alphaMode: 1, //使用colors色带透明度
    colors: {
    gradient: true,
    invalidColor: [0, 0, 0, 1],
    colorStops: [
    {
    value: 0,
    color: [0, 0, 1, 0.2]
    },
    {
    value: 0.25,
    color: [0, 1, 1, 0.2]
    },
    {
    value: 0.5,
    color: [0, 1, 0, 0.2]
    },
    {
    value: 0.75,
    color: [1, 1, 0, 0.2]
    },
    {
    value: 1,
    color: [1, 0, 0, 0.2]
    }
    ]
    }
    }
    await fdapi.hydrodynamic2d.add(hydrodynamic2d_add)

    await fdapi.hydrodynamic2d.focus('hdm_shp_heat', 200)

    let index = 0
    let hydrodynamicModel_for_update = {
    id: 'hdm_shp_heat',
    updateTime: 1,
    shpDataFilePath: ''
    }

    //使用dat数据填充shp网格
    let updateTimer = setInterval(async () => {
    hydrodynamicModel_for_update.shpDataFilePath = '@path:【孪创启航营】HydroDynamic2D/dat/hydrodynamic_' + index + '.dat'

    if (index > 9) {
    clearInterval(updateTimer)
    } else {
    await __g.hydrodynamic2d.update(hydrodynamicModel_for_update)
    index = index + 1
    }
    }, 1000)
    }



demo运行


缺乏数据源的小伙伴可以尝试运行我们准备好的demo示例,感受一下水动力的效果与参数调用。



  1. **下载资源:**下载百度网盘数据资源

  2. 替换资源:把【文件资源】@path的文件放到cloud文件资源路径下

  3. **启动cloud:**cloud启动demo工程

  4. 替换sdk:【示例代码】code\lib\aircity中的ac.min.jsac.min.js,替换为cloud右上角"sdk"路径的对应文件

  5. **运行:**双击运行示例代码】code\二维水动力.html 代码里的 shpFilePathshpDataFilePath路径得和第2步中一致


二维水动力效果1.gif

.dat 数据转换?


在数据源中,网格对应的水深、流速、流向数据,大家获取到可能不是标准的dat数据,有可能是json、csv甚至是excel数据。所以这里教大家如何把常见的数据转为dat二进制文件!


大象进冰箱需要三步,咱们转数据也需要三步



  1. 解析数据:读取文件,把不同数据源中的id,h,u,v(网格id、水深、流速流向u、流速流向v)提取出来。

  2. 转为二进制数据:把id,h,u,v转化为二进制的格式。

  3. 文件创建并写入:把二进制的格式数据保存为.dat文件


其中解析数据每份数据可能各不相同,都需要单独编写。这里我以一个json数据格式为例子,教大家如何转换为.dat,例如我们有一个data.json文件数据示例如下:


[
{
"index": 0,
"time": "08:30:00",
"data": [
{
"id": 0,
"h": 2,
"u": 0,
"v": 0
},
{
"id": 1,
"h": 2,
"u": 0,
"v": 0
}
]
},
{
"index": 1,
"time": "09:00:00",
"data": [
{
"id": 0,
"h": 2,
"u": 0,
"v": 0
},
{
"id": 1,
"h": 2.001,
"u": 0.1,
"v": 0.1
}
]
}
]


我们可以使用不同的编程手段来处理,如node.js、python、java,这里直接把转换的代码贴给大家~


注意:这三种编程手段都需要单独的安装对应的环境,如果没有环境可以选择一种自行百度安装



node官网:Node.js — 在任何地方运行 JavaScript


python官网:python.org


java官网:Java | Oracle



node.js


  1. 解析数据:使用require('./data.json')同步地引入并解析JSON数据文件,将其内容存储在jsonData变量中。

  2. 转为二进制数据:pamarToBuffer函数将idhuv转换为小端字节序的二进制Buffer。

  3. 文件创建并写入:遍历JSON数据,对每个时间点,使用path.join构建.dat文件路径,fs.createWriteStream创建写入流,datStream.write写入二进制Buffer,最后datStream.end关闭写入流。


// 引入必要的模块
const fs = require('fs') // 用于文件的读写操作
const path = require('path') // 用于处理文件路径
const jsonData = require('./data.json') // 引入 JSON 数据文件

// 确保 ./dat 目录存在
const datDir = path.join(__dirname, 'dat')
if (!fs.existsSync(datDir)) {
fs.mkdirSync(datDir)
}

// 遍历 JSON 数据 time_i 是当前时间点的索引
for (let time_i = 0; time_i < jsonData.length; time_i++) {
// 创建 .dat 文件路径
const datFilePath = path.join(datDir, `hydrodynamic_${time_i}.dat`)
// 创建写入流
const datStream = fs.createWriteStream(datFilePath)

// 获取并遍历时间点的数据
const timeData = jsonData[time_i].data
for (let grid_i = 0; grid_i < timeData.length; grid_i++) {
// 数据转换和写入
const { id, h, u, v } = timeData[grid_i]
const buffer = pamarToBuffer(id, h, u, v)
datStream.write(buffer)
}

datStream.end()
}

function pamarToBuffer(id, h, u, v) {
// 创建一个 Buffer 来存储二进制数据
const buffer = Buffer.alloc(4 + 8 + 8 + 8) // 分配足够的空间:4 字节用于 id,3 个 8 字节用于 double 值
// 向 Buffer 中写入数据
buffer.writeInt32LE(id, 0) // 从索引 0 开始写入 id(32 位整数)
buffer.writeDoubleLE(h, 4) // 从索引 4 开始写入 h(64 位浮点数)
buffer.writeDoubleLE(u, 12) // 从索引 12 开始写入 u(64 位浮点数)
buffer.writeDoubleLE(v, 20) // 从索引 20 开始写入 v(64 位浮点数)

return buffer
}


python


  1. 解析数据:使用json.load(f)方法从打开的JSON文件对象f中读取并解析数据,将JSON格式的数据转换为Python的字典或列表结构,存储在变量json_data中。

  2. 转为二进制数据:使用struct.pack('=iddd', id, h, u, v)方法将这些数据按照指定的格式(=表示本地字节顺序,i表示整数,d表示双精度浮点数)打包成二进制数据。

  3. 文件创建并写入:使用open函数以二进制写入模式打开(或创建)文件,最后通过write方法将转换好的二进制数据写入到该文件中。


import json
import os
import struct

# 读取JSON文件
json_file_path = './data.json'
with open(json_file_path, 'r') as f:
json_data = json.load(f)

# 定义输出目录
output_dir = os.path.join(os.getcwd(), 'dat')
if not os.path.exists(output_dir):
os.makedirs(output_dir)

# 遍历JSON数据
for time_i, time_node in enumerate(json_data):
# 创建.dat文件路径
dat_file_path = os.path.join(output_dir, f"hydrodynamic_{time_i}.dat")

# 打开文件以二进制写入模式
with open(dat_file_path, 'wb') as dat_file:
# 获取并遍历时间点的数据
time_data = time_node['data']
for grid_i, data_element in enumerate(time_data):

# 数据转换和写入
id = int(data_element['id'])
h = float(data_element['h'])
u = float(data_element['u'])
v = float(data_element['v'])

# 使用struct模块将数据转换为二进制格式
binary_data = struct.pack('=iddd', id, h, u, v)
# 写入二进制数据到文件
dat_file.write(binary_data)

print("Data processing complete.")

Java

java需要安装对应的 jackson json解析依赖才能使用,这里给大家提供了一个最简洁的版本,只需要有了对应的java环境运行目录下的start.bat文件即可生成dat文件。



  1. 解析数据:使用ObjectMapperdata.json文件中读取JSON数据,并解析为TimePoint对象的列表。

  2. 转为二进制数据:convertToBytes方法将TimePointData对象的idhuv字段转换为小端字节序的字节数组。

  3. 文件创建并写入:遍历TimePoint列表,为每个时间点创建.dat文件,并使用FileOutputStream将转换后的字节数组写入文件。


import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

class TimePointData {
int id;
double h;
double u;
double v;

public TimePointData(int id, double h, double u, double v) {
this.id = id;
this.h = h;
this.u = u;
this.v = v;
}

}

class TimePoint {
List<TimePointData> data;

public TimePoint(List<TimePointData> data) {
this.data = data;
}

}

public class JsonToDatConverter {

public static void main(String[] args) {
String jsonFilePath = "data.json";
String datDir = "dat";

// 读取JSON文件
ObjectMapper objectMapper = new ObjectMapper();
try {
JsonNode rootNode = objectMapper.readTree(Files.newInputStream(Paths.get(jsonFilePath), StandardOpenOption.READ));
List<TimePoint> timePoints = parseJsonToTimePoints(rootNode);

Path dirPath = Paths.get(datDir);
if (!Files.exists(dirPath)) {
Files.createDirectory(dirPath);
}

for (int time_i = 0; time_i < timePoints.size(); time_i++) {
TimePoint timePoint = timePoints.get(time_i);
String datFilePath = Paths.get(datDir, "hydrodynamic_" + time_i + ".dat").toString();

try (FileOutputStream fos = new FileOutputStream(datFilePath)) {
for (TimePointData data : timePoint.data) {
byte[] bytes = convertToBytes(data.id, data.h, data.u, data.v);
fos.write(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

private static List<TimePoint> parseJsonToTimePoints(JsonNode rootNode) {
if (rootNode == null || !rootNode.isArray()) {
throw new IllegalArgumentException("Invalid JSON structure: 'timePoints' field is missing or not an array");
}

List<TimePoint> timePoints = new ArrayList<>();
for (JsonNode timePointNode : rootNode) {
JsonNode dataNode = timePointNode.get("data");
if (dataNode == null || !dataNode.isArray()) {
throw new IllegalArgumentException(
"Invalid JSON structure: 'data' field is missing or not an array within a 'timePoints' object");
}

List<TimePointData> dataList = new ArrayList<>();
for (JsonNode dataItemNode : dataNode) {
int id = dataItemNode.get("id").asInt();
double h = dataItemNode.get("h").asDouble();
double u = dataItemNode.get("u").asDouble();
double v = dataItemNode.get("v").asDouble();
dataList.add(new TimePointData(id, h, u, v));
}

timePoints.add(new TimePoint(dataList));
}

return timePoints;
}

private static byte[] convertToBytes(int id, double h, double u, double v) {
ByteBuffer buffer = ByteBuffer.allocate(4 + 8 + 8 + 8).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(id);
buffer.putDouble(h);
buffer.putDouble(u);
buffer.putDouble(v);
return buffer.array();
}
}

二维水动力添加参数详解


通用参数

通用参数比较简单理解,这里就简单列举出来



  • id (string) HydroDynamic2D对象ID

  • groupId (string) 可选,Gr0up分组

  • userData (string) 可选,用户自定义数据

  • offset (array) 二维水动力模型的整体偏移,默认值:[0, 0, 0]

  • collision (boolean) 是否开启碰撞,注意:开启后会影响加载效率


数据参数

数据参数前面介绍所需数据源已有详细介绍



  • shpFilePath(string)添加二维水动力模型整体范围的shp文件路径,取值示例:"C:/shpFile/xxx.shp"。

    • 此shp文件包含水动力模型所有网格的范围

    • shp类型为Polygon

    • 坐标系必须与工程坐标系保持一致

    • 必须包含 ID和Elev 两个字段:ID是网格ID;Elev是网格的高程值,单位是米



  • shpDataFilePath (string)可选参数,仅在update()方法执行生效。更新二维水动力模型时包含水面网格的dat类型文件路径,取值示例:"C:/datFile/xxx.dat"。

    • 注意:dat文件是一种二进制文件,它提取了某一时刻包含的所有水面网格的信息,并把这些信息依次写入了二进制文件dat,一个水面网格信息包含如下一组四个值:id,h,u,v。id对应shp属性表ID字段(int类型),h是网格对应的水深(double类型,单位是米),uv是流速和流向(double类型,单位米/秒,u朝东,v朝北)。




显示样式参数


  • displayMode (number) 显示样式,取值范围:[0,1],0水体样式(默认值),1热力样式



    • displayMode为0时,样式就只需要控制waterModewaterColor设置水体样式



      • waterMode (number) 水面显示模型,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式

      • waterColor (Color) 水体颜色和透明度,注意:仅在displayMode=0时生效



    • displayMode为1时,样式就需要通过valueRangecolors控制热力样式



      • valueRange (array) ,二维水动力模型颜色插值对应的数值区间

      • colors (object) 二维水动力模型自定义调色板对象,包含颜色渐变控制、无效像素颜色和调色板区间数组



        • gradient (boolean) 是否渐变

        • invalidColor (Color) 无效像素点的默认颜色,默认白色

        • colorStops (array) 调色板对象数组,每一个对象包含热力值和对应颜色值,结构示例:[{"value":0, "color":[0,0,1,1]}],每一个调色板对象支持以下属性:

          • color (Color) 值对应的调色板颜色

          • value (number) 值





      • colors代码示例


        // colors示例
        {
        gradient: true,// 是否渐变
        invalidColor: [0, 0, 0, 1],// 无效像素点的默认颜色
        colorStops: [
        {
        value: 0,
        color: [0, 0, 1, 1]
        },
        {
        value: 0.25,
        color: [0, 1, 1, 1]
        },
        {
        value: 0.5,
        color: [0, 1, 0, 1]
        },
        {
        value: 0.75,
        color: [1, 1, 0, 1]
        },
        {
        value: 1,
        color: [1, 0, 0, 0]
        }
        ]
        }






  • alphaComposite (boolean) 是否使用混合透明度 取值:true / false 默认:true

  • alphaMode (number) 透明模式,取值:[0,1],0 : 使用colors调色板的不透明度值 1 : 使用系统默认值


箭头相关参数

箭头方向根据每个格网的uv流向决定



  • arrowDisplayMode (number) 箭头显示模式 取值范围:[0,1],0默认样式(受arrowColor参数影响),1热力样式(受arrowColors调色板参数影响)



    • arrowDisplayMode 为0,则设置arrowAlphaMode = 0,并通过arrowColor调整箭头的颜色和透明度

      • arrowColor (Color) 箭头颜色和透明度



    • arrowDisplayMode 为1,则设置arrowAlphaMode = 1,并通过arrowColors调整箭头的颜色和透明度

      • arrowColors (object)箭头颜色调色板 仅在arrowDisplayMode=1时生效,河道箭头热力样式下的调色板配色对象,包含颜色渐变控制、无效像素颜色和调色板区间数组

        • 格式同上方的显示样式参数colors







  • arrowAlphaMode (number) 箭头透明度模式,仅在arrowDisplayMode=0时生效,取值:[0,1],0使用arrowColor的透明度,1使用调色板的透明度

  • arrowTiling (number) 箭头平铺系数 值越小则箭头越小越密集,反之则更大更疏松


箭头.png
水面效果参数


  • foamWidth (number) 泡沫宽度取值范围:[0~10000],默认值:1米

  • foamIntensity (number) 泡沫强度 取值范围:[0~1],默认值:0.5

  • speedFactor (number) 速度因子


速度因子.gif

  • flowThreshold (array) 水浪效果漫延的范围 即把水动力模型[minSpeed,maxSpeed],最小最大流速的范围映射到[0~~1],取值示例:[0.1,0.4],取值范围[0-1]


水浪效果漫延的范围.png

  • rippleDensity (number)水波纹辐射强度


水波纹辐射强度.gif

  • rippleTiling (number) 水波纹辐射平铺系数


水波纹辐射平铺系数.gif


以上就是本篇文章的所有内容,相信大家看完这篇文章后可以轻松的通过DTS实现二维水动力效果。


在DTS中还有各式各样的水分析相关接口,如FloodFill 水淹分析、Fluid 流体仿真对象、HydroDynamic1D 一维水动力、WaterFlowField 水流场,大家可以根据自身需求选择,这里给大家推荐一篇《开闸放水》的教程,后续也会陆续推出更多教程~


不再需要UE美术,前端轻松解决水利开闸放水难题!!!


作者:女前端浅入数字孪生
来源:juejin.cn/post/7452181029994971147
收起阅读 »

神了,Chrome 这个记录器简直是开发测试提效神器🚀🚀🚀

web
在开发工作中,你是否遇到过这样的场景: 当你需要开发某个功能时,这个功能依赖一系列的点击或者选择操作,才能获取到最终的数据。而在开发和调试的过程中,你往往需要多次验证流程的正确性。早期的时候,这种验证通常非常繁琐——你可能需要反复提交表单、重新执行操作流程,才...
继续阅读 »

在开发工作中,你是否遇到过这样的场景:


当你需要开发某个功能时,这个功能依赖一系列的点击或者选择操作,才能获取到最终的数据。而在开发和调试的过程中,你往往需要多次验证流程的正确性。早期的时候,这种验证通常非常繁琐——你可能需要反复提交表单、重新执行操作流程,才能完成一次完整的自测。


如今,这一切变得更加高效了。


现在,我们可以使用记录器(Recorder)来优化这一开发流程。这个工具允许你将整个操作过程录制下来,保存为一个可复现的操作记录。每次需要重新验证或提交流程时,只需一键执行这条记录,便能完成所有的重复性操作。


更棒的是,这个功能还支持二次编辑。如果你需要在某个步骤后面新增额外的操作,或者减少不必要的步骤,都可以轻松修改操作记录,而无需重新录制整个流程。



文章同步在公众号:萌萌哒草头将军,欢迎关注哦~



🚀 功能亮点与用途


1. 高效的开发与调试


对于开发者来说,这个功能不仅可以节省大量时间,还能确保操作流程的准确性,避免因手动操作而导致的遗漏或错误。


2. 性能监控的得力助手


谷歌推出这个功能的主要目的是为了帮助开发者更方便地监听用户在某些操作流程中的性能体验。例如,在查看录制的操作流程时,你可以直接点击某个步骤,跳转到性能面板(Performance Panel),并且工具会自动锁定当前帧的数据。这种体验优化相比以往手动查找性能问题,提升了不少效率。


3. 测试自动化的天然工具


如果你是一名测试人员,这个功能同样非常实用。操作流程录制完成后,你可以直接将其导出为Puppeteer脚本,方便地将其集成到你的自动化测试中,进一步提升测试的覆盖率和效率。


🚀 使用方法


我们以表单提交为例子展示


image.png


以下是如何使用记录器功能的步骤:


1. 💎 打开记录器


image.png


并点击创建新录制按钮


image.png


2. 💎 开始录制流程


可以重命名下,方便后续复用,然后点击最下方的开始录制按钮


image.png


我们在填写完表单,并且点击 sumbit按钮,然后点击控制台的结束录制按钮,可以看到我们的每个步骤都被记录下来


image.png


3. 💎 执行录制



  1. 记录器 面板中,点击 播放 按钮,浏览器会自动按照录制的流程重新执行操作。

  2. 你可以在执行过程中观察页面行为,确认流程是否正确。

  3. 如果遇到下面的情况,说明是超时了,需要设置下超时时间


image.png


点击这个地方展开就可以重新设置超时限制参数了


image.png


然后你点击播放按钮就一切正常了


nalmal.gif


4. 💎 查看和编辑录制


你可以在 记录器 面板中,看到录制的每个步骤,包含操作类型(如点击、输入、导航等)和目标元素。


你也可以点击每个步骤进行详细查看,也可以通过右键菜单进行编辑,例如增加新步骤、删除步骤或修改操作。


🚀 应用场景


1. 💎 表单提交及验证


录制复杂的表单提交流程,方便反复验证数据的提交逻辑是否正确。这个场景如上,相信你也感受它的便利性了。


2. 💎 性能优化


在模拟用户真实操作的同时,快速捕捉性能瓶颈,定位问题并优化。


点击性能面板按钮,等待自动回填数据,然后跳到性能面板,为了压缩我把很多帧去掉了


preform.gif


最终你可以在如下的性能面板开始分析了


353.png


3. 💎 自动化测试开发


如果需要将录制的流程用于自动化测试,可以点击 导出 按钮,将其导出为 Puppeteer 脚本或者 json数据,这样可以减少编写测试脚本的时间,通过导出的 Puppeteer 脚本直接复用操作流程。我不是测试人员就不多赘述了。


🚀 小结


“记录器”功能的出现,不仅让开发和调试更加高效,还为性能监控和测试自动化提供了重要支持。它减少了重复操作的浪费,让开发者和测试人员都能将更多精力集中在核心工作上。


是不是觉得这个功能非常有趣又实用?赶紧试试看吧!


如果有用记得关注我的公众号:萌萌哒草头将军


作者:萌萌哒草头将军
来源:juejin.cn/post/7447456628284244005
收起阅读 »

4点起床不一定行,但早睡早起很可以

最近快速读完了《4点起床》这本书。 这本书很简单,主要内容就是为什么要4点起床、如何做到4点起床和起床后要做什么。 整体上给它打 3 颗星,可读可不读那种。 而这本书是我买其他书时,凑单顺手买的,目前已经挂在多抓鱼上卖掉了。 那为什么还是给大家分享呢?主要是...
继续阅读 »

截屏2024-10-10 17.28.12.png


最近快速读完了《4点起床》这本书。


这本书很简单,主要内容就是为什么要4点起床、如何做到4点起床和起床后要做什么。


截屏2024-10-10 16.36.30.png


整体上给它打 3 颗星,可读可不读那种。


而这本书是我买其他书时,凑单顺手买的,目前已经挂在多抓鱼上卖掉了。


那为什么还是给大家分享呢?主要是它提供了一种生活方式。


几年前听说过日本留行过一阵“朝忙族”,指的是那些,选择早上早起工作,而不是晚上加班的人群。查看这本书的出版时间,正好是那个时候,可以确定这本书与这阵风潮有一定关系。


如今这股潮流已经过去,但这也说明了一些问题。毕竟对于大多数人来说,凌晨四点起床确实非常艰难。


不过,书中提倡的早睡早起理念,我还是很推荐的。在这里分享几点对我触动较大的内容。


为什么选择早起干活,而不是晚上加班干活


很多上班族有一个习惯:晚上加班才能完成任务。然而,作者更提倡早起干活,因为这是由人类的生理规律决定的。


事实上,研究表明大脑在下班后会逐渐进入休息状态,而早起则能够充分利用身体中荷尔蒙的激活,从而完成这些事情更轻松。


写作也是如此。虽然一些创作者可能认为灵感是他们工作的关键,但许多欧美作家已经开始提倡将写作日常化,使用具体方法来进行写作,而不是仅仅依靠灵感。中国也有不少作家开始模仿这一做法,例如已完成多部畅销小说的马伯庸先生,他每年完成几部小说,其著作量远超同行。


我个人观点认为,晚上工作的人往往需要一个没人打扰的环境,而早上同样可以实现这一目标。


我曾经问过一些早睡早起的朋友,他们都说有过一种平静的喜悦,这种感觉很难用言语描述,也许是来自于长期规律生活的多巴胺。我个人也是如此,在一段早睡早起的时间后,感到一种特殊的满足感。如果你也想尝试这种感觉,我建议你至少持续一个月(甚至更久)的早睡早起。


早起之后做什么?


早起之后可以进行哪些活动?


享用一顿营养均衡的早餐是必不可少的,这有助于确保一整天的精神饱满。如果你是有家庭的人,与家人共度一段早餐时光,还能够增进家庭成员之间的情感联系。


书中还提到很多建议都围绕着输入和输出的。例如,多读书、多写作等。虽然直接开始大量输出对许多人来说可能有些困难,但其实每天记录一些日志会更加可行。


这里所说的“日志”并非传统的个人日记,而是指一种记录日常活动的笔记形式。


通过撰写日志,您可以总结前一天所做的事情,并为当天制定详细的计划清单。明确哪些任务是重要的,哪些是非做不可的。这样做有助于您更好地管理自己的时间与工作安排,确保每一天都有条不紊地进行,有利于你告别无意义感和瞎忙的状态。


此外,这样的习惯还有助于推进那些重要但并非紧急的任务,避免它们被忽视或延误。


早起后,是否应该获取信息


早起后,是否应该获取信息?


自人类进入现代文明以来,报纸、收音机、电视和互联网的出现使得人们进入了信息过载的时代。如果我们仍然像以前一样被动地接收信息,最终只会接收到大量无用的信息。


书中提出的一个观点我认为颇有参考价值,即批判性阅读新闻。例如,在阅读股市财经类新闻时,应当质疑股价与该新闻的相关性。


当然,这种做法对个人的要求似乎较高,需要每次都能动用自己的大脑进行思考。


我个人的做法更为彻底,我删除了所有的新闻应用程序,甚至包括抖音在内的所有字节系应用。这些应用的推送机制非常奇怪,故意模糊了时间点,导致你看到的可能是几年前的旧闻。后来,许多应用都采用了类似的推送方式。


因此,我干脆屏蔽了所有的新闻客户端。


有人可能会问,那你的新闻资讯从哪里获取呢?实际上,在日常生活中,自然会有同事,在茶歇或饭间,讨论重要的新闻事件,所以根本不用担心自己会错过重要信息。


熬夜怎么办?


程序员熬夜.jpeg


书中关于应对熬夜的方法,我个人觉得非常有参考价值。


我们都知道,作为程序员或白领,偶尔熬夜在所难免,而一次熬夜可能会导致接下来两到三天内精神状态不佳,感觉昏昏沉沉。


书中提供了一个很好的方法:如果知道自己要熬夜,可以先睡3个小时,然后再起来继续熬夜。这种方法能在一定程度上缓解因熬夜带来的负面影响。


这本书相关的内容就讲这么多。剩下的大部分都是讲职场的作者个人分享,不适合我,我感受也不深。所以就分享到这里。


谢谢大家的观看。


作者:陈佬昔的编程人生
来源:juejin.cn/post/7423944621720354842
收起阅读 »

程序员就得会偷懒,重写了一个electron小工具,解放美女运营老师!

web
前言 接前一篇美女运营天天找我改配置,给她写了个脚本,终于安静了 之前只是写了一个脚本,本地运行,通过读取文件流获取文件数据,格式化对应数据,运营老师也不会安装node,还是需要我去操作。现在我用electron生成一个桌面应用直接生成后复制json,去配置,...
继续阅读 »

前言


接前一篇美女运营天天找我改配置,给她写了个脚本,终于安静了


之前只是写了一个脚本,本地运行,通过读取文件流获取文件数据,格式化对应数据,运营老师也不会安装node,还是需要我去操作。现在我用electron生成一个桌面应用直接生成后复制json,去配置,全程不需要我参与了。


之前的脚本


const fs = require('fs')
const csv = require('csv-parser');

const csvfilePath = './xxx.csv';
const uidsfilePath = './uids.json';

const results = [];
let newarr = [];
let lineCount = 0;

fs.createReadStream(csvfilePath)
.pipe(csv({ headers: true }))
.on('data', (data) => {
results.push(data);
lineCount++;
})
.on('end',async () => {
console.log(results[0])
await format(results);
fs.writeFile(uidsfilePath, JSON.stringify(newarr), () => {
console.log('done')
})
});
const format = (results) => {
newarr = results.map(item => {
if(item._0 === 'key' || item._1 === 'value') {
return {}
}
return {
label: `${item._1}-${item._0}`,
value: item._1
}
})
}

electron


简介


Electron 是一个用于使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用程序的框架。它由 GitHub 开发,基于 Chromium 和 Node.js。这意味着开发者可以利用他们熟悉的 Web 开发技术来创建桌面应用。


优势



  • 跨平台开发

  • 快速开发迭代

  • 丰富的生态系统


架构与核心概念



  • 主进程和渲染进程:


主进程:主进程是整个 Electron 应用的核心,它负责创建和管理应用程序的窗口。主进程通过BrowserWindow模块来创建浏览器窗口,这个窗口就是用户看到的应用界面的载体。


渲染进程:渲染进程主要负责渲染应用的用户界面。每个BrowserWindow都有自己独立的渲染进程,它使用 Chromium 浏览器内核来解析 HTML 和 CSS 文件,执行 JavaScript 代码。



  • 进程间通信(IPC):
    由于 Electron 应用有主进程和渲染进程之分,进程间通信就显得尤为重要。Electron 提供了ipcMain(用于主进程)和ipcRenderer(用于渲染进程)模块来实现进程间的消息传递。


使用vue3和vite创建vue的项目然后引入electron


安装vite


npm create vite@latest electron-desktop-tool

安装 引入electron&插件


npm install -D electron // electron
npm install -D electron-builder //用于打包可安装exe程序和绿色版免安装exe程序
npm install -D electron-devtools-installer // 调试
npm install -D vite-plugin-electron // vite构建插件

创建主进程


在vue 同级src目录下,创建src-electron 文件夹 新建main.js


// src-electron/main.js
const { app, BrowserWindow } = require('electron')
const { join } = require('path')

// 屏蔽安全警告
// ectron Security Warning (Insecure Content-Security-Policy)
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'

// 创建浏览器窗口时,调用这个函数。
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
title: 'electron-vite',
// icon: join(__dirname, '../public/logo.ico'),
})

// win.loadURL('http://localhost:3000')
// development模式
if(process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL)
// 开启调试台
win.webContents.openDevTools()
}else {
win.loadFile(join(__dirname, '../dist/index.html'))
}
}

// Electron 会在初始化后并准备
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

配置插件入口


在vite.config.ts中配置vite-plugin-electron 插件入口


import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
electron({
// 主进程入口文件
entry: './src-electron/main.js'
})
],
/*开发服务器选项*/
server: {
// 端口
port: 3000,
}
})

配置package.json


在package.json 新增入口文件 "main": "./src-electron/main.js",


原神启动 emmm electron启动


运行 npm run dev 启动项目


请在此添加图片描述


打包配置


首先配置一下打包的命令,在package.json "scripts"里面配置这个打包命令


  "scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"postinstall": "patch-package",
"electron:build": "vite build && electron-builder",
"pack32": "vite build && electron-builder --win --ia32",
"pack64": "vite build && electron-builder --win --x64"
},

同样package.json 需要添加打包配置


  "scripts": {
...
},
"build": {
"productName": "ElectronDeskTopTool",
"appId": "dyy.dongyuanwai",
"copyright": "dyy.dongyuanwai © 2024",
"compression": "maximum",
"asar": true,
"directories": {
"output": "release/"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"perMachine": true,
"deleteAppDataOnUninstall": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "ElectronDeskTopTool"
},
"win": {
"icon": "./public/logo.ico",
"artifactName": "${productName}-v${version}-${platform}-setup.${ext}",
"target": [
{
"target": "nsis"
}
]
},
"mac": {
"icon": "./public/logo.ico",
"artifactName": "${productName}-v${version}-${platform}-setup.${ext}"
},
"linux": {
"icon": "./public/logo.ico",
"artifactName": "${productName}-v${version}-${platform}-setup.${ext}"
}
},

然后npm run electron:build


请在此添加图片描述


页面效果


请在此添加图片描述


github地址


后续还会继续更新~


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

悲惨!刚入职没几天,无意间把数据库删了,很尴尬,原因很奇葩

1. offer收割机,就职新公司 5年前的就业环境非常好,当时面试了很多家公司,收到了很多 offer。最终我决定入职一家互联网教育公司,新的公司福利非常好,各种零食随便吃,据说还能正点下班,一切都超出我的期望,“可算让我找着神仙公司了”,我的心里一阵窃喜。...
继续阅读 »

1. offer收割机,就职新公司


5年前的就业环境非常好,当时面试了很多家公司,收到了很多 offer。最终我决定入职一家互联网教育公司,新的公司福利非常好,各种零食随便吃,据说还能正点下班,一切都超出我的期望,“可算让我找着神仙公司了”,我的心里一阵窃喜。


在熟悉环境之后,我趁着上厕所的时候,顺便去旁边的零食摊挑了点零食。接下来的一天里,我专注地配置开发环境、阅读新人文档,当然我也不忘兼顾手边的零食。


入职几天后 ,领导给安排了一个小需求,我和同事沟通完技术方案后,就开始开发了。


2. 单元测试有点奇怪


完成开发后,我决定写个单元测试验证下,在研究单元测试代码后,我发现这种单测写法和我之前的写法不太一样。


这家公司的单测好像没有启动整个项目,仅加载了部分类,而且不能访问测试环境数据库~ 于是我决定按照前东家写单测的方式重新写单元测试。


于是我新增了一个单测基类,在单测中启动整个SpringBoot,直接访问测试环境数据库。然而也并不是很顺利,启动阶段总是会遇到各种异常报错,需要一个一个排查…… 所幸项目排期不紧张,还有充足时间。


我做梦也没有想到,此刻,已经铸成大错。


3. 故障现场


我身边的工位旁慢慢地聚集了越来越多的人,本来我还在安安静静的调试单元测试,注意力不自觉的被吸引了过去。


“测试环境为什么这么多异常,访问不通啊。到处都是 500 报错”,不知道谁在说话。


“嗯,我们还在排查,稍等一下”,我旁边的同事一边认真排查日志,一边轻声回复道。


“为什么数据库报的异常是, 查不到数据呢?” ,同事在小声嘀咕,然后打开 命令行,立即登上 MySQL。


我亲眼看着他在操作,奇怪的是数据库表里的数据全部被删掉了,其他的几个表数据也都被删除了。


简直太奇怪了,此刻的我还处于吃瓜心态。


有一个瞬间我在考虑,是否和我执行的单元测试有关系? 但我很快就否决掉了这个想法,因为我只是在调试单元测试,我没有删数据库啊,单测里也不可能删库啊。 我还在笑话自己 胡思乱想……


很快 DBA 就抱着电脑过来,指着电脑说,你们看这些日志,确实有人把这些表删除了。


"有 IP 吗,定位下是谁删除的, 另外线上环境有问题吗?”,旁边的大组长过来和 DBA 说。


“嗯,我找到ip 了,我找运维看下,这个ip是谁的”。DBA 回复道。


4. 庭审现场


当 DBA 找到我的时候,我感到无辜和无助,我懵逼了,我寻思我啥也没干啊,我怎么可能删库呢。 (他们知道我刚入职,我现在怀疑:那一刻他们可能会怀疑 我是友商派过来的卧底、间谍,执行删库的秘密任务)


经过一系列的掰扯和分析,最终定位 确实是我新增的单元测试把数据库删了。


5. 故障原因


需要明确的是,原单元测试执行时不会删除数据库;测试环境启动时也不会删除数据库。


只要在单元测试中连接测试数据库,就会删除掉数据库的所有数据。为什么呢?


5.1 为什么单元测试删除了所有数据?


原单元测试 使用的是 H2 内存数据库,即Java 开发的嵌入式(内存级别)数据库,它本身只是一个类库,也就是只有一个 jar 文件,可以直接嵌入到项目中。H2数据库又被称为内存数据库,因为它支持在内存中创建数据库和表。所以如果我们使用H2数据库的内存模式,那么我们创建的数据库和表都只是保存在内存中,一旦应用重启,那么内存中的数据库和表就不存在了。 所以非常适合用来做单元测试。


H2 数据库在启动阶段,需要执行用户指定的 SQL 脚本,脚本中一般包含表创建语句,用来构建需要使用的表。


但是我司的 SQL 脚本除了创建表语句,还包含了删除表语句。即在创建表之前先删除表。 为什么呢? 据他们说,是因为这个 SQL 脚本可能会重复执行,当重复执行时创建表语句 会报错。所以他们在创建表之前,先尝试删除表。这样确保 SQL 脚本可重复执行。( 其实可以用 Create if not exists )


故障的原因就是:测试数据库执行了这个删表再建表的 SQL 脚本,导致所有数据都被清除了。


5.2 为什么测试数据库会执行这条 SQL 脚本呢?


1) 我新建的单元测试把H2 内存数据库换成了测试数据库。


2) spring.data.initialize=默认值为 true; 默认情况下,会自动执行 sql 脚本。


所以测试数据库 执行了 SQL 脚本。


5.3 为什么在测试环境正常启动时,没有问题,不会删除所有数据呢?


只有单测引入测试数据库才会出问题,在测试环境正常启动项目是没问题的。


当编译项目时,测试目录下的文件、代码和正式代码编译后的结果不会放到一起。因为 SQL脚本被放在了 测试目录下, 所以正式代码在测试环境启动时,不会执行到这个 SQL脚本,自然不会有问题。


image.png


6. 深刻教训


最终数据被修复了,DBA有测试数据库的备份,然而快照并非实时的,不可避免地还是丢失了一部分数据。


所幸的是出问题的是测试环境,并非线上环境。 否则,我会不会被起诉,也未可知。


后续的改进措施包括



  1. 收回了数据库账户的部分权限,只有管理账户才可以修改数据库表结构。代码中执行 DML语句的账户不允许执行 DDL 语句。

  2. DBA 盘点测试数据库的快照能力,确保快照间隔足够短,另外新增一个调研课题:删库后如何快速恢复,参照下其他公司的方案。

  3. 所有的项目 spring.data.initialize 全部声明为 false。不自动执行 SQL 脚本

  4. SQL脚本一律不许出现 删除表的语句。SQL不能重复执行的问题,想其他办法解决。

  5. 另外的一个项目急需人手,把新来的那谁 调到其他项目上


这可能是程序员们在技术上越来越保守的原因……不经意的一个调整可能引发无法承受的滔天巨浪


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

antd 对 ai 下手了!Vue 开发者表示羡慕!

web
前端开发者应该对 Ant Design 不陌生,特别是 React 开发者,antd 应该是组件库的标配了。 近年来随着 AI 的爆火,凡是想要接入 AI 的都想搞一套自己的 AI 交互界面。专注于 AI 场景组件库的开源项目倒不是很多见,近日 antd 宣布...
继续阅读 »


前端开发者应该对 Ant Design 不陌生,特别是 React 开发者,antd 应该是组件库的标配了。


近年来随着 AI 的爆火,凡是想要接入 AI 的都想搞一套自己的 AI 交互界面。专注于 AI 场景组件库的开源项目倒不是很多见,近日 antd 宣布推出 Ant Design X 1.0 🚀 ,这是一个基于 Ant Design 的全新 AGI 组件库,使用 React 构建 AI 驱动的用户交互变得更简单了,它可以无缝集成 AI 聊天组件和 API 服务,简化 AI 界面的开发流程。


该项目已在 Github 开源,拥有 1.6K Star!



看了网友的评论,看来大家还是需要的!当前的 Ant Design X 只支持 React 项目,看来 Vue 开发者要羡慕了...



ant-design-x 特性



  • 🌈 源自企业级 AI 产品的最佳实践:基于 RICH 交互范式,提供卓越的 AI 交互体验

  • 🧩 灵活多样的原子组件:覆盖绝大部分 AI 对话场景,助力快速构建个性化 AI 交互页面

  • ⚡ 开箱即用的模型对接能力:轻松对接符合 OpenAI 标准的模型推理服务

  • 🔄 高效管理对话数据流:提供好用的数据流管理功能,让开发更高效

  • 📦 丰富的样板间支持:提供多种模板,快速启动 LUI 应用开发

  • 🛡 TypeScript 全覆盖:采用 TypeScript 开发,提供完整类型支持,提升开发体验与可靠性

  • 🎨 深度主题定制能力:支持细粒度的样式调整,满足各种场景的个性化需求


支持组件


以下圈中的部分为 ant-design-x 支持的组件。可以看到主要都是基于 AI Chat 场景的组件设计。现在你可以基于这些组件自由组装搭建一个自己的 AI 界面。



ant-design-x 也提供了一个完整 AI Chat 的 Demo 演示,可以查看 Demo 的代码并直接使用。



更多组件详细内容可参考 组件文档


使用


以下命令安装 @ant-design/x 依赖。


注意,ant-design-x 是基于 Ant Design,因此还需要安装依赖 antd


yarn add antd @ant-design/x

import React from 'react';
import {
// 消息气泡
Bubble,
// 发送框
Sender,
} from '@ant-design/x';

const messages = [
{
content: 'Hello, Ant Design X!',
role: 'user',
},
];
const App = () => (
<div>
<Bubble.List items={messages} />
<Sender />
</div>

);

export default App;

Ant Design X 前生 ProChat


不知道有没有小伙伴们使用过 ProChat,这个库后面的维护可能会有些不确定性,其维护者表示 “24 年下半年后就没有更多精力来维护这个项目了,Github 上的 Issue 存留了很多,这边只能尽量把一些恶性 Bug 修复



如上所示,也回答了其和 Ant Design X 的关系:ProChat 是 x 的前生,新用户请直接使用 x,老用户也请尽快迁移到 x


感兴趣的朋友们可以去试试哦!


作者:五月君
来源:juejin.cn/post/7444878635717443595
收起阅读 »

30岁,我开启了人生副业

我有工作,我还在写代码,大家别再重复问了😭😭😭 大家好,我是朽木白,一名非常普通的前端程序员,在前端这条路上淌七年的浑水,毫无建树。在互联网泡沫的末法时代,不得已换了一份工作,自降6k去了一家教育公司,由于个人原因,在今年的三月份我决定辞职。当时辞职后的第一...
继续阅读 »

我有工作,我还在写代码,大家别再重复问了😭😭😭



大家好,我是朽木白,一名非常普通的前端程序员,在前端这条路上淌七年的浑水,毫无建树。在互联网泡沫的末法时代,不得已换了一份工作,自降6k去了一家教育公司,由于个人原因,在今年的三月份我决定辞职。当时辞职后的第一想法是再换个公司继续干前端,正好是金三银四,可能行情好一点。很显然,现实给了我狠狠的一个大逼兜。在我辞职之前,我粉丝群的500个道友们都劝我苟住,不要任性,我没听劝,非要看看怎么个事。准备好了面试题,写好了简历,开始投递,结果一投一个不吱声。要么是未读,要么是已读不回,要么是不合适(岗位是资深、专家、架构师),唯一回复的都是外包,薪资只有十几k,心高气傲的我也是一口回绝。找了半个月,心气彻底凉了。这时候我想着打工已经不是很好的选择了,心里落差很大,于是决定创业。


在之前,我也在创业的路上进行过尝试。主要是面试辅导。在职的时候我就给粉丝修改简历,辅导面试,因此也收获了很多粉丝,大家给面子,都亲切的叫一声白哥。至于现在为什么放弃了呢?当时白天上班,抽空给粉丝看看简历,晚上回家腾讯会议一对一的指导如何修改简历,并且一直都是免费的。后面我又在某音开直播,在直播间连线粉丝,有任何问题都可以问的那种。还有一个收费的就是面试辅导,针对每个人不同的情况进行辅导,给他们指导项目,修改简历,每天晚上讲解面试题,划重点,面试复盘等等。如果不在公司加班(加班一般都是11点才能下班,到家都12点多了),晚上回家的任务就是看简历,辅导学员。因为当时全部都是我自己一个人干,没有任何团队,也没有和任何人合作过,也没有接过任何第三方广告。干了一个月,我遭不住了,身心疲惫。除了收获了一批忠实的粉丝,钱基本没有挣到多少。这时候我已经陷入了一种困境和迷茫的状态。我到底要不要继续坚持下去,我的这种付出到底能给我带来啥,更多的是给我带来的一些焦虑和内耗,尤其是一些粉丝找工作的现状,惨不忍睹,我看着也跟着心急。后来我就索性不干了,在群里活跃气氛,跟大家扯扯皮,聊聊八卦(我摆烂了),创业宣告失败。


接下来就是第二次创业了,在失业的这两月当中,我合伙跟别人开了一家眼镜店,目前已经正常运营。因为我媳妇是内行,所以开起来非常的顺利。我的身份也从一名前端程序员转变成了一名眼镜从业人员。跨度非常大,因为此前我从未接触过这个行业。简单讲讲开店的过程吧。涉及到金额方面的我就不提了。


第一选开店的地址,在北京的这四九城里转悠了一周多,看了好多地方,地段有好有坏,房租有高有低。我选择店铺的依据根据这几个方面来,交通方便,周围上班族、学校多,人流大,周围2KM没有同行,租金一万左右的底商为主。最终我选择了靠近霍营地铁站的一个商铺,距离不过500m。


image.png


第二店铺装修,找熟人,省了不少钱。装修也是要考虑很多,眼镜柜的各种尺寸要测量标准,因为上面要加装玻璃柜,严丝合缝,不能有偏差。还有柜台的设计,各个机器的摆放要合理的腾出位置,因为店铺的面积不是很大,空间要合理利用起来。还有其他的一些柜子,眼镜的摆放等等都是有学问的,没有经验的人来根本干不了一点。
这是装修完完刚收拾的样子


image.png
这是柜台


image.png


第三进货,货主要有三种,镜片,镜框,仪器。这些都是在潘家园眼镜城那边有厂家可以拿货。因为我媳妇之前是开店的,她有认识的人脉资源,进货的时候都是她带着去挑。进货花钱是大头,尤其是那些机器,更先进了,也更贵了。机器会有专门的安装人员上门安装调试。眼镜摆好了,机器调试好了,差不多就可以开业,我的店是05.17正式开业的,在没有开业之前就已经有人来配眼镜了,所以我开业提前了几天。
开业了


image.png


现在开了差不多半个多月了,差不多每天都能开张,时不时也能来个大单,虽然没有我上班每个月挣的多了,但是没有那么累了,因为眼镜店一天人流量没那么大,一天能进店五六个人就够了,所以我有很多时间干自己想干的事情,比如拍拍短视频,写写技术文章,学学自己之前没有来得及学的技术,我依然热爱代码。当然开眼镜店最重要的是为顾客配一副戴着舒适的眼镜,人家才会信任你,帮你带新,回购,因为眼镜这个东西很多人一旦配了一次很舒服的,下次就认准你了,别的店他不信任。


目前生活状态:已经入职新公司,还是继续写代码,不加班,不打卡,没有倒排期,没有PUA,按时发工资。店铺一切经营正常。希望我的这个店能一直开下去,红红火火。


最近好多不在北京的粉丝反馈,让我做个小程序,可以直接查看店里的眼镜,他们可以挑选心仪的镜框,方便线上配镜。目前小程序正在开发中,大家敬请期待!


作者:白哥学前端
来源:juejin.cn/post/7377001684159774760
收起阅读 »

什么黑科技?纯血鸿蒙又可以运行Android应用了!

背景 纯血鸿蒙OS Next系统最近出现了两款热门应用:出境易、卓易通,其功能是:让你在出境后可以方便安装到各种Android应用。 发生了什么? 「出境易」这款应用可以在纯血鸿蒙OS NEXT里直接安装&运行Android应用! 纯血鸿蒙官方声...
继续阅读 »


背景


纯血鸿蒙OS Next系统最近出现了两款热门应用:出境易、卓易通,其功能是:让你在出境后可以方便安装到各种Android应用




发生了什么?


「出境易」这款应用可以在纯血鸿蒙OS NEXT里直接安装&运行Android应用!



纯血鸿蒙官方声称:「不支持运行Android应用」!



于是:



  • 很多正在努力开发、兼容纯血鸿蒙应用的开发者都在议论:是不是可以停鸿蒙开发、继续写回Android了?

  • 也有很多粉丝后台私信Carson,问:目前继续学鸿蒙开发到底还有没意义






本文意图


今天Carson来带大家扒扒「出境易」、「卓易通」到底是怎么能在纯血鸿蒙OS NEXT里运行Android应用的。具体包括:「出境易」、「卓易通」这两款Android应用



  1. 在纯血鸿蒙系统里运行的底层支持是什么?

  2. 在纯血鸿蒙系统里的运行环境是什么?

  3. 「出境易」、「卓易通」本质是什么?

  4. 在「出境易」、「卓易通」上的Android应用性能、体验如何?




问题1:在纯血鸿蒙系统里运行的底层支持是什么?



  • 无论黑科技有多 “黑”,总需要底层给与相关支持才有运行的可能。

  • 在初次安装「出境易」、「卓易通」时需下载一个环境,抓包&解包可得到:



其中最为关键的文件:anco_hmos.img,从字面解释来看:



  • anco:AndroidCompatible = 安卓兼容

  • hmos:HarmonyOS = 鸿蒙OS系统

  • 整体看,即**「鸿蒙OS系统里的安卓兼容」**


实际上,这其实是一个安卓镜像文件,是一个嵌入到鸿蒙OS系统层面的安卓运行环境(类似虚拟机的作用,但实际不是虚拟机)。



其实是类似wsl技术,即Windows Subsystem for Linux = Windows的Linux子系统,能让开发者在Windows操作系统中直接运行Linux环境,而无需任何虚拟机。



所以,要在纯血鸿蒙OS 上安装「出境易」、「卓易通」不仅需要下载很大的安卓镜像资源包,还需要重启系统,因为「出境易」和 「卓易通」是单独的“运行环境”。


值得一提的是:



  • 因为本身鸿蒙OS内核就兼容了Linux ABI(应用程序二进制接口),即鸿蒙OS内核本身就可以运行为Linux设计的应用。

  • 所以,虽然这是一个安卓镜像,但这个属于鸿蒙的安卓镜像并没有包含Linux 内核,只是包含运行时(Runtime)部分。 以下是鸿蒙内核架构图:
    鸿蒙内核示意图




问题2:在纯血鸿蒙系统里的运行环境是什么?


那么,这类Android应用到底是运行在什么环境上的呢?打开「出境易」内的app后,通过执行shell ps -ef会出现以下进程:



  • 即其运行环境是:通过lxc-start命令启动了一个基于iSulad的容器的进程。

  • iSulad 是华为自研的容器引擎,是一个非常通用的容器引擎,具有轻、快、 易、灵的特点。以下是其架构图:



iSulad官网介绍:http://www.openeuler.org/zh/other/pr…






问题3:「出境易」、「卓易通」本质是什么?



二者的功能都是:让你在出境后可以方便安装到各种app。听起来是不是有点类似国内的应用商店



  • 实际上,二者在鸿蒙next商店下载的是一层壳,负责与纯血鸿蒙OS进行权限交互(图片、文件IO等)

  • 本体也是Android应用的apk,即出境易.apk、卓易通.apk。拿出来也是可以在Android手机上安装的。(如下图)


除此以外,「出境易」还含有一个「文件共享.apk」、「卓易通」还有一个「搜应用.apk」、「文件共享.apk」。


这里值得一提的是,两个“应用商店”可搜到的应用原理不同:



  • 出境易:白名单方式,即只有与其合作的Android应用可以安装;

  • 卓易通:黑名单方式,即只有纯血鸿蒙OS上架的应用不可以安装;


下面附上视频:纯血鸿蒙OS 「出境易」、「卓易通」安装Android应用实机演示



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





性能如何?


既然能跑了,那么用户体验如何呢?网友们已经开始跑分了:



  • 测试环境:麒麟9000s;

  • 结论:单核心正常跑分1000,目前「出境易」是930分左右,效率是93%;




  • 分析:上面提到其底层支持是类似wsl的技术,同时运行环境是采用华为自研的iSulad 容器引擎的方式,并非所谓的虚拟机环境。这种嵌入方式可以使得安卓应用能够在鸿蒙系统上运行,但又不会占用过多的资源或影响系统的稳定性

  • 结合业界常见容器水平93%左右,华为的iSulad容器达到了业界水平,可理解为:GPU性能几乎无损。


但是对于内存使用就不太友好了,容器本身内存占用极大,基本一个容器进程就是8GB,随便开两个应用12GB就没了。

同时结合网上使用的评价:手机容易发烫(功耗高)、应用Bug较多等等,可以总结为:以这种方式在纯血鸿蒙OS上运行的Android应用 「能用」,但是「不好用」,与原生体验还是存在很大差距




结论



  • 技术角度 分析:基于anco_hmos,采用类似wsl的方式同时结合iSulad容器引擎,使得在纯血鸿蒙OS上运行Android应用成为了板上钉钉的现实

  • 性能角度 实践:在CPU性能可认为几乎无损的情况下,内存跟功耗问题短时间内还是无法解决;

  • 用户体验 观察:应用Bug较多,结合性能内存问题,目前暂时仅处于一个**「能用」**的状态。


基于上述分析 & 问题表现,在纯血鸿蒙OS上运行Android应用在国内大范围使用短时间内几乎不可能,更多的是在一些小众、边缘、尝试探索的场景,比如一些使用频率较低的小众app、尝试出海境外的场景(如本文提到的「出境易」等)


最后


如何看待这次在纯血鸿蒙OS上运行Android应用的事件呢?评论区留言你的看法!


参考文章:



作者:Carson带你学Android
来源:juejin.cn/post/7448576110823047202
收起阅读 »

我的 2024 年终总结

2024 年,我离开了待了两年的互联网公司,来到了一家聚焦教育机器人和激光切割机的公司,没错,是一家硬件公司,从未接触过的领域,但这还不是我今年最重要的里程碑事件 5 月份的时候,正式提出了离职,没有骑驴找马,完全裸辞。对于 gap 的这段时间,做了简单的规划...
继续阅读 »

2024 年,我离开了待了两年的互联网公司,来到了一家聚焦教育机器人和激光切割机的公司,没错,是一家硬件公司,从未接触过的领域,但这还不是我今年最重要的里程碑事件


5 月份的时候,正式提出了离职,没有骑驴找马,完全裸辞。对于 gap 的这段时间,做了简单的规划,先去旅游一趟,然后用一个半个月时间备考雅思,九月、十月重新找工作


随后的两个月,公司找到接替我的新同学,站好最好一班岗,跟同事做了告别。7 月 5 号,拿着离职证明,收拾好东西,离开了西丽。隔天就参加 VueConf 大会,见到了尤雨溪和 Anthony Fu




做了这么多年精神股东,终于来支持一回,希望 React 越办越好 👏



VueConf 结束之后,就去流放岭南,在广西桂林,体会到了什么叫做“江作青罗带,山如碧玉簪”



这里的山水真的很美,养得蚊子珠圆玉润,也感受到喀斯特地貌对于经济发展的阻碍,拔地而起的峭壁山峰,割裂了交通,住房和经济


在去到九马画山的偏方小路上,看到一路上种植着柚子,玉米等等,这是属于亚热带季风气候的回馈,光照充足,雨水充沛,水果种植,也成了当地人除旅游业之外的另一条生计


这一次是 p 人之旅,没有特种兵定点打卡,而是更多感受地理,人文。嗦了三天的桂林米粉之后,又回到了深圳,开启了雅思备考


关于考雅思,一个是本身对英语感兴趣,另一个是看过许多老前辈的经验之谈,学好英语对于程序员职业发展来说,是长期利好的事情


在这段时间,还学会做饭,个人以为,做饭和编程一样,都是属于创造的艺术


心血来潮还买了一个小米空气炸锅,第一次用觉得超级神奇的,仅仅依靠空气就能够把鸡米花烤得 tree tree 的



接下来的一个半月的时间,把自己当成高考生,按部就班地学习



8 月 30 号早上踏入考场,上午听力阅读写作,下午口语。成绩出得很快,三天后如期而至,得到了一个非常低的分数,这给了我当头一棒


付出了大量的时间精力,并不意味着有美好的收获,我花了三天时间做了痛苦而又深刻的复盘,是能力基础问题?是自律问题?是备考策略问题?是心态问题?最后结论是出来的是备考策略出现重大失误,罔顾实际情况,按照自己天真烂漫的想法去复习



复盘这几天我似乎有点“龙场悟道”,这个“道”后来我把它总结了「成长型思维」,关于成长型思维具象表现是,对于雅思考试的折戟失败,我没有消沉,在这段经历中,让我学会了第一件事情是——做人、做事需要符合物理、客观的规律,也就是雷军所说的「顺势而为


无业 gap 的时期,并不美好,每天处于没有收入、消耗积蓄的焦虑,所以到了 9 月份,我需要重新开始找工作,自媒体和独立开发被我 pass 掉了,这两者现下并不能带来稳定的收入


又回到了起点,这一次我反而更加自信,充分吸取教训,我需要做好正确的求职策略,搜集各路面试资料,结合自身的实际情况,并且在 AI 的辅助下,写了一份《前端求职大攻略》



把自己求职涉及到的方方面都罗列出来,同时使用 PDCJ 模型,P = Plan(计划),D = Do (执行),C = Check(检查),J = Just(调整),也就是阶段性地计划、施行,再不断地检查、调整,确保自己的面试正确而又高效地进行


就业环境并不会因为你认真做好求职攻略、努力复习就给到丰厚机会,相反,每天投递的数十份简历都石沉大海,一个星期可能都约不到面试机会,这大概就是铜九铁十了吧


求职遇冷,那就好好抓住每一个来之不易面试机会,比如大学舍友的朋友内推了的转转前端岗位,面试前两天,刷完了转转的前端面经,一遍一遍地背前端面试题宝典的八股文,甚至于上厕所,也会拿着小程序刷题



但是,转转还是把我挂了,二面都没进


沮丧是没有用的,迅速复盘转转的整场面试,重听了当时的录音,把自己回答不上的问题找补,犯错的点做具体的纠正,为下一次面试做好准备


不久,意外接到了童心制物 HR 的简历邀请,是一个 Nodejs 的岗位,我拒绝了,如实说目前仅接受前端岗位,HR 却跟我要了简历转发给他们前端部门,没想到还通过了简历筛选,得到了一面的机会


一面发挥的很好,其中问到了一个难题,“如果不用 eval,还有其他方式去执行 js 函数吗”,刚好之前研究过 laf 的源码,知道可以用 vm 模块可以实现。之后又是二面和 HR 面,都顺利通过了,拿到了最终的 offer


童心制物涉及教育方向,这也是我很看重的一个点,大学的时候参加过支教,一直对教育这一块感兴趣,另外,这家公司给的薪资和福利都比较合理,所以……我入职啦



求职告一段落,顺便写了一篇博客作为输出,感兴趣可以查看——《前端开发实用的面试备考分享(内含资料 + 内推)》


这一段求职经历,总体上算是成功的,找到了理想的公司,理想的职位,但人不可能躺在功劳簿上过一辈子,新阶段会有新挑战,需要重新改变,重新努力


入职之后,体验了公司的激光设备,感受到了巨大的创造力:



用 F1 Ultra 雕刻的金属卡片,用 P2S 切割+雕刻的木板:



用 M1 Ultra 和热压机做的工服:



加入了心心念念的篮球社,开启了每周打篮球的固定活动,没想到老板也是篮球迷,现在每周都会一起打,看着他能明显感受身上的年轻、务实,身边的同事也是对他好评,我相信这些称赞不是人情世故


公司没有设立职称体系,转而实行扁平化管理,此举弱化了上下级概念。此外,公司还推行 AMA(Ask Me Anything,即问我任何事)活动,所有员工均可匿名提出任意问题,老板会逐一进行解答。可以看到,公司正在着力打造更为平等、透明、高效的职场环境,这也促使我对自身与企业之间的关系展开深入思考。


思考的结果为:作为员工,其行为与目标应当与公司的发展相契合。基于这一思考,我察觉到自己过往存在一些不良习惯,有时为了维护同事关系,或是顾及上下级的地位差异,对问题视而不见,对待工作打折妥协。


知行合一,慢慢将思考转化为行动,比如与同事协作后,即时给予反馈,对于公司内部平台出现的 bug,立即同步给相应负责人,尽量做到事事有反馈、发挥 owner 精神等等


童心制物很开放,有很多的活动可以参加,比如 Factory Day 工厂日,在周四那天乘着大巴,去到惠州工厂,参观了整条产线,看到了机器从零到一的生产过程



还有 MakeX 机器人挑战赛,这是一个具有重要影响力的国际化机器人赛事和教育平台,今年是第七年了,MakeX 总决赛又回到了深圳举办,立即报名了技术支持,参与了一天的活动现场



场馆设有各个国家的文化摊位,期间还被俄罗斯的大朋友投喂了巧克力零食



看大朋友、小朋友们激烈比赛:



以及让我难过了一天的创客马拉松,这是公司的特色活动,参与的最新的一期比赛,在决赛路演输了,作为队长很自责,忙碌了一周并没有好的结果,促使我进行反思,是带队伍问题?是项目推进问题?是质量问题?还是路演问题?




失败或许不是一件坏事,让我刻骨铭心,反而推着我进步



回头来看,今年最大的里程碑节点是备考雅思失败后的悟“道”,也就是成长性思维,这种认知层面的巨大改变,进而影响了我的工作、生活


我似乎不再害怕去面对我从未遇到的难题,我知道肯定方法、有途径能够去解决它,即使事与愿违,这过程中我也能收获到经验、智慧和勇气


我不再陷入完美主义,先做一个垃圾出来,再慢慢去迭代、优化,不要幻想一下子就能得到 100 分,但先拿到 60 分,再一点点进步,日拱一卒,等待那个增长拐点


我也变得更加正向,“世上只有一种英雄主义,就是在认清生活真相之后依然热爱生活”,坚持开放、乐观的心态,拒绝被悲观、消极所同化


或许我的所谓悟“道”、成长型思维,这些认知只是浅薄的,缺乏更多实践和经历,但是没有关系,做时间的朋友,随着年岁和阅历的增长,这套成长型思维也会随之”成长“


碎碎念的年终总结到此为止,写完之后还是很感慨,今年发生了很多事情,但在当时,只觉得是一个风平浪静的日子,或许此时此刻,也觉得是风平浪静的深夜。弱智吧有一句话,“有人看不到未来,其实是看到了未来”,初看的时候不理解,后来明白了,未来是动态的、不确定的,这就是未来的真面目,以为的看不到,其实恰恰是看到了,所以 2025,未来见


作者:楷鹏Dev
来源:juejin.cn/post/7451924452538548260
收起阅读 »

2024年,30岁前最后一次年度思考

没错!95年,还剩几个月就奔三了。2024年,注定是人生中意义非凡的一年,忐忑、裁员、出书、求职、转正这几个词贯穿了一整年。 忐忑 在上一家公司时,我从面试开始和到入职半年转正后,其实内心对于公司的状况一直保持一种忐忑不安的心情,这种感觉跟我老婆说过几次,我们...
继续阅读 »

没错!95年,还剩几个月就奔三了。2024年,注定是人生中意义非凡的一年,忐忑、裁员、出书、求职、转正这几个词贯穿了一整年。


忐忑


在上一家公司时,我从面试开始和到入职半年转正后,其实内心对于公司的状况一直保持一种忐忑不安的心情,这种感觉跟我老婆说过几次,我们一致认为应当有心理准备。原因在于薪资与公司的组织架构、基础建设、日常工作量安排和人员扩充速度都让人感到迷惑。


公司是在一个包括高层话事人不断更换,高层(副总裁)突然接受停止调查;技术部门仅仅作为辅助,技术氛围低沉,基建缺失,直属leader作用甚微;工作量与人员匹配失常,人多活少,尽管如此年初还在不断扩招中,泡沫感极强,伴随着薪酬发放日漂浮不定,每到月底像是在开盲盒,你永远不知道银彳亍卡何时会有一笔款到账。


裁员


一系列薪酬制度改革和薪酬拖欠不得不怀疑高层战略的正确性,直到四月某一天CTO私聊我,泡沫破裂,裁员尘埃落定。


我被归属于第一批裁员名单中,与CTO交谈中,似乎也流露一丝对高层决策的不满,但没有明说,给我的理由是当前工作任务都很简单,匹配不了我的能力,所以给了我一个名额。


这放在当时听上去有些许意外,但我接受了这种措辞,并不是因为CTO说了几句好听的话,更多是我作为一个技术人的直觉认为这个CTO靠谱。离职过程中对人事提出的补偿计算方式以及分期发放,我都拒绝了,最后经过与人事反复讨论之后拿到了补偿,少不了他的协助,所以内心表示感谢。从现在的视角看来,似乎是他已经意料到公司的发展趋势,以致于后来被裁员的人有很大一部分都没有赔偿。


出书


离职后我在家休息了一个月,期间也为了帮一个粉丝忙,接手了他工作的一部分任务,主要是做游戏业务的动画。期间有被一个后端恶心到,业务不熟悉,接口一直不通就算了,关键还理直气壮说是前端问题;我佩服那个粉丝能够忍气吞声这么久,换做其他人也很难不高血压,为此特意发圈宣泄。
image.png


由于后端提供的接口迟迟不通,需求没有预期上线,为此他们老板还大发雷霆,最后把锅推给了这个前端粉丝,声称把他给炒了。没过一个月,粉丝的这个公司被帽子叔叔查封,业务涉及到了灰产,老板和负责人进去了。员工的工资都没发,但我的报酬是因为签了合约,在deadline之前要求他们打款,对我没有影响,这是苦了这个粉丝。


在此之后我便全职写书,《NestJS全栈开发解析:快速上手与实践》 这本书临近结尾,我一鼓作气完成了并在5.1号劳动节那天交稿;写书的想法也有一部分是来源于CTO的启发,后面图书审阅也是找了CTO帮忙,熬夜帮我看完并给了这个评语,为此我很感谢他。


经过几个月的审批和改稿,图书在9月份正式发布了各大平台,这是一件值得高兴的事情。


image.png
而对于前司的后续,据说后面还搬到一个CBD进行办公,但当时员工已经欠薪几个月,以至于到年底,公司被迫全员原地解散,很遗憾这不是一个好结果。


求职


交稿完成后,花了一个月左右时间求职,拿到了3个offer,最后选择了去深圳的美图,这是凭借NestJS的图书写作获得的一个岗位。之后由于组织架构变化,我在转正前夕面临选择继续从事Node全栈还是Go语言开发,考虑一番后我选择了后者,顺利转到了后端架构组,负责go语言开发,这对我来说又是一个新的尝试和挑战,我选择了这种变化,与框架和语言无关,只不过是践行我的人生哲学:【不断变化】,让自己处于一种长期乐观、短期痛苦、当下快乐的舒适区边缘中。


觉醒


关于成长,过去我一直不喜欢看历史,或许归根于上学时代对于历史学科的厌倦,没看过基本历史文献。2024年底,我看了教员的《毛选》、《实践论》、《矛盾论》、《寻乌调查》,第一种感受是成功绝不是偶然,环环相扣的逻辑能力令人惊叹。我想这些书籍回答了我一直以来的问题:



如何成为一个独立、深度思考的人?



我们人生中做了一个坏的决定,在股市中选择了不争气的股票,最坏的结果无非是让自己从头再来。但革命不同,选择错了就有可能让整个民族处于被毁灭的境地中,每一步都步履蹒跚,这该有怎样的智慧与思维?


第二种感受是遗憾没有早点开悟,在临近30岁时才开始阅读这些书籍,当然也很庆幸没有太晚,一切都来得及!


特别的是,《寻乌调查》报告里面的细节,应该是我人生中读过的一本最详细的一本书籍,里面还记载了寻乌与我老家(兴宁)相关的历史宜了,没有一句多余的,都是干货。第一次感受原来伟人离我这么近。


image.png


教员做了这个调查报告之后,便留下一句千古格言:没有调查,就没有发言权!反观自身,何尝不是应该这样呢?


关于家庭,今年整个过程中家里的大大小小的事基本上都是我老婆操办,为我们的小家默默付出了很多,加上我去了深圳之后,我的衣食住大部分也是她来打理,一个人照顾小孩,现在甜筒一岁半了,如我们所愿健康成长,这隶属她的功劳。


一个家庭要想变好,靠一个人努力不行,需要“拉拢”有能力的人一起,话事人脑子要清醒,能够明辨是非,唯唯诺诺绝对是会出问题的。


一个家族要想变好,靠一两个人不行,得靠一两个家庭真正向好,大家庭才会有希望。


最后,没有Flag,年度总结中对未来进行遐想没有意义,沉浸于自己完成所有Todo List的那种兴奋是虚构的,而实践中那种痛苦、无助才是我们最真实的感受,人不能总活在无限遐想的递归当中


我看过那些在新年Flag列举诸多愿望,买了一堆书籍想要读完的,来年能真正落地完成的少之又少,毕竟我亦如此。


2025年,爱自己,爱家人,步步为营,不负将来!祝所有支持我的粉丝朋友们,一切如意,事业感情双丰收~


作者:元兮
来源:juejin.cn/post/7455282891535302708
收起阅读 »

博弈论(一):身在大厂,平衡工作和家庭太难了

引言 什么是博弈论(Game Theory)?百度百科的介绍如下 博弈论,又称为对策论(Game Theory)、赛局理论等,既是现代数学的一个新分支,也是运筹学的一个重要学科。 博弈论主要研究公式化了的激励结构间的相互作用,是研究具有斗争或竞争性质现象的数...
继续阅读 »

引言


什么是博弈论(Game Theory)?百度百科的介绍如下



博弈论,又称为对策论(Game Theory)、赛局理论等,既是现代数学的一个新分支,也是运筹学的一个重要学科。


博弈论主要研究公式化了的激励结构间的相互作用,是研究具有斗争或竞争性质现象的数学理论和方法。博弈论考虑游戏中的个体的预测行为和实际行为,并研究它们的优化策略。生物学家使用博弈理论来理解和预测进化论的某些结果。


博弈论已经成为经济学的标准分析工具之一。在金融学、证券学、生物学、经济学、国际关系、计算机科学、政治学、军事战略和其他很多学科都有广泛的应用。



博弈论在生活中的影响无处不在,比如许多朋友会面临一种选择选择,两家公司给你发了offer,一家公司薪资高,职位高,但可能需要频繁加班,压力很大。另一家公司薪资比你现在低一些,但是可以保证你准时下班,让你有充足的业余时间。对于单身的人来说这个并不难选择,但是当你结婚生子,这个问题就会瞬间变得复杂。


还有一个现象,为什么当团队内有一个人下班晚,整个团队的下班平均时间就都会往后延。


学习博弈论,只为了一件事情:拥有识别博弈格局的眼光,拥有改变规则的意识。简单来说,先理解长期存在的现象,并能够去改变不好的局面。


看完这篇文章,你或许就会拥有一个全新的视角去看这个世界。


介绍博弈论,我们先从三个基本概念开始,分别是帕累托最优,囚徒困境和纳什均衡。


帕累托最优


每一个成家、有孩子的人,再找工作时都会考虑如何平衡工作、收入、家庭三者之间的关系,说白了就是钱多事少离家近。


你现在有一份不错的工作,有一份稳定的收入,到了下班时间,你回家陪伴家人,辅导孩子写作业,顺便还能做做家务。公司、家人对你很满意,有房有车,收入足以满足日常花销,甚至有点小积蓄。


其实外面有其他公司给你抛来了橄榄枝,有的公司承诺如果你过去,会给你升值加薪。当然也有别的机会,你工作可能更清闲,但你的收入也会下降。


你很难做出抉择,你不是一个个体,你还需要考虑别人的选择。因为你一旦选择更高收入,就意味着要做更多的工作,没法按时下班,家人会对你有意见。新工作压力可能很大,你的状态也会变差,你不但没法照顾家人,可能家人还要额外提供给你情绪价值,家人一定会不满意。


此时,你在工作、收入、家庭取得了平衡,你无法在不影响收入和家人满意度的情况下去换另一份工作,此时的局面就叫做“帕累托最优”。


我们都希望工作和家庭最理想的状态是帕累托最优的,可我们都知道,生活中似乎很难达到这个状态,帕累托最优这个状态很理想化,因为它是不稳定的。


博弈论要求我们需要考虑竞争对手怎么做,虽然我不想把工作称之为竞争对手,但是在这个三角关系中,这里的竞争对手就是你的工作。


工作中公司一定要求你尽职尽责的完成本质工作,你要能够承担一定的责任,最好你要有一点创新,能够给公司带来一定改变,带来额外的价值。


一直以来你自己负责一个项目,这时候你的工作由你自己安排,要做的事情有很多,但你有没有三头六臂,那你只能一件一件来。当天既然没法做完所有工作,那到点下班就好。


可你的项目越来越有前景,公司决定增加人手,公司招进来了另一位同事,我们叫他小齐。现在你俩需要一起负责这个项目,你们既是合作关系,又是竞争关系。


那工作还是这么多,却有两个人来干了。如果你按点下班,可同事还想“承担”,他多做一点,你就少做一点,时间长了对你可不是一个好消息,你不得不选择加班。


compressed_pexels-cottonbro-6944006.jpg


这也就解释了为什么有时候一个团队只要有一个人加班,大家普遍的下边时间都会变晚,也称之为内卷。


这时候你想了一个办法,你请同事吃了一顿饭,你说你还单身,你得丰富一下业余生活。我认识一个单身的女同事,听说你俩爱好挺像,我把她微信推给你,以后你们下班可以一起约着出去玩。


后来你们都不加班了,你们依然是平等竞争,而且你又可以按时回家陪娃了,这就叫“帕累托改进”。


压倒性策略


或许你听过囚徒困境的故事,囚徒困境的故事最适合解释这个概念,但你和小齐的故事还在继续。


(注:下面的例子纯属虚构,如有雷同,纯属巧合)


你和小齐接手了全新的项目,有大量的代码开发工作,你俩的技术水平相当。可之前你给他介绍的单身女同事,后来找了男朋友,可惜那个人不是他。他感觉被套路了,给你说在你没有给他介绍下一个对象之前,他暂时决定和你势不两立。


可工作还得继续,你们都可以选择写出高质量的代码,需要花费更多的精力,但很明显你的口碑会变好。也可以得过且过,写出质量一般的代码,工作量明显变少,但项目质量可能一般。


根据两人的选择,存在几种情况



  • 两人都贡献高质量代码,项目质量得到广泛认可和领导称赞,声誉增加了

  • 如果一人费心费力提供了高质量代码,而另一个人敷衍了事,代码不规范、质量差。那么项目依然会正常上线,写低质量代码的人可以说是被大佬带飞(+7),而高质量代码因为加班、熬夜,所获的收益变少了(+2)

  • 如果两人都敷衍了事,上线之后问题频发,虽然工作完成了,可产品运营会不停的和你领导吐槽,领导对你们的印象大打折扣(+3)


注意:我们这个例子不考虑这个工作对于后续年终奖或职业生涯发展如何。就只针对这个工作,我们来分析一下你和小齐的博弈策略。首先我们把不同的策略和结果画在下面这个矩阵图里。多说一句,这种画法是美国经济学家托马斯·谢林发明的。


小齐贡献高质量代码小齐贡献低质量代码
你贡献高质量代码你:+5,小齐+5你:+2,小齐+7
你贡献低质量代码你:+7,小齐+2你:+3,小齐+3

其实你明显看可以看出,最好的结果就是两个人都贡献高质量的代码,然后项目稳定,还能收获良好的口碑。


但是,博弈论要求我们每次做判断都要考虑对方-----并不是说怎么样对对方好,而是对方会如何选择,然后你根据对方的选择再考虑怎么做。


我们具体看看你的收益分析:



  • 如果小齐开源高质量代码:你选择开源高质量代码,收益为 +5。你选择写低质量代码,收益为 +7。 (低质量代码收益更高)

  • 如果小齐开源低质量代码: 你选择开源高质量代码,收益为 +2。你选择开源低质量代码,收益为 +3。 (低质量代码收益更高)


你发现,无论小齐怎么做,你选择写一些低质量的代码,你的收益都会更高!于是你来说,选择写份低质量的代码,就是一个压倒性策略


可反过来对小齐来说呢,这就是一个被压倒策略!但博弈论的前提是大家都是理性人,小齐也聪明的很,于是他也意识到选择写低质量代码,是一个压倒性策略。


对理性人来说,在博弈中一定要选择压倒性策略,任何情况下都不要选择被压倒性策略。


你俩同时这样选择,这就引出接下来的一个最重要的概念,纳什均衡。


纳什均衡


上面说到你俩同时放松了对自己的要求,写的代码缺乏设计,代码不健壮,上线之后问题频发,领导对你俩很不满。


这个结果可不是帕累托最优,但这个结果是稳定的,不管我们是否喜欢这个局面,但我们认可这个局面了。


线上一直报警,产品运营一直来找我们排查问题,这些都惹人心烦。我们或许都想把项目做好,但在这个组合里,没有一个人愿意单方面改变自己的策略。


因为无论对谁来说,此时选择去写高质量代码,都会让自己的利益受损,谁不想更轻松一点呢?


这种没有任何一方愿意单方面改变策略的局面,就是纳什均衡


纳什均衡的概念由约翰·纳什提出,他是著名数学家、经济学家,还是《美丽心灵》男主角原型,他与另外两位数学家在非合作博弈的均衡分析理论方面做出了开创性的贡献,对博弈论和经济学产生了重大影响,而获得1994年诺贝尔经济学奖。


FE826890-FA8F-4813-98D6-5695D394BFD7.png


文章开头的对于找工作和家庭的例子就是一个纳什均衡,要记住纳什均衡不是最优的局面,如果你观察到身边的人在一家公司干了5年甚至10年以上,他的薪资或许会落后于市场平均水平,但对他来说,这份工作对于工作、生活、收入来说,大概就是一个纳什均衡。当然有的工作也可能不让你准时下班,甚至可能让你没有业余时间,就像一个在大厂996,忙到没有时间顾家和看娃的人来说,这也是一个纳什均衡。


如果你观察到社会上的一个现场长期稳定的存在,它对于所有参与方来说就是一个纳什均衡。纳什均衡告诉我们评价一个事情不能看它是不是对整体最好,它必须要让所有参与者都不愿单方面改变才行。


说在最后


公司领导发现项目问题不断,悄悄从HR那里调出了你和小齐的打卡记录,赫然发现你们天天5:30准时打卡,项目问题这么多,你俩准时下班,难怪运营天天说问题解决不及时,他决定宣布,在这个项目结束之前,项目组实行996工作制。


本来还想平衡好工作和家庭,这下可好了,不但日常加班,还损失了周六的时间。


那你说,这该怎么办呢?欢迎你在评论区说出你的看法,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~


本篇文章是第65篇原创文章,2024目标进度65/100,欢迎有趣的你,关注我。


作者:东东拿铁
来源:juejin.cn/post/7449172888689836047
收起阅读 »

前端ssr项目被打崩后,连夜做限流!

web
token-bucket-limiter-redis 是一个令牌桶算法 + redis 的高效限流器,用于Node服务接口限流。 当然作为一个前端你可能很少接触Node接口开发,用的接口应该都是后端同学提供的,他们有自己的限流策略,但是你一定使用过SSR框架来...
继续阅读 »

token-bucket-limiter-redis 是一个令牌桶算法 + redis 的高效限流器,用于Node服务接口限流。


当然作为一个前端你可能很少接触Node接口开发,用的接口应该都是后端同学提供的,他们有自己的限流策略,但是你一定使用过SSR框架来开发服务端渲染项目,那么此时你的项目就只能靠我们自己来做限流了,否则遇到突发流量时,你的项目可能很容易崩溃。



  • 使用令牌桶算法实现

  • 支持基于内存和基于 redis 存储的两种选择,满足分布式限流需要

  • 高性能,令牌生产的方式为每次请求进来时一次性生产上一次请求到本次请求这一段时间内的令牌,而不是定时器生成令牌

  • 快速,使用 lua 脚本与redis通讯,lua 支持将多个请求通过脚本的形式一次发送到服务器,减少通讯,并且脚本支持缓存,多客户端可以复用

  • 安全,lua 脚本保证redis命令执行的原子性

  • 内存效率高,键过期后自动删除,不占用过多内存

  • 提供多种极端场景下的降级和容错措施



其他限流方法的对比,大家可以自行搜索,这里就不赘述了,令牌桶算法是更适合大部分场景的限流方案。


令牌桶算法:按照一定的速率生产令牌并放入令牌桶中,最大容量为桶的容量,如果桶中令牌已满,则丢弃令牌,请求过来时先到桶中拿令牌,拿到令牌则放行通过,否则拒绝请求。这种算法能够把请求均匀的分配在时间区间内,又能接受服务可承受范围内的突发请求。所以令牌桶算法在业内较为常用。




该项目github地址:token-bucket-limiter-redis



安装


npm i --save token-bucket-limiter-redis

引入


import { RateLimiterTokenBucket, RateLimiterTokenBucketRedis } from 'token-bucket-limiter-redis';

使用


限流方案我们分为无状态限流器和有状态限流器两种:


有状态的限流器(区分key的限流器):这种限流器会根据某种标识(如IP地址、用户ID、url等)来进行区分,并对每个标识进行单独的限流。可以更精细地控制每个用户或者每个IP的访问频率。


无状态的限流器(不区分key的限流器):这种限流器不会区分请求的来源,只是简单地对所有请求进行统一的限制。


基于内存的无状态限流器


const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 100,
capacity: 1000,
});

const globalTokens = globalRateLimiter.getToken();

if(globalTokens > 0){
// pass
}


基于内存的有状态限流器,自定义key


const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
});

const key = ip + uid; // 标识用户信息的key

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


这里附上 node 端获取ip的方法



function getClientIp(req) {
// 获取 X-Real-IP 头部字段
const xRealIP = req.headers['x-real-ip'];

// 优先使用 X-Real-IP 头部字段
if (xRealIP) {
return xRealIP;
}

// 获取 X-Forwarded-For 头部字段,通常包含一个或多个IP地址,最左侧的是最初的客户端IP
const xForwardedFor = req.headers['x-forwarded-for'];

// 如果 X-Real-IP 不存在,但 X-Forwarded-For 存在,则使用最左侧的IP地址
if (xForwardedFor) {
const ipList = xForwardedFor.split(',');
return ipList[0].trim();
}

// 获取连接的远程IP地址
const remoteAddress = req.connection?.remoteAddress;
// 如果都不存在,使用连接的远程IP地址
if (remoteAddress) {
return remoteAddress;
}

return '';
}

基于内存的有状态限流器,使用ip作为默认key


const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
});

// 使用 ip 作为key,无需传入,自动获取ip
const globalTokens = globalRateLimiter.getTokenUseIp(req);

// 使用 ip 加上自定义的其他key,如传入则组合在ip后 ip+uid
const globalTokens = globalRateLimiter.getTokenUseIp(req, uid);

if(globalTokens > 0){
// pass
}



注意,单纯使用ip作为限流key可能会有问题,有以下几种可能过个机器的外网ip相同的情况:



  • 使用共享的公共 IP 地址: 在一些特殊的网络环境下,多个设备可能共享同一个公共 IP 地址,如咖啡馆、图书馆等提供 Wi-Fi 服务的地方。在这种情况下,所有连接到同一网络的设备都会共享相同的公共 IP。

  • 使用代理服务器: 如果多个机器通过相同的代理服务器访问互联网,它们可能会在外网上表现为相同的 IP 地址,因为代理服务器向互联网发起请求,而不是直接来自每个终端设备。

  • 使用 NAT(网络地址转换): 在家庭或企业网络中,使用了 NAT 技术的路由器可能会导致多个内部设备共享同一个外网 IP 地址,同一公司下的内网设备公网ip可能是同一个。



综上,如果你需要考虑以上集中情况的话,你需要结合其他可以标识用户身份的key,如uid,浏览器指纹等:


// 使用 ip 加上自定义的其他key,如传入则组合在ip后 ip+uid
const globalTokens = globalRateLimiter.getTokenUseIp(req, uid);

附上浏览器指纹获取方法:


function generateFingerprint() {
try {
// 收集一些浏览器属性
const userAgent = navigator.userAgent || '';
const screenResolution = `${window.screen.width}x${window.screen.height}`;
const language = navigator.language || '';
const platform = navigator.platform || '';

// 将这些属性组合成一个简单的指纹
const fingerprint = userAgent + screenResolution + language + platform;

// 返回指纹
return fingerprint;
} catch (error) {
return '';
}
}

在 express 中使用


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

const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
});

// 全局中间件
app.use((req, res, next) => {
console.log('Express global middleware');
// 使用 ip 作为key,无需传入,自动获取ip
const tokens = globalRateLimiter.getTokenUseIp(req);

if(tokens > 0){
next();
}else {
res.status(429).send({ message: 'Too Many Requests' })
}
});

app.listen(3000, () => {
console.log('Express app listening on port 3000');
});

在 koa 中使用


const Koa = require('koa');
const app = new Koa();

const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test',
});

// 全局中间件
app.use(async (ctx, next) => {
console.log('Koa global middleware');
// 使用 ip 作为 key,无需传入,自动获取 ip
const tokens = globalRateLimiter.getTokenUseIp(ctx.req);

if (tokens > 0) {
await next();
} else {
ctx.status = 429;
ctx.body = { message: 'Too Many Requests' };
}
});

app.listen(3000, () => {
console.log('Koa app listening on port 3000');
});


在 fastify 中使用


const fastify = require('fastify')();

const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test',
});

// 全局中间件
fastify.addHook('onRequest', (request, reply, done) => {
console.log('Fastify global middleware');
// 使用 ip 作为 key,无需传入,自动获取 ip
const tokens = globalRateLimiter.getTokenUseIp(request);

if (tokens > 0) {
done();
} else {
reply.status(429).send({ message: 'Too Many Requests' });
}
});

fastify.listen(3000, (err) => {
if (err) throw err;
console.log('Fastify app listening on port 3000');
});


基于redis的无状态限流器,传入redis客户端


支持分布式限流,外部传入redis客户端 (由ioredis包创建)


import Redis from 'ioredis';

const redis = new Redis({});

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 100,
capacity: 1000,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisClient: redis,
});

const key = 'myproject'; // 使用全局唯一key (当key省略时,默认为RateLimiterTokenBucketGlobalKey)

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


基于redis的有状态限流器,传入redis客户端


支持分布式限流,外部传入redis客户端 (ioredis)


import Redis from 'ioredis';

const redis = new Redis({});

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisClient: redis,
});

const key = ip + uid; // 标识用户信息的key
const globalTokens = globalRateLimiter.getToken(key);

// 使用 ip 作为key
const globalTokens = globalRateLimiter.getTokenUseIp(req);

// 使用 ip + 自定义key
const globalTokens = globalRateLimiter.getTokenUseIp(req, key);

if(globalTokens > 0){
// pass
}


基于redis的有状态限流器,使用内置redis


外部仅需传入redis配置(ioredis)


const redisOptions = {
port: 6379, // Redis 端口
host: 'localhost', // Redis 主机名
password: 'password' // 如果有的话,你的 Redis 密码
db: 0,
};

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisOptions: redis,
});

const key = ip + uid; // 标识用户信息的key

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


添加内存阻塞策略


内存阻塞策略可以保护redis服务器,抵御DDoS攻击


const redisOptions = {
port: 6379, // Redis 端口
host: 'localhost', // Redis 主机名
password: 'password' // 如果有的话,你的 Redis 密码
db: 0,
};

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisOptions: redis,

// 内存阻塞策略(只计算当前服务器或实例的请求数,非分布式)
inMemoryBlockOnConsumed: 50, // 如果某个key在一分钟内消耗的令牌数量超过 50,将在内存中阻塞该key的请求,不会发起redis,防止DDoS攻击
inMemoryBlockDuration: 10, // 阻塞持续时间s
});

const key = ip + uid; // 标识用户信息的key

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


getToken 方法支持第二个参数,传入判断阻塞的标识键,通常是ip或用户id,因为我们要阻塞的是某个具体的用户或机器,不传的话默认使用第一个参数,即令牌标识键。


当你使用无状态限流器,或是有状态限流器的键无法标识某个具体用户时可能需要填写该参数:


const key = 'myproject'; // 无状态限流器
const key = 'url'; // 有状态限流器,但是只限制某个路由

const blockKey = 'ip'; // 阻塞标识键须使用ip或用户id

const globalTokens = globalRateLimiter.getToken(key, blockKey);

// 使用 ip + 自定义key
const globalTokens = globalRateLimiter.getTokenUseIp(req, key, blockKey);

if(globalTokens > 0){
// pass
}


内存阻塞策略优先于redis限流器以及redis保险策略,即使redis不可用时内存阻塞策略依旧生效。


添加保险策略,配置当redis服务错误时是否自动使用内存限制器


const redisOptions = {
port: 6379, // Redis 端口
host: 'localhost', // Redis 主机名
password: 'password' // 如果有的话,你的 Redis 密码
db: 0,
};

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisOptions: redis,

// 内存阻塞策略
inMemoryBlockOnConsumed: 50, // 如果某个key在一分钟内消耗的令牌数量超过 50,将在内存中阻塞该key的请求,不会发起redis,防止DDoS攻击
inMemoryBlockDuration: 10, // 阻塞持续时间s

// 保险策略,使用内存限流器
insuranceLimiter: true,
insuranceLimiterTokenPerSecond: 3, // 如果未填写将取tokenPerSecond的值
insuranceLimiterCapacity: 3, // 如果未填写将取capacity的值
});

const key = ip + uid; // 标识用户信息的key

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


开启保险策略后,支持传入保险限制器的每秒令牌数和令牌桶容量,如果不传,将取redis限流器的值。


当你的服务是集群部署时,例如使用 pm2 的集群模式时,会用到这些选项,因为使用redis时令牌是共享的,而集群模式下每个服务是一个实例,每个实例有自己的内存空间,所以你要适当地考虑使用内存限流器时每个实例的限流速率。


注意事项



  1. 基于内存的限流器更适用于单机限流的场景,集群或分布式部署时,如果你不能计算出每一个实例的合适限流配置的话推荐使用基于redis的限流器。


FAQ


不使用定时器生成令牌有什么好处?


时间精度:定时器的精度可能会受到系统调度和网络延迟的影响,这可能导致令牌的生成速率无法精确控制。


资源消耗:如果令牌桶的数量非常多,那么需要维护的定时器也会非常多,这可能会消耗大量的系统资源。


时间同步:由于精度问题,如果系统中存在多个令牌桶,且每个令牌桶都使用自己的定时器,那么这些定时器之间可能并不同步。


冷启动问题:如果使用定时器生成令牌,那么在服务刚启动时,令牌桶可能会是空的,这可能导致在服务启动初期无法处理请求。


除了ip还有哪些可以标识具体用户的key



  • 浏览器指纹

  • 用户id

  • 用户名

  • 邮箱

  • 手机号

  • 其他可以标识用户身份的key


// 生成浏览器指纹
export function generateFingerprint() {
try {
// 收集一些浏览器属性
const userAgent = navigator.userAgent || '';
const screenResolution = `${window.screen.width}x${window.screen.height}`;
const language = navigator.language || '';
const platform = navigator.platform || '';

// 将这些属性组合成一个简单的指纹
const fingerprint = userAgent + screenResolution + language + platform;

// 返回指纹
return fingerprint;
} catch (error) {
return '';
}
}

作者:Pursue_LLL
来源:juejin.cn/post/7454095190379888666
收起阅读 »

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


---程序员职场闲聊公众号:网管叨bi叨


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

用java做物品识别和姿态识别

前言 之前搞得语音识别突然发现浏览器就有接口可以直接用,而且识别又快又准,参考:使用 JavaScript 的 SpeechRecognition API 实现语音识别_speechrecognition js-CSDN博客 进入正题 这个功能首先要感谢一下作...
继续阅读 »

前言


之前搞得语音识别突然发现浏览器就有接口可以直接用,而且识别又快又准,参考:使用 JavaScript 的 SpeechRecognition API 实现语音识别_speechrecognition js-CSDN博客


进入正题


这个功能首先要感谢一下作者常康,仓库地址(gitee.com/agriculture… 这个项目很早之前就关注了,最近这段时间正好要用才真正实践了一下,只是初步测试了一下,在性能方面还需要进一步测试,本人电脑就很拉识别就很卡。


先看效果


20240912_090041 00_00_00-00_00_30.gif


20240912_091337 00_00_00-00_00_08 00_00_00-00_00_30.gif


改动


主要对姿态识别做了一些小改动,将原图片识别改成视频视频识别,如果要调用摄像头将video.open(0);的代码注释放开即可


package cn.ck;

import ai.onnxruntime.OnnxTensor;
import ai.onnxruntime.OrtEnvironment;
import ai.onnxruntime.OrtException;
import ai.onnxruntime.OrtSession;
import cn.ck.config.PEConfig;
import cn.ck.domain.KeyPoint;
import cn.ck.domain.PEResult;
import cn.ck.utils.Letterbox;
import nu.pattern.OpenCV;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.highgui.HighGui;
import org.opencv.imgproc.Imgproc;
import org.opencv.videoio.VideoCapture;
import org.opencv.videoio.Videoio;

import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/*
* 姿态识别,可以识别动作等等.,比如跳绳技术
*/

public class PoseEstimation {

static {
// 加载opencv动态库
//System.load(ClassLoader.getSystemResource("lib/opencv_java470-无用.dll").getPath());
OpenCV.loadLocally();
}

public static void main(String[] args) throws OrtException {

String model_path = "src\main\resources\model\yolov7-w6-pose-nms.onnx";
// 加载ONNX模型
OrtEnvironment environment = OrtEnvironment.getEnvironment();
OrtSession.SessionOptions sessionOptions = new OrtSession.SessionOptions();
OrtSession session = environment.createSession(model_path, sessionOptions);
// 输出基本信息
session.getInputInfo().keySet().forEach(x -> {
try {
System.out.println("input name = " + x);
System.out.println(session.getInputInfo().get(x).getInfo().toString());
} catch (OrtException e) {
throw new RuntimeException(e);
}
});

VideoCapture video = new VideoCapture();

// video.open(0); //获取电脑上第0个摄像头

//可以把识别后的视频在通过rtmp转发到其他流媒体服务器,就可以远程预览视频后视频,需要使用ffmpeg将连续图片合成flv 等等,很简单。
if (!video.isOpened()) {
System.err.println("打开视频流失败,未检测到监控,请先用vlc软件测试链接是否可以播放!,下面试用默认测试视频进行预览效果!");
video.open("video/test2.mp4");
}
// 跳帧检测,一般设置为3,毫秒内视频画面变化是不大的,快了无意义,反而浪费性能
int detect_skip = 4;

// 跳帧计数
int detect_skip_index = 1;

// 最新一帧也就是上一帧推理结果
float[][] outputData = null;

//当前最新一帧。上一帧也可以暂存一下
Mat img = new Mat();


// 在这里先定义下线的粗细、关键的半径(按比例设置大小粗细比较好一些)
int minDwDh = Math.min((int)video.get(Videoio.CAP_PROP_FRAME_WIDTH), (int)video.get(Videoio.CAP_PROP_FRAME_HEIGHT));
int thickness = minDwDh / PEConfig.lineThicknessRatio;
int radius = minDwDh / PEConfig.dotRadiusRatio;
// 转换颜色空间
Mat image = new Mat();

// 图像预处理
Letterbox letterbox = new Letterbox();
letterbox.setNewShape(new Size(960, 960));
letterbox.setStride(64);


// 使用多线程和GPU可以提升帧率,线上项目必须多线程!!!,一个线程拉流,将图像存到[定长]队列或数组或者集合,一个线程模型推理,中间通过变量或者队列交换数据,代码示例仅仅使用单线程
while (video.read(img)) {
if ((detect_skip_index % detect_skip == 0) || outputData == null) {
Imgproc.cvtColor(img, image, Imgproc.COLOR_BGR2RGB);
image = letterbox.letterbox(image);
int rows = letterbox.getHeight();
int cols = letterbox.getWidth();
int channels = image.channels();
// 将图像转换为模型输入格式
float[] pixels = new float[channels * rows * cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
double[] pixel = image.get(j, i);
for (int k = 0; k < channels; k++) {
pixels[rows * cols * k + j * cols + i] = (float) pixel[k] / 255.0f;
}
}
}
detect_skip_index = 1;
OnnxTensor tensor = OnnxTensor.createTensor(environment, FloatBuffer.wrap(pixels), new long[]{1L, (long) channels, (long) rows, (long) cols});
OrtSession.Result output = session.run(Collections.singletonMap(session.getInputInfo().keySet().iterator().next(), tensor));

// 处理输出结果并绘制
outputData = ((float[][]) output.get(0).getValue());
}else{
detect_skip_index = detect_skip_index + 1;
}
double ratio = letterbox.getRatio();
double dw =letterbox.getDw();
double dh = letterbox.getDh();
List<PEResult> peResults = new ArrayList<>();
for (float[] outputDatum : outputData) {
PEResult result = new PEResult(outputDatum);
if (result.getScore() > PEConfig.personScoreThreshold) {
peResults.add(result);
}
}

// 对结果进行非极大值抑制
peResults = nms(peResults, PEConfig.IoUThreshold);

for (PEResult peResult: peResults) {
System.out.println(peResult);
// 画框
Point topLeft = new Point((peResult.getX0()-dw)/ratio, (peResult.getY0()-dh)/ratio);
Point bottomRight = new Point((peResult.getX1()-dw)/ratio, (peResult.getY1()-dh)/ratio);
// Imgproc.rectangle(img, topLeft, bottomRight, new Scalar(255,0,0), thickness);
List<KeyPoint> keyPoints = peResult.getKeyPointList();
// 画点
keyPoints.forEach(keyPoint->{
if (keyPoint.getScore()>PEConfig.keyPointScoreThreshold) {
Point center = new Point((keyPoint.getX()-dw)/ratio, (keyPoint.getY()-dh)/ratio);
Scalar color = PEConfig.poseKptColor.get(keyPoint.getId());
Imgproc.circle(img, center, radius, color, -1); //-1表示实心
}
});
// 画线
for (int i = 0; i< PEConfig.skeleton.length; i++){
int indexPoint1 = PEConfig.skeleton[i][0]-1;
int indexPoint2 = PEConfig.skeleton[i][1]-1;
if ( keyPoints.get(indexPoint1).getScore()>PEConfig.keyPointScoreThreshold &&
keyPoints.get(indexPoint2).getScore()>PEConfig.keyPointScoreThreshold ) {
Scalar coler = PEConfig.poseLimbColor.get(i);
Point point1 = new Point(
(keyPoints.get(indexPoint1).getX()-dw)/ratio,
(keyPoints.get(indexPoint1).getY()-dh)/ratio
);
Point point2 = new Point(
(keyPoints.get(indexPoint2).getX()-dw)/ratio,
(keyPoints.get(indexPoint2).getY()-dh)/ratio
);
Imgproc.line(img, point1, point2, coler, thickness);
}
}
}
//服务器部署:由于服务器没有桌面,所以无法弹出画面预览,主要注释一下代码
HighGui.imshow("result", img);

// 多次按任意按键关闭弹窗画面,结束程序
if(HighGui.waitKey(1) != -1){
break;
}
}

HighGui.destroyAllWindows();
video.release();
System.exit(0);

}

public static List<PEResult> nms(List<PEResult> boxes, float iouThreshold) {
// 根据score从大到小对List进行排序
boxes.sort((b1, b2) -> Float.compare(b2.getScore(), b1.getScore()));
List<PEResult> resultList = new ArrayList<>();
for (int i = 0; i < boxes.size(); i++) {
PEResult box = boxes.get(i);
boolean keep = true;
// 从i+1开始,遍历之后的所有boxes,移除与box的IOU大于阈值的元素
for (int j = i + 1; j < boxes.size(); j++) {
PEResult otherBox = boxes.get(j);
float iou = getIntersectionOverUnion(box, otherBox);
if (iou > iouThreshold) {
keep = false;
break;
}
}
if (keep) {
resultList.add(box);
}
}
return resultList;
}
private static float getIntersectionOverUnion(PEResult box1, PEResult box2) {
float x1 = Math.max(box1.getX0(), box2.getX0());
float y1 = Math.max(box1.getY0(), box2.getY0());
float x2 = Math.min(box1.getX1(), box2.getX1());
float y2 = Math.min(box1.getY1(), box2.getY1());
float intersectionArea = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
float box1Area = (box1.getX1() - box1.getX0()) * (box1.getY1() - box1.getY0());
float box2Area = (box2.getX1() - box2.getX0()) * (box2.getY1() - box2.getY0());
float unionArea = box1Area + box2Area - intersectionArea;
return intersectionArea / unionArea;
}
}

姿态识别模型提取链接,
通过网盘分享的文件:yolov7-w6-pose-nms.onnx
链接: pan.baidu.com/s/1UdAUPWr1… 提取码: du6y


后言


就像原作者说的,不是每个同学都会python,不是每个项目都是python语言开发,不是每个岗位都会深度学习。

希望java在AI领域能有更好的发展


作者:北冥有鱼518
来源:juejin.cn/post/7413234304278970404
收起阅读 »

为什么很多程序员会觉得领导没能力

相信很多人在职场里待久了,都会遇到自己觉得比较差劲的领导,这些人可能除了向上管理能力很强外(会舔老板),其他能力在你看来都挺一般,专业能力一般,超级缝合怪--上级给他的任何任务他都能分配给你们,然后他再缝合一遍完事。 那么遇到这种领导我们该怎么办呢?多数人想到...
继续阅读 »

相信很多人在职场里待久了,都会遇到自己觉得比较差劲的领导,这些人可能除了向上管理能力很强外(会舔老板),其他能力在你看来都挺一般,专业能力一般,超级缝合怪--上级给他的任何任务他都能分配给你们,然后他再缝合一遍完事。


那么遇到这种领导我们该怎么办呢?多数人想到的是跳槽,这确实是一个解法,但你跳到下家公司也保不齐会有这样的领导呀,今天咱们讨论的这个话题就先把条件限定成你不能跳槽,这个时候你该采用什么方法让自己的上班体验变好一些。


多元化自己的评估标准


首先,不能用鄙视的眼光去看待你的领导,觉得他只会舔老板(能舔、会舔也是一种很强的能力呀),有的时候你觉得你领导能力不行,很有可能是因为你的能力评估标准太单一了。


他或许在工作的某个方面不如你,但是他必定在某些方面有自己的长处,努力发现他的长处,认可他的长处,学习他的长处,可以更有助于你和他的相处,也有利于你的进步。


社会是一个大熔炉,你需要的不仅仅是业务能力和展现的舞台,也需要与社会中不同个体的和谐共处。包容、接纳,都是立身处世的能力。


学会变通和沟通,满足领导的存在感


领导之所以会在很多工作上提意见、瞎指挥、乱指挥,更多的情况可能是他知道自己对工作不熟悉,但觉得自己是领导,会有自己独特的见解,想刷自己的存在感。这种情况下,要学会满足领导的存在感。


举个例子说,你在工作中,领导过来给你提了个意见,这个意见明显是不合适的,那你就可以说,“领导,这个思路好,我们之前没往这个角度想,可以从这个角度延展一下……。”他走了,还不是我们自己把控,毕竟他只是过来刷个存在感的,只要最后的方案让客户满意,业绩给领导,把一些光环放在他身上,让他觉得他起到了作用,这些方案和他有关,他通常也不会计较了。


摸清领导管理的思想和套路


说到这里,找到领导心中的关键因素,是非常必要的。在一个项目里,员工承担的通常只是局部,而领导看的是整体,由于高度不同,所以你们考虑的关键因素是不同的。


所以你要知道领导心里到底想要的是什么,提前做好这方面的预期和准备,以及针对领导提出的你们没有考虑到的方面要虚心接受(毕竟领导跟高层接触的更多,有些战略方向上的事情他们会更清楚)。 


比如说,你是一个小编,你在意的是按时完成写作任务、及时发表、赚取眼球,而你的领导主编可能更在意的是你文章的各种数据真实性、转化人群、是否会产生舆情、是否zzzq这些。所以,要搞清领导在意的重要维度,工作才能更有效。


这里有三句话分享给大家:



  • 要能够分清你可以改变的事、无法改变的事;

  • 不去抱怨你服务改变的事;

  • 把精力用在你可以改变的地方。


你的上司,是你改变不了的,但你自己,是可以把握的。当然这篇文章也不是教你怎么委屈自己,只是提供一个不同的角度来讨论"领导不行” 这个事情,以及让你在无法立刻更换环境时,该怎样让当前的环境变得不那么恶劣。


想跳槽的同学还是应该按部就班的准备,骑驴找马有更合适的地方该跳就跳,跳过去了说不定今天学到的这些还能用的上……。


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

留在家乡还是 奔向一线城市

前言导读 这个专题主要是发表一些生活的感想感悟,言论主观得一塌糊涂哈 各位网友有自己的想法都可以分享发出来。 为什么要写这个话题 1 第一个原因 父母养老问题 父母养老的问题, 相信各位北漂 深漂的游子都比较有感触吧。 自己远在深圳 广州 北京 上...
继续阅读 »

前言导读


这个专题主要是发表一些生活的感想感悟,言论主观得一塌糊涂哈 各位网友有自己的想法都可以分享发出来。




  • 为什么要写这个话题




  • 1 第一个原因 父母养老问题



父母养老的问题, 相信各位北漂 深漂的游子都比较有感触吧。 自己远在深圳 广州 北京 上海 等地工作。一旦有个什么事情,自己也很难第一时间赶回去处理。即使回来了 也是各种奔波。身心巨累,所以这时候很多人,在这个时候犹豫了。是否考虑回去。




  • 2 第二个原因 生活节奏



经历过一线城市高压力快节奏的生活,自己再次回到小县城,之后那种骑着小电驴能到处游玩。 能够干着轻松的工作,能陪伴自己的家人。这种鲜明对比 好似在疫情之后房价暴跌之后愈发明显。 好像去到省城 去到一线城市,不再是我们唯一的选择。似乎对了很对选择。




  • 3 第三个原因 居住环境和工作环境



工作的规划的居住问题, 相对一线贵的让头疼的房价 小县城好像也有他独特的优势。虽然在工作靠的
更多的是人情关系, 人情世故貌似是这样的必修课,当然这些也是一部分年轻人比较反感的, 这个见仁见智, 有些人被迫接受这些, 有人反感这些就远离这些。这都很正常。


具体自己所见 所闻


从一线新家回去一次的经历


image.png


image.png


image.png


image.png


image.png


上图可以看到 回一次家过程非常曲折 新塘站 - 广州白云站 -武昌火车站- 汉口北地铁站 - 红安城乡公交总站。经历了 九九八十一难 我才到达老家。感觉非常折磨人, 此刻我是真的有点动摇了,是否自己几年的前的决定是个错误的决定。 有时候也感慨每年就回来那么两次。忍忍就好了,一切都会过去。


和人交流想法转变


但是这次回来住在表弟家里就在晚上的时候,吃完晚饭 ,去他堂哥家里 休息了,也是交流了很多, 也都是程序员但是他们选择不去武汉留下老家了, 可以避开那些快节奏的生活和高压力。 虽然收入相对可以减少些。 当时就很感触这个不正是我们想要的稳定的生活吗。 可能 20 多岁的时候, 那时候想着有很多很多目标, 几年几年做到某一个高度。 要是实现的自己的某个目标啥的, 但是我们发现,那些真的能成的毕竟是少数, 大多数人还是选择了相对平衡和稳定的生活。


抉择问题


各位好像都有遇到我们,高考要上什么样的学校,选择什么样的专业都不是自己选的,都是父母为自己选择,好像都没有自己为自己的人生真正负责过, 等自己长大了,有一些成就后,想定居哪里以后在哪里发展, 和什么样的人结婚生儿育女,好像都是父母长辈替我们决定的。 这时候内心就会有很多疑问,只要自己决定还是说,尊重父母意见, 以前我很不理解一些选择, 为什么可以留在一线还要回去,现在才知道有部分是权衡利弊的选择,有部分人也只是为了选择而选择 或者是为了父母的选择,
父母都只想子女留在身边(这个想法个人看来有点自私,但是也是目前现状),经历这么多的事情,也是切身感受到,人跟人观念的不同,我们既要找到和我们同频共振的那部人人, 同时也要能接受和尊重那些跟我们不同观念人,因为这个世界允许存在差异,正是因为存在差异才会有不同的观点想法。 也会让我们能多思考怎样能过好自己的生活 ,怎样才能规划好自己的未来。


小县城慢节奏优点


小县城没有快节奏 高压力,相对比较适合那些慢节奏生活的人, 也方便照顾年迈的父母, 如果一旦一个什么事情都可以随时有个照应。这些都是在一线头痛的事情。


image.png


image.png


一线职业发展优点


一线城市有更多的就业机会,和各种线下活动。技术交流,能让自己职业发展更上一层楼。也能节假日去看看各种游玩地区 。


image.png


image.png


image.png


最后总结:


无论是选择留在家乡小县城还是奔向一线城市,我们都要承担对应带来的负面的后果,也能享受对应能带来的福利。这些就看自己选择,如果像我这样的情况父母身体真的很差,建议可以送去敬老院 然后自己再去拼搏 或者留在自己身边 这个看父母自理能力和我们自己是否愿意长期照顾年迈的父母了,以上的观点都是我自己主观分享不代表所有人 各位可以结合自己实际情况做出最佳选择。也预祝各位 2024 国庆节快乐祖国成立 75 周年普天同庆。


作者:坚果派_xq9527
来源:juejin.cn/post/7421185520339124287
收起阅读 »

当我入手一台 MacBookPro 之后

从 13 年实习开始,开发环境从 Ubuntu 转战 MacOS,中间换了好几次电脑,每次都是直接用 Mac 自带的 Time Machine 来迁移数据,仅需一块移动硬盘或者一根 type c 线,经过一个晚上的数据迁移,第二天就可以用新电脑工作了,除了配置...
继续阅读 »


从 13 年实习开始,开发环境从 Ubuntu 转战 MacOS,中间换了好几次电脑,每次都是直接用 Mac 自带的 Time Machine 来迁移数据,仅需一块移动硬盘或者一根 type c 线,经过一个晚上的数据迁移,第二天就可以用新电脑工作了,除了配置升级了,几乎感受不到换电脑的乐趣,并且升级过程中,也积累了不少系统升级的旧疾,这次从Intel芯片到 M3 Max 芯片,我打算从零开始,重新蒸腾一番,顺带更新一下工具库,说干就干,Go!
先介绍下新电脑的配置



  • 太空黑:从经典的银色、到太空灰,这次体验一下太空黑

  • 14 寸:用了大概 3 年的 14 寸,就一直用 15/16寸,因为这台不是用于办公,考虑携带方便,所以入手 14 寸(大屏幕肯定爽,但是在家主要也是外接显示器)




  • M3 Max:想要体验一下本地大模型,直接入手 Max(找个借口🤐)

  • 64G 内存:一直有在 macbook 上装虚拟机(Parallels Desktop)运行 Windows的习惯,升级了一下内存

  • 2TB SSD:以前 512 的时候,由于各种 npm 包、docker 镜像,还是隔一段时间就要重启一下、硬盘清理等方式来释放空间,一步到位





后面还换过几台,从最开始的 touchbar ,蝶式键盘,再到取消 touchbar,这时候更多的是工作工具的更换,连拍照的激情都没有🥱🥱


开发工具


科学上网工具


作为开发,第一件事情是需要一个趁手的科学上网工具,不然类似下载 Google Chrome、安装 Homebrew 等基础的工具都会十分麻烦
我的科学上网工具,支持按照规则配置自动代理,同时也支持终端代理,以下是终端代理,这里不方便推荐具体工具


# 防止终端命令无法确定是否需要科学上网,不建议把这个命令持久化到 bashrc/zshrc,在需要时打开终端输入即可
export https_proxy=http://127.0.0.1:1235 http_proxy=http://127.0.0.1:1235 all_proxy=socks5://127.0.0.1:1234

Xcode


Xcode 命令行工具,许多开发工具和依赖所需的基础,运行一下命令,选择安装,稍等一会即可


xcode-select --install

Homebrew


通过 homebrew 来管理一些开发工具包,如 git、node等等,由于需要下载 github 地址,这里需要借助你的翻墙工具


/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

然后按照提示,把命令加到 PATH


(echo; echo 'eval "$(/opt/homebrew/bin/brew shellenv)"') >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

Git/Tig


brew install git
# brew install git-gui
# brew install tig 个人是 vim 用户,偏向这种终端 gui
# 这里会自动安装 zsh 的自动补全工具,后续安装 zsh 可用
# /opt/homebrew/share/zsh/site-functions

安装 git 和 tig 都会默认新增 zsh 的补全方法,好吧,这是提醒我要立马安装 zsh
tigrc 可以用来自定义 tig 的一些配置和快捷键绑定


A sample of the default configuration has been installed to:
/opt/homebrew/opt/tig/share/tig/examples/tigrc
to override the system-wide default configuration, copy the sample to:
/opt/homebrew/etc/tigrc

zsh completions and functions have been installed to:
/opt/homebrew/share/zsh/site-functions

在任意已经初始化 git 的项目,打入 tig ,然后你就可以使用 vim 的方式来操作了 jk 等等

另外,使用 git 我还会额外安装两个 git 相关的小插件
一个是 tj 大神开发的 git-extras


brew install git-extras

# 添加到 ~/.zshrc
source /opt/homebrew/opt/git-extras/share/git-extras/git-extras-completion.zsh

详细的命令可查看文档,我比较常用了是 git summary、git undo、git setup

然后通过git 的 alias 来实现一个自定义的命令,git up 来实现每次切换到一个仓库时,有意思的更新一下最新代码


git config --global alias.up '!git remote update -p && git pull --rebase && git submodule update --init --recursive'


iTerm


实现通过 command + ecs 键,快速切换显示/隐藏 iTerm



  • 设置默认终端

  • 安装 shell integration




  • 选配色:Solarized

  • 安装 oh-my-zsh


sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"


  • 修改主题,配置插件等等


brew install zsh-syntax-highlighting
brew install zsh-autosuggestions
brew install autojump
brew install fzf

#ZSH_THEME="robbyrussell"
#ZSH_THEME="agnoster"
#ZSH_THEME="miloshadzic"
#ZSH_THEME="sunrise"
# ZSH_THEME="ys"
ZSH_THEME="gnzh"

plugins=(git ruby autojump osx rake rails lighthouse sublime)
plugins=(bgnotify)
plugins=(web-search)
plugins=(node)

source /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh
[ -f /opt/homebrew/etc/profile.d/autojump.sh ] && . /opt/homebrew/etc/profile.d/autojump.sh
source <(fzf --zsh)

这就起飞了!看看效果


Docker Desktop


作为开发,docker 技能必不可少,mac 下直接使用 docker desktop,可以省掉很多事情,特别是当你如果需要在本地跑 k8s 环境时,直接勾上 Enable Kubernetes 即可;另外新版本查看镜像,也可以扫描镜像每一层是否有安全漏洞,十分方便


VSCode


vscode 不仅适合前端开发,对于 Go、Rust 等开发者,整体体验都不错。安装后的第一件事,就是把 code 命令加到 Terminal

然后第二件事就是安装 github 的 Copilot 插件,开发、看源码现在是少不了它了

第三件事就是在 vscode 开启 vim 模式(安装 vim 插件)从 vim - sublime - vscode,一直保留 vim 的使用习惯,改不掉了😂
其他的就是各种高亮、开发辅助插件,大家按需安装即可


其他



  • 前端

    • NVM:node 版本管理

    • pnpm:上一台电脑只有 512G,在动不动就几个 G 的前端项目,硬盘一直告警,至此只用 pnpm



  • Go

    • GVM: go 版本管理

    • GoLand:虽然 vscode 可以开发 go,但是整体体验还是比不上收费的 goland




环境搭建可参考 Go + React 指北


效率工具


ChatGPT


说到效率工具,ChatGPT 绝对是提高效率神器,从日常问题到开发、图片生成、画图等等,哦,还可以帮忙挑西瓜🍉


对了,用了苹果芯片,ChatGPT app 直接通过 option + space 即可随时调出,支持实时对话、支持截屏问问题等等,好用程度大幅上升⬆️⬆️⬆️


Bartender($)


吐血推荐,让你状态栏更加一目了然
支持自定义显示哪些 icon,配置哪些 icon 始终不显示,哪些第二状态栏显示

快捷键切换第二状态栏,一下子清爽颜值高


iStat Menus($)


拥有时刻关注着网速、CPU使用率、内存使用率的强迫症,绝对不少


Yoink


当你要把一个文件从一个地方移到另外一个地方时,当你想快速复制一张图片时,剪切板记录、跨设备文件接力等等,这个小小的工具都能帮助你
有时候通过截屏软件截图,可以一次性把需要的截屏操作完,然后在剪贴板直接拖下来使用,十分方便!


BettersnapTool


一款小而美的工具,用来快速调整你的窗口,比如当前窗口在两个显示屏直接切换;全屏,左右分屏等等


iShot


本来我一直使用 Skitch 的,但是这次切换新电脑之后,发现它下架了,之前有朋友推荐了,也使用了一段时间,感觉很不错,除了普通的截图,还有长截图、带壳截图;还有其他的小工具,官方宣传是
截图、长截图、全屏带壳截图、贴图、标注、取色、录屏、录音、OCR、翻译一个顶十个,样样皆优秀!


Draw.io


好吧,这个绝对画图神器,日常写文档几乎离不开他,在线版直接打开即可使用,也可以安装 vscode 插件,不过我还是习惯下载一个 app,这样本地的文件,直接打开即可使用
距离成为架构师,你只差一个 draw.io


Parallels Desktop


如果你有使用 Windows 的诉求,那么我建议你花点钱买个 pd,融合模式一开,原来我的 16 年的机器,玩个魔兽、英雄联盟完全没问题
安装直接点击下载 Windows 11,网速好的话,10 来分钟就安装成功了

融合模式,应用和直接使用 mac 的应用没任何差别

全屏模式,可以看到截图的时候有一部分黑色,应该是没有兼容刘海屏的原因


Markdown 编辑器


Quiver,原来所有的笔记、文档基本都靠它来记录,21 年的时候作者停止维护了,再加上使用纯 markdown 工具,还需要自己找图床,最后都转到语雀、飞书等在线文档
中间还用过其他的、Mweb、Typora 等等,如果自己搭建图床,推荐使用 Typora


image-20240802223626203


image-20240802223739246


image-20240802223841320

Mweb 包含PC 和 移动端,通过 iCloud 同步,也是十分方便!


图床工具


原来写 markdown 的时候,使用的是微博免费的图床,2 年后,然后发现图片都失效了!失效了!


所以,图床还是自己维护比较靠谱!


PicList,免费开源,我自己是购买了阿里云按量付费的 OSS,简单配置一下 aks,即可上传图片,配合 Typora,轻松完成写作


image-20240802224136523


配置好之后,图片拖到 markdown 编辑框,即可实现自动上传


image-20240802224555813


BreakTime 定时提醒工具


为了你的健康,你可以让电脑提醒你,每隔30分钟休息一下,倒杯水,看看风景


DaisyDisk (付费)磁盘空间,文件大小分析工具


作为只能买 256G 的屌丝,每天困扰我的一件事就是磁盘空间不足
现在我是 2T 了,可以不用了
也可以使用 腾讯柠檬用过一段时间,也很好用


微信输入法


搜狗输入法、RIME、百度输入法(作恶多端,还用)
上一次推荐,我还是使用搜狗输入法,有朋友推荐微信输入法,体验了一把,简洁、功能齐全,所以手机、PC 全部改用微信输入法
推荐跨设备复制黏贴,速度比苹果自带的快了许多


思维导图:Xmind, MindNode


脑图应用,一般在项目开发过程中用于 需求分解,Model Design 等等。


其他小应用



  • Caffeine

  • Manico, 在 macOS 强大的触摸板下,一直认为这个软件没什么用, 而且快捷键还有很多冲突

  • tmate, 搞基神器,结对编程,定位问题必备神器


Chrome 插件推荐



  • Vimium, 通过键盘快捷键操作网页,比如打开,关闭,查找书签等等

  • FeHelper(前端助手):JSON自动格式化、手动格式化,支持排序、解码、下载等,更多功能可在配置页按需安装

  • Axure RP Extension for Chrome

  • Grammarly for Chrome,语法检查

  • Octotree,github源码查看神器

  • OneTab,节省高达95%的内存,并减轻标签页混乱现象

  • Postman Interceptor

  • React Developer Tools

  • Redux DevTools

  • Yet Another Drag and Go:超级拖拽.向四个方向拖拽文字即可进行相应的搜索.拖拽链接可在前台/后台,左侧/右侧打开

  • 掘金

  • Sider: ChatGPT 侧边栏 + GPT-4o, Claude 3.5, Gemini 1.5 & AI工具”的产品徽标图片 Sider: ChatGPT 侧边栏 + GPT-4o, Claude 3.5, Gemini 1.5 & AI工具

  • xSwitch:前端开发代理神器,在线 debug 问题,把线上资源代理到本地,方便复现问题


作者:Justin_lu
来源:juejin.cn/post/7398351048777842729
收起阅读 »

BOE(京东方)“向新2025”年终媒体智享会落地深圳 “屏”实力赋能产业创新发展

12月27日,BOE(京东方)“向新 2025”年终媒体智享会的收官之站在创新之都深圳圆满举行,为这场为期两周、横跨三地的年度科技盛会画上了完美句号。活动期间,全面回顾了 BOE(京东方)2024年在多个关键领域取得的卓越成绩,深入剖析其在六大维度构建的“向新...
继续阅读 »

12月27日,BOE(京东方)“向新 2025”年终媒体智享会的收官之站在创新之都深圳圆满举行,为这场为期两周、横跨三地的年度科技盛会画上了完美句号。活动期间,全面回顾了 BOE(京东方)2024年在多个关键领域取得的卓越成绩,深入剖析其在六大维度构建的“向新”发展格局,精彩呈现了以“屏”为核心搭建起的技术引领、伙伴赋能以及绿色发展等平台,全方位赋能全球生态合作伙伴,充分彰显BOE(京东方)作为全球领先的物联网创新企业的引领地位与责任担当。深圳活动现场,BOE(京东方)执行委员会委员、副总裁刘竞以及 BOE(京东方)副总裁、首席品牌官司达亲临现场,发表了主旨演讲。此次系列智享会的成功举办,进一步加深了与会嘉宾对 BOE(京东方)发展理念、技术实力与创新成果的认知和理解,也为BOE(京东方)新一年的发展拉开了充满希望和活力的序幕。

经过三十余年创新发展,秉持着对技术的尊重和对创新的坚持,在“屏之物联”战略指导下,BOE(京东方)从半导体显示领域当之无愧的领军巨擘迅速蝶变,成功转型为全球瞩目的物联网创新企业,并不断引领行业发展风潮。面对下一发展周期,BOE(京东方)将从战略、技术、应用、生态、模式、ESG六大方面全方位“向新”突破,以实现全面跃迁,并为产业高质发展注入强劲动力。

战略向新:自2021年“屏之物联”战略重磅发布以来,BOE(京东方)又于2024年京东方全球创新伙伴大会(BOE IPC·2024)上发布了基于“屏之物联”战略升维的“第N曲线”理论,以半导体显示技术、玻璃基加工、大规模集成智能制造三大核心优势为基础,精准布局玻璃基封装、钙钛矿光伏器件等前沿新兴领域,全力塑造业务增长新赛道。目前,玻璃基封装领域,BOE(京东方)已布局试验线,成立了玻璃基先进封装项目组,实现样机产出;钙钛矿领域,仅用38天就已成功产出行业首片2.4×1.2m中试线样品,标志着钙钛矿产业化迈出了重要一步。

技术向新:2021年,BOE(京东方)发布了中国半导体显示领域首个技术品牌,开创了产业“技术+品牌”双价值驱动的新纪元。以技术品牌为着力点,BOE(京东方)深入赋能超5000家全球顶尖品牌厂商和生态合作伙伴,包括AOC、ROG、创维、华硕、机械师、雷神、联想等,助力行业向高价值增长的路径迈进,也为用户提供了众多行业领先、全球首发的更优选择。BOE(京东方)还将全力深化人工智能与半导体显示技术以及产业发展的深度融合,并在AI+产品、AI+制造、AI+运营三大关键领域持续深耕,并依托半导体显示、物联网创新、传感器件三大技术策源地建设,与产业伙伴和产学研合作伙伴共同创新,为产业高质量可持续发展保驾护航。

应用向新:BOE(京东方)不仅是半导体显示领域的领军企业,也是应用场景创新领域的领跑者,BOE(京东方)秉持“屏之物联”战略,以全面领先的显示技术为基础,通过极致惊艳的显示效果、颠覆性的形态创新,为智慧座舱、电竞、视觉艺术、户外地标等场景注入了新鲜血液,带给用户更加美好智慧的使用体验。以智慧座舱为例,根据市场调研机构Omdia最新数据显示,2024年前三季度BOE(京东方)车载显示出货量及出货面积持续保持全球第一,在此基础上BOE(京东方)还推出“HERO”车载场景创新计划,进一步描绘智能化时代汽车座舱蓝图。

生态向新:BOE(京东方)持续深化与电视、手机、显示器、汽车等众多品牌伙伴的合作,共同打造“Powered by BOE”产业生态集群,赢得众多客户的认可与赞誉。与此同时,BOE(京东方)还持续拓展跨产业生态,通过与上海电影集团、故宫博物院、微博等文化产业领先机构展开跨界合作,以创新技术赋能传统文化艺术与影像艺术。此外,通过战略直投、产业链基金等股权投资方式协同众多生态合作伙伴,通过协同合作、资源聚合共同构筑产业生态发展圈层。

模式向新:为适配公司国际化、市场化、专业化的长远发展,BOE(京东方)持续深化“1+4+N+生态链”的业务发展架构,以及“三横三纵”组织架构和运营机制。在充分市场化和充分授权的机制保障下,形成了以半导体显示核心业务为牵引,传感、物联网创新、MLED业务、智慧医工四大高潜航道全面开花,聚焦包括智慧车联、工业互联、数字艺术、3D光场等规模化应用场景,生态链确保产业上下游合作伙伴协同跃迁的“万马奔腾”的发展图景。此外,BOE(京东方)还鼓励员工创新创业,通过激发人才创新热情,共同为集团发展注入强劲内生动力。

ESG向新:2024年,BOE(京东方)承诺将在2050年实现自身运营碳中和,并通过坚持“Green+”、“Innovation+”、“Community+”可持续发展理念,推动全球显示产业高质永续发展。“Green+”方面,BOE(京东方)依托超过16 家国家级绿色工厂、显示领域唯一1家国家级无废工厂、1 座灯塔工厂及2座零碳工厂,以绿色产品、制造与运营践行低碳路径;“Innovation+”方面,BOE(京东方)凭借全部为自主创新的9万件专利的行业佳绩,以及技术策源地、技术公益池等举措,携手产业上下游伙伴协同创新;“Community+”方面,BOE(京东方)在教育、医疗、环境等公益领域持续投入,积极履行社会责任,例如在“照亮成长路”公益项目中,BOE(京东方)十年间在偏远地区建设的智慧教室已经突破120所。

BOE(京东方):屏即平台赋能创新

在新一轮数智化浪潮中,全球显示行业的龙头企业 BOE(京东方)以屏为核心,充分发挥技术引领作用,积极赋能合作伙伴,并秉持绿色发展理念,全力构建产业高质量、可持续发展的创新生态平台,引领行业在高速发展的科技浪潮中稳步前行,为全球用户缔造更加智能美好的生活体验。作为 BOE(京东方)全球创新生态布局的关键一环,珠三角区域不仅是其创新要素汇聚的高地,更是其全球化发展的重要窗口与强大驱动力,为“屏之物联”战略落地提供了有效支撑。

技术引领方面,BOE(京东方)多年来始终秉持对技术的尊重和对创新的坚持,致力于推动显示技术全面向新发展,以完美画质、AI+显示、无界形态、氧化物(Oxide)关键技术等关键领域,持续挖掘“屏”在物联网领域的无限潜力。

完美画质,BOE(京东方)深入洞察用户真实需求,基于ADS Pro技术优化升级的高端LCD解决方案UB Cell,所呈现的完美画质可以媲美OLED,堪称LCD显示技术发展的重要里程碑。目前,BOE(京东方)已携手合作伙伴推出了一系列搭载UB Cell高端液晶电视旗舰产品,引领液晶显示技术升级风向标;

AI+显示,BOE(京东方)在软硬件层面均已为AI的深度应用构筑完美平台,不仅实现了光感、温感、NFC等传感器件的屏内集成,还开发了屏幕局部刷新、远端功能监测等软件技术,显著增强了用户感知,提升了交互体验;

无界形态,BOE(京东方)作为国内在柔性OLED领域布局早、技术优、市场应用广的领军企业,在材料、工艺等领域具备全面优势,不仅在屏幕轻薄化、超清化方面性能卓越,更能够实现折叠、卷曲等形态变化,同时不断探索屏下摄像、屏下指纹、3D touch等多功能的智慧集成,更是打造出行业首款三折屏等具有行业里程碑意义的产品,带领用户迈入更多变、更智能的未来生活;

氧化物技术,BOE(京东方)在产能、技术以及产品性能上均位居行业领先地位,凭借高刷新率、高分辨率、低功耗等优势,在未来高端IT产品领域展现出广阔的应用前景。

伙伴赋能方面,BOE(京东方)始终以合作共赢为宗旨,高效整合资源,与生态伙伴携手向新发展,共筑高价值发展空间。BOE(京东方)坚持第一时间捕捉行业及市场需求动向,通过内部研发及运营保障机制,完成技术开发应用,组织建设和人才培养,完善流程、数据、组织以及IT能力建设,输出市场化、专业化、国际化的服务能力,并联动上下游及科研机构等生态伙伴,共同探讨“以人为本”的最优解决方案,深度拓展更多高端应用场景;同时,持续进行智能制造实践探索,确保稳定交付,赋能终端伙伴,使其能更好融入更真实、更丰富的消费者使用场景,实现产业高价值增长。

绿色发展方面,BOE(京东方)早已将可持续发展刻入企业基因,融入企业日常经营与管理的全链路,从绿色规划、低碳设计到碳足迹量化认证等各个环节,全力实现极致降碳目标。原材料环节,BOE(京东方)通过打造绿色供应链,积极使用可回收、可降解以及清洁材料,为产品低碳化发展奠定坚实基础;生产制造阶段实现全面绿色低碳;产品流通及回收阶段,BOE(京东方)已完成49个产品的碳足迹认证,凭借可回收、可降解的绿色材料,在产品的全生命周期中均实现了最大化降碳,让“科技创新+绿色发展”成为产业升级的主旋律。

“向新2025”年终媒体智享会,是BOE(京东方)2024创新营销的收官之作和全新实践,系统深化了大众对BOE(京东方)品牌和技术创新实力的认知与理解。近年来,BOE(京东方)通过多种创意独具的品牌破圈推广,包括“你好BOE”系列品牌线下活动、技术科普综艺《BOE解忧实验室》等生动鲜活地传递出BOE(京东方)以创新科技赋能美好生活的理念,为企业业务增长提供了强大动力,也为科技企业品牌推广打造了全新范式。BOE(京东方)“向新2025”主题系列活动已先后于上海、成都、深圳成功举办,为BOE(京东方)2024创新传播划上圆满句号。

面向未来,BOE(京东方)将胸怀“Best on Earth”宏伟愿景,坚持“屏之物联”战略引领,持续推动显示技术和物联网、AI等前沿技术的深度融合。从提升产品视觉体验到优化产业生态协同,从升级智能制造体系到践行社会责任担当,BOE(京东方)将砥砺奋进、创新不辍,为全球用户呈献超凡科技体验,领航全球产业创新发展的新篇章。

收起阅读 »

坚持背单词2000天,能带来哪些变化

近三年前,我写了一篇背单词坚持1000天的小总结,从三个方面整理了这坚持给我带来的变化: 一是背单词最本质用处,它让我认识的英文单词多了许多,我甚至能试着看看英文原版书;二是一种心理暗示,既然我能坚持每天背单词,那看书、摸球或是锻炼,也将是可以坚持的;三是多出...
继续阅读 »

近三年前,我写了一篇背单词坚持1000天的小总结,从三个方面整理了这坚持给我带来的变化:


一是背单词最本质用处,它让我认识的英文单词多了许多,我甚至能试着看看英文原版书;二是一种心理暗示,既然我能坚持每天背单词,那看书、摸球或是锻炼,也将是可以坚持的;三是多出一种对未来的展望,我很期待,背单词到2000天、5000天时我的改变。


图片


打卡1999天


时间过得真快,过完明天,我坚持背单词便满2000天了。


但当下的我,却想对1000天前的我说一句抱歉:“对不起,似乎你未来1000天的坚持,并没有为你带来更多改变,你还是你。你英文的听说读写,似乎和当时水平相差并不太远,你也并没有发展出更多的小坚持……”


当然不只有抱歉,“除了抱歉,你依然还拥有着希望。”


即便当下感受到的改变不多,我依然很期待,第3000天、第4000天、第5000天时我的改变。


从坚持背单词满1400天时起,之后的每一个100天,我都在掘金沸点上面打个卡,我很享受那种一天一天慢慢攒出一个整数值后有东西可以分享的感觉。其中,有一些骄傲,也有一点虚荣。


在第1800、1900天时,我便起了心思到2000天时再写一篇小总结。之后大概每隔两三周,我都会想一想这总结中该写些怎样内容:


“考研词汇对我来说很简单,进入复习阶段,一天背80个,有时候前面一个不错,到最后几个单词,我会很紧张。”


“背单词的时间,一直在变化。大多数是在出门等电梯,独自吃早餐或是吃完早餐后赶地铁、去工位的步行路上。那些长一些的独处时间,我更愿意用来看书,而不是背单词。”


(边走路边看手机,当然是一个不好习惯。)(于此处批评自己让我想到我会在某些时候进入“成人状态”,会对他人提出建议如“少喝些酒”“早点睡”之类,我隐隐感觉这种建议方式似乎正让我的行事方式越来越固化,晚点睡怎么了嘛?)(我简直要变成括号之神。最后这个括号说我思维杂乱,发散太快:我到底要不要删掉这几个括号呢?我决定不删。)


“《百词斩》,一直在进步。除了广告越来越多之外,他们还推出专门背单词的学习机,他们在单词详细界面添加了近义词、反义词、形近词;除了看图选词,他们还推出了汉译英、听音选词等更多帮助强化记忆的方式。”


“好几位朋友问过我,你背单词的目的是什么?出国?和外国人交流?还是怎样?我当时给予的答复都是:‘好像都不是,背单词对我来说只是一种提醒,提醒我别忘记坚持’。”


“背单词对我来说,似乎也变成跟吃饭睡觉一样,是每天都会做的事情,已经可以不用‘坚持’就能持续下去,这使得我已经连续561天不间断。这种稀疏平常的每日行为,似乎没什么总结好写的。”


一周以前,我决定本周公众号的更新主题为“坚持背单词2000天”,翻阅过去想法的汇集,却只有上面寥寥几句。这几句,只是一些零碎想法,当下的我似乎想要表达更多些。我在碎片时间想这个话题,最终冒出一个稍显负面但我却很想知道答案的问句:坚持背单词这么久,是为了什么呢?


这问题甚至被我更扩大些:坚持看书是为了什么?坚持写东西是为了什么?总之,坚持,是为了什么呢?


我将自己过去靠坚持开始到现在还一直在持续的事情一一列举。


坚持最久的一件事,是每天都写日记,它始于15年10月,累积8年半时间。我最初的想法,是“记录生活中的事情,以作总结用”,日记中片段被我用作总结的很少,它更多的用处,是帮我回忆起许多忘记的场景和情绪。


坚持背单词,2000天。


坚持周更公众号3年半。周更公众号,最初是为了获得睡后收入,睡后收入每天只有几分,但它慢慢变成我生活中很重要的一部分,我较远些的目标,是想着写一本完整的延续性强的细节丰富的诉说平凡的书。写公众号为收入,为练习写作,也为总结整理自己。是的,相较日记里的随意,公众号内容是更可以称之为总结的:我想得更多也尽量想得更广更深。


坚持每天看书近3年,《微信阅读》上的记录是“连续阅读581天”。我最初看书是为了让自己有深度,让自己公众号有内容可写,现在则演变成一种爱好,一种习惯;这爱好有些不好之处是它让我很依赖作者的想法,“作者在我脑中跑马”是常发生情况。


坚持每天11点前睡觉,一年又11个月。这习惯的养成,多少和读书有些关系,首先来自于从Why We Sleep中收获的认知:睡眠好了一切都好(我又多出一点自己的理解:睡眠好说明一切都还好);然后是《习惯的力量》,暗示、惯常行为和奖赏围成一个圈,早早洗漱暗示自己想要睡觉,准点睡觉是惯常行为,第二天醒来能量满满便是奖赏。


每天做10个俯卧撑,坚持20天。每天做俯卧撑这件事情,我尝试过很多次,也放弃过很多次,今年过年回家吃饭毫不控制导致肚子又大起来,我想借这坚持作为锻炼的入门提醒。


写日记、背单词、周更公众号、看书和睡觉,是我一直在坚持做着的事情。所以,坚持的意义到底是什么呢?


对书籍已经产生依赖的我想去找一找关于“坚持”主题的图书,简单搜索一番后没找到答案,于是只输出自己的当下理解。


首先,它是一种价值观的体现,我相信这每天的一点点积攒,是肯定会为我带来些提升的,不管是读写能力还是好的身体状态的更长时间延续。


其次,是一种人生不会停止的希望,我坚持做的这些事情,不管四十五十甚至六十七十岁,都可以一直进行下去;我很期待看到五年十年甚至三十年后还依然做着这些事情的自己的样子。


然后,是提醒自己思考的工具,我该为这每一份坚持加上一个目标一个方向?比如背单词是为考研或是雅思?是不是没有方向的坚持,只是在原地转圈呢?


然后,就没有更多想法了,我只告诉自己,且继续将这几件当下自己认为对自己有用事情坚持下去。


至于坚持的意义,等下一个1000天到来时,再想想。


作者:我要改名叫嘟嘟
来源:juejin.cn/post/7352091152584376331
收起阅读 »

三十而立却未立,缺少的是女朋友还是技术能力?

作为一个从事 Web 工作 8 年来的相关人员的一点心路历程,希望我的经历能给大家带来稍许乐趣。 迷茫,特别迷茫 俗话说得好:“岂能尽如人意,但求无愧于心”,工作 8 年来,我经常这样自我安慰。不过这并不影响我也经常感觉无所适从,烦闷与迷茫。尤其是到了一些特殊...
继续阅读 »

作为一个从事 Web 工作 8 年来的相关人员的一点心路历程,希望我的经历能给大家带来稍许乐趣。


迷茫,特别迷茫


俗话说得好:“岂能尽如人意,但求无愧于心”,工作 8 年来,我经常这样自我安慰。不过这并不影响我也经常感觉无所适从,烦闷与迷茫。尤其是到了一些特殊的年月节点,这种焦虑感总是更加强烈。


那到底有什么迷茫的呢?一言以蔽之,有了对比,就有了伤害。正如标题所言,女朋友和技术能力,换一个通俗的话,也可以叫“美女与金钱”,当然更常规的说法,是“家庭与事业”。


如果简单横向对比起来,我迷茫确实看起来不意外:



  • 我好歹也是正儿八经 985 大学软件工程方向本科毕业,也算是科班出身;

  • 工作了 8 年,不仅是被同学、绝大部分同行从业人员从薪资水平、发展前景、人际交往、生活质量等各方向甩在身后,甚至都比不上复读一年考上不知名二本学校、去年才毕业的表弟;

  • 没房没车,没有成婚,还背井离乡,漂泊千里之外;

  • 日子看起来浑浑噩噩,没有什么远大志向,也没什么乐衷的兴趣……


怎么就变成这样了呢,我觉得我有老老实实、脚踏实地地做事情啊。回想自己从业这些年:



  • 从一开始的 JSP + Spring MVC + MySQL 这套原始的 Java Web 开发;

  • 到当时外面还比较时髦的 MEAN(MongoDB、Express.js、Angular 和 Node.js);

  • 后来回归到 Angular + Spring 这套,然后改为现在常用的 Vue + Spring,其中还一度以为 WebFlux 会有大用;

  • 当然前几年除了做些全栈开发,还不得不兼备 K8s 相关一大套的运维技能;

  • TiDB、Redis、ES、Prometheus 什么的都要搞一搞,Flink 什么的也得弄一弄,加上一大堆第三方自动化、监控等工具的使用配置;

  • 现在没事时用 Python 写个脚本处理一些批量任务,自己搞搞 Flutter 练手自己用的 APP。


我都觉得自己还是挺厉害的,因为这些就没一个是学校里教的东西,都是出来挨打自学的。


但实际上的现状呢,我还是呆在一个电子厂里面,拿着千把块,做着鸡毛蒜皮的事情,下班就回到公司的宿舍,龟缩起来。这样 855 毫无意义的日子,居然一呆就是 8 年了。


“可怜之人必有可恨之处?”


那我当然是自以为是的可怜了,毕竟如果真得像我说的那样出色,是金子自然会发光了,也怎么可能愿意继续呆在这种地方,离最近的地铁站、火车站都要30多分钟公交的制造业工厂里面?


确实,扯开嘴巴滋哇乱叫谁不会,有什么因就有什么果了。



  • 大四的时候,跨专业自学准备心理学方向的考研,错过了秋招;没考上之后,当时的技术能力,已经不支撑找个满意的工作了。

  • 做中学,两年后的 18 年正是行业发展高潮,准备出去看看。结果年轻,血气方刚,在领导的 PUA 和自以为是没能干出一点功绩就离开,不满意,然后留下来。

  • 又之后的一年之余,已经发现技术水平和人生阅历和同行差距过大,还是骑驴找马。在得到几个 offer 之后,却不知原因突然想回老家城市,这些深圳广州的机会就莫名其妙放弃了,重庆的眼高手低又没找到满意的。

  • 之后疫情时代,在一些大城市比如 SH、SZ 等出现强烈的排外现象之后,越发想要回家。但重庆的互联网行业,和主流城市差距可太大了。当时当地政府甚至在大力发展实体制造业,老家区县招商建工厂,租 100 亩送 100 亩。

  • 疫情尾期和这两年,什么“前端已死”、行业落寞,找工作难度陡升,试想,什么样的公司会找一个 8 年工作经验的初中级前端?全栈?运维?……


去年我找工作从 5 月份找到 10 月份,沟通了 200 多个岗位,只有 20 多个接收了简历,约到 3 个网上面试,最后一个没过。除了一些需要线下面试的没法去,也有面试的匹配度也不够、岁数不够年轻等其他因素。8 年来最多就管理过不到 10 人的小团队,当然不到一年就结束了,也没有能力发展管理岗。


与自己和解是不是自欺欺人?


会不会有种“咎由自取”的感觉,我偶尔也会想:



  • 如果 18 年我去了深圳而不是听信领导的话留在了东莞这里,我的发展轨迹会不会有所改善?

  • 更有甚,如果大学不是脑袋一热为了自救去考什么心理学专业的研究生,好好学习技能找工作或者考本校,会不会又是另一番风景?

  • 甚至更早,如果当年高考没有发挥失常,或者要是考得更差一点,去个师范,实现我儿时的理想,成为一名教师,情感上是不是更能自洽?


有句网络流行语是这样说的:有人看段子,有人照镜子。曾几何时,我也这样觉得:



  • 反正现在没车没房没女友,离家又远没外债;

  • 物质能力虽不高,但消费欲望不强;

  • 不能为国家做大贡献,但也还没有给社会添乱;

  • 下班回宿舍看看视频、打打游戏、玩玩手机,偶尔出去打打球,散散步……


没有复杂的人际关系,没有太大的家庭工作压力,清闲时间也比较充足,简简单单三餐一宿,我明明很惬意的,也明明已经惬意了 8 年来。


——“你一个月多少工资?” 、“怎么才这点?”

——“你现在什么级别?” 、“怎么才这个级别?”

——“你开什么车?” 、“什么?你连驾-照都没有?”

——“你孩子几岁了?” 、“啊,你还单身?”

——“天啦,你怎么混成这样了?”
……


“人的悲喜并不相通,我只觉得他们吵闹”。“墨镜一带,谁都不爱”,我脑袋摇成螺旋桨,我飞走咯,千里之外~


未立,缺少的是女朋友?


我的看法认为:可能不是。


没有什么是一成不变的,比如年龄。我这个年纪可能不仅和更年轻的同行抢岗位抢不过,也可能在另一个相亲市场也抢不过。


虽然嘴巴上可能有的人觉得单身好,而且现在这个男女关系和社会认同比较复杂的时代。前段时候和老同学聊天聊到近况,他们都一直以为我是一个不婚主义者。当然,这并不影响我们老一辈甚至再老一辈亲戚的期盼,他们偶尔也会认为,结婚之后,一个人才成长了,他们才会放心。


你别说,你还真别说。这半年我没有写博客,也没有太多了解“行业寒冬”的发展情况,有一部分原因还真是因为年初聊见了个相亲对象。这对我是一个完全没有经历过的赛道,难得的是我感觉还不差,虽然发展极为缓慢,但还没有遇到网上那样的“悲惨经历”,当然,也可能是异地的原因。


我要经历这种事,只能是亲戚朋友帮忙,加上微信之后聊了聊,整体氛围很好,就这么聊了一个多月。本来过年的时候约个见面的,但没想到升级了,直接他们父母到我家来坐了坐,然后又邀请我父母去她家吃了饭。这在农村的意思就是老一辈的过场已经走完了,双方家长没有意见,我们能不能成、就全看自己了。


这半年虽然几乎天天都有聊,绝大多数情况下都很愉快,我也变得有些期待每次的聊天;平时也有礼尚往来,偶尔互有一些小惊喜小礼物;五一节我也回去见了面,牵牵小手,后来得知当天她出门之后才发现来例假、身体不适但还是陪我走了将近三万步的路、甚至没让我发现异样……


但问题的关键在于,似乎都没有聊到什么重点和关键的问题,没有实际的发展,感觉温度没有理想上升。仔细想想,把这每天和她相关的一两个小时删除掉,那和我这些年的日子几乎没什么区别,好像一样是挺自在惬意的,她甚至都没有给我一些需要我去翻视频学点“人情世故”才能处理的问题和情景。


本来以为是好事,但我的榆木脑袋才终于不得不承认异地一定是个大问题。所以到现在,我这股子想回家的心情就变成了内因和外因相结合的无懈可击的推力。但是却还没有热切到一拍脑袋裸辞先回家,再看天的程度。


未立,缺少的是技术能力?


我的看法认为:可能也不是。


虽然我个人学的东西有一点点乱,但怎么说呢,并不影响我自娱自乐。偶尔开发一个自用的小玩意儿,还盲目觉得挺有成就感。


而且,从实际情况来讲,现在的“技术能力”真的不是那么的重要,如果是做产品,可能一些经验能力也不可或缺,但会写代码的人,可是一抓一大把。


比如说,现在的 AI 大模型几乎是热到爆的话题,也算是百花齐放,也各自杀红了眼,现在的新东西,不说自己有个 AI,都不好意思大声讲话,新出的 PC 都挂上 AI PC,魅族都不做手机,改名为 AI 终端了。


作为普通用户和普通个人开发者角度来讲,现在使用这些大模型 API 其实非常便宜了。价格战百万 token 才几十块甚至几块钱,文本对话、文生图、图生文,也都有一定的可用性了。


但是呢,但是呢,能拿来做什么呢?有创造性的同行都已经借着东风,扶摇直上九万里了,我还在感慨好便宜啊,除了BAT平台,这两天还去零一万物、深度求索等平台注册了账号,部分也少少充值了些。但是,虽然好便宜啊,可是能用来做点什么呢?我还真的没有创造性。




既然都说到这里,也厚脸皮顺便说一句,最近弄了个比较简陋的,使用Flutter开发,支持诸多AI大模型API调用的,假装类似智能工作生活助手应用。顺带加上之前的极简记账、随机菜品、猫狗写真,放在 Github 上 Sanotsu/swmate ,虽然很简陋也不完善,但感兴趣的朋友可以看看。


智能助手功能展示.jpg


生活不需要别人来定义


可能“三十而立”意思是指人在三十岁前后有所成就。少年老成的例子很多,大器晚成的人物也不少,但到最后,这都是别人来定义的这个“立”的含义。


就如见世面,有的人是“周游列国、追求自由”,有的人是“四体勤、五谷分”,有的人的成就是“成家立业,香车美女环绕”,有的人是“著作等身”,也有的人却是成为“艾尔登之王”……外面的人看到的或许不同,但那份自己内心的快乐,是为了、也是应该能够取悦自己的。


今天是我三十岁生日,大概500天前我列了三十岁前想要完成的 10 件小事,结果当然只完成了小部分:



  • 体重减到正常 BMI 值;

  • 开发一个能自用的 APP/入门一门外语;

  • LOL 上个白金/LOLM 上个宗师;

  • 谈一次恋爱;

  • 出去旅游一次;

  • 换一份工作,换一个城市;

  • 补上自己的网站博客,整理自己的硬盘;

  • 看 10 本名著,并写下每本不多于 5000 字的读后感;

  • 完成一部中篇小说;

  • 完成 50 篇用心写的博文,可包含那 10 篇读后感。


人生是一条连续的时间线,除了起止点,中间这段旅程,并不会因为某一刻的变化而停下来,最多是慢下来;三十岁之前没有完成的事情,三十岁之后依旧可以去做;以前看得太重的东西,以后还可以改变很多;珍惜的事情太多,抱怨的时间太少;人生这段路,就这么些年,就该为自己走走看;路虽然走得不同,但走路的心情,却可以自己来定。


取悦自己真的比迎合他人要轻松和快乐许多。


共勉吧诸君,感谢垂阅。


作者:小流苏生
来源:juejin.cn/post/7385474787698065417
收起阅读 »

哭了,朋友当韭菜被割惨了

最近我的朋友,被某些知识付费坑得很惨。全程毫无干货可言。内容仅仅只适用于初级、或者说部分中级的程序员。为此,我的朋友交了大几千的学费,却收获甚微。 当然,你可能说,是你的朋友问题啊?你朋友烂泥扶不上墙,学习方法不对,别人都有很多成功的案例。什么offer收到...
继续阅读 »

最近我的朋友,被某些知识付费坑得很惨。全程毫无干货可言。内容仅仅只适用于初级、或者说部分中级的程序员。为此,我的朋友交了大几千的学费,却收获甚微。



当然,你可能说,是你的朋友问题啊?你朋友烂泥扶不上墙,学习方法不对,别人都有很多成功的案例。什么offer收到手酸,外包入大厂。




我买这些课就是为了学习,入门一些语言。知识付费很合理呀!!



于是我跟我朋友在微信彻夜长谈,有了如下分析


先说结论



请擦亮你的慧眼,你的一分一毫来之不易。不到迫不得已,才当学费



为什么这么说?


首先,不管你是想就业,还是想学习一些新的技术,网上都有例子,github上也会有前沿的项目提供学习。


类型结论
学习新技术某项技术开源出来,作为技术的布道者,恨不得你免费过去学习,然后你再发一篇文章,越来越多人学习你的技术。
就业简历包装无非就是抄抄抄,抄别人的优秀代码。github开源项目就非常合适

其次,你学费,一定要做到利益最大化。必须要有以下两点



  • 能学到大部分人都学不到的技术亮点。记住,是大部分人,一定要做到差异化

  • 能学到优秀的学习方法,push你前进。


开启慧眼


现在市面的学习机构,鱼龙混杂。,B站大学,某识xin球,某ke时jian 甚至,在某音上,都有那种连麦做模拟面试,然后引导你付费学习。


就业环境不好,买方市场竞争激烈,某些人就抓住你的焦虑心理,坑你一把。回想你的求学生涯,是否也有类似被坑经历?醒醒吧,少年。能救你的,只有你自己


当然,小海也会有潜龙。不可否认,知识付费为我们提供了便利性。



  • 原本散乱无章的知识点,人家给你整理好了,你尽管就是学习,实践

  • 面对焦虑,你觉得很迷茫,需要一个人指点你前进

  • 能认识更多同样诉求的人,为以后学习,就业,甚至做生意提供可能


但是,某些不法分子,就是抓住你的这个心理,疯狂ge你韭菜。什么10块钱知识手册,19.9面试题,100块钱的项目视频。天天一大早,就转发一些公众号到你群上,dddd。


这些内容,不是说没有用。我们讨论适合人群,这类东西不适合中高级程序员



说那么多,你得学会判断这个人是不是大佬




你都可以简历包装,为什么‘大佬’就不会是被包装的



那就稍微整理一下,哪些是真大佬,伪大佬


真伪大佬


某佬博客开源项目学习人群是否顺眼
伪大佬面试题居多,很多基础内容,没有干货无,或者很少。动不动就是商城,博客应届生占比较多可能顺眼
真大佬博客、论坛内容干货。整理分类完善,你能学到东西有,某些大项目的贡献,同时也有优秀开源项目应届生,中高级都有大多数不顺眼,因为实在优秀

就学习人群做一个说明



  • 在就业容易程度上,相对于初中高级别的程序员,应届生无论从考察的内容,招聘的人数。都会容易丢丢。

  • 他说跟着他学,offer赢麻了。但是其中,找到工作的大多数都是应届生


就这些点,我们其实可以能判断个大概了。


记住,你想知识付费。一定要摸清他的底细,不能认为他说得都是对的。人家也是会包装的


你的hello world


或许每个程序员的第一行代码,都是


    print("hello world")

我想说的是,请你记住你的初心。



  • 转行过来当程序员,就是为了狠狠赚他一笔

  • 喜欢写代码,苦中作乐


情况每个人都不太一样,这里不细说。明白你是谁,你还是否有动力能坚持下去。明白这一点,远比你在迷茫的时候病急乱投医更为重要,请勿过度焦虑


为此,后面会说一下如何学习,以及找工作如何不被骗


力量大会


事关钱包的问题,我们都得谨慎谨慎。就业市场那恶劣,朋友找不到工作还被坑了一把。骗子实在可恶。请你先自身强大,先自己找出问题,不花冤枉钱,避免传销式编程


如有雷同,纯属巧合,没有针对任何人,也没有动某些人的饭碗。


作者:Goland猫
来源:juejin.cn/post/7357231056288055336
收起阅读 »

勇敢的人先拿到结果

上周许久未见的大学学长叫我出去喝酒,他这次来贵阳是为开分店的事情而来的,他比我高一个年级,在我毕业的时候,他就自己开始做生意了,短短两三年,到现在他已经开了七八个分店了,还在不断发展,并且加盟的人也不少,平均下来,现在每个月的收入也是很可观的。 对于我们这种末...
继续阅读 »

上周许久未见的大学学长叫我出去喝酒,他这次来贵阳是为开分店的事情而来的,他比我高一个年级,在我毕业的时候,他就自己开始做生意了,短短两三年,到现在他已经开了七八个分店了,还在不断发展,并且加盟的人也不少,平均下来,现在每个月的收入也是很可观的。


对于我们这种末流二本院校毕业的学生,特别还是在贵州这个经济相对比较落后的地区,拿到这个成绩还是挺厉害的,并且这个收入并不是固定的,还是不断增长。


学长是学市场营销的,这也算是个天坑专业,所以那会他就知道自己将来肯定是从事不了这个行业的,所以自己就在宿舍开了一个小卖部,每天下课后就骑着电瓶车去送货,虽然每个月赚不了多少钱,但是对于做生意这一块,他的思维肯定是得到了锻炼。


因为我们是在广西读书,所以螺蛳粉就比较多,在毕业后,他就去柳州考察做螺蛳粉,联系好各种渠道后,回到贵州就直接开干。


因为那会贵州的各个市里面卖螺蛳粉的还很少,并且没有特色和品牌效应,所以自己就先设计名称,logo,最后先开了一个店铺,自己亲自下厨,因为比较有特色,一个月直接干到了全市螺蛳粉餐饮销量的第二名。


随后又开了第二家,第三家......别人在看到他赚了钱后,其它市区的人也纷纷向他学习,他自己就收加盟费用,现在他要做的事情就是玩,还有考察门店,然后扩展。


从他的事迹中,我说两个点。


勇于放弃


对于很多人而言,读书的目的就是为了找一份稳定的工作,最好是体制内。


如果你读完大学后出去做销售,做生意,那么对于你身边的很多人而言,他们会觉得你这个大学白读了,因为在他们眼中,只有坐在办公室里面才是最体面了。


你和他说做生意,创业这些东西,他会给你说:这些不稳,以后没有退休工资。


但是如果你真听他们的,那么后面后悔的一定是你。


就像学长,如果他也和别人一样毕业后回到自己那地方加入考编大军,那么他现在肯定和别人一样,也在背书,焦虑,但是他选择了其它的路。


这时候有些人就会抬杠:考上了就能吃一辈子,而你做生意如果运气不好那么就直接亏光,到时候你就知道编制的香了。


这也是很多人的通病。


我觉得如果一件事情你看不到希望,就别过于去迷恋它,舍不得它,不然会被它束缚,比如学历,经验等等。


敢想敢干


可能你会觉得他家里应该有底子的,不然毕业后怎么就能开店。


但是我们问一下自己,就算你家里有底子,毕业后就给你十万块让你开店,你觉得你行吗?恐怕大部分人都不知道自己该做什么吧。


首先躬身入局本身就是一件很难的事情,我们多数人能够拼命上班,但是如果让你脱离平台去自己干一件事就比登天还难。


因为你在公司有别人给你安排好,你去做就行了,换句话来说,你就是个干苦力的,真让你去谈判,去闯市场,大多数人是没这个能力的。


这也是一种损失厌恶心态,因为你怕自己花时间去做,到后面不仅亏了钱,还把自己弄得很累,而安安稳稳打工不一样,它是“稳赚不赔”的。


但是这个世界上很难有稳赚不赔的东西,就说安安稳稳打工拿工资,但是工资不高,那一定是在亏着走的,除非你觉得自己的时间毫无价值,那么就是赚的。


作者:苏格拉的底牌
来源:juejin.cn/post/7340898858556178432
收起阅读 »