注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

wasm真的比js快吗?

web
一. 前言 首先提一句话,本人是Rust新手!如果有什么不足的地方麻烦指出哈! 最近一直在玩Rust(摸鱼),本来是想着,多学一点,多练一练,之后把我们这边的一些可视化项目里面核心的代码用Rust重构一下。但是我最近在练习一个demo的时候,发现了跟我预期不一...
继续阅读 »

一. 前言


首先提一句话,本人是Rust新手!如果有什么不足的地方麻烦指出哈!


最近一直在玩Rust(摸鱼),本来是想着,多学一点,多练一练,之后把我们这边的一些可视化项目里面核心的代码用Rust重构一下。但是我最近在练习一个demo的时候,发现了跟我预期不一样的地方。


WechatIMG68186.png


具体如何,我用下面几个案例展开细说。


二. 案例1: 使用canvas绘制十万个不同颜色的圆


首先我想到的是,把canvas的复杂图像绘制功能用Rust重写一下。这里我用canvas绘制大量的圆形为例子。


2.1 Rust绘制canvas


跟上一篇文章的流程差不多,我们需要先新建一个Rust项目:


   cargo new canvas_circel_random --lib

然后更新一下Cargo.toml文件里面的依赖内容:


[package]
name = "canvas_circle_random"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["HtmlCanvasElement", "CanvasRenderingContext2d"] }

完成之后,我们简单在src/lib.rs写一点代码:


// 引入相关的依赖
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
use js_sys::Math;

// 给js调用的方法
#[wasm_bindgen]
pub fn draw_circles(canvas: HtmlCanvasElement) {
// 获取ctx绘画上下文
let context = canvas.get_context("2d").unwrap().unwrap().dyn_int0::<CanvasRenderingContext2d>().unwrap();
let width = canvas.client_width() as f64;
let height = canvas.client_height() as f64;

// 循环绘制
for _ in 0..100_0000 {
// 设置一下写x,y的位置
let x = Math::random() * width;
let y = Math::random() * height;
let radius = 10.0;
let color = format!(
"rgba({}, {}, {}, {})",
(Math::random() * 255.0) as u8,
(Math::random() * 255.0) as u8,
(Math::random() * 255.0) as u8,
Math::random()
);
draw_circle(&context, x, y, radius, &color);
}
}

fn draw_circle(context: &CanvasRenderingContext2d, x: f64, y: f64, radius: f64, color: &str) {
// 调用canvas的API绘制
context.begin_path();
context.arc(x, y, radius, 0.0, 2.0 * std::f64::consts::PI).unwrap();
context.set_fill_style(&JsValue::from_str(color));
context.fill();
context.stroke();```

}


简单解释一下代码:



  • 0..100_0000 创建了一个从 0 开始到 999,999 结束的范围注意,Rust 的范围是左闭右开的,这意味着它包含起始值但不包含结束值。

  • &JsValue::from_str(color)从变量中取值。


完成之后,我们去打包一下。


wasm-pack build --target web     

然后我们得到一个pkg包,如下图:


image.png


然后我们在项目中引入一下,具体流程可以看我上一篇文章。
回到我们的Vue项目中,我们引入一下:


import init, { draw_circles } from 'canvas_circle_random/canvas_circle_random'
onMounted(async () => {
await init();
const begin = new Date().getTime();
drawWasmCircle();
const end = new Date().getTime();
console.log('wasm cost time: ' + (end - begin) + 'ms');
})

之后我们打开一下页面:


image.png


多次加载了几次,加载范围大概在2750ms~2900ms之间。


2.2 使用js绘制canvas


const drawJsCircle = () => {
const canvas = document.getElementById('my-canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
for (let i = 0; i < 1000000; i++) {
drawRandomCircle(ctx, 800, 600);
}
}

const drawRandomCircle = (ctx: CanvasRenderingContext2D, width: number, height: number) => {
const radius = 10;
const x = Math.random() * (width - 2 * radius) + radius;
const y = Math.random() * (height - 2 * radius) + radius;
const color = `rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.random().toFixed(2)})`;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.stroke();
}


没什么好说的,有手就会。


然后我们在页面上试一下:


image.png
加载范围大概在1950ms~2200ms之间。


卧槽,难道说js的性能比wasm快???


然后我又对绘制的数量和绘制的形状做了多次实验:



  • 绘制10000个圆, wasm用时大概在1000ms,js用时大概在700ms。

  • 绘制100000个长方形wasm用时大概在1700ms, js用时在1100ms。


无一例外,在绘制canvas上面,js的性能确实优于wasm


三. 案例2:算法性能


考虑到通过canvas绘制图形来判断性能,有点太前端化了,我想可不可以通过写一些算法来做一下性能的比较。
试了很多算法,这里我用一下斐波那契算法,比较简单也比较有代表性。


在同级目录下新建一个Rust项目:


    cargo new fb-lib --lib

然后在fb-lib中修改一下Cargo.toml


[package]
name = "fb-lib"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"

把斐波那契数列代码写到src/lib.rs文件中:


use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fb_wasm(n: i32) -> i32 {
if n <= 1 {
1
}
else {
fb_wasm(n - 1) + fb_wasm(n - 2)
}
}

很简单,没什么好说的。完成之后,我们在项目中使用一下。


<script setup lang="ts">
import init, { fb_wasm } from 'fb-lib/fb_lib'
import { onMounted } from 'vue';

onMounted(async () => {
await init();
const begin = new Date().getTime();
fb_wasm(42);
const end = new Date().getTime();
console.log('wasm cost time: ' + (end - begin) + 'ms');
})

</script>

image.png


大概试了一下时间在1700ms~1850ms左右。


然后我们用js实现一下:代码如下:


import init, { fb_wasm } from 'fb-lib/fb_lib'
import { onMounted } from 'vue';

onMounted(async () => {
await init();
const begin = new Date().getTime();
fn_js(42);
const end = new Date().getTime();
console.log('js cost time: ' + (end - begin) + 'ms');
})

const fn_js = (n: number): number => {
if (n <= 1) {
return 1;
} else {
return fn_js(n - 1) + fn_js(n - 2);
}
}

然后我们在页面上看一下:


image.png


大概试了一下时间在2550ms~2700ms左右。


很明显,这时的wasm的性能是要优秀于js


四. 总结


大概试了一下canvas,dom操作,高性能算法(排序、递归)等。我大概得出了一个这样的结论:



  • Wasm代码比JavaScript代码更加精简,因此从网络上获取Wasm代码的速度更快。

  • 对于一些高性能的算法,在基数足够大的情况下,wasm的性能确实高于js,但是当基数比较小的时候,两者其实差不多。

  • 由于Wasm是一种二进制格式,需要将DOM操作的数据进行转换,才能在Wasmjs之间进行传递。这个数据转换过程可能导致额外的开销。以及 Wasm操作DOM时,需要通过js提供的API进行通信。每次调用js的API都会引入一定的开销,从而影响性能。所以在一些页面交互操作上,wasm的性能并不会优于js


综上,个人觉得wasmjs之间是一种互相选择互相依靠的关系,并不是取代的关系。日常开发中,也要结合实际情况选择不同的方式进行开发。


往期文章:前端仔,用rust来写canvas


作者:尝尝你的优乐美
来源:juejin.cn/post/7444450769488674825
收起阅读 »

8年前端,那就聊聊被裁的感悟吧!!

前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。 另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的...
继续阅读 »

前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。
另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的际遇


我的经历


第一家公司


第一家公司说来也巧,本来是准备入职一家外包的,在杭州和同学吃个饭,接到了面试通知,一看地址就在楼上,上去一共就3轮面试,不到2个小时直接给了offer。有些东西真的就是命中注定


第二家公司


第二家公司我入职以后挖了上家公司诸多墙角,我一共挖了6个前端,2个后端。拯救朋友们于水深火热之中。





我本以为我能开启美好的新生活,结果第二年就传来我父亲重病的噩耗 肺癌晚期,我学习了大量的肺癌知识什么小细胞,非小细胞,基因检测呀等等。。。





可是最后还是没有挽留住他的生命,我记得我俩在最后一次去武汉的时候,睡在一起,他给我说了很多。


他说:治不好就算了,只是没能看到自己的孙子有些可惜罢了。

他说:我这一辈碌碌无为,没给你带来多么优越的条件,结婚、买房、工作都没给到任何帮助,唯一让我感到欣慰的是你那么努力,比我强多了,家里邻居很多都眼馋你呢。
他说:你小孩的名字想好了吗?你媳妇真是个孝顺的孩子,性格也好,心地善良,你要好好对待她。

他说了很多。。。我都快忘了他说了啥了,我不想忘来着,可是可是,想起来就又好难过。


这只是我人生历程的一部分,我把这些讲出来,是为了让大家明白,你现在所经历的困苦其实没有那么严重,人在逆境之中会放大自己的困难,以博得同情。所以现在很多人给我倒苦水的时候,我总有点不屑一顾的感觉,并不是我有多强,我只是觉得都能过去。


在灰暗的时候,工作总是心不在焉,情绪莫名冲动,我和领导吵过架,和ui妹妹撕破脸,导致人家天天投诉我。我leader说我态度极其嚣张,我说你再多说一句,我干死你所以不裁我裁谁


我的人生感悟


我时常以我爸的角度换位思考,我在得知这个消息后我该咋办?是积极面对,还是放弃治疗?可是所有的都是在假设的前提之下,一切不可为真。只有在其中的才最能明白其中的感受。
那一年我看着他积极想活着的毅力,也看到了他身体日渐消瘦的无奈,无奈之余还要应付各种亲戚的嘘寒问暖


我现在很能明白《天道》中那段,丁元英说的如果为了孝顺的名声,让父亲痛苦没有尊严地活着,还不如让父亲走了。 的意思了。在他昏迷不醒的时候,大小便失禁的时候,真不如有尊严的走了。


我其实已经预感到自己要被裁,我原本是挺担心的,可是后来想想父亲的话,我总结成一句话圆滑对事,诚以待人。 这句话看上去前后矛盾,无外乎俩个观点。


圆滑对事的意思是:就是要学会嘴甜,事嘛能少干就少干,能干几分是几分,累的是别人,爽的是自己,在规则中寻求最大的自我利益化。


诚以待人的意思是:圆滑归圆滑,不能对谁都圆滑,你得有把事情办的很好的能力,你需要给真正需要的人创造价值,而不是为了给压榨者提供以自我健康为代价的价值。



用现在最流行的词来说就是「佛系」。


什么叫活明白了,通常被理解为不争不抢,得之淡然、失之泰然、顺其自然的一种心理状态。


活明白的人一般知道自己要什么样的生活,他们不世故、不圆滑,坦荡的、磊落的做自己应该做的事儿。他们与社会上潜规则里的不良之风格格不入,却不相互抵触,甚至受到局中人的青睐与欣赏。


活明白的人看着更为洒脱,得不张扬,失不气馁,心态随和、随遇而安。


不过,还有一种活明白的人,不被多数人所接受。他们玩世不恭、好吃懒做,把所有一切交给命运去背锅。这种人极度自我,没有什么可以超越他自己的利益,无法想象这种活法,简直就是在浪费六道轮回的名额。


总之,有的人活明白了,是调整自己的心态,维护社会的稳定和安宁。有的人活明白了,是以自我为中心,一边依赖着社会救济,一边责备社会龌蹉。


所以,活明白的人也分善与恶,同样是一种积极向善,另一种是消极向恶,二者同出而异名。



我对生活的态度


离职的第一个月,便独自一人去了南京,杭州,长沙,武汉,孝感。我见了很多老朋友,听听他们发发牢骚,然后找一些小众的景点完成探险。


在南京看了看中医,在杭州露营看了看日落,在长沙夜爬了岳麓山,在武汉坐了超级大摆锤,在孝感去了无名矿坑并在一个奶奶家蹭了中午饭。


我的感受极其良好,我体验了前所未有生活态度,我热情待人,嘻嘻笑笑,我站在山顶敞怀吹风,在无尽的树林中悠然自得,治愈我不少的失落情绪。我将继续为生活的不易奔波,也将继续热爱生活,还会心怀感恩对待他人,也会圆滑处事 事事佛系。


背景1.png


图层 1.png


IMG_6214.JPG


IMG_6198.JPG


IMG_6279.JPG


可能能解决你的问题


要不要和家里人说


我屏蔽了家里人,把负面情绪隐藏,避免波及母亲本就脆弱的内心世界,我还骗她说公司今年不挣钱,提前让我们放假,只给基础工资。如果你家境殷实,家庭和睦,我建议大方的说,这样你和父母又多了一个可以聊的话题,不妨和他们多多交流,耐心一些。


裁员,真不是你的问题


请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术,你得让自己变得精彩,别虚度了这如花般的时光。可能你懒,可能也没什么规划,那就想到啥就做啥好了,可能前几次需要鼓足干劲,后面就会发现轻而易举。


如何度过很丧的阶段


沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

不要总在家待着,要想办法出门,多建立与社会的联系,多和朋友吹吹牛逼,别把脸面看的那么重要,死皮赖脸反而是一种讨人喜欢的性格。



不管环境怎样,希望你始终向前,披荆斩棘

如果你也正在经历这个阶段,希望你放平心态,积极应对

如果你也在人生的至暗时刻,也请不要彷徨,时间总会治愈一切

不妨试试大胆一点,生活给的惊喜也同样不少

我在一个冬天的夜晚写着文字,希望能对你有些帮助



进群聊:技术、段子我都有





群已满,请加15757172732入群


作者:顾昂_
来源:juejin.cn/post/7331657679012380722
收起阅读 »

原来科技感的三维地图可以这么简单实现

web
前言 2024.02.20 下午摸鱼时接到一个客户的数字孪生项目的需求,客户什么也没说,就要求“炫酷”和“科技感”地图,还要把他们的模型都放上去,起初我以为又是一个可视化大屏的项目,准备用高德地图应付过去,然后他们又在群里发什么要求高之类的,我们的数据种...
继续阅读 »

前言


Screen-2023-06-15-135413_0001.gif



2024.02.20



下午摸鱼时接到一个客户的数字孪生项目的需求,客户什么也没说,就要求“炫酷”和“科技感”地图,还要把他们的模型都放上去,起初我以为又是一个可视化大屏的项目,准备用高德地图应付过去,然后他们又在群里发什么要求高之类的,我们的数据种类多,说什么高德、百度、Mapbox、Cesium之类框架都试过了,满足不了需求,好嘛,这下给我犯了难了,会的技术栈全都给我排除了, 手撸Three.js源码我可不干,于是就在网上晃悠,尝试找一些灵感



2024.02.24



又试了几个地图框架,还是不行,被客户和经理催了一顿,再不行他们要换人了



2024.02.28



在QQ群里闲逛,发现了群友发的一个叫 Mapmost SDK for WebGL 的地图框架,于是抱着试一试的态度做了一下,好家伙,一下就对客户味了


image.png



2024.03.04



后面我发现这个SDK蛮有意思,于是把我实现客户需求的过程记录下来,分享给大家


初始化


这个SDK看似是个商用软件,不过目前是免费试用,官网上申请一下就行了,然后按照他们的文档,填一下参数,就能初始化一个地图,和一般的地图SDK用法差不多


image.png


image.png


    <script src ='https://delivery.mapmost.com/cdn/sdk/webgl/v3.5.0/mapmost-webgl-min.js'></script>
<script>
let mapmost = window.mapmost
/*
* 初始化地图
*/

let map = new mapmost.Map({
container: 'map', //地图容器
style:'http://192.168.126.44/mms-style/darkMap.json', //矢量底图
center: [120.74014004382997, 31.32975410974069], //地图中心点
bearing: 50.399999999999636, //方位
pitch: 78.99999999999993, //倾斜角
zoom: 19.964625761228117, //缩放
userId: '***',

})
</script>



image.png


不过,在此之前,要把底图中的矢量建筑图层隐藏掉,客户要加载真正的建筑三维模型。


代码和效果图如下:


const buildingLayers = [
'buildings-di',
'buildings-faguang-1',
'buildings-faguang-2',
'buildings-faguang-3',
'buildings-high',
'buildings-high-top',
'buildings-low',
'buildings-low-top',
'buildings-lowmid',
'buildings-lowmid-top',
'buildings-mid',
'buildings-mid-top',
'buildings-midhigh',
'buildings-midhigh-copy',
]

map.on('load', (e) => {
buildingLayers.forEach((layer, index) => {
let layerObj = map.getLayer(layer)
map.setLayoutProperty(layerObj.id, 'visibility', 'none');
})
})


image.png


加载建筑三维模型


这里我们准备了城市建筑的模型,格式为glb,按照文档描述,我们添加一个idmodelLayer的三维图层,代码和效果图如下


//...

/*
* 加城市f载建筑模型
*/


let Gr0up = null
let Layer = null

let models = ["./model/YQ.glb"].map(item => ({
type: 'glb',
url: item
}));

map.on('load',(e) => {
let modelOptions = {
id: 'modelLayer',
type: 'model',
models: models,
sky: "./sky/satara_night_no_lamps_2k.hdr",
exposure: 2.4,
center: [120.74155610348487, 31.328251735532746, 0],
callback: (group, layer) => {
Gr0up = group
Layer = layer
}
};

map.addLayer(modelOptions);
})


image.png


image.png


添加三维效果


接下来就是客户的G点了,为三维场景添加特效:


添加建筑流光渐变效果


参考SDK的接口文档,给建筑加上流光渐变的效果


image.png


image.png


定义一个添加特效的函数addModelEffect,然后按照文档上的参数说明来配置相关属性


 const  addModelEffect = () =>{
Layer.addModelEffect(Gr0up, [{
type: "gradient",
startColor: "rgba(63, 177, 245,.5)",
endColor: "rgba(58, 142, 255,.8)",
opacity: 0.8,
percent: 0.5
}, {
type: "flow",
speed: 1,
color: "rgba(241, 99, 114, .4)",
opacity: 0.8,
percent: 0.05
}])
Gr0up.addFrame(0x3FB1F5);

}


然后我们在模型加载完成后调用这个函数:



//...

map.on('load',(e) => {
let modelOptions = {
id: 'modelLayer',
type: 'model',
models: models,
sky: "./sky/satara_night_no_lamps_2k.hdr",
exposure: 2.4,
center: [120.74155610348487, 31.328251735532746, 0],
callback: (group, layer) => {
Gr0up = group
Layer = layer
addModelEffect()
}
};

map.addLayer(modelOptions);
})

效果如下:


image.png


image.png


添加粒子飞线


同样,在SDK的文档上找到添加流动线的接口,定义一个addFlowLine的函数,然后按照要求配置参数:



这里我们借助了一个生成贝塞尔曲线的函数,以及一些随机的坐标点位数据。
他们的作用是为我们提供必要的模拟数据



import { getBSRPoints } from './bezierFunction.js'
import { flowLineData } from './flowLineData.js'

//...

const addFlowLine = () => {
//生成贝塞尔曲线测试数据
let data_trail1 = getBSRPoints(120.71541557869517, 31.316503949907542,
120.73787560336916, 31.321925190347713, 800);
let data_trail2 = getBSRPoints(120.71541557869517, 31.316503949907542,
120.72619950480242, 31.33360076088249, 1500);
let data_trail3 = getBSRPoints(120.71541557869517, 31.316503949907542,
120.69933418653403, 31.332725809914024, 900);

[data_trail1, data_trail2, data_trail3].map(data => {
Layer.addFlowLine({
type: "trail",
color: '#1ffff8',
speed: 4,
opacity: 0.9,
width: 8,
data: {
coordinate: data
}
});
})

flowLineData.map(data => {
Layer.addFlowLine({
type: "flow",
color: '#ff680d',
speed: 4,
opacity: 1,
percent: 0.08,
gradient: 0.02,
width: 5,
data: {
coordinate: data
}
});
})
}


同样,我们在模型加载完成后调用这个函数:



//...

map.on('load',(e) => {
let modelOptions = {
id: 'modelLayer',
type: 'model',
models: models,
sky: "./sky/satara_night_no_lamps_2k.hdr",
exposure: 2.4,
center: [120.74155610348487, 31.328251735532746, 0],
callback: (group, layer) => {
Gr0up = group
Layer = layer
addModelEffect()
addFlowLine()
}
};

map.addLayer(modelOptions);
})

效果图如下:


image.png


image.png


添加特效球


类似的,文档上的添加特效球的接口,给场景里添加两个”能量半球“


image.png


代码和效果图如下:



const addSphere = () => {
let sphere = Layer.addSphere({
color: "rgb(53, 108, 222)",
radius: 3300, //半径,单位米,默认为10米
segment: 256, //构成圆的面片数
phiLength: Math.PI,
speed: 3,
opacity: 1,
center: [120.67727020663829, 31.31997024841401, 0.0]
});

let sphere2 = Layer.addSphere({
color: "rgb(219, 74, 51)",
radius: 2300, //半径,单位米,默认为10米
segment: 256, //构成圆的面片数
phiLength: Math.PI,
speed: 6,
opacity: 1,
center: [120.67727020663829, 31.31997024841401, 0.0]
});
}


//...

map.on('load',(e) => {
let modelOptions = {
id: 'modelLayer',
type: 'model',
models: models,
sky: "./sky/satara_night_no_lamps_2k.hdr",
exposure: 2.4,
center: [120.74155610348487, 31.328251735532746, 0],
callback: (group, layer) => {
Gr0up = group
Layer = layer
addModelEffect()
addFlowLine()
addSphere()
}
};
map.addLayer(modelOptions);
})


image.png


环境效果调优


仔细看整个环境,发现天是白的,和整体环境不搭配


image.png


更改一下地图初始化时的参数,将天空设置为暗色:


let map = new mapmost.Map({
container: 'map', //地图容器
style: 'http://192.168.126.44/mms-style/darkMap.json', //矢量底图
center: [120.74014004382997, 31.32975410974069], //地图中心点
bearing: 60.399999999999636, //方位
pitch: 78.99999999999993, //倾斜角
zoom: 14.964625761228117, //缩放
sky: 'dark' //天空颜色
})


然后整体效果如下:


image.png


如果觉得场景本身太亮,可以降低添加模型时的曝光度:



let modelOptions = {
exposure: .4,
callback: (group, layer) => {
//...
}
};


这样整体环境就会偏暗一点,更有黑夜下的赛博朋克城市的味道


image.png


当然,在这我又换了一张更暗的底图:


image.png


最后,再调整一下特效球的半径和位置,行了,这就是客户喜欢的样子,哈哈哈,2小时搞定,而且不用手撸Three.js代码:


  const addSphere = () => {
let sphere = Layer.addSphere({
color: "rgb(53, 108, 222)",
radius: 3300, //半径,单位米,默认为10米
segment: 256, //构成圆的面片数
phiLength: 180,
speed: 3,
opacity: 1,
center: [120.67943361712065, 31.306450929918768]
});

let sphere2 = Layer.addSphere({
color: "rgb(219, 74, 51)",
radius: 2300, //半径,单位米,默认为10米
segment: 256, //构成圆的面片数
phiLength: 180,
speed: 6,
opacity: 1,
center: [120.67727020663829, 31.31997024841401, 0.0]
});
}


image.png


总结


仔细看,不难发现,这个SDK集成了 Mapbox 和 Three.js 的核心功能,主打的是一个高颜值的三维地图引擎,当然除了好看之外,其他地图框架该有的功能它也具备,只是官网给人的感觉过于粗糙,不够吸引人;另外产品试用的门槛有些高,希望后面能优化吧


image.png


image.png


作者:清风夜半
来源:juejin.cn/post/7342279484488138802
收起阅读 »

盘点一下用了这么长时间遇到的Wujie 无界微前端的坑

web
目前也用无界微前端很长时间了,当时选择无界没有选择乾坤的原因就是无界的保活模式更加丝滑,客户就想要这种方式,但是在这段过程也遇到了很多问题,甚至有些就是无界目前解决不了的问题,所以希望大家今后遇到了也能提前避免 已经解决的问题 1、子应用使用wangEdit...
继续阅读 »

目前也用无界微前端很长时间了,当时选择无界没有选择乾坤的原因就是无界的保活模式更加丝滑,客户就想要这种方式,但是在这段过程也遇到了很多问题,甚至有些就是无界目前解决不了的问题,所以希望大家今后遇到了也能提前避免



已经解决的问题


1、子应用使用wangEditor 在主应用中无法编辑 ,无法粘贴,工具栏无法正常使用


✨问题复现



  • 子项目中正常


子项目的wangEditor.gif



  • 主项目无法选中,无法粘贴


主项目的wangEditor.gif


✨出现这种问题的原因:



  1. 子应用运行在 iframe内,dom在主应用的shadowdom中,当选中文字时,在主应用监听selectionchange,并且通过 document.getSelection()获取选中的selection,在wangEditor中 会判断这个 selection instanceof window.Selection,很明显主应用的selection 不可能是 iframe 里面window Selection的实例,所以出现了问题

  2. shadowDom 大坑,在shadowDom中 Selection.isCollapsed永远为true,相当于永远没有选中,所以只能修改 wangEditor 的代码,让读取 isCollapsed 修改成 baseOffset 和 focusOffset的对比,就知道是否选中了文字了


✨解决方案


1、将 wangeditor 替换成 wangEditor-next,因为wangeditor 作者大大已经说因为种种原因后面不会再继续更新了,所以担心更新的同学可以使用 wangEditor-next 这个还有人在继续更新


2、直接替换一个富文本组件 vue-quill,我使用的就是这个,因为我们项目对富文本的需求没有那么重,只要能用就行,所以你也可以替换一个在无界中没有问题的富文本组件


3、由此我们知道了在无界中getSelection是有问题的 如果遇到了可以用这个插件尝试解决 wujie-polyfill.github.io/doc/plugins…


2、子应用使用vue-office 的 pdf 预览,在主应用中白屏


✨问题复现



  • 子应用 正常显示


pdf-1.jpg



  • 主应用直接加载不出来


pdf-2.jpg


✨解决方案:


直接换个轮子,因为vue-office 源码不对外开放,你根本不知道是它内部做了何种处理,所以最好的办法就是直接换个其他的能够预览pdf 的方式,我这边选择的是 kkfile 的方式,kkfile 不仅可以预览pdf 还可以预览很多其他的格式的文件,让后端去生成预览地址 然后前端直接用 iframe 去打开即可


3、开发环境下 vite4 或者 vite5 子应用的 element-plus的样式丢失或者自己写的:root 不生效


✨问题复现



  • 子应用正常


css.jpg



  • 主应用样式丢失


css2.jpg


✨出现这种问题的原因:


主要的原因是因为子应用的 :root 失效了,因为无界中是将:root 转成了:host ,但是如果你是在main.js 中外链的样式


import 'element-plus/dist/index.css'

这样的话无界将无法劫持到将 :root 转成:host
css3.jpg


✨解决办法:


增加插件


 <WujieVue
width="100%"
height="100%"
name="pro1"
:url
:sync="true"
:exec="true"
:alive="true"
:props="{ jump }"
:plugins="[{
patchElementHook(element, iframeWindow) {
if (element.nodeName === "
STYLE") {
element.insertAdjacentElement = function (_position, ele) {
iframeWindow.document.head.appendChild(ele);
};
}
},
}]"
>
</WujieVue>

如果不生效请清除缓存重新启动,多试几次就行


4、el-select 位置偏移以及 el-table tootip 位置偏移的问题


✨问题复现:


select.jpg


✨出现这种问题的原因:


el中依赖了poper.js 的fixed定位偏移,子应用的dom挂载在shadowRoot上,导致计算错误


官网解决办法


select-2.jpg


试了,发现没啥用,所以维护官网的大大请更新快点,要不然哪个还想用无界啊!!


✨最后解决办法:


使用插件 wujie-polyfill.github.io/doc/plugins…


// import { DocElementRectPlugin } from "wujie-polyfill";
<WujieVue
width="100%"
height="100%"
name="xxx"
:url="xxx"
:plugins="[DocElementRectPlugin()]"
>
</WujieVue>

我的element-plus 的版本:"element-plus": "^2.9.0",


这个版本比较新,如果你们使用的是比较老的版本或者使用的是element-ui的话,上面的插件可能不生效,可以看下面的解决方案


github.com/Tencent/wuj…


总结下来无非是几个办法,要么是改element-ui 中的源码,然后在项目中打补丁


要么直接在子应用加代码


body{position: relative !important} 
.el-popper {position: absolute !important}

大家都可以都试一下,说不准哪个生效了


5、异步获取e.target 的 e.target 变成了 wujie-app


✨问题复现:


eTarget.jpg


eTarget2.jpg


官网文档方法
eTarget3.jpg


上面尝试了不行


✨最后解决办法:


使用 插件 wujie-polyfill.github.io/doc/plugins…


import { EventTargetPlugin } from "wujie-polyfill";
// vue
<WujieVue
width="100%"
height="100%"
name="xxx"
:url="xxx"
:plugins=“[EventTargetPlugin()]”
></WujieVue>

完美解决


6、全局样式污染了子应用元素的样式


✨问题复现:


scope.jpg


scope2.jpg


✨最后解决办法:


比如主项目中你给 html 或者 body 文字居中,在子项目中也会受影响,其实这个不算是框架的问题,因为你写了这个是全局样式,那就说明了这个会影响所有的,所以建议大家样式尽量写scope 并且对全局的样式尽量主项目和子项目同步,不要出现不一样的情况,要不然很难去排查这种问题


目前还没找到解决办法的问题:


1、自定义事件


很多项目集成了第三方sdk 比如埋点、错误监控、数据通信,其中sdk 可能会使用了js 的自定义事件,这个时候在子组件中会失效


✨问题复现:


const customEvent = new CustomEvent('update', {
bubbles: true,
composed: true,
detail: {
msg:'我的数据更新喽'
}
})
setTimeout(() => {
console.log(window.__WUJIE_RAW_WINDOW__,'window.__WUJIE_RAW_WINDOW__');
window.dispatchEvent(customEvent)
window.__WUJIE_RAW_WINDOW__ && window.__WUJIE_RAW_WINDOW__.dispatchEvent(customEvent)
}, 2000)


window.addEventListener('update', function(event) {
// 主应用没有反应,子组件中正常
console.log(event)
})
window.__WUJIE_RAW_WINDOW__ && window.__WUJIE_RAW_WINDOW__ .addEventListener('testEvent', function(event) {
// 主应用没有反应,子组件中正常
console.log(event)
})

会发现使用 window.addEventListener 或者 window.WUJIE_RAW_WINDOW .addEventListener 都没有用


✨出现这种问题的原因:


看了issue中作者说这个是无界中的bug ,所以如果有子组件用到这个自定义事件的,只能先将子组件用iframe 嵌入进去,等作者更新了再改回来


2、主应用路由切换和子应用路由切换点击浏览器退回没反应


✨问题复现:


官方示例:wujie-micro.github.io/demo-main-v…


大家也可以试一下,先点击主应用中的左侧vite 下的页面切换,然后再点击子应用中中间的页面切换,会发现需要点击两次浏览器的返回才能正常返回,可以看到我录屏下的点击返回前进和退回都没反应,只有多点一次才可以,目前还没找到好的解决办法,如果大家有办法解决可以告诉我。


两次点击.gif


结语


用了无界这么长时间给我的感觉还是比较好的,子应用保活非常丝滑,开箱即用,子应用基本上不需要更改任何代码直接可以继承到无界之中,这个优点真的是非常棒!不像qiankun 还得写很多适配的代码,当然qiankun 的社区真的比无界强大很多,很多问题你都能找到解决方案,只能说各有优缺点吧,主要看你自己怎么选择了


作者:前端摸鱼杭小哥
来源:juejin.cn/post/7444134659719610380
收起阅读 »

把鸽鸽放到地图上唱跳rap篮球需要几步?

web
事情的起因 最近在做地图类的创意应用没什么灵感,有一天晚上看到我弟弟玩游戏,发现机箱里有个ikun手办,这创作灵感就来了,鸽鸽+唱歌跳舞有没有搞头? 说做就做,WebStorm启动 1.初始化地图 这里的地图框架我用的是Mapmost SDK for We...
继续阅读 »

事情的起因


最近在做地图类的创意应用没什么灵感,有一天晚上看到我弟弟玩游戏,发现机箱里有个ikun手办,这创作灵感就来了,鸽鸽+唱歌跳舞有没有搞头?


image.png


说做就做,WebStorm启动


image.png


1.初始化地图


这里的地图框架我用的是Mapmost SDK for WebGL,代码和效果如下



官网地址:http://www.mapmost.com




<script src="https://delivery.mapmost.com/cdn/sdk/webgl/v9.3.0/mapmost-webgl-min.js"></script>
<script>
let map = new mapmost.Map({
container: 'map',
style: 'https://www.mapmost.com/cdn/styles/sample_data.json',
center: [120.71330725753552, 31.29683781822105],
zoom: 16,
userId: '*******************', //填入你自的授权码
pitch: 60,
bearing: 75,
sky:"light",
env3D:{
exposure:0.3,
defaultLights: true,
envMap: "./yun.hdr",
}
});
</script>


image.png


2.设置地图样式


这里为了和模型本身的颜色契合,我隐藏了一些图层,然后把水系、道路、和陆地的颜色改了,代码和效果如下


image.png


 //更改背景、水系、道路配色
map.setPaintProperty('bacground', 'background-color', 'rgb(159, 208, 137)')
map.setPaintProperty('ground_grass2', 'fill-color', 'rgb(103, 173, 144)')
map.setPaintProperty('water_big', 'fill-color', 'rgb(106, 190, 190)')
map.setPaintProperty('water_samll', 'fill-color', '#ADDCDF')
map.setPaintProperty('road_city_polygon-tex', 'fill-color', '#F1ECCC')
map.setPaintProperty('ground_playground', 'fill-color', '#FBD9E1')

//隐藏道路名
map.setLayoutProperty('road_metroline_line','visibility','none')
map.setLayoutProperty('road_metro_line', 'visibility', 'none')
map.setLayoutProperty('road_metroline_name', 'visibility', 'none')
map.setLayoutProperty('road_metro_name', 'visibility', 'none')
map.setLayoutProperty('road_city_name', 'visibility', 'none')
map.setLayoutProperty('road_country_name', 'visibility', 'none')
map.setLayoutProperty('road_others_name', 'visibility', 'none')

image.png


3.加载模型和图标


然后从网上下载了鸽鸽的obj模型


image.png


直接加载上去,这里作为模型图层添加,用法参考下面的文档:



http://www.mapmost.com/mapmost_doc…




//定义模型对象
let models_obj = [{
type: 'obj',
url: "./XHJ.obj",
mtl: "./XHJ.mtl",
}]

//配置模型图层参数
let options = {
id: 'model_id',
type: 'model',
models: models_obj,
center: [120.71482081366986, 31.299511106127838, 145],
callback: function (group, layer) {
}
};

//添加图层
map.addLayer(options);

鸽鸽就这样水灵灵的出现了


image.png


然后我们加几个图标上去,这里利用geojson数据,添加的自定义图层


image.png


//增加ikun图标
map.addSource("ikun", {
"type": "geojson",
"data": "./ikun.geojson"
})
map.loadImage('./111.png', (error, image) => {
if (error) {
throw error;
}
map.addImage('icon', image)
map.addLayer({
"id": "icon_ikun",
"type": "symbol",
"source": "ikun",
"layout": {
"icon-image": "icon",
"icon-size": 0.15,
"visibility": "visible"
}
})
})

好了,大功告成了
image.png


后续:如何实现唱跳rap篮球?


当然只看模型肯定不行,主要得让鸽鸽动起来,唱、跳、rap、篮球一样不能少
怎么办,MasterGO启动, 急的我UI和交互都给一起做了,不会UI设计的开发不是好前端。


image.png


image.png


后面越想功能越多,干脆搞个小游戏算了,游戏名字我都想好了,叫:唤醒鸽鸽,输入不同的口令,激活鸽鸽不一样的动作,伴随着地图一起舞动。


image.png


但是开发遇到了点困难,手机和模型材质的适配还在解决中....,另外模型骨骼动画有点僵硬,这个等我解决了再给大家分享,目前的效果还比较粗糙:


Screen-2024-12-18-152936_0001.gif


Screen-2024-12-18-152936_0002.gif


如果大家想到什么好玩的功能,也可以评论区讨论一下,不过一定要与地图结合才好玩。


关于代码:源码地址在这


基础场景的代码我先发给大家:
链接: pan.baidu.com/s/1G-r5qIXN… 提取码: y1p5




完整的代码等我解决掉bug了再分享


作者:清风夜半
来源:juejin.cn/post/7449599345371283482
收起阅读 »

来自全韩国最好的程序员的 Nest.js 中 TypeSafe 方案

web
Nest.js 中的 TypeSafe 方案 在现代 Web 开发中,类型安全(TypeSafe)是提升代码质量和减少运行时错误的关键因素。 Nest.js 作为一个渐进式的 Node.js 框架,结合了 TypeScript 的强大功能,提供了构建高效、可扩...
继续阅读 »

Nest.js 中的 TypeSafe 方案


在现代 Web 开发中,类型安全(TypeSafe)是提升代码质量和减少运行时错误的关键因素。


Nest.js 作为一个渐进式的 Node.js 框架,结合了 TypeScript 的强大功能,提供了构建高效、可扩展服务器端应用的理想平台。


笔者在使用 Nest.js 构建全栈应用时最大的痛点是写了这么多类型检查,好像没有办法和前端通用啊~。相信许多人也有这个问题,所以也冒出了在 Nest.js 中集成 tRPC 的教程。


而本文介绍的 Nestia,一个专为 Nest.js 设计的类型安全解决方案,帮助开发者在构建应用时实现更高的类型安全性和开发效率。


韩国最好的程序员


Jeongho Nam,GitHub用户名为 samchon他在 README 中自称为韩国最好的程序员。他自1998年开始编程,拥有25年的丰富经验。在这段时间里,他开发了许多程序,并不断提升自己的技能。他不仅在工作中开发程序,还在业余时间开发开源项目,以满足自身需求或改进现有功能。这些开源项目逐渐形成了新的开源库,其中最著名的就是 typianestia


什么是Nestia?


Nestia 是一个专为 Nest.js 开发的库,旨在通过利用 TypeScript 的类型系统,提供更高效和类型安全的开发体验。Nestia 的核心目标是简化数据传输对象(DTOs)的定义和验证,减少类型错误,并提升代码的可维护性。


Nestia的主要功能



  1. 类型安全的 DTO 定义和验证


    Nestia 利用 TypeScript 的类型系统,允许开发者定义类型安全的 DTOs。通过自动生成的类型定义,Nestia 确保了数据在传输和处理过程中的一致性,避免了常见的类型错误。


    NestJS需要三个重复的 DTO 模式定义。第一个是定义 TypeScript 类型,第二个和第三个是调用 class-validator@nestjs/swagger 的装饰器函数。这不仅繁琐,而且容易出错。如果你在第 2 或第 3 处写错了的话,TypeScript 编译器是无法检测到的。只有在运行时才能检测到。换句话说,它并不是类型安全的。


  2. 自动生成API客户端


    Nestia 可以根据服务器端的 API 定义,自动生成类型安全的 API 客户端。这种方式不仅减少了手动编写客户端代码的工作量,还确保了前后端的一致性


    这一功能与著名的 tRPC 库有相似之处。tRPC 是一个端到端类型安全的 RPC 框架,它允许你轻松构建和使用完全类型安全的 API,无需模式或代码生成。tRPC 的主要作用是在全栈 TypeScript 项目中提供类型安全的 API 调用,大大提高了开发效率和代码质量。但 tRPC 的问题是通常要求前后端代码位于同一个 monorepo 中,以便共享类型定义。这种紧耦合的架构可能不适合所有项目,特别是那些前后端分离开发或需要为第三方提供 API 的场景。相比之下,Nestia 通过自动生成独立的 API 客户端解决了这个问题。它允许你在保持类型安全的同时,将生成的 SDK 作为独立的包分发给客户端开发者。这种方式既保留了类型安全的优势,又提供了更大的灵活性,使得 Nestia 在更广泛的项目结构和开发场景中都能发挥作用。


  3. 高效的JSON序列化和反序列化


    Nestia 提供了高效的 JSON 序列化和反序列化功能,利用 TypeScript 的类型信息,显著提升了性能和类型安全性。



如何使用Nestia


安装Nestia


你可以运行以下命令通过模版代码来快速上手 Nestia。
模板将自动构建在<directory>中。作为参考,这是一个最小的模板项目,只集中于从 NestJS 服务器生成 SDK。它不包含数据库连接。


npx nestia start <directory>

你也可以运行下面的命令来将 Nestia 集成至现有的项目中。设置向导将自动安装并配置所有内容。


npx nestia setup

定义类型安全的DTO


你不需要掌握特殊的语法只使用 TypeScript 就可以编写一个带有类型检查的 DTO。当然,Nestia 也通过 Typia 提供了编写复杂类型检查的可能,例如,我们可以定义一个论坛完整的 DTO:


import { tags } from "typia";

export interface IBbsArticle {
/* Primary Key. */
id: string & tags.Format<"uuid">;

/* Title of the article. */
title: null | (string & tags.MinLength<5> & tags.MaxLength<100>);

/* Main content body of the article. */
body: string;

/* Creation time of article. */
created_at: string & tags.Format<"date-time">;
}


Controller 中调用 Nestia 的装饰器


NestJS 原生的装饰器(如 @Get(), @Post() 等)虽然使用方便,但在性能和类型安全方面存在一些局限:



  • 使用 class-validator 和 class-transformer 进行验证和转换,性能相对较低

  • 需要定义额外的 DTO 类和装饰器,增加了代码量

  • 类型安全性不够强,运行时可能出现类型错误


Nestia 的装饰器(如 @TypedRoute.Get(), @TypedBody() 等)则解决了这些问题:



  • 利用 typia 库进行高性能的运行时类型验证,比 class-validator 快 20,000 倍

  • 支持使用纯 TypeScript 接口定义 DTO,无需额外的类定义

  • 在编译时进行类型检查,提供更强的类型安全性

  • JSON 序列化速度比 class-transformer 快 200 倍


import { TypedRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";

import { IBbsArticle } from "./IBbsArticle";

@Controller("bbs/articles")
export class BbsArticlesController {

@TypedRoute.Get("random")
public async random(): Promise<IBbsArticle> {
return {
id: "2b5e21d8-0e44-4482-bd3e-4540dee7f3d6",
title: "Hello nestia users",
body: "Just use `TypedRoute.Get()` function like this",
created_at: "2023-04-23T12:04:54.168Z",
files: [],
};
}

@TypedRoute.Post()
public async store(
@TypedBody() input: IBbsArticle.IStore,
): Promise<IBbsArticle> {
return {
...input,
id: "2b5e21d8-0e44-4482-bd3e-4540dee7f3d6",
created_at: "2023-04-23T12:04:54.168Z",
};
}
}

自动生成API客户端


Nestia可以根据服务器端的API定义,自动生成类型安全的API客户端。在根目录配置 nestia.config.ts 文件


import { INestiaConfig } from '@nestia/sdk';
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter } from '@nestjs/platform-fastify';

import { AppModule } from './src/app.module';

const NESTIA_CONFIG: INestiaConfig = {
input: async () => {
const app = await NestFactory.create(AppModule, new FastifyAdapter());
app.setGlobalPrefix('api');
return app;
},
output: 'src/api',
clone: true,
distribute: 'sdk',
};
export default NESTIA_CONFIG;

运行命令 npx nestia sdk 即可生成 SDK package,你可以直接在 Monorepo 中使用它,也可以将其分发到 npm.


尝试使用生成的 SDK 看到类型提示的那一刻,整个人都通畅了~


703_1x_shots_so.png


结论


Nestia 作为一个专为 Nest.js 设计的类型安全解决方案,通过简化 DTO 定义和验证、自动生成 API 客户端以及高效的 JSON 序列化和反序列化功能,帮助开发者在构建应用时实现更高的类型安全性和开发效率。无论是大型企业级应用还是个人项目,Nestia 都是提升代码质量和开发效率的理想选择。


通过本文的介绍,希望您对 Nestia 有了更深入的了解,并能在您的 Nest.js 项目中尝试使用这一强大的工具,享受类型安全带来的诸多好处。


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

准备离开杭州

上个月的时候,我被公司裁掉了,陆陆续续找了 1 个月的工作,没有拿到 1 份 Offer,从网上看着各式各样的消息和自己的亲身体会,原来对于像我这样的普通打工族,找工作是如此的难。我相信,任何时候只要实力强,都能有满意的工作,但我不知道,能达到那样的水平还需要...
继续阅读 »

上个月的时候,我被公司裁掉了,陆陆续续找了 1 个月的工作,没有拿到 1 份 Offer,从网上看着各式各样的消息和自己的亲身体会,原来对于像我这样的普通打工族,找工作是如此的难。我相信,任何时候只要实力强,都能有满意的工作,但我不知道,能达到那样的水平还需要多久。


本人是前端,工作 6 年,期间经历过 4 家公司,前两份是外包,后面两份都是领大礼包走的,回想起来,职业生涯也是够惨的。虽然说惨,但是最近领的这一份大礼包个人认为还是值得,工作很难待下去,也没有任何成长,继续待着也是慢性死亡。


这几天我每天都会在 BOSS 上面投十几家公司,能回复非常少,邀请面试的就更少了。外包公司倒是挺多的,而我是从那个火坑里出来的,是不会选择再进去的。于是,我需要做好打持久战的准备,说不定不做程序员了。


我的房子 7 月底就要到期了,我必须要马上做决定,杭州的行情对我来说很不友好,短期内我大概率找不到工作。基于对未来的悲观考虑,我不想把过多的钱花费在房租上面,所以希望就近找一个三线城市,我搜了一下嘉兴,整租 95 平左右的房子只需要 1200 块钱,还是民用水电,思前想后,打算移居到那里嘉兴去。


一方面,我想尝试一下在三线城市生活是一种什么感觉。另一方面,这可以省钱,如果一个月的房租是 1000,民用水电,一个月的开销只要 2500 块。我搜索了一下货拉拉,从我的位置运到嘉兴,需要花费 600 块钱,这个价格也是可以接受的。思考了这些,我觉得是时候离开待了 5 年的杭州。


未来要到哪里去呢,目前可能的选择是上海。我还得想想未来能做什么,我想学一门手艺傍身,比如修理电器、炒菜。毕竟骑手行业太拥挤了,估计也不是长久之计。


房租降下来了,等我把行李都安置妥当,我打算回老家待一段时间。自从上大学以来,很少有长时间待在家里的时候,眼看父母年纪也越来越大了,很想多陪陪他们。如果进入正常的工作节奏,想做到这样还是会受到局限,这次也算是一个弥补的机会。


被裁也是一件好事,可以让我提前考虑一下未来的出路。


这段时间我想把时间用来专门学英语,再自己做几个项目,学英语的目的是为了 35 岁之后做打算,做项目是为了写到简历上面,并且个人觉得自己需要多做一个项目,这样自己才能成长到下一个级别。虽然不知道收益怎么样,但是我想尝试一下。人还活着,有精力,就还是瞎折腾一下。


离职没敢和家里说,说了估计要担心死了,反正是年轻人,有事就先自己扛一扛,我前几天把我的行李寄回去了一批,我妈问我,怎么,寄东西回来了?我回答说要搬家了。本来也想找机会开口说自己离职了,她说,这次搬家也别离公司远了,我也把话憋了进去,只好说“没事的,放心就行”。我自己没觉得离职有什么,正常的起起落落,只是觉得父母可能会过度的担心。


如果做最坏的打算,那就是回去种地,应该这几年还饿不死。有还没离职的同学,建议还是继续苟着。希望社会的低谷期早点过去,希望我们都能有美好的未来。




作者:mysteryven
来源:juejin.cn/post/7395523104743178279
收起阅读 »

Taro v4框架开发微信小程序(配置)

web
环境变量文件 将 .env.dev 文件重命名为 .env.development,以及将 .env.prod 文件重命名为 .env.production,以适配环境配置。 为了演示如何使用环境变量,我们在 .env.development 文件中添加两...
继续阅读 »

环境变量文件


image.png


.env.dev 文件重命名为 .env.development,以及将 .env.prod 文件重命名为 .env.production,以适配环境配置。


image.png


为了演示如何使用环境变量,我们在 .env.development 文件中添加两个变量 TARO_APP_IDTARO_APP_API,然后在源代码中读取这些变量的值。


TARO_APP_ID="xxxxxxxxxxxxxxxxxx"

TARO_APP_API="https://api.tarojs.com"

image.png


接下来需要在 project.config.json 文件中更新 appid 的值。因为上一章节中为了测试修改了这个值,现在我们需要把它改回原来的 appid


"appid": "touristappid",

在完成以上操作后,重新启动项目(使用命令 pnpm dev:weapp),控制台会显示相关的提示信息,并且可以看到 dist/project.config.json 文件中的 appid 已经变成了我们在 .env.development 文件中指定的 TARO_APP_ID 值。


image.png


image.png


为了在代码中使用环境变量,可以在 src/pages/index/index.tsx 文件的 useLoad 钩子中添加 console.log 语句来打印 TARO_APP_API 的值:


console.log(process.env.TARO_APP_API)

这样做的结果是,当程序运行时,可以在微信开发者工具的控制台中看到 TARO_APP_API 环境变量的值被成功打印出来。


image.png


这里需要记得将环境变量的appid改为你正常使用的appid,否则小程序会报错。


之后运行程序,并在微信开发者工具中浏览:


image.png


需要注意的是,只有以 TARO_APP_ 开头的环境变量才会被 webpack 的 DefinePlugin 插件静态嵌入到客户端代码中。这是为了避免环境变量与系统内置变量冲突。在构建过程中,代码中的 process.env.TARO_APP_API 会被替换为实际的环境变量值。例如,我们在小程序开发者工具中查看编译后的代码,会看到 console.log(process.env.TARO_APP_API) 被替换成了 console.log("https://api.tarojs.com");


image.png


编译配置


编译配置是 Taro 项目开发过程中重要的一部分,它决定了项目的编译行为。Taro 的编译配置主要存放在项目根目录下的 config 文件夹内,由 index.ts 文件统一导出。其中,index.ts 通过合并 dev.tsprod.ts 来分别处理开发时的配置和构建时的生产配置。dev.js 适用于项目预览时的设置,而 prod.js 则适用于项目打包时的设置。


在 Taro 的编译配置中,可以设置项目名称、创建日期、设计稿尺寸、源码目录等基本配置信息。下面的代码片段展示了一部分编译配置的内容:


const config = {
// 项目名称
projectName: 'learn-taro-wxapp',
// 项目创建日期
date: '2024-3-11',
// 设计稿尺寸
designWidth: 750,
// 设计稿尺寸换算规则
deviceRatio: {
640: 2.34 / 2,
750: 1,
375: 2,
828: 1.81 / 2
},
// 项目源码目录
sourceRoot: 'src',
// 项目产出目录
outputRoot: 'dist',
// Taro 插件配置
plugins: [],
// 全局变量设置
defineConstants: {},
// 文件 copy 配置
copy: {
patterns: [],
options: {},
},
// 框架,react,nerv,vue, vue3 等
framework: 'react',
// 使用 webpack5 编译
compiler: 'webpack5',
cache: {
enable: false // Webpack 持久化缓存配置,建议开启
},
// 小程序端专用配置
mini: {
postcss: {
pxtransform: {
enable: true,
config: {

}
},
autoprefixer: {
enable: true,
},
cssModules: {
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
config: {
namingPattern: 'module', // 转换模式,取值为 global/module
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
},
},
// 自定义 Webpack 配置
webpackChain(chain) {
chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
}
},
// H5 端专用配置
h5: {
publicPath: '/',
staticDirectory: 'static',
postcss: {
autoprefixer: {
enable: true,
},
cssModules: {
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
config: {
namingPattern: 'module', // 转换模式,取值为 global/module
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
},
},
// 自定义 Webpack 配置
webpackChain(chain, webpack) {},
devServer: {},
},
rn: {
appName: 'taroDemo',
postcss: {
cssModules: {
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
}
}
}
}

module.exports = function (merge) {
if (process.env.NODE_ENV === 'development') {
return merge({}, config, require('./dev'))
}
return merge({}, config, require('./prod'))
}

在编译配置文件中,alias 被用来设置路径别名,避免在代码中书写过多的相对路径。在配置文件中,默认已经将 @ 设置为指向 src 目录,这样,在代码中就可以使用 @ 快捷引用 src 下的文件了。


我们还可以增加额外的配置,例如:


alias: {
'@/components': path.resolve(__dirname, '..', 'src/components'),
}

使用 defineConstants 可以定义全局常量,例如,可以基于不同的环境设置不同的全局变量。


defineConstants: {
__DEV__: JSON.stringify(process.env.NODE_ENV === 'development'),
__PROD__: JSON.stringify(process.env.NODE_ENV === 'production')
}

等等...
如果想要查阅每个配置项的具体意义和用法,可以按住 Ctrl + 鼠标左键 点击属性名,跳转到 project.d.ts 类型声明文件中查看对应注释和示例代码。


动画.gif


designWidth 用于指定设计稿的宽度,这里设置的是 750px,这意味着使用的 UI 设计稿的宽度标准是 750px。Taro 提供了多个设计稿尺寸的换算规则,当前项目中已经设置了几种不同尺寸对应的换算比例。如下所示:


  // 设计稿尺寸换算规则
deviceRatio: {
640: 2.34 / 2,
750: 1,
375: 2,
828: 1.81 / 2
},

对于 UI 设计师而言,推荐使用 750px 作为设计尺寸标准,它便于开发者使用 Taro 进行开发时,进行适配和转换。


对于更详细的编译配置,可以查询官方文档中的编译配置详情


app.config.ts 通用配置文件


在 Taro 框架中,app.config.ts 是小程序的通用配置文件,其主要职责是定义小程序的页面及其全局属性。以下是针对 app.config.ts 文件中一些关键配置项的说明:


pages


pages 数组用于指明组成小程序的页面列表,每个元素都指向一个页面的路径加文件名,无需包含文件扩展名。由于 Taro 会自动处理寻找文件,这会带来方便。改变小程序页面结构时,如新增或删除页面,都应相应地更新 pages 配置。


pages: [
'pages/index/index',
'pages/other/other',
// ... 其他页面路径
]

其中,数组中的第一个条目表示小程序的入口页面(首页)。


window


window 对象用于设置小程序的状态栏、导航条、标题和窗口背景色等界面表现。


window: {
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColor: '#eeeeee',
// ... 其他窗口属性
}

动画.gif


查看属性详细信息和支持程度,你可以通过按住 Ctrl + 鼠标左键 点击任意属性,跳转至 taro.config.d.ts 类型声明文件。支持程度不同的平台详细请查阅官方文档


tabBar


对于包含多个 tab(在客户端窗口底部或顶部有 tab 栏切换页面的)的小程序,tabBar 配置用于定义 tab 栏的外观以及每个 tab 对应的页面。


tabBar: {
color: "#434343",
selectedColor: "#333333",
// ... tabBar 属性和 tab 列表
}

tabBar 中的 list 属性是一个包含若干对象的数组,每个对象定义了一个 tab 项的页面路径、图标和文字等。点击 tab 时,应用会切换到对应的页面。


关于 tabBar 的更多详细配置项,也可以通过点击属性,跳转至 TypeScript 的类型声明文件中查看功能描述。


动画.gif


支持程度不同的平台详细请查阅官方文档


页面的config.ts配置文件


单个页面也可以有自己的配置文件,通常是 config.ts。页面配置会被 definePageConfig 函数包裹,并作为参数传入,其参数类型是 Taro.PageConfigPageConfig 继承自 CommonConfig 类型。


export default definePageConfig({
navigationBarTitleText: '页面标题'
})

动画.gif


project.config.json 微信小程序配置文件


除了 Taro 的配置外,微信小程序也需一个 project.config.json,这个配置文件包含了微信小程序的专有配置。关于此配置你可以参考微信小程序官方文档


作者:辰火流光
来源:juejin.cn/post/7345063548705718312
收起阅读 »

8年老前端开发,成仙路已断

前言 到今年,我已经写了8年代码。在这段时间里,我的职业生涯一直处于不上不下的状态。尽管在二线城市的薪资看起来还不错,但我早就明白,这行不过是用青春换取血汗钱。从疫情开始,我内心变得愈发焦躁不安,尤其是今年,心态更加低迷。无论是职场上的无尽业务,还是长期的工作...
继续阅读 »

前言


到今年,我已经写了8年代码。在这段时间里,我的职业生涯一直处于不上不下的状态。尽管在二线城市的薪资看起来还不错,但我早就明白,这行不过是用青春换取血汗钱。从疫情开始,我内心变得愈发焦躁不安,尤其是今年,心态更加低迷。无论是职场上的无尽业务,还是长期的工作倦怠,都在促使我做出改变的决定。


最终,在三月底,我主动离职了(没错,在最艰难的时刻做出了这个决定)。从那时起,这三个多月来,我一直在思考和寻求变化。转眼间,已经到了七月底。虽然这段时间收入不如上班时稳定,但我的状态却越来越好。


简单回顾下个人开发经历


本人是双非辣鸡学校毕业。


2016年 初开始入行前端开发,那时候还处在前后端不分离的时代,开始主要是写 JSP,主要以原生JS和JQ为主,那时候前端主要是司职辅助功能,事情也比较少,相对比较轻松。


2017年 开始前后端分离大行其道,也开始用“所谓”的现代化前端框架,那时候主要是用react全家桶,当时觉得,卧槽,这TM比一个个操作dom节点确实方便不少,最关键的是webpack之流的工具让前端开发更加成体系,不像以前是寄生在jsp里面了,工程化更明显了,当时,前端真正独立成了一个岗位,不再是纯辅助,而是研发流程的重要一环。


2018年 一整年都在用RN开发APP,第一次接触移动端开发,RN是我一生的噩梦,以至于后面我看到跨平台的东西做复杂应用就谨慎的很,当时还是0.5x的时代,各种安卓、ios端的BUG。我记得RN官网写的slogan一次编写,多端运行,后面可能是投诉太多了,官网后面好像改成还是需要原生安卓/ios知识了。我记得那时候做一个IM应用,长列表白屏,realm数据库大量消息写入卡顿,各种原生不兼容,我都不记得晚上花了多少时间在github的issue上找兼容方案和各种hack了。再加上当时五大厂商各种魔改安卓,华为刚刚切换鸿蒙系统(就这么巧),无尽的debuff(现在想想当初直接学原生梭哈更好,泪水不争气的落下)


2019年 开始用vue开发web,比起react,更多的语法糖,当时觉得slot插槽这种封组件的方法确实要更方便。后面接私单果断vue梭哈,做公司产品还是react,毕竟没有slot给队友乱用


2020年-至今 从疫情开始整个职业规划紊乱。一开始为了更具竞争力,开始往全栈方向发展,努力自学node.js/koa ,开始用公司的几个小型产品实践,当时一度以为自己全栈,后面才知道我这叫全不占工程师。后面又因为公司业务需求,又开始用electron开发桌面应用,用uniapp开发多端小程序,用python套壳gpt的api(有了后面的经历,我才知道不是巨复杂的应用跨平台的坑也还行),并且机缘巧合之下,还有了机会用laya引擎开发微信小游戏。直至最后,彻底成为全不栈工程师,纯纯牛马。


总结职业生涯 没什么规划,跟着公司的业务随波逐流,属于先知后觉,觉醒了又不够决心,总是想着现在行情不好,实在不行再攒一点钱,然后就一直蹉跎到了2024年,一事无成。


我的反思



既然已经这样了,只能自救了,不然咋办呢哈哈哈哈



1、定位问题



  1. 离开了公司无法生存,没法形成独立生产单元

  2. 学历、年纪、技术都不占优势

  3. 行业环境急剧恶化,现在长沙公司基本都是大小周或者单休,属于买方市场,并且加班严重


2、分析原因


说白了,核心原因就几个:



  1. 程序员不能形成独立生产单元,寄生于公司,也没有机会了解整个商品从规划到收款的全生命周期

  2. 可替代性高,开源生态太强,且总有更厉害的人,总有学历更好的人,怎么卷最终都会失败

  3. 自身可出售的商品只有时间一项,缺少其他收入


image.png


3、解决方案


3.1、最优解



  1. 开发独立产品,这是每一个程序员的梦想,却也是最难得路,属于是下限低,上限高。

  2. 卖课,也就是所谓的知识付费,现在卖课的平台很多,但是需要人设、背景和标签,还需要运营推广,属于极稳的做法。

  3. 找个一辈子的养老公司,不是没有,只是不好进,我上次还看见图书馆和纪念馆招人,只是有编制很难。


3.2、临时解



  1. 接外包,我现在也在接,个中滋味,只能说懂的都懂。

  2. 找个班上,如果实在缺钱或者不知道做什么,也可以先干着。

  3. 技术顾问,不同于外包,就是靠着一点经验解决点冷门问题。


3.3、风险解



  1. 开店,重投入,不成功便成仁,需要梭哈。

  2. 出海,去日本、东南亚、甚至是更远的地方,写代码也好,干其他的也好,需要勇气。

  3. 开公司,只要有业务,就能苟活,问题是怎么有业务,需要资源。


4、我的解题思路



  1. 做自己的独立产品,以尝试的心态,,将自己内心的idea实现,切勿抱着什么多少爆款的思路,多学习别的独立开发者好的思路和见解。做了就上线,反复试错。

  2. 关于上班,现在很少有什么新公司,新产品出现,都是旧项目在维护,成仙路已断,注定走不了了。现在基本只接外包单子,上班继续做web是不可能了。

  3. 关于小游戏开发,参照发达国家的经验,可以一直做下去,创意类很难被替代。并且不像web,这行业的经验不怎么共享,开源代码也少,这是良性生态,由于岗位少,对于职场不友好,但是对于个人开发很不错。做了就上线,反复试错。

  4. 关于转行,盲目转行不可取,大环境下都是大哥笑二哥。

  5. 关于技术自媒体,不给自己设限,也可以去做做。


我这3个月做了什么


今年4月迷茫了半个月,然后躺平了半个月,真的好开心。


今年5月上半个月学习开传奇私服,然后去自驾去青海湖玩了半个月,真的好开心。


今年6月



  • 做了一个AI做菜小程序,套壳子那种,微信不给上架,说个人不能做信息咨询,放弃。

  • 写了一个模拟用户操作爬取数据的桌面工具,后面可能有用。

  • 不信邪,做了一个躺平记账的小程序,刚刚上架,是人都要做一次三件套。

  • 用laya和cocos做了几个入门小游戏,并开源,给想web转游戏的小伙伴们降低一点曲线,因为游戏引擎的文档不是一般的烂(我原以为小程序的文档很差,直到我看到了几个游戏引擎的文档),大部分时间一边看源码一边开发


今年7月



  • 做了10+传奇私服,并且赚了几百元(聊胜于无),不敢做大,不敢宣传,容易被封印。

  • 做了一个创意肉鸽小游戏(有没有美术搭子,全靠AI生图走的很艰难),在到处寻找美术资源,代码已写完(等替换完资源就上)。


image.png


我的规划


2024年度规划



  • 继续做肉鸽、塔防类游戏

  • 继续开私服

  • 学习新的私服技术(暂定DNF)

  • 继续做自己的独立产品


远景规划



  • 追随自己的内心,不给自己设限

  • 动态寻找可长期耕耘的赛道


写在最后


多年以后,我总是想起2016年刚入行时,只用写写js就能拿薪水的那个下午


作者:飞翔的羊驼
来源:juejin.cn/post/7393312386348138530
收起阅读 »

站住!送你一个漂亮的毛玻璃渐变背景

web
大家好呀,我是 HOHO。 不知道你之前有没有接触过这种需求,实现一个下面这样的背景: 一层毛玻璃,然后后面有一些渐变的色块动来动去。这种效果大家应该都比较熟悉,之前有段时间 Apple 很热衷于使用类似的特效,说实话确实挺好看。我们一般管这种效果叫做 bl...
继续阅读 »

大家好呀,我是 HOHO。


不知道你之前有没有接触过这种需求,实现一个下面这样的背景:


image.png


一层毛玻璃,然后后面有一些渐变的色块动来动去。这种效果大家应该都比较熟悉,之前有段时间 Apple 很热衷于使用类似的特效,说实话确实挺好看。我们一般管这种效果叫做 blurry gradient、或者模糊渐变、磨砂渐变、毛玻璃渐变。


本来以为一个背景而已,有什么难度,没成想一路走来还真踩到了不少的坑。本着我摔了一身泥不能让大家再摔一次的原则。我把这种效果封装成了一个 React 组件 react-blurry-gradient,大家可以直接拿来用,省的再抄代码浪费脑细胞。


在讲组件之前,我先来介绍一下都踩了哪些坑,如果你急用的话,前面直接无视就行。OK 话不多说,我们现在开始。


心路历程


1、shader gradient


其实一开始和 UI 沟通的时候,这个效果并不是典型的模糊渐变。而是一个 Shader,你可以在 这个网站 看到类似的效果,这个 Shader Gradient 还有对应的 Figma 插件


这个效果其实实现起来不难,因为它提供了对应的 react 插件 ruucm/shadergradient: Create beautiful moving gradients on Framer, Figma and React。只需要把 UI 给的参数填进去就可以了。


但是事情并没有想象的那么简单,单纯的复刻效果倒没什么问题,问题出在这玩意居然自带一个十分离谱的入场特效:


bug1.gif


可以看到,这个效果初始化的时候会有个旋转缩放的“入场动画”,让人忍俊不禁。不仅如此,这个背景还非常离谱的是可以拖动和缩放的:


bug2.gif


这两个问题在组件的文档里并没有任何说明,我猜测这个效果组件是基于 threejs 实现的,出现这两个问题应该是 threejs 的一些默认设置没有关闭导致的。


不过这些也不是什么大问题,我们可以通过控制入场透明度和添加蒙层来解决。真正阻止我继续使用的是性能问题。因为这个项目要支持 H5 端,而老板的破水果手机打开这个效果都要燃起来了。没办法只能作罢。


2、css-doodle


听说这个效果差点让老板换手机之后,UI 挠了挠头,说要不我给你点颜色,你干脆写个毛玻璃渐变得了。我觉得他说的有道理,随手一搜,这不就来了:妙用滤镜构建高级感拉满的磨砂玻璃渐变背景


css-doodle 我之前研究过,虽然不太喜欢这种写法风格,但是谁能拒绝直接抄的代码呢?三下五除二就搞成了 React 版本:


import 'css-doodle';
import styles from './index.module.less';

const DOODLE_RULES = `
:doodle {
@grid: 1x8 / 100vmin;
width: 100vw;
height: 100vh;
}
@place-cell: center;
width: @rand(40vw, 80vw);
height: @rand(40vh, 80vh);
transform: translate(@rand(-50%, 50%), @rand(-60%, 60%)) scale(@rand(.8, 1.8)) skew(@rand(45deg));
clip-path: polygon(
@r(0, 30%) @r(0, 50%),
@r(30%, 60%) @r(0%, 30%),
@r(60%, 100%) @r(0%, 50%),
@r(60%, 100%) @r(50%, 100%),
@r(30%, 60%) @r(60%, 100%),
@r(0, 30%) @r(60%, 100%)
);
background: @pick(#FBF1F7, #B27CEE, #E280AE, #c469ee, #a443ee, #e261bb, #e488ee);
opacity: @rand(.3, .8);
position: relative;
top: @rand(-80%, 80%);
left: @rand(-80%, 80%);

animation: pos-change @rand(4.1s, 10.1s) infinite 0s linear alternate;
@keyframes pos-change {
100% {
left: 0;
top: 0;
transform: translate(@rand(-50%, 50%), @rand(-60%, 60%)) scale(@rand(.8, 1.8)) skew(@rand(45deg))
}
}`
;

export const Bg = () => {
return (
<div className={styles.loginBg}>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<css-doodle>{DOODLE_RULES}</css-doodle>
</div>

);
};

index.module.less


.loginBg {
position: absolute;
margin: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background-color: #fff;
z-index: -1;

&::after {
content: '';
position: absolute;
top: -200%;
left: -200%;
right: -200%;
bottom: -200%;
backdrop-filter: blur(200px);
z-index: 1;
}
}

windows 上打开正常,但是 Safari 打开之后发现毛玻璃效果直接消失了,就像下面这样:


image.png


这给我整不会了,按理说 Safari 是支持 backdrop-filter 的。是 css-doodle 在 safari 上有什么兼容性问题?还是 react 和 css-doodle 的集成上出了什么毛病?我没深入了解,再加上本来不小的包体积,于是 css-doodle 方案也被我放弃了。


image.png


看看新组件 react-blurry-gradient


OK,踩了一圈子坑,下面该请出本文的主角 react-blurry-gradient 了,我们直接看效果:


banner.gif


如果 GIF 比较模糊的话可以试一下这个 codesandbox 在线 demo


用法也很简单,安装,然后引入组件和对应的 css 文件即可:


npm install react-blurry-gradient

import { BlurryGradient } from 'react-blurry-gradient';
import 'react-blurry-gradient/style.css';

const colors = ['#bfdbfe', '#60a5fa', '#2563eb', '#c7d2fe', '#818cf8', '#4f46e5'];

export default function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<BlurryGradient colors={colors} />
</div>

);
}

组件会自动的从你指定的颜色列表中随机挑选颜色来生成渐变和动效。


如果你颜色也不想找,没问题,组件还内置了一套渐变颜色组,直接用就行了(目前包含红黄蓝绿紫五套):


import { BlurryGradient, COLORS } from 'react-blurry-gradient';
import 'react-blurry-gradient/style.css';

export default function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<BlurryGradient colors={COLORS.BLUE} />
</div>

);
}

这些颜色就是单纯的字符串数组,所以你可以把这些颜色兑在一起来实现撞色的效果:


<BlurryGradient colors={[...COLORS.BLUE, ...COLORS.RED]} />

image.png


image.png


如果你想设置背景色,没问题,react-blurry-gradient 本身就是透明的,也就是说你背景用什么都可以,直接设置给父元素就行了:


export default function App() {
return (
<div style={{ backgroundColor: COLORS.BLUE[0], width: '100vw', height: '100vh' }}>
<BlurryGradient colors={COLORS.BLUE} />
</div>

);
}

预设的 COLORS 第一个元素都是颜色最淡的,你可以直接拿来当作背景


把这个背景色设置给 BlurryGradient 组件的 style 也可以,不过这样会在边缘形成一圈扩散的效果,而设置给父元素的话背景色就会更均一。


另外毛玻璃的模糊效果也是可以调整的,只需要设置 blur 参数即可,比如你可以调低一点,我觉得也挺好看的:


<BlurryGradient colors={[...COLORS.BLUE, ...COLORS.RED]} blur='20px' />

image.png


image.png


除此之外还有很多额外参数,例如可以通过 itemNumber 来控制生成色块的数量。可以通过 itemTopitemLeft 来控制色块随机生成的位置范围。更多的属性可以在 react-blurry-gradient 配置参数 找到。


如果你不希望随机生成,想精确控制每个色块的颜色、尺寸、位置和运动轨迹,没问题,BlurryGradient 组件的 items 参数允许你指定每个色块的详细配置。参数说明可以看 这里。但是要注意,启用了 items 之后,colors 的配置就会被忽视了。


零依赖 & 极小体积


这个组件从一开始就以够轻量为宗旨,所以你可以看到它没有依赖任何第三方包,只要你项目里有 React,那它就能用:


image.png


而包本身也足够的清爽,不会往你电脑里拉屎:


image.png


组件所有的代码加起来只有 4Kb 不到。


image.png


当然,如果你真的不想因为这种小事再引一个包,没问题,都帮你收拾好了,把 这个文件夹 直接复制到你的项目里就能用,给个 star 意思一下就行~


作者:HOHO
来源:juejin.cn/post/7446018863504506907
收起阅读 »

都快2025年了,你们的前端代码都上装饰器了没?

web
可能很多人都听说过 TypeScript 的装饰器,也可能很多人已经在很多 NestJS 项目中使用装饰器了,也有一些前端开发者可能在某些前端框架中使用过一些简单的装饰器,那么你真的知道装饰器在前端还能玩出哪些花吗? 我们今天不讲基础概念,也不写一些你可能在很...
继续阅读 »

可能很多人都听说过 TypeScript 的装饰器,也可能很多人已经在很多 NestJS 项目中使用装饰器了,也有一些前端开发者可能在某些前端框架中使用过一些简单的装饰器,那么你真的知道装饰器在前端还能玩出哪些花吗?


我们今天不讲基础概念,也不写一些你可能在很多文章里都看到过的没有意义的示例代码,我们直接拿装饰器来实战实现一些需求:


一、类装饰器


虽然很多前端对于类和面向对象是排斥的、抵触的,但不影响我们这篇文章继续来基于面向对象通过装饰器玩一些事情。



我已经写了很多关于面向对象在前端的使用了,实在是不想在这个问题上继续扯了,可以参考本专栏内的其他文章。


虽然但是,不论如何,你可以不用,但你不能不会,更不能不学。



不管在前端还是后端,我们可能都会用到类的实例来做一些事情,比如声明一个用户的类,让用户的类来完成一些事情。


我们可能会为类配置名称,比如给 User 类定义为 用户:


// 声明一个装饰器,用来保存类的文案
function Label(label: string) {
return (target: any) => {
Reflect.set(target, "label", label)
}
}

@Label("用户")
class User {

}


我们不限制被标记的类,你可以把any 用泛型约束一下,限制这个装饰器可以标记到哪些类的子类上。



我们可以通过 Reflect 来获取到类上的元数据,比如 Label 这个类上的 name 属性,通过 Reflect.getMetadata('name', User) 来获取到:


// 将打印 "用户"
console.log(Reflect.get(User, "label"))

通过这种方式,我们可以为类标记很多配置,然后在使用的时候就不会在代码里再出现很多类似 “用户” 的魔法值了。如果有改动的话,也只需要将 @Label("用户") 改成 @Label("XXX") 就好了。


当然,事实上我们不会单独为了一个小功能去声明一个装饰器,那样到时候会给类上标记很多的 @ 看着难受,于是我们可以直接声明一个 ClassConfig 的装饰器,用来保存类的各种配置:


interface IClassConfig {
// 刚才的 label
label?: string

// 添加一些其他的配置

// 表格的空数据文案
tableEmptyText?: string

// 表格删除提醒文案
tableDeleteTips?: string
}
function ClassConfig(config: IClassConfig){
return (target: any) => {
Reflect.set(target, "config", config)
}
}
@ClassConfig({
label: "用户",
tableEmptyText: "用户们都跑光啦",
tableDeleteTips: "你确定要删除这个牛逼的人物吗?"
})

当然,我们可以通过 Reflect.getMetadata('config', User) 来获取到 ClassConfig 这个类上的配置,然后就可以在代码里使用这些配置了.



比如,我们还封装了一个 Table 组件,我们就只需要将 User 这个类传过去,表格就自动知道没数据的时候应该显示什么文案了:



<Table :model="User" :list="list" />

上面的表格内部,可以获取 model 传入的类,再通过 Reflect 来获取到这些配置进行使用,如果没有配置装饰器或者装饰器没有传入这个参数,那么就使用默认值。


二、属性装饰器


很多人都知道,装饰器不仅仅可以配置到类上,属性上的装饰器用处更多。


这个和上面第一点中的一样,也可以为属性做一些配置,比如给用户的账号属性做配置,而且我们还可以根据主要功能来声明不同的装饰器,比如表单的 @Form,表格的 @Table 等等。


class User {
@Field({
label: "账号",
// 如果下面的没有配置,那我来兜底。
isEmail: true,
})
@Form({
// 表单验证的时候必须是邮箱
isEmail: true,
// 表单验证的时候不能为空
isRequired: true,
placeholder: "请输入你牛逼的邮箱账号"
})
@Table({
// 表示表格列的邮箱点击后会打开邮件 App
isEmail: true,

// 表格列的宽度
width: 200,

// 需要脱敏显示
isMask: true
})
account!: string
}

当然,属性的装饰器声明和类的声明方式不太一致:


interface IFieldConfig {
label?: string
isEmail?: boolean
}
function Field(config: any) {
return (target: any, propertyKey: string) => {
Reflect.set(target, propertyKey, config)
}
}

使用 Reflect 获取的时候也不太一致:


const fieldConfig = Reflect.get(User.prototype, "account")
// 将打印出 `@Field` 配置的属性对象
console.log(fieldConfig)

想象一下,你封装的表格我也这么使用,我虽然没有传入有哪些表格列,但你是不是能够通过属性是否标记了 @Table 装饰器来判断是否需要显示这个邮箱列呢?


<Table :model="User" :list="list" />

你也可以再封装一些其他的组件,比如表单,比如搜索等等等等,像这样:


<Input :model="User" :field="account" />

上面的 Input 组件就会自动读取 User 这个类上的 account 属性的配置,然后根据配置来渲染表单和验证表单,是不是美滋滋?


三、方法装饰器和参数装饰器


这两个方式的装饰器我们在这篇文章不细讲,等装饰器这玩意在前端能被大家接受,或者前端娱乐圈骂面向对象不再那么狠的时候再细化一下吧,今天我们只讲讲简单使用:


3.1 方法装饰器


说到方法装饰器,我想先提一嘴 AOP 编程范式。



AOP(Aspect Oriented Programming) 是一种编程范式,它把应用程序切面化,即把应用程序的各个部分封装成可重用的模块,然后通过组合这些模块来构建应用程序。



举个简单的例子,我们最开始写好了很多代码和方法:


class User {
add(name: string) {
console.log("user " + name + " added!")
}
delete(name: string) {
console.log("user " + id + " deleted!")
}
}

const user = new User();
user.add("Hamm")
user.delete("Hamm")

以前调用这些方法都是正常的,突然有一天需求变了,只允许超级管理员才能调用这两个方法,你可能会这么写:


class User {
add(name: string) {
checkAdminPermission()
console.log("user " + name + " added!")
}
// 其他方法
}

function checkAdminPermission() {
if(!你的条件){
throw new Error("没有权限")
}
}
const user = new User();
user.add("Hamm")

虽然也没毛病,但需要去方法内部添加代码,这属于改动了已有的逻辑。



而 AOP 存在的意义,就是通过切面来修改已有的代码,比如在方法执行前,执行一段代码,在方法执行后,执行一段代码,在方法执行出错时,执行一段代码,等等。用更小的粒度来减少对已有代码的入侵。像这样:



class User {
@AdminRequired
add(name: string) {
console.log("user " + name + " added!")
}
}

function AdminRequired(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
if (你的条件) {
return originalMethod.apply(this, args)
}
throw new Error("没有权限")
}
}

const user = new User()
console.log(user.add("Hamm"))

乍一看,我就知道又会有人说:“你这代码不是更多了么?” 看起来好像是。


但事实上,从代码架构上来说,这没有对原有的代码做任何改动,只是通过 AOP 的方式,在原有代码的基础上,添加了一些前置方法处理,所以看起来好像多了。但当我再加上一些后置的方法处理的话,代码量并没有添加多少,但结构会更清晰,代码入侵也没有。


传统写法(入侵)


class Test{
张三的方法(){
// 李四的前置代码

// 张三巴拉巴拉写好的代码

// 李四的后置代码
}
}


张三:“李四,你为什么用你的代码包围了我的代码!”



装饰器写法(不入侵)


class Test {
@LiSiWantToDoSomething
张三的方法() {
// 张三巴拉巴拉写好的代码
}
}
function LiSiWantToDoSomething(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
console.log("李四的前置代码")
const result = originalMethod.apply(this, args)
console.log("张三干完了,结果是" + result)
return "我是李四,张三的结果被我偷走了"
}
}

这时,张三的代码完全在不改动的情况下添加了前置和后置代码。


3.2 参数装饰器


参数装饰器的使用场景在前端比较少,在 Nest 中比较多,这篇文章就不过多介绍了,如果后续大伙有兴趣我们再聊。


四、总结


装饰器是一种新的语法,可以让你的前端代码更加的架构化,增加代码的可维护性。


如果你有兴趣,还可以阅读本专栏内的这些文章:


用TypeScript和装饰器优雅的为前端数据脱敏


TypeScript使用枚举封装和装饰器优雅的定义字典]


TypeScript中如何用装饰器替代JSON配置项封装表单


TypeScript装饰器之我们是这么做表单和校验的


当然,其他文章也很有意思哟~


今天就这样,欢迎继续关注我们的专栏 《用TypeScript写前端》


也欢迎关注我们的开源项目: AirPower4T,里面有很多装饰器在前端的应用场景,也许可以让你耳目一新。


Bye.


作者:Hamm
来源:juejin.cn/post/7449313175920459811
收起阅读 »

不要再二次封装 axios 了,它真的是“灵丹妙药”吗?

web
引言 最近,看到不少开发者在讨论如何“优雅地”封装 axios 时,我的内心不禁发出一声叹息——“收手吧阿祖,别再封装了!”我们都知道,axios 是一个轻量级的 http 客户端库,广泛用于前端和 node.js 环境中,因其简洁易用、功能丰富而备受喜爱。但...
继续阅读 »

引言


最近,看到不少开发者在讨论如何“优雅地”封装 axios 时,我的内心不禁发出一声叹息——“收手吧阿祖,别再封装了!”我们都知道,axios 是一个轻量级的 http 客户端库,广泛用于前端和 node.js 环境中,因其简洁易用、功能丰富而备受喜爱。但问题来了:为什么那么多人非要二次封装它?是想追求什么“优雅”代码,还是只是满足某种程序员的“封装癖”?在我看来,二次封装 axios 的行为,其实更多的是“低效”和“麻烦”!


在这篇文章中,我将分析二次封装 axios 的现象,揭示它的弊端,并提出更合理的解决方案。


二次封装 axios 背后的动机


首先,得承认,在许多开发者的心目中,二次封装 axios 是“提升代码复用性”、“提升可维护性”的一种手段。究竟是什么驱动他们这么做呢?让我们来看看,通常的封装动机有哪些。



  1. 全局配置管理

    很多人为了避免在每个请求中都写重复的配置(如 baseURL、timeout、headers 等),于是将 axios 封装成一个单独的模块,统一管理。这样一来,代码看似简洁了。

  2. 请求/响应拦截器

    除了常见的全局配置外,二次封装通常还会加入请求和响应拦截器,用于做统一的错误处理、日志记录、token 刷新等。这听起来很有吸引力,似乎让项目更加“健壮”了。

  3. 封装错误处理

    统一处理 HTTP 错误,诸如 400、500 错误等,这让开发者避免了在每个请求中重复编写错误处理逻辑。

  4. 功能扩展

    比如:增加一些额外的功能,比如请求重试、自动刷新 token、请求取消等。


这些动机听起来有理有据,似乎是为了减少重复代码、提高效率。但,二次封装真的是解决问题的最佳方法吗?


二次封装 axios 的弊端:看似优雅,实际繁琐


虽然二次封装看起来很“高级”,但它带来的问题也是显而易见的。


1. 失去灵活性,降低可维护性


当我们通过二次封装 axios 将所有请求逻辑集中到一个地方时,代码复用的确得到了提高,但灵活性却大大下降。每当我们需要调整请求方式、处理特殊错误、或者添加新的请求功能时,必须在封装层修改代码,这意味着对每一个新请求的修改都变得更加复杂,导致代码膨胀,维护成本上升。


举个例子,假设你有一个简单的请求需要添加一个额外的请求头或参数,但你的封装类已经把一切都“包裹”得很严实,你不得不进入封装类的内部进行修改。这种情况下,封装的意义反而变得虚假。


2. 过度封装,增加不必要的复杂性


封装本应是为了简化代码,但过度封装反而让事情变得更加复杂。例如,很多二次封装的 axios 都包含了一堆的“自定义配置”,导致请求时你不得不先了解封装类的具体实现,甚至可能在不同项目之间迁移时也要重新学习一套封装规范。每次需要调用 api 的时候,都要与一个封装层打交道,这显然不是开发者想要的高效体验。


3. 性能问题:拦截器是双刃剑


请求和响应拦截器的设计初衷无疑是为了统一处理请求逻辑,但过多的拦截器往往会导致性能瓶颈。特别是在大型项目中,拦截器的链式执行可能带来额外的延迟。此外,拦截器中常常会加入错误处理、token 刷新等额外逻辑,这会影响整个请求链的执行效率。


4. 可能引发版本兼容性问题


随着项目的不断迭代,封装的代码与 axios 原生的更新频繁不一致,导致二次封装的代码容易发生维护上的“断层”或兼容性问题。每当 axios 更新时,可能需要你手动修复封装类中的依赖,甚至重构整个封装层,造成额外的开发工作量。


为什么我们不需要二次封装 axios?


那么,既然二次封装带来了这么多麻烦,我们应该如何解决 axios 使用中的痛点呢?


1. 使用 axios 的内置功能


axios 本身就有非常强大的功能,很多二次封装中提到的配置(比如 baseURL、headers 等)都可以直接通过 axios 的实例化来轻松解决。例如:


const axiosInstance = axios.create({
baseURL: 'https://api.example.com',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});

这样,所有请求都可以通过统一的实例管理,无需复杂的封装,且灵活性保持不变。


2. 合理使用拦截器


axios 的请求和响应拦截器非常强大,用得好可以让你的代码更简洁。错误处理、token 刷新、请求取消等功能,都可以直接在 axios 拦截器中完成,而不需要一个额外的封装类。


axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
// Token 刷新逻辑
}
return Promise.reject(error);
}
);

通过这种方式,我们在全局进行处理,而不需要一层一层的封装,让代码保持简洁并且具有良好的扩展性。


3. 利用第三方库增强功能


如果你确实需要一些特殊的功能(比如请求重试、缓存、自动重定向等),可以使用现成的第三方库,而不是自己重复造轮子。比如:



  • axios-retry:轻松实现请求重试

  • axios-cache-adapter:请求缓存

  • axios-auth-refresh:自动刷新 token


这些库都能与 axios 配合得很好,帮助你解决二次封装中出现的某些功能问题,避免在项目中增加冗余的封装层。


4. 模块化的请求管理


而对于需要统一管理的 api 请求,推荐将每个请求模块化,分层管理,而不是在一个封装类中把所有请求都硬编码。你可以根据需求将每个 api 的请求抽象成一个独立的函数或模块,保持高内聚低耦合的设计。


// api/user.js
export function getUserInfo(userId) {
return axios.get(`/users/${userId}`);
}

这样做的好处是,当某个接口发生变化时,只需要修改相应的模块,而不需要担心影响到其他的请求。


总结


二次封装 axios 是一种源自“代码复用”的良好初衷,但它往往带来了灵活性不足、复杂度增加、性能损失等一系列问题。在面对实际开发中的 http请求时,我们可以通过直接使用 axios 的内置功能、合理利用拦截器、借助现成的第三方库以及模块化管理等方式来更高效、更优雅地解决问题。


所以,不要再二次封装 axios 了,它并不是“灵丹妙药”。让我们回归简单,享受 axios 原生的优雅与高效吧!


作者:d2w
来源:juejin.cn/post/7441853217522204681
收起阅读 »

不懂这些GIS基础,开发Cesium寸步难行!

web
大家好,我是日拱一卒的攻城师不浪,专注可视化、数字孪生、前端提效、nodejs、AI学习、GIS等学习沉淀,这是2024年输出的第30/100篇文章。 前言 想学Cesium开发,你如果对一些GIS基础,特别是坐标系概念不了解的话,会让你非常难受,今天我们就...
继续阅读 »

大家好,我是日拱一卒的攻城师不浪,专注可视化、数字孪生、前端提效、nodejs、AI学习、GIS等学习沉淀,这是2024年输出的第30/100篇文章。



前言


想学Cesium开发,你如果对一些GIS基础,特别是坐标系概念不了解的话,会让你非常难受,今天我们就来聊聊WebGiser开发过程中常用到的一些坐标系概念。


GIS坐标系


要熟悉Cesium中常用到的一些坐标类型以及它们之间是如何进行转换的,到了真正用到的时候可以再返回来细看,加深理解。


经纬度坐标(球面坐标)


经纬度坐标通常被称为地理坐标地球坐标,它是一种基于地球表面的坐标系统,用于确定地球上任何点的位置。这种坐标系统使用两个主要的数值来表示位置:经度和纬度。




  1. 经度(Longitude):表示从本初子午线(通常通过英国伦敦的格林尼治天文台)向东或向西的角度距离。经度的范围是从 -180° 到 +180°,其中 0° 表示本初子午线。

  2. 纬度(Latitude):表示从赤道向北或向南的角度距离。纬度的范围是从 -90°(南极点)到 +90°(北极点),其中 0° 表示赤道。


经纬度坐标也常常被称为:



  • 球面坐标(Spherical Coordinates):因为地球近似为一个球体,经纬度坐标可以看作是在球面上确定点的位置。

  • 大地坐标(Geodetic Coordinates):在大地测量学中,这种坐标系统用于描述地球表面的点。

  • WGS84坐标:WGS84(World Geodetic System 1984)是一种广泛使用的全球地理坐标系统,它提供了一个标准化的参考框架,用于地理定位。


经纬度坐标广泛应用于地图制作、导航、地理信息系统(GIS)、航空和海洋导航等领域。在数字地图服务和应用程序中,经纬度坐标是最常见的位置表示方式之一。


地理坐标(弧度)


在地理信息系统(GIS)中,地理坐标通常指的是地球上某个点的位置,使用经纬度来表示。然而,由于地球是一个近似的椭球体,使用弧度而非角度来表示经纬度坐标可以避免在计算中引入的某些复杂性,尤其是在进行距离和面积的测量时。



弧度是一种角度的度量单位,它基于圆的周长和半径之间的关系。一个完整的圆周被定义为 2π弧度。弧度与角度的转换关系如下:



在GIS中,使用弧度的地理坐标可以简化一些数学运算,尤其是涉及到三角函数和地球曲率的计算。例如,计算两点之间的大圆距离(即地球表面的最短路径)时,使用弧度可以更直接地应用球面三角学公式。


地理坐标(弧度)的应用



  1. 距离计算:使用球面三角学公式,可以更准确地计算出两点之间的距离。

  2. 方向计算:确定从一个点到另一个点的方向,使用弧度可以简化计算过程。

  3. 地图投影:在某些地图投影中,使用弧度可以更自然地处理地球表面的曲率。


屏幕坐标系


屏幕坐标系(Screen Coordinate System)是一种二维坐标系统,它用于描述屏幕上的点或区域的位置。屏幕坐标系通常以屏幕的左上角为原点,水平向右为 x 轴正方向,垂直向下为 y 轴正方向。



屏幕坐标系在Cesium中叫做二维笛卡尔平面坐标。


new Cesium.Cartesian2(x, y)

屏幕坐标系的特点:



  1. 原点位置:屏幕坐标系的原点(0,0)位于屏幕的左上角

  2. 正方向:x 轴正方向向右,y 轴正方向向下。

  3. 单位:通常使用像素(px)作为单位。

  4. 范围:坐标值的范围取决于屏幕或窗口的大小。


空间直角坐标系


在地理信息系统(GIS)中,空间直角坐标系(Spatial Cartesian Coordinate System)是一种三维坐标系统,用于在三维空间中精确地表示点、线、面的位置。这种坐标系通常由三个正交的坐标轴组成:X、Y 和 Z 轴。



空间直角坐标系的特点:



  1. 正交性:X、Y 和 Z 轴相互垂直,形成一个直角坐标系。

  2. 三维性:可以表示三维空间中的任何位置,包括高度或深度信息。

  3. 标准化:通常以地球的质心或某个参考点为原点,建立一个标准化的坐标系统。

  4. 应用广泛:广泛应用于地理测量、城市规划、建筑设计、3D 建模等领域。


Cesium中的坐标系


Cesium中支持两种坐标系:3D笛卡尔坐标系经纬度坐标系


3D笛卡尔坐标系


先来了解下笛卡尔空间直角坐标系,它的X、Y、Z三个轴的正方向如下图所示:



坐标系的原点位于地球的中心。因此,这些坐标通常是负的。单位通常是


Cesium.Cartesian3(x, y, z)

地理坐标系


是一种基于经度和纬度的坐标系,它使用度数来表示位置。


在Cesium中,地理坐标可以通过将经度、纬度和高度值传递给Cartographic对象来表示。


其中经度和纬度是以度数表示的,高度值可以是以米或其他单位表示的。


Cesium将地理坐标转换为笛卡尔坐标以在地球表面上进行可视化。


坐标系转换


Cesium提供了很多坐标系互相转换的大类。


经纬度转空间直角


const cartesian3 = Cesium.Cartesian3.fromDegrees(lng, lat, height);

经纬度转地理坐标(弧度)


const radians = Cesium.Math.toRadians(degrees) 

地理坐标(弧度)转经纬度


const degrees = Cesium.Math.toDegrees(radians) 

空间直角转经纬度


// 先将3D笛卡尔坐标转为地理坐标(弧度) 
const cartographic = Cesium.Cartographic.fromCartesian(cartesian3);
// 再将地理坐标(弧度)转为经纬度
const lat = Cesium.Math.toDegrees(cartographic.latitude);
const lng = Cesium.Math.toDegrees(cartographic.longitude);
const height = cartographic.height;

屏幕坐标转经纬度


// 监听点击事件,拾取坐标
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction((e) => {
const clickPosition = viewer.scene.camera.pickEllipsoid(e.position);
const randiansPos = Cesium.Cartographic.fromCartesian(clickPosition);
console.log(
"经度:" +
Cesium.Math.toDegrees(randiansPos.longitude) +
", 纬度:" +
Cesium.Math.toDegrees(randiansPos.latitude)
);
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

屏幕坐标转空间直角坐标


var cartesian3 = viewer.scene.globe.pick(viewer.camera.getPickRay(windowPostion),    viewer.scene); 

世界坐标转屏幕坐标


windowPostion = Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene, cartesian3); 

结语



作者的Cesium系列课程**《Cesium从入门到实战》**即将完结,课程介绍:ww7rybwvygd.feishu.cn/docx/PG1TdA…



如果想自学Cesium的也可以参考我的【开源项目】:github.com/tingyuxuan2…



有需要进技术产品开发交流群(可视化&GIS)可以加我:brown_7778(备注来意),也欢迎数字孪生可视化领域的交流合作。



最后,如果觉得文章对你有帮助,也希望可以一键三连👏👏👏,支持我持续开源和分享~


作者:攻城师不浪
来源:juejin.cn/post/7404091675666055209
收起阅读 »

苹果 visionOS for web

web
苹果的 Vision Pro 已经发布了,虽然没有拿到手,但我还是对它的操作界面充满了好奇。 我看到很多小伙伴写了 Windows for Web,Mac OS for Web,所以我也想来实现一下 Vision Pro 的系统主页。 一开始,我以为这不会太难...
继续阅读 »

苹果的 Vision Pro 已经发布了,虽然没有拿到手,但我还是对它的操作界面充满了好奇。


我看到很多小伙伴写了 Windows for Web,Mac OS for Web,所以我也想来实现一下 Vision Pro 的系统主页。


一开始,我以为这不会太难,当头一棒的就是苹果祖传优势: 动画。


CPT2401291503-845x461.gif


这动画,这模糊,还是从中心点开始逐渐向外层扩散,应该根据人眼的视觉特征进行设计的。


问题是,该如何实现呢?


模糊我知道怎么实现,


filter: blur(15px);

从中心点开始逐渐向外层扩散的效果,我直接来个
transition-delay: 0.1s;


一通操作之下,也实现就似是而非的效果。而且边缘处app图标的缓缓落下的效果也不好。


CPT2401291508-1281x733.gif


然后就是光影效果的实现,因为它的很美,让人很难忽略。


在 Vision Pro 系统演示中可以看出,为了模拟菜单栏使用了磨砂玻璃材质,而为了营造真实感,会模拟光照射到玻璃上而形成的光线边框。


我不知道这是不是菲涅尔效应,但问题是,这又该如何在前端实现呢?


我想到了 CSS Houdini,可以利用 Houdini 开放的底层能力 paint 函数来实现一个菜单栏效果。


if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('data:text/javascript,' + encodeURIComponent(`

class FresnelAppRectPainter {
static get inputProperties() { return ['--light-angle']; }

paint(ctx, size, properties) {
const borderRadius = 30;
const fresnelColor = 'rgba(255, 255, 255, .9)';
const lightAngle = parseFloat(properties.get('--light-angle')[0]) || 0;

// 绘制圆角矩形
ctx.beginPath();
ctx.moveTo(borderRadius, 0);
ctx.lineTo(size.width - borderRadius, 0);
ctx.arcTo(size.width, 0, size.width, borderRadius, borderRadius);
ctx.lineTo(size.width, size.height - borderRadius);
ctx.arcTo(size.width, size.height, size.width - borderRadius, size.height, borderRadius);
ctx.lineTo(borderRadius, size.height);
ctx.arcTo(0, size.height, 0, size.height - borderRadius, borderRadius);
ctx.lineTo(0, borderRadius);
ctx.arcTo(0, 0, borderRadius, 0, borderRadius);
ctx.closePath();
ctx.fillStyle = 'rgba(163, 163, 163)';
ctx.fill();

// 模拟光照效果
const gradient = create360Gradient(ctx, size, lightAngle)
ctx.fillStyle = gradient;
ctx.fill();

// 添加菲涅尔效果
const borderGradient = ctx.createLinearGradient(0, 0, size.width, size.height);
borderGradient.addColorStop(0, fresnelColor);
borderGradient.addColorStop(0.2, 'rgba(255,255,255, 0.7)');
borderGradient.addColorStop(1, fresnelColor);

ctx.strokeStyle = borderGradient;
ctx.lineWidth = 1.5;
ctx.stroke();
}
}

registerPaint('fresnelAppRect', FresnelAppRectPainter);
`));
}

结果效果还可以,我甚至可以接收一个光的入射角度,来实时绘制光影效果。


 function create360Gradient(ctx, size, angle) {
// 将角度转换为弧度
const radians = angle * Math.PI / 180;

// 计算渐变的起点和终点
const x1 = size.width / 2 + size.width / 2 * Math.cos(radians);
const y1 = size.height / 2 + size.height / 2 * Math.sin(radians);
const x2 = size.width / 2 - size.width / 2 * Math.cos(radians);
const y2 = size.height / 2 - size.height / 2 * Math.sin(radians);

// 创建线性渐变
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.2)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');

return gradient;
}

CPT2401291454-249x209.gif


演示效果图


哦对了,还有一个弹层底边角的缩放效果,我目前还没想到什么好办法来实现,年底还得抓紧搬砖,只能先搁置了,如果小伙伴们有好办法,欢迎告知或者讨论。


1706511484530.png


最终效果图


这里是 Demo 地址


本来是冲着纯粹娱乐(蹭流量)来写的,但写着写着就发现好像没那么简单,三个晚上过去,也只写了个首页,不得不感慨苹果真的太细了呀。


以上。


作者:于益
来源:juejin.cn/post/7329280514627600425
收起阅读 »

美女运营老师,天天找我改配置,我用node给她写了个脚本,终于安静了

web
美女运营老师,天天找我改配置,给她写了个脚本,终于安静了 事情的起因是,加入到新的小组中,在开发低代码后台管理页面的需求,需要配置一些下拉选项,后端因为一些特殊的原因,没法提供api接口,所以需要前端写成配置选项。这样问题就来了,新增了选项,但是没有给前端配置...
继续阅读 »

美女运营老师,天天找我改配置,给她写了个脚本,终于安静了


事情的起因是,加入到新的小组中,在开发低代码后台管理页面的需求,需要配置一些下拉选项,后端因为一些特殊的原因,没法提供api接口,所以需要前端写成配置选项。这样问题就来了,新增了选项,但是没有给前端配置。美女运营老师都会来找开发,说:为什么新导入的数据没有显示啊,是不是有bug。。找了一圈发现是配置没加


请在此添加图片描述


我让运营老师,把新增数据表格给我配置下,丢过来新增数据上来就是1000+,手动加要哭死。于是我就想能否用脚本生成一个。


刚开始借用在线CSV转换JSON


在线CSV转换JSON


把csv下载到本地,转换成json,返回数据倒是能返回,但是不符合运营老师的要求,key值需要是 key: ${data.value}-${data.key}


请在此添加图片描述


请在此添加图片描述


于是我就写了下面第一个简单版的node脚本


const fs = require('fs')
const csv = require('csv-parser');

const uidsfilePath = './uids.json';

const datas = [`复制生成的json数据`];
let newarr = [];
format(datas);

fs.writeFile(uidsfilePath, JSON.stringify(newarr), () => {
console.log('done')
})

const format = (results) => {
newarr = results.map(item => {
return {
label: `${item.value}-${item.key}`,
value: item.value
}
})
}

okok 到这里可以生成了。但是吧,想把这个事情还给运营老师,嘿


于是我又在这个基础上加上了读取CSV文件,格式化数据,输出JSON文件


使用 csv-parser读取 csv文件


csv-parser 是一个为Node.js设计的高效且流线型库,专注于快速解析CSV数据。它旨在提供最小的开销,保持简单轻量,特别适配于Node.js的流处理。此库遵循RFC 4180标准,并通过csv-spectrum的酸性测试套件,确保了对各种CSV变体的广泛兼容性和正确性。性能方面,csv-parser在处理大文件时表现出色,无论是带引号还是不带引号的CSV数据。


快速使用csv-parser


开始使用csv-parser,首先确保你的环境中已安装Node.js。接着,通过npm安装


csv-parser:


npm install csv-parser

示例代码

const fs = require('fs');
const parse = require('csv-parser');
fs.createReadStream('yourfile.csv')
.pipe(parse({ headers: true }))
.on('data', (row) => {
console.log(row);
})
.on('end', () => {
console.log('CSV file successfully processed');
});

第二版脚本


直接获取csv文件,生成输出JSON


const fs = require('fs')
const csv = require('csv-parser');

const csvfilePath = './新增UID.csv';
const uidsfilePath = './uids.json';

const results = [];
let newarr = [];

fs.createReadStream(csvfilePath)
.pipe(csv({ headers: true }))
.on('data', (data) => {
results.push(data);
})
.on('end',async () => {
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
}
})
}

部分生成的数据


请在此添加图片描述


到这里又节省了一些时间,但是运营老师既不会安装node,也不会使用命令执行node CSVtoJSON.js,太复杂了不会弄。。。我说你提个需求吧,后面给您做成页面上传csv文件,返回JSON数据,点击一键复制好不好。


仅供娱乐,欢迎吐槽


未完待续,持续更新中...


感谢关注点赞评论~


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

【环信uniapp uikit】手把手教你uniapp uikit运行到鸿蒙

写在前面:好消息好消息,环信uniapp出uikit啦~更好的消息,环信uniapp sdk也支持鸿蒙系统啦!!!!那么我们一起来看看uniapp uikit如何运行到鸿蒙系统~~let's gouniapp uikit以及支持鸿蒙系统的uniapp sdk版...
继续阅读 »

写在前面:
好消息好消息,环信uniapp出uikit啦~
更好的消息,环信uniapp sdk也支持鸿蒙系统啦!!!!那么我们一起来看看uniapp uikit如何运行到鸿蒙系统~~let's go
uniapp uikit以及支持鸿蒙系统的uniapp sdk版本都是`4.11.0`



准备工作
1. HBuilderX 4.36
2. DevEco-Studio 5.0.5.310
3. sass:sass-loader 10.1.1 及之前版本
4. node:12.13.0 - 17.0.0,推荐 LTS 版本 16.17.0
5. npm:版本需与 Node.js 版本匹配
6. 已经在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了App Key
7. 了解uniapp创建运行鸿蒙系统
8. 了解uniapp UIkit各功能以及api调用



开始集成:
第一步:创建一个uniapp+vue3项目进度10%

第二步:安装依赖  进度15%
npm init -y

npm i easemob-websdk@4.11.0 pinyin-pro@3.26.0 mobx@6.13.4 --save
第三步:下载uniapp uikit源码 进度20%

git clone https://github.com/easemob/easemob-uikit-uniapp.git
第四步:拷贝uikit组件 进度50%

mkdir -p ./ChatUIKit 

 # macOS 

 mv ${组件项目路径}/ChatUIKit/* ./ChatUIKit 

 # windows 

 move ${组件项目路径}/ChatUIKit/* .\ChatUIKit



第五步:替换pages/index/index.vue文件 进度70%






第六步:替换app.vue文件 进度80%




第七步:在pages.json配置路由 进度90%

{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "ChatUIKit/modules/Chat/index",
"style": {
"navigationStyle": "custom",
// #ifdef MP-WEIXIN
"disableScroll": true,
// #endif
"app-plus": {
"bounce": "none",
"softinputNavBar": "none"
}
}
},
{
"path": "ChatUIKit/modules/VideoPreview/index",
"style": {
"navigationBarTitleText": "Video Preview",
"app-plus": {
"bounce": "none"
}
}
}
]
}





第八步:运行到chrome浏览器看下效果 进度90% 



 第九步:运行到鸿蒙并发送一条消息 进度100%





遇到的问题:
问题1:
详细报错信息如下
hvigor ERROR: Invalid product for target 'default'.
Detail: Check the target applyToProducts field for 'default': [ 'default', 'release' ].
at /Users/admin/Desktop/ouyeel_worksheet/unpackage/debug/app-harmony-2f573459/build-profile.json5
解决方案: 
在harmony-configs/build-profile.json5文件,复制default配置,将default改为relese,参考


问题2:
登录报错

解决方案:
在/harmony-configs/entry/src/main/module.json5文件添加以下代码

"requestPermissions": [ {"name": "ohos.permission.GET_NETWORK_INFO"}, { "name": "ohos.permission.INTERNET"}, ],


问题3:
HBuilderX无限重连

解决方案:

看看sdk 是不是最新版,无限重连的问题已经在 4.12.0版本的sdk修复~

总结:
初步运行到鸿蒙的话问题会比较多,大家可以善用百度大法解决掉它!!!


收起阅读 »

老板不让用ECharts,还想让我画很多圆环!

web
需求简介 这几天下来个新需求:要在页面上动态渲染多个表格,每个表格内还要实现若干圆环! 刚拿到这个需求的时候,我第一反应是用echarts实现,简单快捷 然而,老板无情的拒绝了我!他说: 咱这个项目就一个独立页面,你引入个ECharts项目又要大很多!而...
继续阅读 »

需求简介


这几天下来个新需求:要在页面上动态渲染多个表格,每个表格内还要实现若干圆环!



刚拿到这个需求的时候,我第一反应是用echarts实现,简单快捷



然而,老板无情的拒绝了我!他说:



咱这个项目就一个独立页面,你引入个ECharts项目又要大很多!而且一个页面这么多ECharts实例,性能怎么保障?不准用ECharts,用CSS实现!



没办法,我只好百度如何用CSS画圆环。幸运的是,我确实找到了类似的文章:



不幸的事,效果和我的差异很大,代码根本 无法复用!没办法,只能用别的办法实现了。经过一番研究,最终借助Canvas搞出来了,简单的分享一下我的实现思路吧。


圆环组件简介


为了方便复用,我把这个功能封装成了项目可直接复用的组件。并支持自定义圆环大小、圆环宽度和圆环颜色比例配置属性。


<Ring
:storkWidth="5"
:size="60"
:ratioList="[
{ ratio: 0.3, color: '#FF5733' },
{ ratio: 0.6, color: '#33FF57' },
{ ratio: 0.1, color: '#3357FF' }
]"

></Ring>


技术方案


实现目标


根据我们的UX需求,我们需要实现一个简单的组件,该组件可以展示一个圆环图表,并根据外部传入的比例数据(如 ratioList)绘制不同颜色的环形区域。



  • 使用 Vue 3 和 TypeScript。

  • 动态绘制环形图,根据传入的数据绘制多个环。

  • 支持自定义环形图的大小和环宽。


创建 Vue 组件框架


首先,我们创建一个名为 RingChart.vue的组件。组件的初始结构非常简单,只包含一个 canvas 元素。


<template>
<!-- 创建一个 canvas 元素,用于绘制图表 -->
<canvas ref="canvasDom"></canvas>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue';

// 获取 canvas DOM 元素的引用
const canvasDom = ref<HTMLCanvasElement | null>(null);

// 初始化 canvas 和上下文变量
let ctx: CanvasRenderingContext2D | null = null;
let width: number, height: number;

// 初始化 canvas 尺寸和绘图环境
const initCanvas = () => {
const dom = canvasDom.value;
if (!dom) return;
ctx = dom.getContext('2d');
if (!ctx) return;

// 设置 canvas 的宽高
dom.width = dom.offsetWidth;
dom.height = dom.offsetHeight;
width = dom.offsetWidth;
height = dom.offsetHeight;
};

// 在组件挂载后执行初始化
onMounted(() => {
initCanvas();
});
</script>

<style scoped>
canvas {
width: 100%;
height: 100%;
}
</style>

上述代码中,我们初始化了 canvas 元素,并且设定了 widthheight 属性。


绘制基本的圆环


接下来,我们添加绘制圆环的功能:通过 arc 方法来绘制圆环,设置 lineWidth 来调整环的宽度。


<script lang="ts" setup>
import { ref, onMounted } from 'vue';

// 获取 canvas DOM 元素的引用
const canvasDom = ref<HTMLCanvasElement | null>(null);

// 初始化 canvas 和上下文变量
let ctx: CanvasRenderingContext2D | null = null;
let width: number, height: number;

// 初始化 canvas 尺寸和绘图环境
const initCanvas = () => {
const dom = canvasDom.value;
if (!dom) return;
ctx = dom.getContext('2d');
if (!ctx) return;

// 设置 canvas 的宽高
dom.width = dom.offsetWidth;
dom.height = dom.offsetHeight;
width = dom.offsetWidth;
height = dom.offsetHeight;

// 调用绘制圆环的方法
drawCircle({
ctx,
x: width / 2,
y: height / 2,
radius: 8,
lineWidth: 4,
color: '#C4C9CF4D',
startAngle: -Math.PI / 2,
endAngle: Math.PI * 1.5,
});
};

// 绘制一个圆环的方法
const drawCircle = ({
ctx,
x,
y,
radius,
lineWidth,
color,
startAngle,
endAngle,
}: {
ctx: CanvasRenderingContext2D;
x: number;
y: number;
radius: number;
lineWidth: number;
color: string;
startAngle: number;
endAngle: number;
}
) => {
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.stroke();
ctx.closePath();
};

onMounted(() => {
initCanvas();
});
</script>


  • drawCircle 函数是绘制圆环的核心。我们通过 arc 方法绘制圆形路径,使用 lineWidth 来调整环的宽度,并用 strokeStyle 给圆环上色。

  • startAngleendAngle 参数决定了圆环的起始和结束角度,通过改变它们可以控制环的覆盖区域。


绘制多个环形区域


现在,我们来实现绘制多个环形区域的功能。我们将通过传入一个 ratioList 数组来动态生成多个环,每个环代表不同的比例区域。


<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';

// 定义 props 的类型
interface RatioItem {
ratio: number;
color: string;
}

const props = defineProps<{
size?: number; // 画布大小
storkWidth?: number; // 环的宽度
ratioList?: RatioItem[]; // 比例列表
}>();

// 默认值
const defaultSize = 200;
const defaultStorkWidth = 4;
const defaultRatioList: RatioItem[] = [
{ ratio: 1, color: '#C4C9CF4D' },
];

// canvas DOM 和上下文
const canvasDom = ref<HTMLCanvasElement | null>(null);
let ctx: CanvasRenderingContext2D | null = null;

// 动态计算 canvas 的中心点和半径
const size = computed(() => props.size || defaultSize);
const center = computed(() => ({
x: size.value / 2,
y: size.value / 2,
}));
const radius = computed(() => size.value / 2 - (props.storkWidth || defaultStorkWidth));

// 初始化 canvas
const initCanvas = () => {
const dom = canvasDom.value;
if (!dom) return;

ctx = dom.getContext('2d');
if (!ctx) return;

dom.width = size.value;
dom.height = size.value;

drawBackgroundCircle();
drawDataRings();
};

// 绘制背景圆环
const drawBackgroundCircle = () => {
if (!ctx) return;

drawCircle({
ctx,
x: center.value.x,
y: center.value.y,
radius: radius.value,
lineWidth: props.storkWidth || defaultStorkWidth,
color: '#C4C9CF4D',
startAngle: -Math.PI / 2,
endAngle: Math.PI * 1.5,
});
};

// 绘制数据圆环
const drawDataRings = () => {
const { ratioList = defaultRatioList } = props;
if (!ctx) return;

let startAngle = -Math.PI / 2;
ratioList.forEach(({ ratio, color }) => {
const endAngle = startAngle + ratio * Math.PI * 2;

drawCircle({
ctx,
x: center.value.x,
y: center.value.y,
radius: radius.value,
lineWidth: props.storkWidth || defaultStorkWidth,
color,
startAngle,
endAngle,
});

startAngle = endAngle;
});
};

// 通用绘制函数
const drawCircle = ({
ctx,
x,
y,
radius,
lineWidth,
color,
startAngle,
endAngle,
}: {
ctx: CanvasRenderingContext2D;
x: number;
y: number;
radius: number;
lineWidth: number;
color: string;
startAngle: number;
endAngle: number;
}
) => {
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.stroke();
ctx.closePath();
};

// 监听画布大小变化
onMounted(() => {
initCanvas();
});
</script>

上述代码中,我们通过 ratioList 数组传递每个环的比例和颜色,使用 startAngleendAngle 来控制每个环的绘制区域。其中,drawDataRings 函数遍历 ratioList,根据每个数据项的比例绘制环形区域。


现在,我们的组件就实现完毕了,可以在其他地方引入使用了


<RingChart
:storkWidth="8"
:size="60"
:ratioList="[
{ ratio: 0.3, color: '#F8766F' },
{ ratio: 0.6, color: '#69CD90' },
{ ratio: 0.1, color: '#FFB800' }
]"

></RRingChart>


组件代码


<template>
<canvas ref="canvasDom"></canvas>
</template>

<script lang="ts" setup>
import { ref, computed, onMounted, watchEffect } from 'vue';

// 定义 props 的类型
interface RatioItem {
ratio: number;
color: string;
}

const props = defineProps<{
size?: number; // 画布大小
storkWidth?: number; // 环的宽度
ratioList?: RatioItem[]; // 比例列表
}>();

// 默认值
const defaultSize = 200; // 默认画布宽高
const defaultStorkWidth = 4;
const defaultRatioList: RatioItem[] = [{ ratio: 1, color: '#C4C9CF4D' }];

// canvas DOM 和上下文
const canvasDom = ref<HTMLCanvasElement | null>(null);
let ctx: CanvasRenderingContext2D | null = null;

// 动态计算 canvas 的中心点和半径
const size = computed(() => props.size || defaultSize);
const center = computed(() => ({
x: size.value / 2,
y: size.value / 2
}));
const radius = computed(() => size.value / 2 - (props.storkWidth || defaultStorkWidth));

// 初始化 canvas
const initCanvas = () => {
const dom = canvasDom.value;
if (!dom) return;

ctx = dom.getContext('2d');
if (!ctx) return;

dom.width = size.value;
dom.height = size.value;

drawBackgroundCircle();
drawDataRings();
};

// 绘制背景圆环
const drawBackgroundCircle = () => {
if (!ctx) return;

drawCircle({
ctx,
x: center.value.x,
y: center.value.y,
radius: radius.value,
lineWidth: props.storkWidth || defaultStorkWidth,
color: '#C4C9CF4D',
startAngle: -Math.PI / 2,
endAngle: Math.PI * 1.5
});
};

// 绘制数据圆环
const drawDataRings = () => {
const { ratioList = defaultRatioList } = props;
if (!ctx) return;

let startAngle = -Math.PI / 2;
ratioList.forEach(({ ratio, color }) => {
const endAngle = startAngle + ratio * Math.PI * 2;

drawCircle({
ctx,
x: center.value.x,
y: center.value.y,
radius: radius.value,
lineWidth: props.storkWidth || defaultStorkWidth,
color,
startAngle,
endAngle
});

startAngle = endAngle;
});
};

// 通用绘制函数
const drawCircle = ({
ctx,
x,
y,
radius,
lineWidth,
color,
startAngle,
endAngle
}: {
ctx: CanvasRenderingContext2D;
x: number;
y: number;
radius: number;
lineWidth: number;
color: string;
startAngle: number;
endAngle: number;
}
) => {
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.stroke();
ctx.closePath();
};

// 监听画布大小变化
watchEffect(() => {
initCanvas();
});

onMounted(() => {
initCanvas();
});
</script>

<style scoped>
canvas {
display: block;
margin: auto;
border-radius: 50%;
}
</style>

使用


<Ring
:storkWidth="5"
:size="60"
:ratioList="[
{ ratio: 0.3, color: '#FF5733' },
{ ratio: 0.6, color: '#33FF57' },
{ ratio: 0.1, color: '#3357FF' }
]"

></Ring>

总结


累了,今天不想写总结!


作者:快乐就是哈哈哈
来源:juejin.cn/post/7444014749321510963
收起阅读 »

三维gis的就业前景如何

一、前言 三维GIS是一个伪概念,GIS是地理信息系统,三维GIS就是三维地理信息系统,在课本上,专业概念上,也没有这一说法吧,所以三维GIS,就是技术人员造概念拼凑造出来的,本质上就是GIS三维可视化。 二、详细介绍 GIS从广义层面分为两大方面,第一是数据...
继续阅读 »

一、前言


三维GIS是一个伪概念,GIS是地理信息系统,三维GIS就是三维地理信息系统,在课本上,专业概念上,也没有这一说法吧,所以三维GIS,就是技术人员造概念拼凑造出来的,本质上就是GIS三维可视化。


二、详细介绍


GIS从广义层面分为两大方面,第一是数据及数据管理,第二是业务功能,其中业务,就是分析、显示、交互。按照云计算服务划分,分别对应着DAAS层和SAAS层。按照所谓三维GIS的说法,那就是三维数据管理、三维分析、三维显示、三维交互。目前三维GIS基本上就是围绕着三维数据、三维分析、三维数据显示以及三维交互这四方面展开,其中三维数据是主要表现力。


1.三维数据


三维GIS场景与游戏场景的最主要也是最本质区别在于,三维GIS的数据都是真实的,来源于真实世界映射数据,包括卫星影像、三维地形DEM/DSM/DTM、倾斜摄影、点云等各式各样数据。游戏场景绝大部分人工建模出来的理想中的场景。一个是面向现实,一个面向未来,如果把两者结合,是不是有“元宇宙”的味道了呢?
在这里插入图片描述


2.三维分析


三维分析,即基于三维数据进行的分析计算,三维即三维空间,三维空间的GIS分析,三维空间距离量算、三维体积量算、三维高度量算……本质上应该比二维分析更丰富才对。可惜的是,三维空间分析,绝大部分在课本上找不到,更别说概念化描述。
在这里插入图片描述


3.三维可视化


三维可视化绝大部分是三维数据的可视化,我们常见的有地形可视化、倾斜摄影可视化
在这里插入图片描述


4.三维交互


除了普通的漫游、缩放、旋转、更有俯仰、翻转等操作。从上面可以看出三维GIS重点还是在于可视化,做三维GIS可视化,两大核心是GIS+图形学。想要在三维GIS深耕,就需要在计算机图形学有很好的基础,较强的GIS开发概念,以及自己的想象力。而图形学是公认的成长缓慢的大后期,比C++成长更慢,更别说与Java、JavaScript等相提并论。因此以后把三维GIS作为自己的就业方向,前景很光明,即使以后不在GIS圈,去互联网游戏行业都能胜任,而且比纯游戏开发更有优势。


image.png


三、结语


目前GIS公司对三维GIS的开发需求很强烈,但是受限于大学所学,能在大学既能懂GIS又懂图形学的,凤毛麟角,所以三维GIS开发很抢手,但是学校都找不到人,绝大部分都是招来基础好的学生,从零开始培养。


学好三维GIS,不仅仅会一个Cesium、MapBox就完了,这最初级阶段,只是二次开发,熟悉了接口,你就需要从原理角度去熟悉底层原理,渲染管线、地形瓦片及3dtiles调度与渲染等等,这些都需要你去认真深入学习的,还有一点,也是很三维GIS的最重要的一件武器:shader。这是必须熟练掌握的,包括glsl,hlsl等。


最后,就是要学游戏引擎,目前,做三维GIS与游戏引擎结合的越来越紧密,随着信创产业的快速发展,UE以后会越来越得到应用,做三维GIS,离不开熟练使用UE或者Unity。以上是我个人的一些看法,希望看到有更多的人投入到三维GIS中去,创造越来越多很酷的三维GIS产品,服务社会,造福人类!


作者:按图索迹
来源:juejin.cn/post/7337188759060267023
收起阅读 »

23年计算机科学与技术毕业生的2024 年终总结

### 2024年年终工作总结     引言随着2024年的落幕,作为一名计算机科学与技术专业的毕业生,我有幸参与到一款聊天应用的开发工作中。这一年,我不仅在技术层面获得了长足的进步,也在团队协作和项目管理方面...
继续阅读 »

### 2024年年终工作总结

     引言

随着2024年的落幕,作为一名计算机科学与技术专业的毕业生,我有幸参与到一款聊天应用的开发工作中。这一年,我不仅在技术层面获得了长足的进步,也在团队协作和项目管理方面积累了宝贵的经验。以下是我这一年工作的详细总结。

     技术成长

1.    安卓开发技能提升    
   在这一年中,我深入学习了Android开发框架,掌握了Kotlin和Java两种编程语言,熟悉了Android Studio开发环境。
   学习并实践了Material Design设计原则,优化了用户界面和用户体验。
   掌握了网络编程,包括HTTP协议、RESTful API以及使用Retrofit和Volley等网络库进行数据通信。

2.    项目管理与协作    
   参与了敏捷开发流程,学习了Scrum框架,每周参与Scrum会议,及时同步项目进度。
   学会了使用Git进行版本控制,以及在GitHub上进行代码管理和团队协作。

3.    性能优化与问题解决    
   学习了Android性能优化的技巧,包括内存管理、布局优化和多线程编程。
   通过日志分析和性能监控工具,定位并解决了多个应用崩溃和性能瓶颈问题。

     项目成果

1.    聊天应用核心功能开发    
   负责聊天应用的核心聊天功能模块开发,包括消息发送、接收和展示。
   实现了消息加密传输,保障用户通信安全。

2.    用户界面与交互设计    
   参与设计并实现了应用的用户界面,使其更加直观和易用。
   根据用户反馈,迭代优化了多个界面元素,提升了用户满意度。

3.    跨平台兼容性测试    
   参与了应用的跨平台测试工作,确保应用在不同设备和操作系统版本上的兼容性和稳定性。

    团队协作与领导力

1.    团队沟通与协作    
   作为团队的一员,我积极参与团队讨论,提出建设性意见,并协助解决同事遇到的问题。
   学会了如何更有效地与团队成员沟通,提高了团队的整体效率。

2.    领导力的培养    
   在项目中担任小组长,负责协调小组内的工作分配和进度跟踪,锻炼了我的领导能力。

    个人成长与反思

1.    自我学习与提升    
   通过在线课程和专业书籍,不断学习新的技术和行业动态,保持自己的竞争力。
   反思过去一年的工作,认识到自己在时间管理和优先级排序上的不足,并制定了改进计划。

2.    职业规划    
   明确了自己的职业发展方向,计划在未来几年内深入学习人工智能和机器学习,为公司的技术创新贡献力量。



2024年对我来说是充满挑战和成长的一年。我不仅在技术上有所提升,也在团队协作和项目管理方面获得了宝贵的经验。展望未来,我将继续努力,为公司的发展贡献自己的力量,并实现个人职业生涯的进一步发展。 环信的easyimkit很好用。


收起阅读 »

半死不活的2024

生活篇:1、今年是否完成了脱单/结婚:又是等花花的一年!2、描述今年的身体状况:马马虎虎,还活着3、描述今年的精神状态:癫疯4、今年最令你高兴/开心的那个瞬间:面的学校都收到offer了5、 今年最大的收获是什么:心态更平稳6、 实现了年初的什么目标:好好学习...
继续阅读 »

生活篇:
1、今年是否完成了脱单/结婚:又是等花花的一年!

2、描述今年的身体状况:马马虎虎,还活着

3、描述今年的精神状态:癫疯

4、今年最令你高兴/开心的那个瞬间:面的学校都收到offer了

5、 今年最大的收获是什么:心态更平稳

6、 实现了年初的什么目标:好好学习(?)

7、新增了哪一项热爱的东西:捡垃圾(包括但不限于手柄、充电宝、路由器)

8、失去了哪一项曾经热衷的东西:心气

9、今年你一定增长了很多见识,请问最让你印象深刻的是什么?为啥?:遇上了很多人,有好有坏,人教人学不会,事儿教人一次就会

10、一句话描述现在的自己:

正在向着理想中的地方的学校半死不活的间歇性挣扎着(冲冲冲)

工作篇:
11、今年跳槽了吗?冇

12、今年失业了吗?冇

13、今年涨薪了吗?冇

14、加班占工作时间的百分比:(或具体描述加班情况)看ddl什么时候通知

15、发量占头的百分比:怒发冲冠

收起阅读 »

2024年年终总结 — 移动端开发者的路该怎么走

2024年即将结束,作为一名移动端开发者,回顾过去的一年,心中充满了感慨与收获。这一年,移动端开发领域依旧充满挑战与机遇,新技术的涌现、行业需求的变化以及团队协作的深化,都促使我们不断调整自己的发展路径和技术积累。2024年,移动端开发领域发生了诸多变化,其中...
继续阅读 »

2024年即将结束,作为一名移动端开发者,回顾过去的一年,心中充满了感慨与收获。这一年,移动端开发领域依旧充满挑战与机遇,新技术的涌现、行业需求的变化以及团队协作的深化,都促使我们不断调整自己的发展路径和技术积累。

2024年,移动端开发领域发生了诸多变化,其中最显著的是跨平台开发的兴起。Flutter 和 UniApp 等等作为最流行的跨平台框架,逐渐在大型项目中得到应用。然而原生开发者们的路越走越窄。

2024年,随着AI技术的发展,越来越多的智能化应用涌现。作为移动端开发者,学习如何将AI技术应用于移动端开发中,未来移动端开发将不再仅仅局限于传统的UI和功能实现,AI、AR/VR以及5G等技术将逐步融入到移动应用中,带来更加丰富和个性化的用户体验。

2025年,是机遇还是更加刺骨的寒冬,拭目以待。

收起阅读 »

我的2024

作为一名前端开发者,2024年的年终总结应该既反映技术层面的成长,又体现工作中的挑战与收获。以下是一个适合前端开发者的年终总结框架,涵盖技术发展、项目经验、软技能提升等多个维度:2024年是我技术进步的重要一年,掌握了很多新的前端技术和工具,深入了解了前端生态...
继续阅读 »

作为一名前端开发者,2024年的年终总结应该既反映技术层面的成长,又体现工作中的挑战与收获。以下是一个适合前端开发者的年终总结框架,涵盖技术发展、项目经验、软技能提升等多个维度:

2024年是我技术进步的重要一年,掌握了很多新的前端技术和工具,深入了解了前端生态的最新趋势。具体而言,我在以下几个方面取得了显著进步:

JavaScript:加强了对ES6+新特性(如解构赋值、箭头函数、Promise、async/await、模块化等)的理解,提升了编写现代化代码的能力。尤其是在异步编程和状态管理方面,有了更深入的掌握。

框架与库:深入学习并应用了ReactVue,尤其是在React中,使用了React Hooks和Context进行组件管理,提升了代码的可维护性与复用性。Vue 3的Composition API也让我在处理复杂状态时更加高效。

前端工具链:深入掌握了WebpackVite等构建工具,优化了构建流程和性能。

                熟悉了TypeScript,不仅能更好地进行类型检查,还提高了开发效率。

               学习了ESLintPrettier等代码规范工具,规范了团队的代码质量。

CSS与设计系统

             在CSS方面,掌握了FlexboxGrid布局,更加灵活地解决了复杂的布局问题。

             参与了公司设计系统的建设,理解并实现了组件化开发,提高了UI组件的复用率与一致性。

            学习了CSS-in-JS的方案(如styled-components),使得在React项目中可以更好地管理样式。

性能优化

            深入理解了前端性能优化的关键技术,如代码分割、懒加载、图片优化、减少重排重绘等,提高了应用的加载速度与响应性能。

前端工程化:引入了CI/CD流程,搭建了自动化构建与部署的流程,减少了人工干预,提升了开发效率。在项目中积极推动模块化和组件化的开发,推动了代码重构,使得代码更加易于维护和扩展。
























回顾2024年,作为一名前端开发者,能运用vue、react等语言进行前端开发。我在技术和工作能力上都有了很大的进步。在未来,我会继续保持学习的热情,不断提升自己的技术水平和软技能,为团队和公司创造更多的价值。希望在接下来的工作中能够实现更多的突破,迎接新的挑战,取得更大的成就!

对于学习了react并且用react开发工程,是我最大的收获

收起阅读 »

我的2024年终终结

        回望过去的这一年,我在技术领域经历了显著的成长与转型。作为一名拥有多年安卓开发经验的工程师,我见证了安卓平台从蓬勃兴起到逐步融入多元化技术生态的历程。尽管安卓原生开...
继续阅读 »

        回望过去的这一年,我在技术领域经历了显著的成长与转型。作为一名拥有多年安卓开发经验的工程师,我见证了安卓平台从蓬勃兴起到逐步融入多元化技术生态的历程。尽管安卓原生开发依然占据重要地位,但不可否认的是,技术的浪潮已经带来了新的变革。

        随着小程序、UniApp等新兴技术的崛起,我深刻感受到了市场对于快速迭代、跨平台兼容性的强烈需求。这些新技术不仅极大地提升了开发效率,还为用户带来了更加流畅、一致的使用体验。因此,我积极拥抱变化,将自己的技能树向这些新领域拓展。

        在过去的一年里,我不仅继续深化了安卓原生开发的能力,还深入学习了小程序和UniApp的开发技术。通过实际项目的历练,我掌握了如何快速构建小程序应用,以及如何利用UniApp实现一次开发、多端运行的高效开发模式。这些经历不仅丰富了我的技术栈,也让我更加自信地面对多变的市场需求。

        同时,我也积极参与了后台开发的工作。通过深入了解后端架构和数据处理流程,我对于整个技术链条有了更加全面的认识。这种跨领域的实践不仅提升了我的综合技术能力,也让我在团队协作中发挥了更大的价值。

        展望未来,我将继续紧跟技术发展的步伐,不断学习和探索新的技术领域。我相信,只有不断适应变化、勇于创新,才能在激烈的市场竞争中立于不败之地。期待在新的一年里,我能够为公司贡献更多的价值,同时也实现自我价值的不断提升。

收起阅读 »

我乱七八糟的2024

**App开发团队2024年年终总结报告****一、引言**随着2024年的圆满结束,我们的App开发团队在这一年中取得了显著的成就。本报告旨在详细回顾我们的工作成果、总结经验教训,并为2025年的发展规划提供指导。**二、项目回顾**1. **项目概览** ...
继续阅读 »


**App开发团队2024年年终总结报告**

**一、引言**

随着2024年的圆满结束,我们的App开发团队在这一年中取得了显著的成就。本报告旨在详细回顾我们的工作成果、总结经验教训,并为2025年的发展规划提供指导。

**二、项目回顾**

1. **项目概览**
- **项目A:健康追踪App**
- 目标:为用户提供一个全面的健康追踪和管理平台。
- 成果:成功上线,用户反馈积极,下载量突破100万。

- **项目B:在线教育平台**
- 目标:打造一个互动性强、资源丰富的在线学习环境。
- 成果:完成Beta测试,用户增长率达到20%。

2. **关键里程碑**
- **项目A:**
- 1月:项目启动。
- 6月:完成初步开发,开始内部测试。
- 9月:正式上线。
- **项目B:**
- 4月:项目启动。
- 8月:完成初步开发,开始Beta测试。
- 12月:收集反馈,准备全面上线。

3. **技术亮点**
- **项目A:**
- 引入了最新的机器学习算法,为用户提供个性化健康建议。
- **项目B:**
- 采用了最新的实时通讯技术,增强了师生间的互动体验。

**三、成果与数据分析**

1. **用户增长**
- **项目A:**
- 新增用户:150万
- 活跃用户:60万
- **项目B:**
- 新增用户:50万
- 活跃用户:10万

2. **用户反馈与市场表现**
- **项目A:**
- 用户满意度:90%
- 应用商店排名:健康类App第3位
- **项目B:**
- 用户满意度:85%
- 应用商店排名:教育类App第5位

3. **财务概况**
- **项目A:**
- 收入:500万美元
- 成本:300万美元
- 利润:200万美元
- **项目B:**
- 收入:100万美元
- 成本:80万美元
- 利润:20万美元

**四、团队建设与协作**

1. **团队规模与构成**
- 团队规模从20人增长至30人,包括5名新加入的高级工程师和2名产品经理。

2. **团队文化与氛围**
- 举办了多次团队建设活动,增强了团队凝聚力和创新能力。

3. **跨部门协作**
- 与市场部门合作,成功推广了项目A,与客服部门合作,提高了用户满意度。

**五、挑战与应对**

1. **遇到的主要挑战**
- **项目A:**
- 用户隐私保护问题。
- **项目B:**
- 内容更新速度跟不上用户需求。

2. **解决方案与调整**
- **项目A:**
- 加强数据加密和隐私政策的透明度。
- **项目B:**
- 增加内容团队,提高内容更新频率。

**六、经验总结**

1. **成功经验**
- **项目A:**
- 个性化推荐系统大幅提升了用户粘性。
- **项目B:**
- 实时反馈机制有效提高了学习效率。

2. **教训与反思**
- **项目A:**
- 需要更早地关注用户隐私问题。
- **项目B:**
- 内容质量比数量更重要,需要更注重内容的深度和质量。

**七、未来规划**

1. **技术趋势与创新**
- 计划引入更多的AI技术,提升用户体验。

2. **项目规划**
- **项目C:**
- 计划开发一款面向儿童的教育游戏App。

3. **团队发展**
- 计划增加10名开发人员,以支持新项目的启动。

**八、结语**

感谢团队成员一年来的辛勤工作和贡献。我们期待在新的一年里,继续携手前进,创造更多的价值和成就。

收起阅读 »

智源最新评测:豆包视觉理解模型排名全球第二

12月19日,智源研究院发布最新一期大模型综合及专项评测结果。在覆盖国内外100余个开源和商业闭源大模型的评测中,豆包通用模型pro获得大语言模型主观评测最高分;在多模态模型评测中,豆包视觉理解模型排名视觉语言模型第二,成绩仅次于GPT-4o;豆包文生图模型、...
继续阅读 »

12月19日,智源研究院发布最新一期大模型综合及专项评测结果。在覆盖国内外100余个开源和商业闭源大模型的评测中,豆包通用模型pro获得大语言模型主观评测最高分;在多模态模型评测中,豆包视觉理解模型排名视觉语言模型第二,成绩仅次于GPT-4o;豆包文生图模型、豆包视频生成模型(即梦P2.0 pro)也分别在相应测试中获得全球第二。

图片1.png

据智源研究院介绍,大模型评测平台FlagEval联合了全国10余家高校和机构合作共建。此次公布的榜单中,大语言模型主观评测重点考察的是模型的中文能力,多模态模型评测榜单中,视觉语言模型主要考察的是模型在图文理解、长尾视觉知识、文字识别以及复杂图文数据分析能力。FlagEval大模型角斗场则是向用户开放的模型对战评测服务,反映了用户对模型的偏好。

在大语言模型主观评测中,豆包通用模型pro的知识运用和推理能力均获得最高分,简单理解、数学能力、安全等项目也排名前三,最终综合成绩排名第一。在FlagEval大模型角斗场榜单中,基于模型对战的用户投票结果,豆包通用模型pro得分排名第二,仅次于OpenAI的o1-mini。

在多模态模型评测榜单中,GPT-4o在视觉语言模型中排名第一,豆包视觉理解模型获第二。在中文的通用知识、文字识别中,豆包表现突出,相比国外模型有较大优势。在文生图测试中,混元和豆包排名前两位;在文生视频测试中,国产模型更是优势显著,可灵1.5高品质版、即梦P2.0 pro、爱诗科技PixVerse v3和海螺AI排名前列。

据悉,豆包视觉理解模型在不久前的火山引擎Force大会上首次发布,现已对企业客户开放使用。火山引擎方面表示,豆包大模型通过算法、工程、软硬件结合的技术创新,大幅优化使用成本,让每一家企业都能用得起,推动AI技术普惠和应用发展。(作者:李双)

收起阅读 »

我发现很多程序员都不会打日志。。

大家好,我是程序员鱼皮。我发现很多程序员都不打日志,有的是 不想 打、有的是 意识不到 要打、还有的是 真不会 打日志啊! 前段时间的模拟面试中,我问了几位应届的 Java 开发同学 “你在项目中是怎么打日志的”,得到的答案竟然是 “支支吾吾”、“阿巴阿巴”,...
继续阅读 »

大家好,我是程序员鱼皮。我发现很多程序员都不打日志,有的是 不想 打、有的是 意识不到 要打、还有的是 真不会 打日志啊!


前段时间的模拟面试中,我问了几位应届的 Java 开发同学 “你在项目中是怎么打日志的”,得到的答案竟然是 “支支吾吾”、“阿巴阿巴”,更有甚者,竟然表示:直接用 System.out.println() 打印一下吧。。。



要知道,日志是我们系统出现错误时,最快速有效的定位工具,没有日志给出的错误信息,遇到报错你就会一脸懵逼;而且日志还可以用来记录业务信息,比如记录用户执行的每个操作,不仅可以用于分析改进系统,同时在遇到非法操作时,也能很快找到凶手。


因此,对于程序员来说,日志记录是重要的基本功。但很多同学并没有系统学习过日志操作、缺乏经验,所以我写下这篇文章,分享自己在开发项目中记录日志的方法和最佳实践,希望对大家有帮助~


一、日志记录的方法


日志框架选型


有很多 Java 的日志框架和工具库,可以帮我们用一行代码快速完成日志记录。


在学习日志记录之前,很多同学应该是通过 System.out.println 输出信息来调试程序的,简单方便。


但是,System.out.println 存在很严重的问题!



首先,System.out.println 是一个同步方法,每次调用都会导致 I/O 操作,比较耗时,频繁使用甚至会严重影响应用程序的性能,所以不建议在生产环境使用。此外,它只能输出简单的信息到标准控制台,无法灵活设置日志级别、格式、输出位置等。


所以我们一般会选择专业的 Java 日志框架或工具库,比如经典的 Apache Log4j 和它的升级版 Log4j 2,还有 Spring Boot 默认集成的 Logback 库。不仅可以帮我们用一行代码更快地完成日志记录,还能灵活调整格式、设置日志级别、将日志写入到文件中、压缩日志等。


可能还有同学听说过 SLF4J(Simple Logging Facade for Java),看英文名就知道了,这玩意并不是一个具体的日志实现,而是为各种日志框架提供简单统一接口的日志门面(抽象层)。


啥是门面?


举个例子,现在我们要记录日志了,先联系到前台接待人员 SLF4J,它说必须要让我们选择日志的级别(debug / info / warn / error),然后要提供日志的内容。确认之后,SLF4J 自己不干活,屁颠屁颠儿地去找具体的日志实现框架,比如 Logback,然后由 Logback 进行日志写入。



这样做有什么好处呢?无论我们选择哪套日志框架、或者后期要切换日志框架,调用的方法始终是相同的,不用再去更改日志调用代码,比如将 log.info 改为 log.printInfo。


既然 SLF4J 只是玩抽象,那么 Log4j、Log4j 2 和 Logback 应该选择哪一个呢?



值得一提的是,SLF4J、Log4j 和 Logback 竟然都是同一个作者(俄罗斯程序员 Ceki Gülcü)。



首先,Log4j 已经停止维护,直接排除。Log4j 2 和 Logback 基本都能满足功能需求,那么就看性能、稳定性和易用性。



  • 从性能来说,Log4j 2 和 Logback 虽然都支持异步日志,但是 Log4j 基于 LMAX Disruptor 高性能异步处理库实现,性能更高。

  • 从稳定性来说,虽然这些日志库都被曝出过漏洞,但 Log4j 2 的漏洞更为致命,姑且算是 Logback 得一分。

  • 从易用性来说,二者差不多,但 Logback 是 SLF4J 的原生实现、Log4j2 需要额外使用 SLF4J 绑定器实现。


再加上 Spring Boot 默认集成了 Logback,如果没有特殊的性能需求,我会更推荐初学者选择 Logback,都不用引入额外的库了~


使用日志框架


日志框架的使用非常简单,一般需要先获取到 Logger 日志对象,然后调用 logger.xxx(比如 logger.info)就能输出日志了。


最传统的方法就是通过 LoggerFactory 手动获取 Logger,示例代码如下:


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyService {
   private static final Logger logger = LoggerFactory.getLogger(MyService.class);

   public void doSomething() {
       logger.info("执行了一些操作");
  }
}

上述代码中,我们通过调用日志工厂并传入当前类,创建了一个 logger。但由于每个类的类名都不同,我们又经常复制这行代码到不同的类中,就很容易忘记修改类名。


所以我们可以使用 this.getClass 动态获取当前类的实例,来创建 Logger 对象:


public class MyService {
   private final Logger logger = LoggerFactory.getLogger(this.getClass());

   public void doSomething() {
       logger.info("执行了一些操作");
  }
}

给每个类都复制一遍这行代码,就能愉快地打日志了。


但我觉得这样做还是有点麻烦,我连复制粘贴都懒得做,怎么办?


还有更简单的方式,使用 Lombok 工具库提供的 @Slf4j 注解,可以自动为当前类生成一个名为 log 的 SLF4J Logger 对象,简化了 Logger 的定义过程。示例代码如下:


import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyService {
   public void doSomething() {
       log.info("执行了一些操作");
  }
}

这也是我比较推荐的方式,效率杠杠的。



此外,你可以通过修改日志配置文件(比如 logback.xmllogback-spring.xml)来设置日志输出的格式、级别、输出路径等。日志配置文件比较复杂,不建议大家去记忆语法,随用随查即可。



二、日志记录的最佳实践


学习完日志记录的方法后,再分享一些我个人记录日志的经验。内容较多,大家可以先了解一下,实际开发中按需运用。


1、合理选择日志级别


日志级别的作用是标识日志的重要程度,常见的级别有:



  • TRACE:最细粒度的信息,通常只在开发过程中使用,用于跟踪程序的执行路径。

  • DEBUG:调试信息,记录程序运行时的内部状态和变量值。

  • INFO:一般信息,记录系统的关键运行状态和业务流程。

  • WARN:警告信息,表示可能存在潜在问题,但系统仍可继续运行。

  • ERROR:错误信息,表示出现了影响系统功能的问题,需要及时处理。

  • FATAL:致命错误,表示系统可能无法继续运行,需要立即关注。


其中,用的最多的当属 DEBUG、INFO、WARN 和 ERROR 了。


建议在开发环境使用低级别日志(比如 DEBUG),以获取详细的信息;生产环境使用高级别日志(比如 INFO 或 WARN),减少日志量,降低性能开销的同时,防止重要信息被无用日志淹没。


注意一点,日志级别未必是一成不变的,假如有一天你的程序出错了,但是看日志找不到任何有效信息,可能就需要降低下日志输出级别了。


2、正确记录日志信息


当要输出的日志内容中存在变量时,建议使用参数化日志,也就是在日志信息中使用占位符(比如 {}),由日志框架在运行时替换为实际参数值。


比如输出一行用户登录日志:


// 不推荐
logger.debug("用户ID:" + userId + " 登录成功。");

// 推荐
logger.debug("用户ID:{} 登录成功。", userId);

这样做不仅让日志清晰易读;而且在日志级别低于当前记录级别时,不会执行字符串拼接,从而避免了字符串拼接带来的性能开销、以及潜在的 NullPointerException 问题。所以建议在所有日志记录中,使用参数化的方式替代字符串拼接。


此外,在输出异常信息时,建议同时记录上下文信息、以及完整的异常堆栈信息,便于排查问题:


try {
   // 业务逻辑
catch (Exception e) {
logger.error("处理用户ID:{} 时发生异常:", userId, e);
}

3、控制日志输出量


过多的日志不仅会占用更多的磁盘空间,还会增加系统的 I/O 负担,影响系统性能。


因此,除了根据环境设置合适的日志级别外,还要尽量避免在循环中输出日志。


可以添加条件来控制,比如在批量处理时,每处理 1000 条数据时才记录一次:


if (index % 1000 == 0) {
   logger.info("已处理 {} 条记录", index);
}

或者在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出:


StringBuilder logBuilder = new StringBuilder("处理结果:");
for (Item item : items) {
   try {
       processItem(item);
       logBuilder.append(String.format("成功[ID=%s], ", item.getId()));
  } catch (Exception e) {
       logBuilder.append(String.format("失败[ID=%s, 原因=%s], ", item.getId(), e.getMessage()));
  }
}
logger.info(logBuilder.toString());

如果参数的计算开销较大,且当前日志级别不需要输出,应该在记录前进行级别检查,从而避免多余的参数计算:


if (logger.isDebugEnabled()) {
   logger.debug("复杂对象信息:{}"expensiveToComputeObject());
}

此外,还可以通过更改日志配置文件整体过滤掉特定级别的日志,来防止日志刷屏:


<!-- Logback 示例 -->
<appender name="LIMITED" class="ch.qos.logback.classic.AsyncAppender">
<!-- 只允许 INFO 级别及以上的日志通过 -->
   <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
       <level>INFO</level>
   </filter>
   <!-- 配置其他属性 -->
</appender>

4、把控时机和内容


很多开发者(尤其是线上经验不丰富的开发者)并没有养成记录日志的习惯,觉得记录日志不重要,等到出了问题无法排查的时候才追悔莫及。


一般情况下,需要在系统的关键流程和重要业务节点记录日志,比如用户登录、订单处理、支付等都是关键业务,建议多记录日志。


对于重要的方法,建议在入口和出口记录重要的参数和返回值,便于快速还原现场、复现问题。


对于调用链较长的操作,确保在每个环节都有日志,以便追踪到问题所在的环节。


如果你不想区分上面这些情况,我的建议是尽量在前期多记录一些日志,后面再慢慢移除掉不需要的日志。比如可以利用 AOP 切面编程在每个业务方法执行前输出执行信息:


@Aspect
@Component
public class LoggingAspect {

   @Before("execution(* com.example.service..*(..))")
   public void logBeforeMethod(JoinPoint joinPoint) {
       Logger logger = LoggerFactory.getLogger(joinPoint.getTarget().getClass());
       logger.info("方法 {} 开始执行", joinPoint.getSignature().getName());
  }
}

利用 AOP,还可以自动打印每个 Controller 接口的请求参数和返回值,这样就不会错过任何一次调用信息了。


不过这样做也有一个很重要的点,注意不要在日志中记录了敏感信息,比如用户密码。万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息。



5、日志管理


随着日志文件的持续增长,会导致磁盘空间耗尽,影响系统正常运行,所以我们需要一些策略来对日志进行管理。


首先是设置日志的滚动策略,可以根据文件大小或日期,自动对日志文件进行切分。比如按文件大小滚动:


<!-- 按大小滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeBasedRollingPolicy">
   <maxFileSize>10MB</maxFileSize>
</rollingPolicy>

如果日志文件大小达到 10MB,Logback 会将当前日志文件重命名为 app.log.1 或其他命名模式(具体由文件名模式决定),然后创建新的 app.log 文件继续写入日志。


还有按照时间日期滚动:


<!-- 按时间滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>

上述配置表示每天创建一个新的日志文件,%d{yyyy-MM-dd} 表示按照日期命名日志文件,例如 app-2024-11-21.log


还可以通过 maxHistory 属性,限制保留的历史日志文件数量或天数:


<maxHistory>30</maxHistory>

这样一来,我们就可以按照天数查看指定的日志,单个日志文件也不会很大,提高了日志检索效率。


对于用户较多的企业级项目,日志的增长是飞快的,因此建议开启日志压缩功能,节省磁盘空间。


<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>

上述配置表示:每天生成一个新的日志文件,旧的日志文件会被压缩存储。


除了配置日志切分和压缩外,我们还需要定期审查日志,查看日志的有效性和空间占用情况,从日志中发现系统的问题、清理无用的日志信息等。


如果你想偷懒,也可以写个自动化清理脚本,定期清理过期的日志文件,释放磁盘空间。比如:


# 每月清理一次超过 90 天的日志文件
find /var/log/myapp/ -type f -mtime +90 -exec rm {} ;

6、统一日志格式


统一的日志格式有助于日志的解析、搜索和分析,特别是在分布式系统中。


我举个例子大家就能感受到这么做的重要性了。


统一的日志格式:


2024-11-21 14:30:15.123 [main] INFO com.example.service.UserService - 用户ID:12345 登录成功
2024-11-21 14:30:16.789 [main] ERROR com.example.service.UserService - 用户ID:12345 登录失败,原因:密码错误
2024-11-21 14:30:17.456 [main] DEBUG com.example.dao.UserDao - 执行SQL:[SELECT * FROM users WHERE id=12345]
2024-11-21 14:30:18.654 [main] WARN com.example.config.AppConfig - 配置项 `timeout` 使用默认值:3000ms
2024-11-21 14:30:19.001 [main] INFO com.example.Main - 应用启动成功,耗时:2.34秒

这段日志整齐清晰,支持按照时间、线程、级别、类名和内容搜索。


不统一的日志格式:


2024/11/21 14:30 登录成功 用户ID: 12345
2024-11-21 14:30:16 错误 用户12345登录失败!密码不对
DEBUG 执行SQL SELECT * FROM users WHERE id=12345
Timeout = default
应用启动成功

emm,看到这种日志我直接原地爆炸!



建议每个项目都要明确约定和配置一套日志输出规范,确保日志中包含时间戳、日志级别、线程、类名、方法名、消息等关键信息。


<!-- 控制台日志输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
   <encoder>
       <!-- 日志格式 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
   </encoder>
</appender>

也可以直接使用标准化格式,比如 JSON,确保所有日志遵循相同的结构,便于后续对日志进行分析处理:


<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
   <!-- 配置 JSON 编码器 -->
</encoder>

此外,你还可以通过 MDC(Mapped Diagnostic Context)给日志添加额外的上下文信息,比如用户 ID、请求 ID 等,方便追踪。在 Java 代码中,可以为 MDC 变量设置值:


MDC.put("requestId""666");
MDC.put("userId""yupi");
logger.info("用户请求处理完成");
MDC.clear();

对应的日志配置如下:


<!-- 文件日志配置 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
   <encoder>
       <!-- 包含 MDC 信息 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
   </encoder>
</appender>

这样,每个请求、每个用户的操作一目了然。


7、使用异步日志


对于追求性能的操作,可以使用异步日志,将日志的写入操作放在单独的线程中,减少对主线程的阻塞,从而提升系统性能。


除了自己开线程去执行 log 操作之外,还可以直接修改配置来开启 Logback 的异步日志功能:


<!-- 异步 Appender -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
   <queueSize>500</queueSize> <!-- 队列大小 -->
   <discardingThreshold>0</discardingThreshold> <!-- 丢弃阈值,0 表示不丢弃 -->
   <neverBlock>true</neverBlock> <!-- 队列满时是否阻塞主线程,true 表示不阻塞 -->
   <appender-ref ref="CONSOLE" /> <!-- 生效的日志目标 -->
   <appender-ref ref="FILE" />
</appender>

上述配置的关键是配置缓冲队列,要设置合适的队列大小和丢弃策略,防止日志积压或丢失。


8、集成日志收集系统


在比较成熟的公司中,我们可能会使用更专业的日志管理和分析系统,比如 ELK(Elasticsearch、Logstash、Kibana)。不仅不用每次都登录到服务器上查看日志文件,还可以更灵活地搜索日志。


但是搭建和运维 ELK 的成本还是比较大的,对于小团队,我的建议是不要急着搞这一套。




OK,就分享到这里,洋洋洒洒 4000 多字,希望这篇文章能帮助大家意识到日志记录的重要性,并养成良好的日志记录习惯。学会的话给鱼皮点个赞吧~


日志不是写给机器看的,是写给未来的你和你的队友看的!


更多


💻 编程学习交流:编程导航

📃 简历快速制作:老鱼简历

✏️ 面试刷题神器:面试鸭


作者:程序员鱼皮
来源:juejin.cn/post/7439785794917072896
收起阅读 »

一种简单粗暴的大屏自适应方案,原理及案例

web
现状 现在最流行的大屏自适应手法: scale缩放 为了解决2d/3d的点击交互问题,通常设计成了2个层容器。图表层和2d/3d层。图表层负责缩放,2d/3d层保持100%显示,避免缩放引起的交互事件event问题。 下图是一个1920*1080的大屏示意...
继续阅读 »

现状



现在最流行的大屏自适应手法: scale缩放

为了解决2d/3d的点击交互问题,通常设计成了2个层容器。图表层和2d/3d层。图表层负责缩放,2d/3d层保持100%显示,避免缩放引起的交互事件event问题。



下图是一个1920*1080的大屏示意图


image.png


使用常规的缩放方法,让大屏在窗口内最大化显示。大屏在不同的窗口中出现了空白区域,并没有充满整个屏幕。


image.png
image.png


新的方法 


在缩放的基础上,对指定的要素进行贴边处理。我们希望上下吸附到窗口最上面和最下面。左右图表吸附到窗口的最左边和最右边。


这里面需要简单的计算,其中a是图表层 scale属性


var halftop = (window.innerHeight- (1080*a.scaleY))/2/a.scaleY;
var halfleft = (window.innerWidth- (1920*a.scaleX))/2/a.scaleX;

对指定id的容器,在resize事件中设置上下左右浮动。如下图


image.png


image.png


image.png


实战项目效果



注,下面图片中的数据指标、城市名、姓名、图像均为虚拟数据。



在实际应用中,一般1920*1080设计稿已宽屏为主,如果是竖屏大屏(下图6),需要设计竖屏UI。


211.png


2024-12-04_134349.jpg


2024-12-04_123059.jpg


2024-12-04_120938.jpg


2024-12-04_120604.jpg


2024-12-04_123119.jpg


你也可以下载该项目demo, 对窗口进行缩放查看效果 pan.baidu.com/s/1hE_C9x9i…


作者:波泼
来源:juejin.cn/post/7444378390843768843
收起阅读 »

还在等后端接口?自己写得了

web
前言 前端:芜湖~静态页面写完,起飞 前端:接口能不能搞快点 后端:没空 前端:emmmmmm 迭代结束..... 老板:前端你怎么回事?搞这么慢 前端: A:跳起来打老板 B:跳起来打后端 C:不干了 D:自己全干 E:继续挨骂 CABABABABABB...
继续阅读 »

前言



前端:芜湖~静态页面写完,起飞image.png


前端:接口能不能搞快点


后端:没空 image.png


前端:emmmmmmimage.png


迭代结束.....


老板:前端你怎么回事?搞这么慢


前端:image.png


A:跳起来打老板

B:跳起来打后端

C:不干了

D:自己全干

E:继续挨骂

CABABABABABBABABABABBABD
image.png


当然是选择Mock.js(骗你的,我自己也不用)
Snipaste_2024-11-26_16-52-11.png



Mock.js 的使用教程


一、什么是 Mock.js?


Mock.js 是一个用于生成随机数据的 JavaScript 库,它可以帮助开发者快速模拟后台接口返回的数据,常用于前端开发中的接口调试和数据展示。通过使用 Mock.js,前端开发者无需依赖后端接口就可以模拟真实的接口数据,提升开发效率。


Mock.js 支持的数据类型非常丰富,包括字符串、数字、日期、图片等,并且可以对数据进行自定义设置,模拟出不同的场景。


二、安装 Mock.js


Mock.js 是一个轻量级的库,可以通过 npmyarn 安装:


# 使用 npm 安装
npm install mockjs --save

# 使用 yarn 安装
yarn add mockjs

如果你没有使用包管理工具,也可以直接在 HTML 页面中通过 <script> 标签引入 Mock.js:


<script src="https://cdn.jsdelivr.net/npm/mockjs@1.1.0/dist/mock.min.js"></script>

三、Mock.js 的基本使用


Mock.js 提供了一个全局的 Mock 对象,使用 Mock 对象,你可以轻松地创建模拟数据。


1. 使用 Mock.mock() 方法


Mock.mock() 是 Mock.js 的核心方法,用于创建模拟数据。它接受一个模板作为参数,根据这个模板生成相应的模拟数据。


示例:生成简单的随机数据

const Mock = require('mockjs');

// 模拟一个简单的用户数据对象
const userData = Mock.mock({
'name': '@name', // 随机生成姓名
'age|18-60': 25, // 随机生成 18-60 之间的年龄
'email': '@email', // 随机生成邮箱地址
});

console.log(userData);

在这个例子中,@name@email 等是 Mock.js 内置的随机数据生成规则,'age|18-60': 25 是一种范围随机生成规则,它会生成 18 到 60 之间的随机数。


模拟输出:

{
"name": "张三",
"age": 34,
"email": "example@example.com"
}

2. 模拟数组数据


Mock.js 还可以生成数组数据,支持定义数组长度以及每个元素的生成规则。


const Mock = require('mockjs');

// 模拟一个包含多个用户的数组
const userList = Mock.mock({
'users|3-5': [{ // 随机生成 3 到 5 个用户对象
'name': '@name',
'age|20-30': 25,
'email': '@email'
}]
});

console.log(userList);

模拟输出:

{
"users": [
{ "name": "李四", "age": 22, "email": "user1@example.com" },
{ "name": "王五", "age": 28, "email": "user2@example.com" },
{ "name": "赵六", "age": 25, "email": "user3@example.com" }
]
}

3. 使用自定义规则生成数据


Mock.js 还支持自定义规则,你可以定义数据生成的规则,或者通过函数来生成特定的数据。


const Mock = require('mockjs');

// 使用自定义函数生成随机数据
const customData = Mock.mock({
'customField': () => {
return Math.random().toString(36).substr(2, 8); // 返回一个随机的 8 位字符串
}
});

console.log(customData);

模拟输出:

{
"customField": "rkf7hbw8"
}

四、常用的 Mock.js 模板规则


Mock.js 提供了丰富的数据生成规则,下面列出一些常用的规则。


1. 字符串相关规则



  • @name:生成一个随机的中文名字。

  • @cname:生成一个随机的中文全名。

  • @word(min, max):生成一个随机的单词,minmax 控制长度。

  • @sentence(min, max):生成一个随机的句子,minmax 控制单词数量。

  • @email:生成一个随机的邮箱地址。

  • @url:生成一个随机的 URL 地址。


2. 数字相关规则



  • @integer(min, max):生成一个随机整数,minmax 控制范围。

  • @float(min, max, dmin, dmax):生成一个随机浮点数,minmax 控制范围,dmindmax 控制小数点位数。

  • @boolean:生成一个随机布尔值。

  • @date(format):生成一个随机日期,format 为日期格式,默认是 yyyy-MM-dd

  • @time(format):生成一个随机时间。


3. 其他类型



  • @image(size, background, foreground):生成一张图片,size 控制图片大小,background 控制背景色,foreground 控制前景色。

  • @guid:生成一个 GUID。

  • @id:生成一个随机的身-份-证号。

  • @province@city@county:生成随机的省、市、区名称。


五、Mock.js 用于模拟接口数据


Mock.js 常用于前端开发中模拟接口数据,帮助前端开发人员在没有后端接口的情况下进行开发和调试。可以通过 Mock.mock() 来拦截 HTTP 请求,并返回模拟的数据。


示例:模拟一个接口请求


假设我们有一个接口需要返回用户数据,我们可以使用 Mock.js 来模拟这个接口。


const Mock = require('mockjs');

// 模拟接口请求
Mock.mock('/api/users', 'get', {
'users|5-10': [{ // 随机生成 5 到 10 个用户数据
'id|+1': 1, // id 从 1 开始递增
'name': '@name',
'email': '@email',
'age|18-60': 25,
}]
});

console.log('接口已模拟,发送请求查看结果');

在上面的代码中,Mock.mock() 拦截了对 /api/users 的 GET 请求,并返回一个包含随机用户数据的对象。当前端代码请求 /api/users 时,Mock.js 会自动返回模拟的数据。


六、Mock.js 高级用法


1. 延迟模拟


有时你可能希望模拟网络延迟,Mock.js 支持使用 timeout 配置来延迟接口响应。


Mock.mock('/api/data', 'get', {
'message': '成功获取数据'
}).timeout = 2000; // 设置延迟时间为 2000ms (2秒)

2. 使用正则表达式生成数据


Mock.js 还支持通过正则表达式来生成数据。例如,生成一个特定格式的电话号码。


const phoneData = Mock.mock({
'phone': /^1[3-9]\d{9}$/ // 正则表达式生成一个中国大陆手机号
});

console.log(phoneData);

3. 动态修改数据


Mock.js 还允许你在数据生成后对其进行动态修改,可以通过调用 Mock.Random 对象来获取随机数据,并进一步自定义。


const random = Mock.Random;
const customData = {
name: random.name(),
email: random.email(),
phone: random.phone(),
};

console.log(customData);

七、总结


Mock.js 是一个强大的工具,可以帮助你快速生成模拟数据,尤其适用于前后端分离的开发模式,前端开发人员可以独立于后端接口进行开发和调试。Mock.js 提供了灵活的数据生成规则,支持随机数、日期、图片等多种类型,并且能够模拟 HTTP 接口请求,极大地提高了开发效率。


掌握 Mock.js 的基本用法,可以帮助你在开发过程中更加高效,减少对后端开发的依赖,提升整个项目的开发速度。


各位彦祖亦菲再见ヾ( ̄▽ ̄)ByeBye


image.png


作者:不爱说话郭德纲
来源:juejin.cn/post/7442515129173262351
收起阅读 »

一封写给离职小伙伴的信

前言 亲爱的小伙伴,当你看到这封信的时候,相信你大概率也是离职浪潮中的一员了,不管是被裁还是主动离职,相信在接下来的日子里,求职路上必定不会一帆风顺,势必要经历一番波折与挑战。 也许你才刚开始,此刻意气风发信心满满;也许你正在经历,此时彻底怀疑自我,将要放弃底...
继续阅读 »

前言


亲爱的小伙伴,当你看到这封信的时候,相信你大概率也是离职浪潮中的一员了,不管是被裁还是主动离职,相信在接下来的日子里,求职路上必定不会一帆风顺,势必要经历一番波折与挑战。


也许你才刚开始,此刻意气风发信心满满;也许你正在经历,此时彻底怀疑自我,将要放弃底线;也许你已经历过了,此时已是遍体鳞伤体无完肤,彻底摆烂……


不管屏幕前的你是哪一种,但请记住,这是每个人入世时社会老师要给我们上的第一节课。


在这里,我先分享一段平凡普通又心酸的求职历程,也希望通过这篇文章能给你一些启发和帮助,也衷心地希望你能够重拾信心,一路披荆斩棘!




关于我


我是双非一本毕业,计算机专业,目前毕业已有九年了,一直在远离家乡的北漂之地工作。由于母校没啥名气且名字中带有的小众地域性,非本省的人很少知道它,所以在外省找工作经常会被问你是不是本科毕业(尴尬😅),学历这一块姑且算是杂牌吧,起码在筛选简历时不具备任何优势。


其次,简历也没有太多的亮眼经历,没有大厂背景,基本都是中小厂的工作经历,普通到不能再普通了。


其三,关于年龄这一块,本人目前很接近35岁的“退休”年龄,属于被重点劝退互联网的年纪,触碰到了互联网年龄红线。


最后,这还不算最糟糕,还有更糟糕的面试门槛。


什么是糟糕的面试门槛?接着往下看。


由于去年家里出事了,辞职回了老家,之后也没着急出来找工作,期间闲来无事,顺便考了个公(原本辞职也不是为了考公),结果可想而知,也没考上,所以也没当回事,该玩玩,该旅游就旅游,彻底放飞自我~(没有房贷车贷毫无顾忌)


等我再次出来找工作的时候,已经离上一份工作的间隔有一年多了(准确的说是一年零两个月),也就是说我已经 Gap 一年多了,我还丝毫没意识到这将成为我求职路上的一个障碍。




面试的准备


7月初,我正式开始准备面试。首先更新了简历,回顾之前的项目并总结,梳理项目架构、流程图、负责的模块以及技术难点。同时看了看八股文复习基础知识,刷刷leetcode,时间过得很快,大概三周后开始投简历。




面试的门槛


Gap一年多的经历,让我在求职中非常被动,一线大厂全都因为这段 Gap 经历被 HR 前期的电话沟通中直接否掉,连一面的面试机会都没有。


只有一家公司例外,那就是字节,也是我面试的第一家公司。首先不得不表扬一下这家公司的用人态度,只是我准备不足仓促应付,以为刷了算法题就没啥大问题,结果人家来了一个手撕题(非算法),当场把我给整懵了,结果可想而知……其实本质上还是综合能力不够,但起码人家给了你面试的机会。


不管最终结果如何,单凭这种不问出身来者不拒,招人包容的胸襟与态度就值得点赞👍


因此,后面我能接到面试的机会只有中小厂,一间大厂都没有,这也许就是 Gap 太久要付出的代价之一吧。




面试的过程


求职的过程比较曲折,毕竟离开岗位已经一年多了,很多零零碎碎的知识要整理起来也不是一蹴而就的,因此求职期间,一边面试,一边不断总结经验,把之前做过的东西以及面试的空白知识慢慢梳理出来,并整理成博客。通过系统化的梳理与表达,自己的思路也开始有了更清晰的脉络和方向。


经过三个多月的面试,期间共投了三十多家公司的简历,排除一线大厂以及被其他公司pass掉的简历,其实真正接到面试机会的一共只有二十家左右。


期间有一段是没有面试的空白期,那段时间真的怀疑自己,很彷徨,是不是真的该投外包和od,这里没有贬低的意思,只是目前自己还没有养老的打算,同时也有自己的职业规划和方向,外包暂时还不在考虑范围之内。


从刚开始投的高薪大中厂,到后来的中小厂,虽然姿态一直在放低,但终归守住了自己的底线——那就是行业方向和薪资待遇。




面试中的奇葩


面试过程中也会遇到各种各样的面试官,结合自己曾经也做过面试官的经验,一些常规的套路基本是熟悉的,幸运的是遇到的绝大多数面试官都非常的nice,当然也遇到个别的奇葩。


比如这次遇到了一个思路清奇的二面面试官,一面聊得还挺好的,本以为二面面缘应该也不会差,没想到上来他就开始板着脸,似乎人人都欠他八百万似的,之所以如此,直到后来我才知道,原来他在怀疑我简历造假。等我介绍完项目,没问技术实现细节,而是开始扣字眼,这个项目公司内部的名字叫什么?为什么简历中没提这个项目的内部名字?xxx公司有海外方向吗?我全程耐心地解释,一度怀疑他才是我前司的员工,而我不是。


最后问了我那个项目的域名地址,那个项目我也只跟进了一期,没有太深的记忆,当时没有找出来,后来就草草结束了。


这里也怪我,由于离开前司一年多,项目域名早变了,而自己在整理项目时没有及时跟进,才导致如此尴尬局面,后来我才知道那个项目的域名早已换成了官网域名,把它整合进海外官网了。


其实我也非常理解他的这种行为,换成是我,遇到这种情况,我也会对面试者产生怀疑。然而从逆向思维的角度分析,像我这种非大厂背景,学历看着又像渣本的简历,怎么造,简历都不会好到哪里去吧,何况还有背调,我又何必费那个心思。想想都觉得有点滑稽~


这里也给自己总结两点经验:



  • 凡是可能涉及到的点,都要一一回顾,有备无患,但说实话经历过那么多年的面试,不管是我面别人还是别人面我,问项目域名的我还是头一次遇到。

  • 怀疑一旦开始,罪名就已经成立。不管你后面如何辩白,结局其实早已注定,还不如趁早结束。即使你有幸通过面试,将来在一位不信任你的领导下干活,也是一件非常心累的事。


面试的结果


经过将近20家公司的轮番摩擦,终于在10月底的时候,陆陆续续有了口头offer,又经过银行流水、背调、体检,最终拿到了3家公司的正式offer,两家小公司和一家独角兽。


那家独角兽公司我很早就知道,其实也一直是我想进的一家公司,因此毫无悬念,我最终选择了那家中厂独角兽,总包降了一点点,但总算还是属于平跳,这个结果在当下的环境,对我来说已经很难得了。


面试经验分享


这里不提供什么面经,因为每面的问题几乎都不一样,几乎没有碰到任何相同的一个问题(自我介绍、项目介绍和HR面除外)。


面了这么多公司,印象中只有一道题是重复的,所以即使给出各家公司的面经,真正轮到你面时,出的题也会因人而异。面经并不是圣经,只是可以作为一个难度级别的参考罢了,所以面经意义其实并不大。


这里我想分享更多的是个人职业的规划与成长。


其实面试的过程本身就是一个提升自我认知的过程。


面对如此困境,我想分享一下我是如何破局的。


作为一个普通、大龄、又gap一年多的普通程序猿,我首先做的便是潜下心来扪心自问,在这么激烈的竞争环境中,与其他人相比,我的优势在哪?





  • 首先是职业稳定性,我虽然没有大厂背景,但还算稳定,总共经历3家公司,只有中间一家是因疫情原因而不满一年,八年时间里没有频繁跳槽还算稳定。

  • 其次是职业方向,个人之前从事的行业一直与电商领域相关,前几段工作经历一直是从事海外电商方向。因此,在海外电商这个领域中我有天然的行业经验优势,从最终拿到的3家offer公司的结果来看,也反向证明了这一点。

  • 其三,投简历时我只会挑选特定的领域方向,我不会进行海投,更不会盲投,因为那没有什么意义,因为人家没看上你的,即便投了也不会有什么结果,最多只会礼貌地给你回访,并不会真正进入面试流程。


    这是我在脉脉上得出的结论,因为对你发起的沟通是系统推荐,是系统发起的并不是他们本人,因此他们并不一定对你感兴趣(当然你若是那种天之骄子又有大厂背书,就请忽略我们这种平凡人的经历)。


    因此,投简历时我一般只在boss上,并且是被动投简历,也就是别人先发起的简历请求。底层逻辑是因为只有人家对你感兴趣,你才会有更大的可能获得面试机会


  • 其四,珍惜每一次面试的机会,看清楚JD里的要求和公司从事的方向,对JD中描述的职位要求和职责,有针对性地准备面试,面试时遇到自己知识盲区要诚实表示不会,不要试图与面试官产生争论,因为即便你是对的,也会对你产生不利。坦然接受任何结果,放松心态。

  • 其五,放低姿态,降低期望,期望越高,失望越高。由于我已经离职并 Gap 太久,没有任何骑驴找马的依仗,谈薪资时显得非常被动,这便是 Gap 太久的代价之二。


    之前也面过几个到终面的公司,谈薪时过高而被Pass掉,我的诉求也很简单,就是期望能平薪而跳,也没指望能涨薪,也愿意接受适度降薪。


    期间,有遇到过很爽快的,也有遇到拼命压你薪的,面了很多小公司都有这样一个相同的经历,到谈薪阶段,他们会故意冷淡你不跟你谈,而是经过多轮对比候选人,看看哪个性价比更高。其实也非常理解,毕竟当前的环境下,哪个老板不希望用低薪招到一个综合能力更强的人呢?


    而有的会比较爽快,他们急着大量招人,流程会很快,也不会过分压薪,碰到这种公司那么恭喜你,你中奖了,这种基本在经济行情好(公司盈利大好)的时候才会出现。


    放低姿态不意味着放弃原则反而要有更清晰的底线,它的底层逻辑是降低期望值,期望值是心理学上一个很巧妙东西,期望锚点值越低,才有更大的可能获得惊喜。


  • 最后,主动沉淀和总结经验,对面试中经常问到的同一类问题进行总结和思考,进而纳入自己的脑海中,逐渐形成自己的知识体系。


    比如,你为什么会从上一家公司离职?问这个问题的背后动机是什么?HR为什么会压你薪资?这些问题的背后原理一定要思考清楚,只有理解底层逻辑,才能应对自如。


    当然,搞清楚这些问题的背后逻辑不是为了让你去说谎,面试时可以结合自身的实际,美化用词,但不要试图说谎。





总结


以上就是一个平凡普通又Gap一年多的打工人自我总结与分享,也希望它能给你带来一些启发和帮助,哪怕只有一点点,那也是我莫大的荣幸!


眼下的就业形势确实不容乐观,招聘越来越挑剔,要找到心仪的工作实属不易,但请不要因此而对生活失去信心,要相信好好总结与反思,总有一个位置属于你。


愿屏幕前的你能渡过难关、顺利上岸!加油!


一封来自远方陌生人的信


作者:九幽归墟
来源:juejin.cn/post/7444773769242116111
收起阅读 »

springboot多种生产打包方式简介

生产上发布 Spring Boot 项目时,流程颇为繁琐且低效。但凡代码有一丁点改动,就得把整个项目重新打包部署,耗时费力不说,生成的 JAR 包还特别臃肿,体积庞大。每次更新项目,光是上传这大文件就得花费不少时间,严重影响工作节奏。为解决这一痛点,我打算把依...
继续阅读 »

生产上发布 Spring Boot 项目时,流程颇为繁琐且低效。但凡代码有一丁点改动,就得把整个项目重新打包部署,耗时费力不说,生成的 JAR 包还特别臃肿,体积庞大。每次更新项目,光是上传这大文件就得花费不少时间,严重影响工作节奏。为解决这一痛点,我打算把依赖库以及配置文件(lib 文件夹下的那些 jar 包,还有config下的applacation.yml等文件)从项目主体里剥离出来,后续部署时,只需发布核心代码就行,这样既能加快部署速度,又能减轻文件传输负担,让项目更新变得轻松便捷


方法一 插件spring-boot-maven-plugin


1. 项目应用的配置文件排除 统一打包到config目录下


利用springboot中resource插件来排除配置,并统一打包到config目录下


<resources>
<resource>
<directory>src/main/resources</directory>
<!--filerting设置为true,则打包过程中会对这些文件进行过滤处理-->
<filtering>true</filtering>
<!--指定目标路径为config-->
<targetPath>${project.build.directory}/config</targetPath>
<includes>
<!--使用通配符-->
<include>**/*.properties</include>
<include>**/*.yml</include>
<include>**/*.xml</include>
<include>mapper/*.xml</include>
<!-- 这里可以根据你实际想要包含的配置文件类型来添加更多的include配置 -->
</includes>
</resource>

</resources>

2. 把我们写代码打包可执行jar,并排除依赖jar包


<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--项目的启动类,如果有多个main就必须指定,没有可以缺失
<mainClass>XXXXX.TwinWebApplication</mainClass>-->

<!--解决windows命令行窗口中文乱码-->
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
<layout>ZIP</layout>
<!--配置需要打包进项目的jar-->
<includes>
<!--填写需要打包所需要的依赖 。没有匹配上任何jar包机排除依赖-->
<include>
<groupId>no-exists-jar</groupId>
<artifactId>non-exists-jar</artifactId>
</include>
</includes>
</configuration>

<executions>
<execution>
<goals>
<!-- 表示当运行mavn package打包时,使用Springboot插件打包 -->
<goal>repackage</goal>
</goals>
</execution>
</executions>

</plugin>

3 配置依赖的jar包 统一打包lib目录


<!--此插件用于将依赖包抽出-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<excludeTransitive>false</excludeTransitive>
<stripVersion>false</stripVersion>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>


打包后目录结构,如下图所示


image.png
执行命令 java -jar -Dloader.path=./lib -jar xxx.jar


注意 springboot启动时候会优先读取config目录下配置文件 所以这里不用指定-Dspring.config.location=XX.yml文件


image.png
注意 例如日志文件配置以及mybits等配置文件 可以配成绝对路径 如下所示:


image.png


方法二 使用maven-jar-plugin插件实现


1 使用插件maven-resources-plugin处理配置文件打包到config目录


<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
</configuration>

<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<!--配置文件打包成config目录下 -->
<outputDirectory>${project.build.directory}/twin-web/config</outputDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>

</plugin>

2. 使用maven-jar-plugin 打包可执行jar 并排除依赖


<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<outputDirectory>
<!--输入打包可执行的jar到twin-web\libs\下-->
${project.build.directory}/twin-web/
</outputDirectory>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<addClasspath>true</addClasspath>
<!-- 增加执行启动jar的依赖jar包目录前缀-->
<classpathPrefix>./libs/</classpathPrefix>
<!-- 指定启动类-->
<mainClass>com.keqing.twinweb.TwinWebApplication</mainClass>
</manifest>
<manifestEntries>
<!-- 增加配置文件的classpath-->
<Class-Path>./config/</Class-Path>
</manifestEntries>
</archive>
<!-- 排除配置文件-->
<excludes>
<exclude>*.yml</exclude>
<exclude>mapper/**</exclude>
<exclude>*.xml</exclude>
</excludes>
</configuration>

</plugin>

3 使用maven-dependency-plugin 打包libs目录下


<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/twin-web/libs</outputDirectory>
<excludeTransitive>false</excludeTransitive>
<stripVersion>false</stripVersion>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>

</plugin>

使用package打包后的目录


image.png
查看自己打包后jar目录,注意这种打包方式弊端,按照一定约定格式规范固定了,一旦依赖jar包(包括配置文件目录等)发生变化就必须重新打包
image.png


启动程序java -jar xxx.jar


image.png


方式三 使用maven-assembly-plugin打包


maven-assembly-plugin 是 Maven 中的一个插件,它允许用户将项目的输出以及依赖、模块、站点文档和其他文件打包成一个可发布的格式,例如 zip、tar.gz、jar 等。以下是使用 maven-assembly-plugin 的一些优势:



  1. 自定义打包格式maven-assembly-plugin 允许你通过定义描述符文件(descriptor)来完全自定义打包的内容和格式。你可以选择包含或排除特定的文件和目录。

  2. 一键打包:通过一个简单的 Maven 命令,你可以创建一个包含所有必需依赖的单一归档文件,这使得分发和部署变得非常简单。

  3. 多环境支持:可以为不同的环境(开发、测试、生产)创建不同的打包配置,使得环境迁移更加容易。

  4. 依赖管理:插件会自动处理项目依赖,将它们打包到最终的归档文件中,无需手动管理。

  5. 模块化项目支持:对于多模块项目,maven-assembly-plugin 可以将所有模块的输出合并到一个归档文件中。

  6. 预配置的描述符:插件提供了一些预定义的描述符,如 binjar-with-dependencies 等,可以直接使用,无需自定义。

  7. 灵活性:你可以通过修改描述符文件来调整打包行为,以适应不同的需求。

  8. 集成性maven-assembly-plugin 与 Maven 生态系统紧密集成,可以与其他 Maven 插件协同工作。

  9. 文档和社区支持:由于 maven-assembly-plugin 是 Maven 的一部分,因此有广泛的文档和社区支持。


1. 项目应用的配置文件排除


<resources>
<resource>
<directory>src/main/resources</directory>
<!--filerting设置为true,则打包过程中会对这些文件进行过滤处理-->
<filtering>true</filtering>
<includes>
<!--使用通配符-->
<include>**/*.properties</include>
<include>**/*.yml</include>
<include>**/*.xml</include>
<include>mapper/*.xml</include>
<!-- 这里可以根据你实际想要包含的配置文件类型来添加更多的include配置 -->
</includes>
</resource>

</resources>

2. 配置spring-boot-maven-plugin



<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--项目的启动类,如果有多个main就必须指定,没有可以缺失
<mainClass>XXXXX.TwinWebApplication</mainClass>-->

<!--解决windows命令行窗口中文乱码-->
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
<layout>ZIP</layout>
<!--配置需要打包进项目的jar-->
<includes>
<!--填写需要打包所需要的依赖 。没有匹配上任何jar包机排除依赖-->
<include>
<groupId>no-exists-jar</groupId>
<artifactId>non-exists-jar</artifactId>
</include>
</includes>
</configuration>

<executions>
<execution>
<goals>
<!-- 表示当运行mavn package打包时,使用Springboot插件打包 -->
<goal>repackage</goal>
</goals>
</execution>
</executions>

</plugin>

3 引入springboot里约定maven-assembly-plugin


<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<!-- 打包文件名字不包含 assembly.xml 中 id -->
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<!--项目所在目录配置文件的 assembly.xml文件 -->
<descriptor>assembly.xml</descriptor>
</descriptors>
</configuration>

<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>

</plugin>

配置assembly.xml文件


<assembly>
<!-- 打包文件名的标识符,用来做后缀-->
<id>make-assembly</id>
<!-- 打包的类型,如果有N个,将会打N个类型的包 -->
<formats>
<format>tar.gz</format>
<format>zip</format>
</formats>

<!-- 压缩包下是否生成和项目名相同的根目录 -->
<includeBaseDirectory>true</includeBaseDirectory>
<!-- 用来设置一组文件在打包时的属性。-->
<fileSets>
<!-- 0755->即用户具有读/写/执行权限,组用户和其它用户具有读写权限;-->
<!-- 0644->即用户具有读写权限,组用户和其它用户具有只读权限;-->
<!-- 将src/bin目录下的jar启动脚本输出到打包后的目录中 -->
<fileSet>
<!--lineEnding选项可用于控制给定的行结束文件 -->
<lineEnding>unix</lineEnding>
<directory>${basedir}/bin</directory>
<outputDirectory>${file.separator}</outputDirectory>
<fileMode>0755</fileMode>
<includes>
<include>**.sh</include>
<include>**.bat</include>
</includes>
</fileSet>
<!-- 把项目的配置文件,打包进压缩文件的config目录 -->
<fileSet>
<directory>${basedir}/src/main/resources</directory>
<outputDirectory>config</outputDirectory>
<fileMode>0644</fileMode>
<includes>
<include>*.properties</include>
<include>*.yml</include>
<include>*.xml</include>
<include>mapper/*.xml</include>
</includes>
</fileSet>
<!-- 把项目自己编译出来的jar文件,打包进zip文件的根目录 -->
<fileSet>
<directory>${project.build.directory}</directory>
<outputDirectory>${file.separator}</outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>

<!-- 依赖包的拷贝-->
<dependencySets>
<dependencySet>
<unpack>false</unpack>
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
<scope>provided</scope>
</dependencySet>
<dependencySet>
<unpack>false</unpack>
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
<scope>system</scope>
</dependencySet>
<dependencySet>
<unpack>false</unpack>
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
<scope>runtime</scope>
</dependencySet>
</dependencySets>

</assembly>

打包后目录


image.png


解压zip目录查看


image.png


使用命令启动项目java -jar -Dloader.path=./lib -jar xxx.jar


image.png


作者:Mason_Ying
来源:juejin.cn/post/7442154802832916530
收起阅读 »

Spring Boot + liteflow竟然这么好用!

liteflow可以帮助我们快速编排复杂的业务规则,并实现动态的规则更新。在liteflow中,主要有以下几种组件:普通组件:集成NodeComponent,用于执行具体的业务逻辑;选择组件:通过业务逻辑选择不同的执行路径;条件组件:基于条件返回结果,决定下一...
继续阅读 »

liteflow可以帮助我们快速编排复杂的业务规则,并实现动态的规则更新。

liteflow中,主要有以下几种组件:

  • 普通组件:集成NodeComponent,用于执行具体的业务逻辑;
  • 选择组件:通过业务逻辑选择不同的执行路径;
  • 条件组件:基于条件返回结果,决定下一步的业务流程。

我们通过代码示例来了解每种组件的用法。

java

// 普通组件示例
@LiteflowComponent("commonNode")
public class CommonNode extends NodeComponent {
@Override
public void process() throws Exception {
// 业务逻辑
System.out.println("Executing commonNode logic");
}
}

// 选择组件示例
@LiteflowComponent("choiceNode")
public class ChoiceNode extends NodeSwitchComponent {
@Override
public String processSwitch() throws Exception {
// 根据条件返回不同的节点ID
return "nextNodeId";
}
}

// 条件组件示例
@LiteflowComponent("conditionNode")
public class ConditionNode extends NodeIfComponent {
@Override
public boolean processIf() throws Exception {
// 判断条件
return true;
}
}

EL规则文件

liteflow中,规则文件可以采用XML格式编写,下面是一个简单的规则文件示例。

图片

如何使用EL规则文件

  1. 创建规则文件:将上述规则文件保存为flow.xml,放在项目的resources目录下;
  2. 配置liteflow:在Spring Boot项目中添加liteflow的配置,指定规则文件的位置;
yaml

liteflow:
rule-source: "classpath:flow.xml"
node-retry: 3
thread-executor:
core-pool-size: 10
max-pool-size: 20
keep-alive-time: 60
  1. 编写业务逻辑组件:按照规则文件中的定义,编写相应的组件逻辑。

数据上下文

liteflow中,数据上下文非常重要,它用于参数传递业务逻辑的执行。

我们可以通过以下代码示例了解数据上下文的用法

图片

配置详解

在使用liteflow时,我们需要对一些参数进行配置,如规则文件地址、节点重试、线程池参数等。

以下是一个配置示例。

yaml

liteflow:
rule-source: "classpath:flow.xml" # 指定规则文件的位置
node-retry: 3 # 节点重试次数
thread-executor:
core-pool-size: 10 # 线程池核心线程数
max-pool-size: 20 # 线程池最大线程数
keep-alive-time: 60 # 线程存活时间

实践案例

接下来,我们来看一个完整的业务实践案例。

在电商场景下,当订单完成后,我们需要同时进行积分发放和消息发送。

这时候,我们可以利用liteflow进行规则编排,处理这些并行任务。

1. 引入依赖

首先,在pom.xml文件中添加liteflow的依赖:

xml

<dependency>
<groupId>com.yomahubgroupId>
<artifactId>liteflow-spring-boot-starterartifactId>
<version>2.6.5version>
dependency>

2. 增加配置

application.yml文件中添加liteflow的配置:

yaml

spring:
application:
name: liteflow-demo

liteflow:
rule-source: "classpath:flow.xml" # 指定规则文件的位置
node-retry: 3 # 节点重试次数
thread-executor:
core-pool-size: 10 # 线程池核心线程数
max-pool-size: 20 # 线程池最大线程数
keep-alive-time: 60 # 线程存活时间

3. 编写规则文件

resources目录下创建flow.xml文件,编写规则文件内容:

xml

<flow>
<parallel>
<node id="pointNode"/>
<node id="messageNode"/>
parallel>
flow>

4. 编写业务逻辑组件

按照规则文件中的定义,编写相应的业务逻辑组件:

java

@LiteflowComponent("pointNode")
public class PointNode extends NodeComponent {
@Override
public void process() throws Exception {
// 发放积分逻辑
System.out.println("Issuing points for the order");
}
}

@LiteflowComponent("messageNode")
public class MessageNode extends NodeComponent {
@Override
public void process() throws Exception {
// 发送消息逻辑
System.out.println("Sending message for the order");
}
}

5. 流程触发

当订单完成后,我们需要触发liteflow的流程来执行积分发放和消息发送的逻辑。

我们可以在订单完成的服务方法中添加如下代码:

java

@Service
public class OrderService {

@Autowired
private FlowExecutor flowExecutor;

public void completeOrder(Order order) {
// 完成订单的其他逻辑
System.out.println("Order completed: " + order.getId());

// 执行liteflow流程
flowExecutor.execute2Resp("flow", order);
}
}

图片

性能统计

图片

图片

总结

图片



作者:程序员蜗牛
来源:juejin.cn/post/7394790673612718092
收起阅读 »

一位转行Java上岸4年到技术专家的经验分享

自我介绍: 大家好, 我是你们的朋友, 晓龙。今天我想分享一下我自己艰难的Java转行经历。 为什么没有学历 , 因为大学上了一学期, 当时因为一些原因, 就辍学了。年少无知 , 发现到社会上找工作, 没有学历就没有敲门砖,最低要求也是大专。在2018年报考了...
继续阅读 »

自我介绍:


大家好, 我是你们的朋友, 晓龙。今天我想分享一下我自己艰难的Java转行经历。


为什么没有学历 , 因为大学上了一学期, 当时因为一些原因, 就辍学了。年少无知 , 发现到社会上找工作, 没有学历就没有敲门砖,最低要求也是大专。在2018年报考了大专的网络教育。同时, 自己也开始了金融学本科的自考。2018年还是比较的迷茫, 也不知道能靠什么挣钱。2019年, 开始在黑马进行Java的培训, 因为大概有4年没有学习了,加之因为0基础, 当时去的时候连Java能干什么也不知道, 所以学起来还是比较的吃力,尤其我记得当时对方法的定义, 是真的理解不了。


开局buffer就叠满了,运气一直比较的好。


工作经历:


2019年9月左右, 在武汉经过几次面似乎,找到了第一家公司, 规模比较的小, 0-20人, 当然对于当时的我来说, 能找到一份工作(上岸),也是非常开心的, 但是很遗憾的是, 这家公司我去只坚持1个月, 公司就倒闭了。


但是也没有急着找工作了,花了20天, 花了20天把自考的科目复习,在11月份继续在武汉找工作,经过面试,入职了一家比较小的公司, 后端就2人,都是培训出来的。当时我的想法很明确, 花时间补知识, 在培训班的6个月, 时间还是比较的紧的,很多东西只是知道, 都没有亲自动手实践过。公司好在不是很忙, 有一定的时间学习。当时上了2个多月,就回家过年,后面因为yq , 就没有去武汉了,这家公司也就上了3个月的班, 一直在家呆到2020年年7月,期间把慕课网的架构课程好好学习了。7月开始在重庆找工作 ,经过了一周多的努力, 找到了一家更加不规范的公司, 后端2个, 前端一个, 产品一个,其他就没有了,还在薪资给的还可以, 这个期间继续学习, 真正的代码都写得比较的少。很遗憾, 公司只坚持了6个月, 2021年1月, 公司又倒闭了。


我清晰的记得当时准备面试的那一周,是压力最大的一周。在经历了前面几家公司后, 我也知道, 如果还去一家不稳定的公司, 结果还是这样, 几个月就倒闭了, 而且整个公司没有研发体系, 自己就永远不能真正的上岸(野路子到正规军)。虽然前面1年多代码没有写多少, 但是还是有一定的知识积累。


自己也没有放弃, 我当时在BOSS上看准了一家公司, 然后就开始好好的准备, 不仅仅是准备技术方面的知识, 还去了解公司的背景, 当时觉得这家公司规模比较的大。当时面试完后, 顺利的拿到了offer , 期间我和面试官(也是我的领导)说, 我非常期待能来公司上班,即使不要钱,但是没办法, 我自己得生活。直到现在, 我还是非常的感谢这位领导, 也会在微信中和他交流近况。




是的 ,从某种意义上讲, 现在才是真正的上岸!


e3d060f3a7fbe92678d66225a7c841c.png


我在这家公司感受到什么是真正的做开发。公司有正规的研发流程, 完善的研发体系, 每一个同事都身怀绝技。在这家公司, 我给自己的定位是2年成长为高级开发工程师,期间也暴露出我的一些问题,比如代码逻辑写得不清楚(之前代码写得太少了),设计做得不好等。


我是渴望成长的, 所以我针对自己具体的问题, 在2年的时间里, 充分利用自己的时间做了这些工作:


第一阶段: 在试用期的前三个月,虽然完成了业务功能开发,但是意识到自己的代码量还是远远不够,在以最快的速度完成业务功能开发后,投入leetcode的算法练习,每一道题就是一个功能的缩影,在完成300+的练习后,业务代码就得心应手了。


第二阶段: 当时培训机构的架构课对我来说,是无法转化为我的能力的,我学习它无疑是浪费我的时间,所以我更多的选择了自己去寻找资料和书籍来学习,主要是针对这几个方面,操作系统,计算机网络,netty,JVM,并发编程,框架源码,此过程历经1年。


第三阶段: 系统设计,在经历前两个阶段后, 是时候提高我的系统设计,架构能力,这个东西学不来, 靠悟,也是最难受的地方!每一次的业务功能开发前,我都会阅读软件设计方面的资料(每阅读一次,都有不一样的收获)再对业务功能进行设计,非常的耗时,但也非常的值得,经历了一年,对模块和系统的设计都有了自己的理解。


第四阶段: 和产品的深度沟通能力,无论是在需求评审,还是自己负责的模块,系统,都要和产品深度的沟通,这个阶段经历了半年,完成了这4个阶段后,自己的能力得到了极大的提高。跳槽成功




50d3627eb62fe1ee94a94520b55feca.png


image.png


05de66c2fa3c081391f4154bb9701d8.png


大家会说, 现在的环境不好, 不好找工作。从我自己的经历来看, 转行培训过来的同学, 开局不管怎么样, 不管遇到多大的困难,不要放弃自己, 请坚持学习, 好好的积累3年,完善自己的各项能力, 那个时候才是真正的上岸,自己也不愁找不到工作,拿到自己满意的薪资。


目前也在写重写自己的系统框架(脚手架)fd-frameworkfd-framework, 今天为了解决springmvc对请求时间进行统一格式处理 , Long精度丢失问题处理问题, 仔细花了大概5小时阅读里面的源码,期间的实现方式改了4版, 每一次的深入,都是有新的实现想法, 真的感觉很开心,哈哈, 预祝各位转行的同学上岸。


作者:玉龙小怪兽
来源:juejin.cn/post/7323408577709080610
收起阅读 »

为什么 Java 大佬都不推荐使用 keySet() 遍历HashMap?

在Java编程中,HashMap 是一种非常常见的数据结构。我们经常需要对其中的键值对进行遍历。通常有多种方法可以遍历 HashMap,其中一种方法是使用 keySet() 方法。 然而,很多Java大佬并不推荐这种方法。为什么呢? 已收录于,我的技术网站:d...
继续阅读 »

在Java编程中,HashMap 是一种非常常见的数据结构。我们经常需要对其中的键值对进行遍历。通常有多种方法可以遍历 HashMap,其中一种方法是使用 keySet() 方法。


然而,很多Java大佬并不推荐这种方法。为什么呢?


已收录于,我的技术网站:ddkk.com 里面有,500套技术系列教程、1万+道,面试八股文、BAT面试真题、简历模版,工作经验分享、架构师成长之路,等等什么都有,欢迎收藏和转发。


keySet() 方法的工作原理


首先,让我们来看一下 keySet() 方法是如何工作的。keySet() 方法返回 HashMap 中所有键的集合 (Set<K>)。然后我们可以使用这些键来获取相应的值。


代码示例如下:


// 创建一个HashMap并填充数据
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

// 使用keySet()方法遍历HashMap
for (String key : map.keySet()) {
// 通过键获取相应的值
Integer value = map.get(key);
System.out.println("Key: " + key + ", Value: " + value);
}

这个代码看起来没什么问题,但在性能和效率上存在一些隐患。


keySet() 方法的缺点


1、 多次哈希查找:如上面的代码所示,使用 keySet() 方法遍历时,需要通过键去调用 map.get(key) 方法来获取值。这意味着每次获取值时,都需要进行一次哈希查找操作。如果 HashMap 很大,这种方法的效率就会明显降低。


2、 额外的内存消耗keySet() 方法会生成一个包含所有键的集合。虽然这个集合是基于 HashMap 的键的视图,但仍然需要额外的内存开销来维护这个集合的结构。如果 HashMap 很大,这个内存开销也会变得显著。


3、 代码可读性和维护性:使用 keySet() 方法的代码可能会让人误解,因为它没有直接表现出键值对的关系。在大型项目中,代码的可读性和维护性尤为重要。


更好的选择:entrySet() 方法


相比之下,使用 entrySet() 方法遍历 HashMap 是一种更好的选择。entrySet() 方法返回的是 HashMap 中所有键值对的集合 (Set<Map.Entry<K, V>>)。通过遍历这个集合,我们可以直接获取每个键值对,从而避免了多次哈希查找和额外的内存消耗。


下面是使用 entrySet() 方法的示例代码:


// 创建一个HashMap并填充数据
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

// 使用entrySet()方法遍历HashMap
for (Map.Entry<String, Integer> entry : map.entrySet()) {
// 直接获取键和值
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}

entrySet() 方法的优势


1、 避免多次哈希查找:在遍历过程中,我们可以直接从 Map.Entry 对象中获取键和值,而不需要再次进行哈希查找,提高了效率。


2、 减少内存消耗entrySet() 方法返回的是 HashMap 内部的一个视图,不需要额外的内存来存储键的集合。


3、 提高代码可读性entrySet() 方法更直观地表现了键值对的关系,使代码更加易读和易维护。


性能比较


我们来更深入地解析性能比较,特别是 keySet()entrySet() 方法在遍历 HashMap 时的性能差异。


主要性能问题


1、 多次哈希查找: 使用 keySet() 方法遍历 HashMap 时,需要通过键调用 map.get(key) 方法获取值。这意味着每次获取值时都需要进行一次哈希查找操作。哈希查找虽然时间复杂度为 O(1),但在大量数据下,频繁的哈希查找会累积较高的时间开销。


2、 额外的内存消耗keySet() 方法返回的是一个包含所有键的集合。虽然这个集合是基于 HashMap 的键的视图,但仍然需要额外的内存来维护这个集合的结构。


更高效的选择:entrySet() 方法


相比之下,entrySet() 方法返回的是 HashMap 中所有键值对的集合 (Set<Map.Entry<K, V>>)。通过遍历这个集合,我们可以直接获取每个键值对,避免了多次哈希查找和额外的内存消耗。


性能比较示例


让我们通过一个具体的性能比较示例来详细说明:


import java.util.HashMap;
import java.util.Map;

public class HashMapTraversalComparison {
public static void main(String[] args) {
// 创建一个大的HashMap
Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < 1000000; i++) {
map.put("key" + i, i);
}

// 测试keySet()方法的性能
long startTime = System.nanoTime(); // 记录开始时间
for (String key : map.keySet()) {
Integer value = map.get(key); // 通过键获取值
}
long endTime = System.nanoTime(); // 记录结束时间
System.out.println("keySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");

// 测试entrySet()方法的性能
startTime = System.nanoTime(); // 记录开始时间
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey(); // 直接获取键
Integer value = entry.getValue(); // 直接获取值
}
endTime = System.nanoTime(); // 记录结束时间
System.out.println("entrySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");
}
}

深度解析性能比较示例


1、 创建一个大的 HashMap



Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < 1000000; i++) {
map.put("key" + i, i);
}


  • 创建一个包含100万个键值对的 HashMap

  • "key" + ii

  • 这个 HashMap 足够大,可以明显展示两种遍历方法的性能差异。


2、 测试 keySet() 方法的性能


long startTime = System.nanoTime(); // 记录开始时间
for (String key : map.keySet()) {
Integer value = map.get(key); // 通过键获取值
}
long endTime = System.nanoTime(); // 记录结束时间
System.out.println("keySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");


  • 使用 keySet() 方法获取所有键,并遍历这些键。

  • 在每次迭代中,通过 map.get(key) 方法获取值。

  • 记录开始时间和结束时间,计算遍历所需的总时间。


3、 测试 entrySet() 方法的性能


startTime = System.nanoTime(); // 记录开始时间
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey(); // 直接获取键
Integer value = entry.getValue(); // 直接获取值
}
endTime = System.nanoTime(); // 记录结束时间
System.out.println("entrySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");


  • 使用 entrySet() 方法获取所有键值对,并遍历这些键值对。

  • 在每次迭代中,直接从 Map.Entry 对象中获取键和值。

  • 记录开始时间和结束时间,计算遍历所需的总时间。


性能结果分析


假设上述代码的运行结果如下:


keySet() 方法遍历时间: 1200000000 纳秒
entrySet() 方法遍历时间: 800000000 纳秒

可以看出,使用 entrySet() 方法的遍历时间明显短于 keySet() 方法。这主要是因为:


1、 避免了多次哈希查找: 使用 keySet() 方法时,每次获取值都需要进行一次哈希查找。而使用 entrySet() 方法时,键和值直接从 Map.Entry 对象中获取,无需再次查找。


2、 减少了内存消耗: 使用 keySet() 方法时,额外生成了一个包含所有键的集合。而使用 entrySet() 方法时,返回的是 HashMap 内部的一个视图,无需额外的内存开销。


小结一下


通过性能比较示例,我们可以清楚地看到 entrySet() 方法在遍历 HashMap 时的效率优势。使用 entrySet() 方法不仅能避免多次哈希查找,提高遍历效率,还能减少内存消耗。


综上所述,在遍历 HashMap 时,entrySet() 方法是更优的选择。


几种高效的替代方案


除了 entrySet() 方法外,还有其他几种高效的替代方案,可以用于遍历 HashMap


以下是几种常见的高效替代方案及其优缺点分析:


1. 使用 entrySet() 方法


我们已经讨论过,entrySet() 方法是遍历 HashMap 时的一个高效选择。它直接返回键值对的集合,避免了多次哈希查找,减少了内存开销。


import java.util.HashMap;
import java.util.Map;

public class EntrySetTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
}
}

2. 使用 forEach 方法


从 Java 8 开始,Map 接口提供了 forEach 方法,可以直接对每个键值对进行操作。这种方式利用了 lambda 表达式,代码更简洁,可读性强。


import java.util.HashMap;
import java.util.Map;

public class ForEachTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

map.forEach((key, value) -> {
System.out.println("Key: " + key + ", Value: " + value);
});
}
}

3. 使用 iterator 方法


另一种遍历 HashMap 的方法是使用迭代器 (Iterator)。这种方法适用于需要在遍历过程中对集合进行修改的情况,比如删除某些元素。


import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class IteratorTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
}
}

4. 使用 Streams API


Java 8 引入了 Streams API,可以结合 stream() 方法和 forEach 方法来遍历 HashMap。这种方法可以对集合进行更复杂的操作,比如过滤、映射等。


import java.util.HashMap;
import java.util.Map;

public class StreamTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

map.entrySet().stream().forEach(entry -> {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
});
}
}

优缺点分析


entrySet() 方法



  • 优点:避免多次哈希查找,减少内存消耗,代码简单明了。

  • 缺点:没有特定缺点,在大多数情况下是最佳选择。


forEach 方法



  • 优点:代码简洁,可读性强,充分利用 lambda 表达式。

  • 缺点:仅适用于 Java 8 及以上版本。


iterator 方法



  • 优点:适用于需要在遍历过程中修改集合的情况,如删除元素。

  • 缺点:代码稍显繁琐,不如 entrySet()forEach 方法直观。


Streams API 方法



  • 优点:支持复杂操作,如过滤、映射等,代码简洁。

  • 缺点:仅适用于 Java 8 及以上版本,性能在某些情况下可能不如 entrySet()forEach


结论


在遍历 HashMap 时,entrySet() 方法是一个高效且广泛推荐的选择。对于更现代的代码风格,forEach 方法和 Streams API 提供了简洁且强大的遍历方式。如果需要在遍历过程中修改集合,可以使用 iterator 方法。根据具体需求选择合适的遍历方法,可以显著提高代码的效率和可读性。


已收录于,我的技术网站:ddkk.com 里面有,500套技术系列教程、1万+道,面试八股文、BAT面试真题、简历模版,工作经验分享、架构师成长之路,等等什么都有,欢迎收藏和转发。


作者:架构师专栏
来源:juejin.cn/post/7393663398406799372
收起阅读 »

threejs 仿抖音漂移停车特效

web
最近刷到了抖音的漂移停车2的视频,感觉还蛮有趣的乍一看,实现这个漂移停车的效果需要一些东西:一辆一直往前开的小车和一个停车点,这里就做成一个小车库吧漂移停车逻辑。这个小游戏是通过往左往右触屏滑动来刹车,附带了转向和车库的碰撞处理停车后的计分逻辑之前的文章实现了...
继续阅读 »

最近刷到了抖音的漂移停车2的视频,感觉还蛮有趣的

录屏2024-01-29 22.45.28.gif

乍一看,实现这个漂移停车的效果需要一些东西:

  • 一辆一直往前开的小车和一个停车点,这里就做成一个小车库吧
  • 漂移停车逻辑。这个小游戏是通过往左往右触屏滑动来刹车,附带了转向
  • 和车库的碰撞处理
  • 停车后的计分逻辑

之前的文章实现了基于threejs的3d场景和一辆麻雀虽小五脏俱全的小车,咱们拿来接着用一下

行车物理模拟

其实之前自己实现的自车行驶超级简单,加减速、转弯都做的比较粗糙,这里引入物理库 cannon-es(cannon.js 的增强版)来帮忙做这块逻辑。物理库的作用其实就是模拟一些真实的物理效果,比如行车、物理碰撞、重力等。具体api文档 戳这里,不过只有英文文档

npm install cannon-es

先初始化一个物理世界,其实和 threejs 场景的初始化有点像,之后也是需要将物理世界的物体和 threejs 的物体一一对应地关联起来,比如这里的地面、小车和车库,这样后面物理库做计算后,再将作用后的物体的位置信息赋值到 threejs 对应物体的属性上,最后通过循环渲染(animate)就能模拟行车场景了

import * as CANNON from "cannon-es";
// ...
const world = new CANNON.World();
// 物理世界预处理,这个可以快速排除明显不发生碰撞的物体对,提高模拟效率
world.broadphase = new CANNON.SAPBroadphase(world);
// 物理世界的重力向量
world.gravity.set(0, -9.8, 0);
// 刚体之间接触面的默认摩擦系数
world.defaultContactMaterial.friction = 0;

小车对象

cannon-es 的 RaycastVehicle 类可以辅助我们管理物理世界的小车对象,它提供了很多蛮好用的api,不仅可以帮助我们更好地管理车轮,而且能很好地根据地形运动

物理世界物体的基本要素有形状(常见的有Box长方体/Plane平面/Sphere球体)、材质 Material 和刚体 Body,类比 threejs 中的几何体、材质和 Mesh。创建刚体后别忘了将它添加到物理世界里,和 threejs 将物体添加到 scene 场景里类似

 // 创建小车底盘形状,这里就是一个长方体
const chassisShape = new CANNON.Box(new CANNON.Vec3(1, 0.3, 2));
// 创建质量为150kg的小车刚体。物理世界的质量单位是kg
const chassisBody = new CANNON.Body({ mass: 150 });
// 关联刚体和形状
chassisBody.addShape(chassisShape);
// 设定刚体位置
chassisBody.position.set(0, 0.4, 0);
// 基于小车底盘创建小车对象
const vehicle = new CANNON.RaycastVehicle({
chassisBody,
// 定义车辆的方向轴(0:x轴,1:y轴,2:z轴),让它符合右手坐标系
// 车辆右侧
indexRightAxis: 0,
// 车辆上方
indexUpAxis: 1,
// 车辆前进方向
indexForwardAxis: 2,
});
// 将小车添加到物理世界里,类比 threejs 的 scene.add()
vehicle.addToWorld(world);

四个车轮

接下来定义下车轮对象,用到了 Cylinder 这种圆柱体的形状,然后要注意做好旋转值 Quaternion 的调整。这部分会稍微复杂些,可以耐心看下注释:

 // 车轮配置,详情配置参考 https://pmndrs.github.io/cannon-es/docs/classes/RaycastVehicle.html#addWheel
const options = {
radius: 0.4, // 轮子半径
directionLocal: new CANNON.Vec3(0, -1, 0), // 轮子方向向量,指轮子从中心点出发的旋转方向
suspensionStiffness: 45,
suspensionRestLength: 0.4,
frictionSlip: 5, // 滑动摩擦系数
dampingRelaxation: 2.3,
dampingCompression: 4.5,
maxSuspensionForce: 200000,
rollInfluence: 0.01,
axleLocal: new CANNON.Vec3(-1, 0, 0),
chassisConnectionPointLocal: new CANNON.Vec3(1, 1, 0),
maxSuspensionTravel: 0.25,
customSlidingRotationalSpeed: -30,
useCustomSlidingRotationalSpeed: true,
};
const axlewidth = 0.7;
// 设置第一个车轮的连接点位置
options.chassisConnectionPointLocal.set(axlewidth, 0, -1);
// 按指定配置给小车添加第一个车轮,其他车轮类似
vehicle.addWheel(options);
options.chassisConnectionPointLocal.set(-axlewidth, 0, -1);
vehicle.addWheel(options);
options.chassisConnectionPointLocal.set(axlewidth, 0, 1);
vehicle.addWheel(options);
options.chassisConnectionPointLocal.set(-axlewidth, 0, 1);
vehicle.addWheel(options);
// 四个车轮
const wheelBodies: CANNON.Body[] = [];
const wheelVisuals: THREE.Mesh[] = [];
vehicle.wheelInfos.forEach(function (wheel) {
const shape = new CANNON.Cylinder(
wheel.radius,
wheel.radius,
wheel.radius / 2,
20
);
const body = new CANNON.Body({ mass: 1, material: wheelMaterial });
// 刚体可以是动态(DYNAMIC)、静态(STATIC)或运动学(KINEMATIC)
body.type = CANNON.Body.KINEMATIC;
// 0表示这个刚体将与所有其他未设置特定过滤组的刚体进行碰撞检测
body.collisionFilterGr0up = 0;
// 使用setFromEuler方法将欧拉角转换为四元数,欧拉角的值为-Math.PI / 2(即-90度或-π/2弧度)
const quaternion = new CANNON.Quaternion().setFromEuler(
-Math.PI / 2,
0,
0
);
body.addShape(shape, new CANNON.Vec3(), quaternion);
wheelBodies.push(body);
// 创建3d世界的车轮对象
const geometry = new THREE.CylinderGeometry(
wheel.radius,
wheel.radius,
0.4,
32
);
const material = new THREE.MeshPhongMaterial({
color: 0xd0901d,
emissive: 0xaa0000,
flatShading: true,
side: THREE.DoubleSide,
});
const cylinder = new THREE.Mesh(geometry, material);
cylinder.geometry.rotateZ(Math.PI / 2);
wheelVisuals.push(cylinder);
scene.add(cylinder);
});

这一步很关键,需要在每次物理模拟计算结束后 (postStep事件的回调函数) 更新车轮的位置和转角

// ...
world.addEventListener("postStep", function () {
for (let i = 0; i < vehicle.wheelInfos.length; i++) {
vehicle.updateWheelTransform(i);
const t = vehicle.wheelInfos[i].worldTransform;
// 更新物理世界车轮对象的属性
wheelBodies[i].position.copy(t.position);
wheelBodies[i].quaternion.copy(t.quaternion);
// 更新3d世界车轮对象的属性
wheelVisuals[i].position.copy(t.position);
wheelVisuals[i].quaternion.copy(t.quaternion);
}
});

车辆行驶和转向

监听键盘事件,按下上下方向键给一个前后的引擎动力,按下左右方向键给车轮一个转角值

// 引擎动力值
const engineForce = 3000;
// 转角值
const maxSteerVal = 0.7;
// 刹车作用力
const brakeForce = 20;
// ...
// 刹车
function brakeVehicle() {
// 四个车轮全部加刹车作用力
vehicle.setBrake(brakeForce, 0);
vehicle.setBrake(brakeForce, 1);
vehicle.setBrake(brakeForce, 2);
vehicle.setBrake(brakeForce, 3);
}
function handleNavigate(e: any) {
if (e.type != "keydown" && e.type != "keyup") {
return;
}
const isKeyup = e.type === "keyup";
switch (e.key) {
case "ArrowUp":
// 给第2/3个车轮加引擎动力
vehicle.applyEngineForce(isKeyup ? 0 : engineForce, 2);
vehicle.applyEngineForce(isKeyup ? 0 : engineForce, 3);
break;
case "ArrowDown":
vehicle.applyEngineForce(isKeyup ? 0 : -engineForce, 2);
vehicle.applyEngineForce(isKeyup ? 0 : -engineForce, 3);
break;
case "ArrowLeft":
// 设置车轮转角
vehicle.setSteeringValue(isKeyup ? 0 : -maxSteerVal, 2);
vehicle.setSteeringValue(isKeyup ? 0 : -maxSteerVal, 3);
break;
case "ArrowRight":
vehicle.setSteeringValue(isKeyup ? 0 : maxSteerVal, 2);
vehicle.setSteeringValue(isKeyup ? 0 : maxSteerVal, 3);
break;
}
brakeVehicle();
}
window.addEventListener("keydown", handleNavigate);
window.addEventListener("keyup", handleNavigate);

然后在每一帧里重新计算物体的物理值,并赋值给 3d 世界的小车属性,就可以实现行车效果

function updatePhysics() {
world.step(1 / 60);
egoCar.position.copy(chassisBody.position);
egoCar.quaternion.copy(chassisBody.quaternion);
}
// ...
const animate = () => {
stats.begin();
// ...
updatePhysics();
// ...
stats.end();
requestAnimationFrame(animate);
};
animate();

地面优化

地面看起来太光滑,显得有点假,咱们先给地面加上有磨砂质感的纹理贴图,同时隐藏掉辅助网格

// ...
// 加载纹理贴图
textureLoader.load("/gta/floor.jpg", (texture) => {
const planeMaterial = new THREE.MeshLambertMaterial({
// 将贴图对象赋值给材质
map: texture,
side: THREE.DoubleSide,
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
// 地面接受阴影
plane.receiveShadow = true;
plane.rotation.x = Math.PI / 2;
scene.add(plane);
});

加载完贴图,生成3d场景的地面对象后,别忘了创建地面刚体并关联。这里还要定义地面刚体的物理材质,类比 threejs 的材质,会影响不同刚体之间摩擦和反弹的效果

// ...
// 定义地板的物理材质
const groundMaterial = new CANNON.Material("groundMaterial");
// 定义车轮的物理材质,其实之前代码用过了,可以留意下
const wheelMaterial = new CANNON.Material("wheelMaterial");
// 定义车轮和地板之间接触面的物理关联,在这里定义摩擦反弹等系数
const wheelGroundContactMaterial = new CANNON.ContactMaterial(
wheelMaterial,
groundMaterial,
{
// 摩擦系数
friction: 0.5,
// 反弹系数,0表示没有反弹
restitution: 0,
}
);
world.addContactMaterial(wheelGroundContactMaterial);
// ...
textureLoader.load("/gta/floor.jpg", (texture) => {
// ...
// 地面刚体
const q = plane.quaternion;
const planeBody = new CANNON.Body({
// 0说明物体是静止的,发生物理碰撞时不会相互移动
mass: 0,
// 应用接触面材质
material: groundMaterial,
shape: new CANNON.Plane(),
// 和3d场景的旋转值保持一致。在Cannon.js中,刚体的旋转可以通过四元数来表示,而不是传统的欧拉角或轴角表示法
quaternion: new CANNON.Quaternion(-q._x, q._y, q._z, q._w),
});
world.addBody(planeBody);
});

这回开起来可顺畅许多了,场景和自车旋转也变得更自然一些,感谢开源 ~

搭建车库

咱就搭个棚,一个背景墙、两个侧边墙、加一个屋顶和地板,其实都是些立方体,拼装成网格对象 Mesh 后,按照一定的位置和旋转拼在一起组成小车库,参考代码:

createParkingHouse() {
if (!this.scene || !this.world) return;
// 创建背景墙
const background = new THREE.Mesh(
new THREE.BoxGeometry(3, 4, 0.1),
new THREE.MeshBasicMaterial({ color: 0xcccccc })
);
background.position.set(0, 0, -53);
this.scene.add(background);
// 创建侧墙
const sider1 = new THREE.Mesh(
new THREE.BoxGeometry(6, 4, 0.3),
new THREE.MeshBasicMaterial({ color: 0xcccccc })
);
sider1.rotation.y = Math.PI / 2;
sider1.position.set(-1.5, 0.1, -50);
this.scene.add(sider1);
const sider2 = new THREE.Mesh(
new THREE.BoxGeometry(6, 4, 0.3),
new THREE.MeshBasicMaterial({ color: 0xcccccc })
);
sider2.rotation.y = Math.PI / 2;
sider2.position.set(1.5, 0.1, -50);
this.scene.add(sider2);
// 创建屋顶
const roof = new THREE.Mesh(
new THREE.BoxGeometry(3, 6, 0.1),
new THREE.MeshBasicMaterial({
color: 0xcccccc,
// 注意:这个值不为true的话,设置opacity是没用的
transparent: true,
opacity: 0.8,
})
);
roof.rotation.x = Math.PI / 2;
roof.position.set(0, 2, -50);
this.scene.add(roof);
// 创建地板
const floor = new THREE.Mesh(
new THREE.BoxGeometry(3, 6, 0.1),
new THREE.MeshBasicMaterial({ color: 0x666666 })
);
floor.rotation.x = Math.PI / 2;
floor.position.set(0, 0.1, -50);
this.scene.add(floor);
}

好了,一个稍微有点模样的小车库就大功告成

创建车库刚体

先加个背景墙的物理刚体

createParkingHouse() {
if (!this.scene || !this.world) return;
// 创建背景墙
const background = new THREE.Mesh(
new THREE.BoxGeometry(3, 4, 0.1),
new THREE.MeshBasicMaterial({ color: 0xcccccc })
);
background.position.set(0, 0, -53);
this.scene.add(background);
// 创建侧墙
// ...
// physic
const houseShape = new CANNON.Box(new CANNON.Vec3(1.5, 4, 0.1));
const houseBody = new CANNON.Body({ mass: 0 });
houseBody.addShape(houseShape);
houseBody.position.set(0, 0, -53);
this.world.addBody(houseBody);
}
// ...

其他的墙体类似的处理,屋顶先不管吧,小车应该也够不着。来,先撞一下试试

漂移停车

其实达到一定速度,通过方向键就能做一个甩尾漂移倒车入库

  1. 提供一个弹射的初始动力
// ...
animate();
setTimeout(() => {
// 给后轮上点动力
vehicle.applyEngineForce(2000, 2);
vehicle.applyEngineForce(2000, 3);
}, 100);
  1. 电脑端根据方向键触发漂移,这里注意要消除后轮的动力
// ...
case "ArrowLeft":
vehicle.setSteeringValue(keyup ? 0 : -maxSteerVal, 2);
vehicle.setSteeringValue(keyup ? 0 : -maxSteerVal, 3);
// 漂移停车游戏需要消除后轮动力,如果要正常行驶,需要去掉下面俩行
vehicle.applyEngineForce(0, 2);
vehicle.applyEngineForce(0, 3);
break;
case "ArrowRight":
vehicle.setSteeringValue(keyup ? 0 : maxSteerVal, 2);
vehicle.setSteeringValue(keyup ? 0 : maxSteerVal, 3);
// 漂移停车游戏需要消除后轮动力,如果要正常行驶,需要去掉下面俩行
vehicle.applyEngineForce(0, 2);
vehicle.applyEngineForce(0, 3);
break;
// ...
  1. 移动端根据触屏方向触发。需要注意此时要把相机控制器关掉,避免和触屏操作冲突。计算触发方向的逻辑参考
 // 计算划过的角度
function getAngle(angx: number, angy: number) {
return (Math.atan2(angy, angx) * 180) / Math.PI;
}
// 计算触屏方向
function getDirection(
startx: number,
starty: number,
endx: number,
endy: number
): ESlideDirection {
const angx = endx - startx;
const angy = endy - starty;
let result = ESlideDirection.;
if (Math.abs(angx) < 2 && Math.abs(angy) < 2) {
return result;
}
const angle = getAngle(angx, angy);
if (angle >= -135 && angle <= -45) {
result = ESlideDirection.Top;
} else if (angle > 45 && angle < 135) {
result = ESlideDirection.Bottom;
} else if (
(angle >= 135 && angle <= 180) ||
(angle >= -180 && angle < -135)
) {
result = ESlideDirection.Left;
} else if (angle >= -45 && angle <= 45) {
result = ESlideDirection.Right;
}
return result;
}
let startx = 0;
let starty = 0;
document.addEventListener("touchstart", (e) => {
startx = e.touches[0].pageX;
starty = e.touches[0].pageY;
});
document.addEventListener("touchend", function (e) {
const endx = e.changedTouches[0].pageX;
const endy = e.changedTouches[0].pageY;
const direction = getDirection(startx, starty, endx, endy);
// 根据方向做转向和刹车的处理,和上面电脑侧左右键的逻辑一致就行了
// ...
})

计算分数

根据小车和车库角度偏差和中心点偏差来综合得分,这里就不细究了,浅浅定个规则:

  • 不入库或没倒车:0分
  • 其他情况:50分 + 角度分(20比例) + 中心分(30比例)

车停住后,先算出分数,再加个数字递增的效果,用 setInterval 实现就好了。不过这里要注意用回调函数的方式更新 state 值,避免闭包引起值不更新的问题

计分组件实现代码参考:

export const Overlay = observer(() => {
const [score, setScore] = useState(0);
useEffect(() => {
if (vehicleStore.score) {
// 计分动画
const timer = setInterval(() => {
// 回调方式更新state
setScore((score) => {
if (score + 1 === vehicleStore.score) {
clearInterval(timer);
}
return score + 1;
});
}, 10);
}
}, [vehicleStore.score]);

if (!vehicleStore.isStop) {
return null;
}

return (
<div className={styles["container"]}>
<div className={styles["score-box"]}>
<div className={styles["score-desc"]}>得分div>
<div>{score}div>
div>
div>
);
});

那么问题来了,怎么监听它停下了?可以加一个速度的阈值 velocityThreshold,如果小车刚体的速度低于这个阈值就判定小车停下了。然后通过 mobx 状态库建立一个 vehicleStore,主要是维护 isStop(是否停止) 和 score(分数) 这两个变量,变化后自动通知计分组件更新,这部分逻辑可以参考源码实现 ~

// ...
const velocityThreshold = 0.01;
function updatePhysics() {
world.step(1 / 60);
// ...
// 检查刚体的速度,小于阈值视为停止
if (
chassisBody.velocity.length() < velocityThreshold &&
// 停车标识
!vehicleStore.isStop
) {
console.log("小车已经停止");
vehicleStore.stop();
// 触发计分逻辑,自行参考源码
// ...
vehicleStore.setScore(score);
}
}
// ...

传送门


作者:_lucas
来源:juejin.cn/post/7331070678693380122
收起阅读 »

2024年的安卓现代开发

大家好 👋🏻, 新的一年即将开始, 我不想错过这个机会, 与大家分享一篇题为《2024年的安卓现代开发》的文章. 🚀 如果你错过了我的上一篇文章, 可以看看2023年的安卓现代开发并回顾一下今年的变化. 免责声明 📝 本文反映了我的个人观点和专业见解, 并参考...
继续阅读 »


大家好 👋🏻, 新的一年即将开始, 我不想错过这个机会, 与大家分享一篇题为《2024年的安卓现代开发》的文章. 🚀


如果你错过了我的上一篇文章, 可以看看2023年的安卓现代开发并回顾一下今年的变化.


免责声明


📝 本文反映了我的个人观点和专业见解, 并参考了 Android 开发者社区中的不同观点. 此外, 我还定期查看 Google 为 Android 提供的指南.


🚨 需要强调的是, 虽然我可能没有明确提及某些引人注目的工具, 模式和体系结构, 但这并不影响它们作为开发 Android 应用的宝贵替代方案的潜力.


Kotlin 无处不在 ❤️



Kotlin是由JetBrains开发的一种编程语言. 由谷歌推荐, 谷歌于 2017 年 5 月正式发布(查看这里). 它是一种与 Java 兼容的现代编程语言, 可以在 JVM 上运行, 这使得它在 Android 应用开发中的采用非常迅速.


无论你是不是安卓新手, 都应该把 Kotlin 作为首选, 不要逆流而上 🏊🏻 😎, 谷歌在 2019 年谷歌 I/O 大会上宣布了这一做法. 有了 Kotlin, 你就能使用现代语言的所有功能, 包括协程的强大功能和使用为 Android 生态系统开发的现代库.


请查看Kotlin 官方文档


Kotlin 是一门多用途语言, 我们不仅可以用它来开发 Android 应用, 尽管它的流行很大程度上是由于 Android 应用, 我们可以从下图中看到这一点.




KotlinConf ‘23


Kotlin 2.0 要来了


另一个需要强调的重要事件是Kotlin 2.0的发布, 它近在眼前. 截至本文发稿之日, 其版本为 2.0.0-beta4



新的 K2 编译器 也是 Kotlin 2.0 的另一个新增功能, 它将带来显著的性能提升, 加速新语言特性的开发, 统一 Kotlin 支持的所有平台, 并为多平台项目提供更好的架构.


请查看 KotlinConf '23 的回顾, 你可以找到更多信息.


Compose 🚀




Jetpack Compose 是 Android 推荐的用于构建本地 UI 的现代工具包. 它简化并加速了 Android 上的 UI 开发. 通过更少的代码, 强大的工具和直观的 Kotlin API, 快速实现你的应用.




Jetpack Compose 是 Android Jetpack 库的一部分, 使用 Kotlin 编程语言轻松创建本地UI. 此外, 它还与 LiveData 和 ViewModel 等其他 Android Jetpack 库集成, 使得构建具有强反应性和可维护性的 Android 应用变得更加容易.


Jetpack Compose 的一些主要功能包括



  1. 声明式UI

  2. 可定制的小部件

  3. 与现有代码(旧视图系统)轻松集成

  4. 实时预览

  5. 改进的性能.


资源:



Android Jetpack ⚙️




Jetpack是一套帮助开发者遵循最佳实践, 减少模板代码, 编写在不同Android版本和设备上一致运行的代码的库, 这样开发者就可以专注于他们的业务代码.


_ Android Jetpack 文档



其中最常用的工具有:



Material You / Material Design 🥰



Material You 是在 Android 12 中引入并在 Material Design 3 中实现的一项新的定制功能, 它使用户能够根据个人喜好定制操作系统的视觉外观. Material Design 是一个由指南, 组件和工具组成的可调整系统, 旨在坚持UI设计的最高标准. 在开源代码的支持下, Material Design 促进了设计师和开发人员之间的无缝协作, 使团队能够高效地创造出令人惊叹的产品.



目前, Material Design 的最新版本是 3, 你可以在这里查看更多信息. 此外, 你还可以利用Material Theme Builder来帮助你定义应用的主题.


代码仓库


使用 Material 3 创建主题


SplashScreen API



Android 中的SplashScreen API对于确保应用在 Android 12 及更高版本上正确显示至关重要. 不更新会影响应用的启动体验. 为了在最新版本的操作系统上获得一致的用户体验, 快速采用该 API 至关重要.


Clean架构



Clean架构的概念是由Robert C. Martin提出的. 它的基础是通过将软件划分为不同层次来实现责任分离.


特点



  1. 独立于框架.

  2. 可测试.

  3. 独立于UI

  4. 独立于数据库

  5. 独立于任何外部机构.


依赖规则


作者在他的博文Clean代码中很好地描述了依赖规则.



依赖规则是使这一架构得以运行的首要规则. 这条规则规定, 源代码的依赖关系只能指向内部. 内圈中的任何东西都不能知道外圈中的任何东西. 特别是, 外圈中声明的东西的名称不能被内圈中的代码提及. 这包括函数, 类, 变量或任何其他命名的软件实体.




安卓中的Clean架构:


Presentation 层: Activitiy, Composable, Fragment, ViewModel和其他视图组件.
Domain层: 用例, 实体, 仓库接口和其他Domain组件.
Data层: 仓库的实现类, Mapper, DTO 等.


Presentation层的架构模式


架构模式是一种更高层次的策略, 旨在帮助设计软件架构, 其特点是在一个可重复使用的框架内为常见的架构问题提供解决方案. 架构模式与设计模式类似, 但它们的规模更大, 解决的问题也更全面, 如系统的整体结构, 组件之间的关系以及数据管理的方式等.


在Presentation层中, 我们有一些架构模式, 我想重点介绍以下几种:



  • MVVM

  • MVI


我不想逐一解释, 因为在互联网上你可以找到太多相关信息. 😅


此外, 你还可以查看应用架构指南.



依赖注入


依赖注入是一种软件设计模式, 它允许客户端从外部获取依赖关系, 而不是自己创建依赖关系. 它是一种在对象及其依赖关系之间实现控制反转(IoC)的技术.



模块化


模块化是一种软件设计技术, 可将应用划分为独立的模块, 每个模块都有自己的功能和职责.



模块化的优势


可重用性: 拥有独立的模块, 就可以在应用的不同部分甚至其他应用中重复使用.


严格的可见性控制: 模块可以让你轻松控制向代码库其他部分公开的内容.


自定义交付: Play特性交付 使用应用Bundle的高级功能, 允许你有条件或按需交付应用的某些功能.


可扩展性: 通过独立模块, 可以添加或删除功能, 而不会影响应用的其他部分.


易于维护: 将应用划分为独立的模块, 每个模块都有自己的功能和责任, 这样就更容易理解和维护代码.


易于测试: 有了独立的模块, 就可以对它们进行隔离测试, 从而便于发现和修复错误.


改进架构: 模块化有助于改进应用的架构, 从而更好地组织和结构代码.


改善协作: 通过独立的模块, 开发人员可以不受干扰地同时开发应用的不同部分.


构建时间: 一些 Gradle 功能, 如增量构建, 构建缓存或并行构建, 可以利用模块化提高构建性能.


更多信息请参阅官方文档.


网络



序列化


在本节中, 我想提及两个我认为非常重要的工具: 与 Retrofit 广泛结合使用的Moshi, 以及 JetBrains 的 Kotlin 团队押宝的Kotlin Serialization.



MoshiKotlin Serialization 是用于 Kotlin/Java 的两个序列化/反序列化库, 可将对象转换为 JSON 或其他序列化格式, 反之亦然. 这两个库都提供了友好的接口, 并针对移动和桌面应用进行了优化. Moshi 主要侧重于 JSON 序列化, 而 Kotlin Serialization 则支持包括 JSON 在内的多种序列化格式.


图像加载



要从互联网上加载图片, 有几个第三方库可以帮助你处理这一过程. 图片加载库为你做了很多繁重的工作; 它们既处理缓存(这样你就不用多次下载图片), 也处理下载图片并将其显示在屏幕上的网络逻辑.


_ 官方安卓文档




响应/线程管理




说到响应式编程和异步进程, Kotlin协程凭借其suspend函数和Flow脱颖而出. 然而, 在 Android 应用开发中, 承认 RxJava 的价值也是至关重要的. 尽管协程和 Flow 的采用率越来越高, 但 RxJava 仍然是多个项目中稳健而受欢迎的选择.


对于新项目, 请始终选择Kotlin协程❤️. 可以在这里探索一些Kotlin协程相关的概念.


本地存储


在构建移动应用时, 重要的一点是能够在本地持久化数据, 例如一些会话数据或缓存数据等. 根据应用的需求选择合适的存储选项非常重要. 我们可以存储键值等非结构化数据, 也可以存储数据库等结构化数据. 请记住, 这一点并没有提到我们可用的所有本地存储类型(如文件存储), 只是提到了允许我们保存数据的工具.



建议:



测试 🕵🏼



软件开发中的测试对于确保产品质量至关重要. 它能检测错误, 验证需求并确保客户满意. 下面是一些最常用的测试工具::



工具文档的测试部分


截屏测试 📸



Android 中的截屏测试涉及自动捕获应用中各种 UI 元素的屏幕截图, 并将其与基线图像进行比较, 以检测任何意外的视觉变化. 它有助于确保不同版本和配置的应用具有一致的UI外观, 并在开发过程的早期捕捉视觉回归.



R8 优化


R8 是默认的编译器, 可将项目的 Java 字节码转换为可在 Android 平台上运行的 DEX 格式. 通过缩短类名及其属性, 它可以帮助我们混淆和减少应用的代码, 从而消除项目中未使用的代码和资源. 要了解更多信息, 请查看有关 缩减, 混淆和优化应用 的 Android 文档. 此外, 你还可以通过ProGuard规则文件禁用某些任务或自定义 R8 的行为.




  • 代码缩减

  • 缩减资源

  • 混淆

  • 优化


第三方工具



  • DexGuard


Play 特性交付





Google Play 的应用服务模式称为动态交付, 它使用 Android 应用Bundle为每个用户的设备配置生成并提供优化的 APK, 因此用户只需下载运行应用所需的代码和资源.




自适应布局



随着具有不同外形尺寸的移动设备使用量的增长, 我们需要一些工具, 使我们的 Android 应用能够适应不同类型的屏幕. 因此, Android 为我们提供了Window Size Class, 简单地说, 就是三大类屏幕格式, 它们是我们开发设计的关键点. 这样, 我们就可以避免考虑许多屏幕设计的复杂性, 从而将可能性减少到三组, 它们是 Compat, MediumExpanded.


Window Size Class




支持不同的屏幕尺寸


我们拥有的另一个重要资源是Canonical Layout, 它是预定义的屏幕设计, 可用于 Android 应用中的大多数场景, 还为我们提供了如何将其适用于大屏幕的指南.



其他相关资源



Form-Factor培训


本地化 🌎



本地化包括调整产品以满足不同地区不同受众的需求. 这包括翻译文本, 调整格式和考虑文化因素. 其优势包括进入全球市场, 增强用户体验, 提高客户满意度, 增强在全球市场的竞争力以及遵守当地法规.


注: BCP 47 是安卓系统使用的国际化标准.


参考资料



性能 🔋⚙️



在开发 Android 应用时, 我们必须确保用户体验更好, 这不仅体现在应用的开始阶段, 还体现在整个执行过程中. 因此, 我们必须使用一些工具, 对可能影响应用性能的情况进行预防性分析和持续监控:



应用内更新



当你的用户在他们的设备上不断更新你的应用时, 他们可以尝试新功能, 并从性能改进和错误修复中获益. 虽然有些用户会在设备连接到未计量的连接时启用后台更新, 但其他用户可能需要提醒才能安装更新. 应用内更新是 Google Play 核心库的一项功能, 可提示活跃用户更新你的应用*.


运行 Android 5.0(API 等级 21)或更高版本的设备支持应用内更新功能. 此外, 应用内更新仅支持 Android 移动设备, Android 平板电脑和 Chrome OS 设备.


- 应用内更新文档




应用内评论



Google Play 应用内评论 API 可让你提示用户提交 Play Store 评级和评论, 而无需离开你的应用或游戏.


一般来说, 应用内评论流程可在用户使用应用的整个过程中随时触发. 在流程中, 用户可以使用 1-5 星系统对你的应用进行评分, 并添加可选评论. 一旦提交, 评论将被发送到 Play Store 并最终显示出来.


*为保护用户隐私并避免 API 被滥用, 你的应用应严格遵守有关何时请求应用内评论评论提示的设计的规定.


- 应用内评论文档




可观察性 👀



在竞争日益激烈的应用生态系统中, 要获得良好的用户体验, 首先要确保应用没有错误. 确保应用无错误的最佳方法之一是在问题出现时立即发现并知道如何着手解决. 使用 Android Vitals 来确定应用中崩溃和响应速度问题最多的区域. 然后, 利用 Firebase Crashlytics 中的自定义崩溃报告获取更多有关根本原因的详细信息, 以便有效地排除故障.


工具



辅助功能



辅助功能是软件设计和构建中的一项重要功能, 除了改善用户体验外, 还能让有辅助功能需求的人使用应用. 这一概念旨在改善的不能包括: 有视力问题, 色盲, 听力问题, 灵敏性问题和认知障碍等.


考虑因素:



  • 增加文字的可视性(颜色对比度, 可调整文字大小)

  • 使用大而简单的控件

  • 描述每个UI元素


更多详情请查看辅助功能 - Android 文档


安全性 🔐



在开发保护设备完整性, 数据安全性和用户信任的应用时, 安全性是我们必须考虑的一个方面, 甚至是最重要的方面.



  • 使用凭证管理器登录用户: 凭据管理器 是一个 Jetpack API, 在一个单一的 API 中支持多种登录方法, 如用户名和密码, 密码匙和联合登录解决方案(如谷歌登录), 从而简化了开发人员的集成.

  • 加密敏感数据和文件: 使用EncryptedSharedPreferencesEncryptedFile.

  • 应用基于签名的权限: 在你可控制的应用之间共享数据时, 使用基于签名的权限.


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<permission android:name="my_custom_permission_name"
android:protectionLevel="signature" />


  • 不要将密钥, 令牌或应用配置所需的敏感数据直接放在项目库中的文件或类中. 请使用local.properties.

  • 实施 SSL Pinning: 使用 SSL Pinning 进一步确保应用与远程服务器之间的通信安全. 这有助于防止中间人攻击, 并确保只与拥有特定 SSL 证书的受信任服务器进行通信.


res/xml/network_security_config.xml


<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set expiration="2018-01-01">
<pin digest="SHA-256">ReplaceWithYourPin</pin>
<!-- backup pin -->
<pin digest="SHA-256">ReplaceWithYourPin</pin>
</pin-set>
</domain-config>
</network-security-config>


  • 实施运行时应用自我保护(RASP): 这是一种在运行时保护应用免受攻击和漏洞的安全技术. RASP 的工作原理是监控应用的行为, 并检测可能预示攻击的可疑活动:



    • 代码混淆.

    • 根检测.

    • 篡改/应用钩子检测.

    • 防止逆向工程攻击.

    • 反调试技术.

    • 虚拟环境检测

    • 应用行为的运行时分析.




想了解更多信息, 请查看安卓应用中的运行时应用自我保护技术(RASP). 还有一些 Android 安全指南.


版本目录


Gradle 提供了一种集中管理项目依赖关系的标准方式, 叫做版本目录; 它在 7.0 版中被试验性地引入, 并在 7.4 版中正式发布.


优点:



  • 对于每个目录, Gradle 都会生成类型安全的访问器, 这样你就可以在 IDE 中通过自动补全轻松添加依赖关系.

  • 每个目录对构建过程中的所有项目都是可见的. 它是声明依赖项版本的中心位置, 并确保该版本的变更适用于每个子项目.

  • 目录可以声明 dependency bundles, 即通常一起使用的 "依赖关系组".

  • 目录可以将依赖项的组和名称与其实际版本分开, 而使用版本引用, 这样就可以在多个依赖项之间共享一个版本声明.


请查看更多信息


Secret Gradle 插件


Google 强烈建议不要将 API key 输入版本控制系统. 相反, 你应该将其存储在本地的 secrets.properties 文件中, 该文件位于项目的根目录下, 但不在版本控制范围内, 然后使用 Secrets Gradle Plugin for Android 来读取 API 密钥.


日志


日志是一种软件工具, 用于记录程序的执行信息, 重要事件, 错误调试信息以及其他有助于诊断问题或了解程序运行情况的信息. 日志可配置为将信息写入不同位置, 如日志文件, 控制台, 数据库, 或将信息发送到日志记录服务器.



Linter / 静态代码分析器



Linter 是一种编程工具, 用于分析程序源代码, 查找代码中的潜在问题或错误. 这些问题可能是语法问题, 代码风格不当, 缺乏文档, 安全问题等, 它们会对代码的质量和可维护性产生影响.



Google Play Instant



Google Play Instant 使本地应用和游戏无需安装即可在运行 Android 5.0(API 等级 21)或更高版本的设备上启动. 你可以使用 Android Studio 构建这类体验, 称为即时应用即时游戏. 通过允许用户运行即时应用或即时游戏(即提供即时体验), 你可以提高应用或游戏的发现率, 从而有助于吸引更多活跃用户或安装.



新设计中心



安卓团队提供了一个新的设计中心, 帮助创建美观, 现代的安卓应用, 这是一个集中的地方, 可以了解安卓在多种形态因素方面的设计.


点击查看新的设计中心


人工智能



GeminiPalM 2是谷歌开发的两个最先进的人工智能(AI)模型, 它们将改变安卓应用开发的格局. 这些模型具有一系列优势, 将推动应用的效率, 用户体验和创新.



人工智能编码助手工具


Studio Bot



Studio Bot 是你的 Android 开发编码助手. 它是 Android Studio 中的一种对话体验, 通过回答 Android 开发问题帮助你提高工作效率. 它采用人工智能技术, 能够理解自然语言, 因此你可以用简单的英语提出开发问题. Studio Bot 可以帮助 Android 开发人员生成代码, 查找相关资源, 学习最佳实践并节省时间.


Studio Bot



Github Copilot


GitHub Copilot 是一个人工智能配对程序员. 你可以使用 GitHub Copilot 直接在编辑器中获取整行或整个函数的建议.


Amazon CodeWhisperer


这是亚马逊的一项服务, 可根据你当前代码的上下文生成代码建议. 它能帮助你编写更高效, 更安全的代码, 并发现新的 API 和工具.


Kotlin Multiplatform 🖥 📱⌚️ 🥰 🚀



最后, 同样重要的是, Kotlin Multiplatform 🎉 是本年度最引人注目的新产品. 它是跨平台应用开发领域的有力竞争者. 虽然我们的主要重点可能是 Android 应用开发, 但 Kotlin Multiplatform 为我们提供了利用 KMP 框架制作完全本地 Android 应用的灵活性. 这一战略举措不仅为我们的项目提供了未来保障, 还为我们提供了必要的基础架构, 以便在我们选择多平台环境时实现无缝过渡. 🚀


如果你想深入了解 Kotlin Multiplatform, 我想与你分享这几篇文章. 在这些文章中, 探讨了这项技术的现状及其对现代软件开发的影响:



作者:bytebeats
来源:juejin.cn/post/7342861726000791603
收起阅读 »

Java 8 魔法:利用 Function 接口告别冗余代码,打造高效断言神器

前言 在 Java 开发的征途中,我们时常与重复代码不期而遇。这些重复代码不仅让项目显得笨重,更增加了维护成本。幸运的是,Java 8 带来了函数式编程的春风,以 Function 接口为代表的一系列新特性,为我们提供了破除这一难题的利剑。本文将以一个实际应用...
继续阅读 »

前言


Java 开发的征途中,我们时常与重复代码不期而遇。这些重复代码不仅让项目显得笨重,更增加了维护成本。幸运的是,Java 8 带来了函数式编程的春风,以 Function 接口为代表的一系列新特性,为我们提供了破除这一难题的利剑。本文将以一个实际应用场景为例,即使用 Java 8 的函数式编程特性来重构数据有效性断言逻辑,展示如何通过 SFunction(基于 Java 8Lambda 表达式封装)减少代码重复,从而提升代码的优雅性和可维护性。


背景故事:数据校验的烦恼


想象一下,在一个复杂的业务系统中,我们可能需要频繁地验证数据库中某个字段值是否有效,是否符合预期值。传统的做法可能充斥着大量相似的查询逻辑,每次都需要手动构建查询条件、执行查询并处理结果,这样的代码既冗长又难以维护。


例如以下两个验证用户 ID 和部门 ID 是否有效的方法,虽然简单,但每次需要校验不同实体或不同条件时,就需要复制粘贴并做相应修改,导致代码库中充满了大量雷同的校验逻辑,给维护带来了困扰。


// 判断用户 ID 是否有效
public void checkUserExistence(String userId) {
User user = userDao.findById(userId);
if (user == null) {
throw new RuntimeException("用户ID无效");
}
}

// 判断部门 ID 是否有效
public void checkDeptExistence(String deptId) {
Dept dept = deptDao.findById(deptId);
if (dept == null) {
throw new RuntimeException("部门ID无效");
}
}

Java 8 的魔法棒:函数式接口


Java 8 引入了函数式接口的概念,其中 Function<T, R> 是最基础的代表,它接受一个类型 T 的输入,返回类型 R 的结果。而在 MyBatis Plus 等框架中常用的 SFunction 是对 Lambda 表达式的进一步封装,使得我们可以更加灵活地操作实体类的属性。


实战演练:重构断言方法


下面的 ensureColumnValueValid 方法正是利用了函数式接口的魅力,实现了对任意实体类指定列值的有效性断言:


/**
* 确认数据库字段值有效(通用)
*
* @param <V> 待验证值的类型
* @param valueToCheck 待验证的值
* @param columnExtractor 实体类属性提取函数
* @param queryExecutor 单条数据查询执行器
* @param errorMessage 异常提示信息模板
*/

public static <T, R, V> void ensureColumnValueValid(V valueToCheck, SFunction<T, R> columnExtractor, SFunction<LambdaQueryWrapper<T>, T> queryExecutor, String errorMessage) {
if (valueToCheck == null) return;

LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
wrapper.select(columnExtractor);
wrapper.eq(columnExtractor, valueToCheck);
wrapper.last("LIMIT 1");

T entity = queryExecutor.apply(wrapper);
R columnValue = columnExtractor.apply(entity);
if (entity == null || columnValue == null)
throw new DataValidationException(String.format(errorMessage, valueToCheck));
}

这个方法接受一个待验证的值、一个实体类属性提取函数、一个单行数据查询执行器和一个异常信息模板作为参数。通过这四个参数,不仅能够进行针对特定属性的有效性检查,而且还能生成具有一致性的异常信息。


对比分析


使用 Function 改造前


// 判断用户 ID 是否有效
public void checkUserExistence(String userId) {
User user = userDao.findById(userId);
if (user == null) {
throw new RuntimeException("用户ID无效");
}
}

// 判断部门 ID 是否有效
public void checkDeptExistence(String deptId) {
Dept dept = deptDao.findById(deptId);
if (dept == null) {
throw new RuntimeException("部门ID无效");
}
}

使用 Function 改造后


public void assignTaskToUser(AddOrderDTO dto) {
ensureColumnValueValid(dto.getUserId(), User::getId, userDao::getOne, "用户ID无效");
ensureColumnValueValid(dto.getDeptId(), Dept::getId, deptDao::getOne, "部门ID无效");
ensureColumnValueValid(dto.getCustomerId(), Customer::getId, customerDao::getOne, "客户ID无效");
ensureColumnValueValid(dto.getDeptId(), Dept::getId, deptDao::getOne, "部门ID无效");
ensureColumnValueValid(dto.getSupplieId(), Supplie::getId, supplierDao::getOne, "供应商ID无效");

// 现在可以确信客户存在
Customer cus = customerDao.findById(dto.getCustomerId());

// 创建订单的逻辑...
}

对比上述两段代码,我们发现后者不仅大幅减少了代码量,而且通过函数式编程,表达出更为清晰的逻辑意图,可读性和可维护性都有所提高。


优点



  1. 减少重复代码: 通过 ensureColumnValueValid 方法,所有涉及数据库字段值有效性检查的地方都可以复用相同的逻辑,将变化的部分作为参数传递,大大减少了因特定校验逻辑而产生的代码量。

  2. 增强代码复用: 抽象化的校验方法适用于多种场景,无论是用户ID、订单号还是其他任何实体属性的校验,一套逻辑即可应对。

  3. 提升可读性和维护性: 通过清晰的函数签名和 Lambda 表达式,代码意图一目了然,降低了后续维护的成本。

  4. 灵活性和扩展性: 当校验规则发生变化时,只需要调整 ensureColumnValueValid 方法或其内部实现,所有调用该方法的地方都会自动受益,提高了系统的灵活性和扩展性。


举一反三:拓展校验逻辑的边界


通过上述的实践,我们见识到了函数式编程在简化数据校验逻辑方面的威力。但这只是冰山一角,我们可以根据不同的业务场景,继续扩展和完善校验逻辑,实现更多样化的校验需求。以下两个示例展示了如何在原有基础上进一步深化,实现更复杂的数据比较和验证功能。


断言指定列值等于预期值


首先,考虑一个场景:除了验证数据的存在性,我们还需确认查询到的某列值是否与预期值相符。这在验证用户角色、状态变更等场景中尤为常见。为此,我们设计了 validateColumnValueMatchesExpected 方法:


/**
* 验证查询结果中指定列的值是否与预期值匹配
*
* @param <T> 实体类型
* @param <R> 目标列值的类型
* @param <C> 查询条件列值的类型
* @param targetColumn 目标列的提取函数,用于获取想要验证的列值
* @param expectedValue 期望的列值
* @param conditionColumn 条件列的提取函数,用于设置查询条件
* @param conditionValue 条件列对应的值
* @param queryMethod 执行查询的方法引用,返回单个实体对象
* @param errorMessage 验证失败时抛出异常的错误信息模板
* @throws RuntimeException 当查询结果中目标列的值与预期值不匹配时抛出异常
*/

public static <T, R, C> void validateColumnValueMatchesExpected(
SFunction<T, R> targetColumn, R expectedValue,
SFunction<T, C> conditionColumn, C conditionValue,
SFunction<LambdaQueryWrapper<T>, T> queryMethod,
String errorMessage)
{

// 创建查询包装器,选择目标列并设置查询条件
LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
wrapper.select(targetColumn);
wrapper.eq(conditionColumn, conditionValue);

// 执行查询方法
T one = queryMethod.apply(wrapper);
// 如果查询结果为空,则直接返回,视为验证通过(或忽略)
if (one == null) return;

// 获取查询结果中目标列的实际值
R actualValue = targetColumn.apply(one);

// 比较实际值与预期值是否匹配,这里假设notMatch是一个自定义方法用于比较不匹配情况
boolean doesNotMatch = notMatch(actualValue, expectedValue);
if (doesNotMatch) {
// 若不匹配,则根据错误信息模板抛出异常
throw new RuntimeException(String.format(errorMessage, expectedValue, actualValue));
}
}

// 假设的辅助方法,用于比较值是否不匹配,根据实际需要实现
private static <R> boolean notMatch(R actual, R expected) {
// 示例简单实现为不相等判断,实际情况可能更复杂
return !Objects.equals(actual, expected);
}


这个方法允许我们指定一个查询目标列(targetColumn)、预期值(expectedValue)、查询条件列(conditionColumn)及其对应的条件值(conditionValue),并提供一个查询方法(queryMethod)来执行查询。如果查询到的列值与预期不符,则抛出异常,错误信息通过 errorMessage 参数定制。


应用场景:
例如在一个权限管理系统中,当需要更新用户角色时,系统需要确保当前用户的角色在更新前是 “普通用户”,才能将其升级为 “管理员”。此场景下,可以使用 validateColumnValueMatchesExpected 方法来验证用户当前的角色是否确实为“普通用户”。


// 当用户角色不是 “普通用户” 时抛异常
validateColumnValueMatchesExpected(User::getRoleType, "普通用户", User::getId, userId, userMapper::getOne, "用户角色不是普通用户,无法升级为管理员!");

断言指定值位于期望值列表内


进一步,某些情况下我们需要验证查询结果中的某一列值是否属于一个预设的值集合。例如,验证用户角色是否合法。为此,我们创建了 validateColumnValueMatchesExpectedList 方法:


/**
* 验证查询结果中指定列的值是否位于预期值列表内
*
* @param <T> 实体类型
* @param <R> 目标列值的类型
* @param <C> 查询条件列值的类型
* @param targetColumn 目标列的提取函数,用于获取想要验证的列值
* @param expectedValueList 期望值的列表
* @param conditionColumn 条件列的提取函数,用于设置查询条件
* @param conditionValue 条件列对应的值
* @param queryMethod 执行查询的方法引用,返回单个实体对象
* @param errorMessage 验证失败时抛出异常的错误信息模板
* @throws RuntimeException 当查询结果中目标列的值不在预期值列表内时抛出异常
*/

public static <T, R, C> void validateColumnValueInExpectedList(
SFunction<T, R> targetColumn, List<R> expectedValueList,
SFunction<T, C> conditionColumn, C conditionValue,
SFunction<LambdaQueryWrapper<T>, T> queryMethod,
String errorMessage)
{

LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
wrapper.select(targetColumn);
wrapper.eq(conditionColumn, conditionValue);

T one = queryMethod.apply(wrapper);
if (one == null) return;

R actualValue = targetColumn.apply(one);
if (actualValue == null) throw new RuntimeException("列查询结果为空");

if (!expectedValueList.contains(actualValue)) {
throw new RuntimeException(errorMessage);
}
}

这个方法接受一个目标列(targetColumn)、一个预期值列表(expectedValueList)、查询条件列(conditionColumn)及其条件值(conditionValue),同样需要一个查询方法(queryMethod)。如果查询到的列值不在预期值列表中,则触发异常。


应用场景: 在一个电商平台的订单处理流程中,系统需要验证订单状态是否处于可取消的状态列表里(如 “待支付”、“待发货”)才允许用户取消订单。此时,validateColumnValueInExpectedList 方法能有效确保操作的合法性。


// 假设 OrderStatusEnum 枚举了所有可能的订单状态,cancelableStatuses 包含可取消的状态
List<String> cancelableStatuses = Arrays.asList(OrderStatusEnum.WAITING_PAYMENT.getValue(), OrderStatusEnum.WAITING_DELIVERY.getValue());

// 验证订单状态是否在可取消状态列表内
validateColumnValueInExpectedList(Order::getStatus, cancelableStatuses, Order::getOrderId, orderId, orderMapper::selectOne, "订单当前状态不允许取消!");

通过这两个扩展方法,我们不仅巩固了函数式编程在减少代码重复、提升代码灵活性方面的优势,还进一步证明了通过抽象和泛型设计,可以轻松应对各种复杂的业务校验需求,使代码更加贴近业务逻辑,易于理解和维护。


核心优势



  1. 代码复用:通过泛型和函数式接口,该方法能够适应任何实体类和属性的校验需求,大大减少了重复的查询逻辑代码。

  2. 清晰表达意图:方法签名直观表达了校验逻辑的目的,提高了代码的可读性和可维护性。

  3. 灵活性:使用者只需提供几个简单的 Lambda 表达式,即可完成复杂的查询逻辑配置,无需关心底层实现细节。

  4. 易于维护与扩展:



    1. 当需要增加新的实体验证时,仅需调用 ensureColumnValueValid 并传入相应的参数,无需编写新的验证逻辑,降低了维护成本。

    2. 修改验证规则时,只需调整 ensureColumnValueValid 内部实现,所有调用处自动遵循新规则,便于统一管理。

    3. 异常处理集中于 ensureColumnValueValid 方法内部,统一了异常抛出行为,避免了在多个地方处理相同的逻辑错误,减少了潜在的错误源。




函数式编程的力量


通过这个实例,我们见证了函数式编程在简化代码、提高抽象层次上的强大能力。在 Java 8 及之后的版本中,拥抱函数式编程思想,不仅能够使我们的代码更加简洁、灵活,还能在一定程度上促进代码的正确性和可测试性。因此,无论是日常开发还是系统设计,都值得我们深入探索和应用这一现代编程范式,让代码如魔法般优雅而高效。


作者:最光阴2023
来源:juejin.cn/post/7384256110280572980
收起阅读 »

火山引擎多款大模型产品能力升级,开发者使用门槛再降低

12 月 19 日,火山引擎 Force 原动力大会开发者论坛在上海世博中心举行。上午主论坛中亮相的火山方舟、扣子、豆包MarsCode 等产品,展示了从高代码到低代码,火山引擎如何在大模型时代通过大模型产品和能力,助力开发者高效创新。作为火山引擎的大模型服务...
继续阅读 »

12 月 19 日,火山引擎 Force 原动力大会开发者论坛在上海世博中心举行。上午主论坛中亮相的火山方舟、扣子、豆包MarsCode 等产品,展示了从高代码到低代码,火山引擎如何在大模型时代通过大模型产品和能力,助力开发者高效创新。

作为火山引擎的大模型服务平台,火山方舟于 2023 年发布,在助力企业高效安全地应用大模型以外,也是开发者的友好帮手。本次会上,火山引擎智能算法负责人吴迪发布了方舟应用实验室——一套高代码 SDK 和企业级示例模板,可为客户和开发者提供完整的场景化解决方案和完整的源代码,支持业务深度自定义拓展,提高代码使用体验。吴迪表示,“我们将着眼于那些高难度,高价值的问题,用开源的方式把应该怎么做告诉大家,也鼓励开发者基于我们的代码去随意自定义和发挥,构造各自的产品”。通过方舟实验室,大模型使用门槛将进一步降低。

除了高代码智能体,火山方舟还推出了 API 接口,开发者通过这些接口即可高效调用大模型。本次亮相的火山 AI 搜推引擎则通过多模态大模型与信息检索技术的融合,支持自然语言理解,多轮多模态交互,使用户能够便捷地获取全域丰富信息,为用户提供一站式搜索推荐服务。

图片1.png

火山引擎智能算法负责人 吴迪

针对数据研发治理场景,火山引擎大数据研发治理套件 DataLeap 在本次会上公布了全新能力——开放平台。据介绍,DataLeap 开放平台提供完整的 IDE 界面扩展和流水线扩展体系以及多种开放能力,让开发者只需根据开发手册,就能低成本、快速完成扩展程序的开发。结合先进的 AI 能力,DataLeap 将智能化融入开发、运维、资产查询等数据研发流程中,并在开放平台中以插件形式开放,为开发者提供开发助手、运维助手等工具,进一步降低开发门槛,提升运维效率。

图片2.png

火山引擎数智平台产品总监 张辉

本次会上展示的大模型应用工具也同样带来了不少亮点,让 AI 应用开发可以更简单。

扣子是新一代应用开发平台,在今年 5 月份的 Force 大会上首次亮相。时隔半年多,扣子来到 1.5,完成了 3 个方面的重要升级。

首先,扣子 1.5 提供全新的应用开发环境,支持 Chatbot 之外更丰富的 AI 应用范式,包括小程序、API、WebSDK 等多种形态。结合扣子推出的无限画布,用扣子已经可以低代码、可视化搭建出完整的 AI 应用。

其次,不仅仅是大语言模型,扣子接入的多模态能力也全面升级。其中在语音交互能力方面,借助火山引擎 RTC 超低延时、智能打断、抗弱网的优势,能够实现更流畅、更自然、更可靠的语音通话功能;在硬件应用方面,通过和火山引擎边缘智能的协同,借助边缘智能理解边缘设备能力,能让大模型更好地连接物理世界。而 12 月 18 日新发布的豆包·视觉理解模型、豆包文生图 2.1 和豆包音乐模型,在扣子也都能通过插件的方式第一时间体验到。

在本次升级中,扣子同步发布了模板功能,旨在帮助所有用户更快地理解扣子各项能力的最佳实践。扣子产品经理姚慧在现场向大家展示了机智云 AI 养鱼项目、猫王妙播 AI 音响、和府捞面顾客点评分析智能体等多个利用扣子实现的 AI 场景案例,鼓励开发者可以快速投入到所希望开发的应用方向中去。

图片3.png

扣子产品经理 姚慧

姚慧表示,扣子已成为 AI 时代开发者首选的应用开发平台。目前扣子有超过100万的活跃开发者,发布超过200万个智能体。扣子 1.5 通过更强的产品能力进一步降低了开发者的使用门槛,让 Al 离应用再近一步。

在大模型时代,开发者还急需更高效的开发工具来提升编程效率。此次大会上,火山引擎协同豆包MarsCode 为开发者提供了智能 AI IDE,在代码补全、Bug 修复、代码问答等各编程阶段为开发者提供协助支持。在关注代码生成 AI 化的同时,豆包MarsCode 也关注开发工具本身的 AI 化,通过交互方式的转变和多种大模型能力的接入,为开发者提供更好的用户体验。

图片4.png

豆包MarsCode 产品经理 王海建

“大模型时代,开发者的定义已经被极大地拓宽,开发者画像变得更加丰富。”正如火山引擎总裁谭待在大会开场时的致辞所言,火山引擎将通过提供更易用的 AI 应用开发平台和工具普惠开发者,与大家一起共创无限可能。(作者:李双)

收起阅读 »

如果你没有必须要离职的原因,我建议你在忍忍

自述 本人成都,由于一些原因我在八月离职了,因为我终于脱离了那个压抑的环境,我没有自己想象中的那么开心,我离职的那天,甚至后面很长的一段时间;离职后的我回了一趟家,刚好在最热的那几天,在家躺了几天,然后又出去逛了逛,玩了差不多一个月吧!我觉得心情逐渐恢复了;然...
继续阅读 »

自述


本人成都,由于一些原因我在八月离职了,因为我终于脱离了那个压抑的环境,我没有自己想象中的那么开心,我离职的那天,甚至后面很长的一段时间;离职后的我回了一趟家,刚好在最热的那几天,在家躺了几天,然后又出去逛了逛,玩了差不多一个月吧!我觉得心情逐渐恢复了;然后开始慢慢的投递简历。


前期


刚投递简历那会,基本上每天都是耍耍哒哒的投递;有面试就去面试,没有面试就在家刷抖音也不看看面试题,可能我找工作的状态还在几年前或者还没从上家公司的状态中走出来,也有可能我目前有一点存款不是特别焦虑,所以也没认真的找。


就这样刷刷哒哒的又过了半月,然后有许多朋友跟我说他们被裁员了,问他们的打算是怎么样的:有的人休息了两三天就开始了找工作当中,而有的人就玩几个月再说。


休息两三天就开始找工作的人基本上都是有家庭有小孩的,反之基本上都是单身。


在跟他们聊天的过程中发现,有些人半年没找到工作了,也有一些人一年都没有找到工作了。可能是年级大了、也可能是工资不想要的太低吧!但是工作机会确实比原来少很多。


在听了大家的话以后,我觉得我差不多也该认真找工作了,我开始逐渐投递简历。


疯狂投递简历


我在9月的下旬开始了简历的修改以及各大招聘App的下载,拉钩、智联、boos以及一下小程序的招聘软件(记不住名字了,因为没啥效果);在我疯狂的投递了几天以后我迎来了第一家面试,是一个线上面试;刚一来就给了我迎头一棒,或许我只忙着修改简历和投递简历去了,没有去背面试题吧(网上说现在都问场景题,所以没准备);


具体的问题我记不全了,但是我记得这么一个问题,面试官问:“深克隆有哪些方法”,我回答的是递归,然后他说还有吗?我直接呆住说不知道了。然后我们就结束了面试,最后他跟我说了这么一句话:“现在的市场行情跟原来没法比,现在的中级基本上要原来的高级的水平,现在的初级也就是原来的中级的水平,所以问的问题会比原来难很多,你可以在学习一下,看你的简历是很不错的;至少简历是这样的。”


当这个面试结束以后我想了想发现是这样的,不知是我还没有接受或者说还没有进入一个面试的状态,还是因为我不想上班的原因,导致我连一些基本的八股文都不清楚,所以我决定开始学习。


给准备离职或者已经离职的朋友们一个忠告:“做任何事情都需提前准备,至少在找工作上是这样的。”


学习


我去看了招聘网站的技术要求(想了解下企业需要的一些技术),不看不知道一看吓一跳,真的奇葩层出不穷,大概给大家概述一下:



  • 开发三班倒:分为早中晚班

  • 要你会vue+react+php+java等技术(工资8-12)

  • 要你会基本的绘画(UI)以及会后端的一些工作,目前这些都需要你一个人完成

  • 要你会vue+react+fluter;了解electron以及3d等

  • 还有就是你的项目跟我们的项目不一致的。


我看到这些稀奇古怪的玩意有点失望,最终我选择了fabricjs进行学习,最开始的时候就是在canvas上画了几个矩形,感觉挺不错的;然后我就想这不是马上快要国庆了吗?我就想用fabric做一个制作头像的这么一个工具插件,在经过两天的开发成功将其制作了出来,并且发布到了网站上(插件tools),发布第一天就有使用的小伙伴给我提一些宝贵的建议了,然后又开始了调整,现在功能也越来越多;


fabricjs在国内的资料很少,基本上就那么几篇文章,没有办法的我就跑去扒拉他们的源码看,然后拷贝需要的代码在修修改改(毕竟比较菜只能这样....);然后在学习fabric的时候也会去学习一些基本知识,比如:js内置方法、手写防抖节流、eventloop、闭包(一些原理逻辑)、深拷贝、内存回收机制等等。


在学习的过程中很难受,感觉每天都是煎熬;每次都想在床上躺着,但是想想还是放弃了,毕竟没有谁会喜欢一个懒惰的人...


在战面试(HR像是刷KPI)


在有所准备的情况下再去面试时就得心应手了,基本上没有太多的胆怯,基本上问啥都知道一些,然后就在面试的时候随机应变即可,10月我基本上接到的面试邀请大概有10多家,然后有几家感觉工资低了就没去面试,去面试了的应该有7/8家的样子,最终只要一家录取。


说说其中一家吧(很像刷KPI的一家):这是一家做ai相关的公司,公司很大,看资料显示时一家中外合资的企业,进去以后先开始了一轮笔试题(3/4页纸),我大概做了50分钟的样子;我基本上8层都答对了(因为他的笔试题很多我都知道嘛,然后有一些还写了几个解决方案的),笔试完了以后,叫我去机试;机试写接口;而且还是在规定的一个网站写(就给我一个网站,然后说写一个接口返回正确结果就行;那个网站我都不会用);我在哪儿磨磨蹭蹭了10多分钟以后,根据node写了一个接口给了hr;然后HR说你这个在我们网站上不能运行。我站起来就走了...


其实我走的原因还有一个,就是他们另一个HR对带我进来的这个HR说:你都没有协调好研发是否有时间,就到处招面试...


是否离职


如果你在你现在这公司还能呆下去的情况下,我建议你还是先呆呆看吧!目前这个市场行情很差,你看到我有10来个面试,但是你知道嘛?我沟通了多少:



  • boos沟通了差不多800-900家公司,邀请我投递简历的只有100家左右。邀请我面试的只有8/9家。

  • 智联招聘我投递了400-600家,邀请我面试的只有1家。

  • 拉钩这个不说了基本上没有招聘的公司(反反复复就那几家);投递了一个月后有一家叫我去面试的,面试了差不多50来分钟;交谈的很开心,他说周一周二给我回复,结果没有回复,我发消息问;也没有回复;看招聘信息发现(邀约面试800+)


我离职情非得已,愿诸君与我不同;如若您已离职,愿您早日找到属于自己的路,不一定是打工的路;若你在职,请在坚持坚持;在坚持的同时去做一些对未来有用的事情,比如:副业、耍个男女朋友、拓展一下圈子等等。


后续的规划


在经历了这次离职以后,我觉得我的人生应该进行好好的规划了;不能为原有的事物所影响,不能为过去所迷茫;未来还很长,望诸君互勉;


未来的计划大致分为几个方向:



  • 拓展自己的圈子(早日脱单)

  • 学习开发鸿蒙(我已经在做了,目前开发的app在审核),发布几款工具类app(也算是为国内唯一的系统贡献一些微弱的力量吧!)

  • 持续更新我在utools上的绘图插件

  • 学习投资理财(最近一月炒股:目前赚了4000多了)

  • 持续更新公众号(前端雾恋)、掘金等网站技术文章


结尾


我们的生活终将回归正轨,所有的昨天也将是历史,不必遗憾昨天,吸取教训继续前进。再见了...


作者:雾恋
来源:juejin.cn/post/7435289649273569334
收起阅读 »

入职第一天,看了公司代码,牛马沉默了

web
入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。打开代码发现问题不断读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置 一边获取WEB-INF下的配置文件,一...
继续阅读 »

入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。

4f7ca8c685324356868f65dd8862f101~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

打开代码发现问题不断

  1. 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置

image.png

image.png

image.png 一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为

prop_c.setProperty(key, value);

value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable

public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
}
  1. 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
role.haveRole("ADMIN_USE")
  1. 日志打印居然sout和log混合双打

image.png

image.png

先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;

4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;

5.随意更改生产数据库,出不出问题全靠开发的职业素养;

6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上

<type>pom

来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教 a972880380654b389246a3179add2cca~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;

那有什么优点呢:

  1. 不用太怎么写文档
  2. 束缚很小
  3. 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)

解决之道

怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar & 来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,

其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;

我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!


作者:小红帽的大灰狼
来源:juejin.cn/post/7371986999164928010
收起阅读 »

金价大跳水?写一个金价监控脚本

web
说在前面 😶‍🌫️国庆过后,金价就大跳水,一直往下跌,看样子暂时是停不下来了,女朋友之前也入手了一点黄金,因此对黄金价格的变化比较关心,为了让她不用整天盯着实时金价,所以就搞了一个金价监控工具,超出设置的阈值就会发送邮件提醒✉。 一、金价信息获取方案 金...
继续阅读 »

说在前面



😶‍🌫️国庆过后,金价就大跳水,一直往下跌,看样子暂时是停不下来了,女朋友之前也入手了一点黄金,因此对黄金价格的变化比较关心,为了让她不用整天盯着实时金价,所以就搞了一个金价监控工具,超出设置的阈值就会发送邮件提醒✉。



c7acc1daf5b30174b34bdd85cd34d25.jpg


一、金价信息获取方案


金价实时信息有两种方案可以获取到:


1、网页信息爬取


我们可以先找到一些官方的金价信息网站,然后直接利用爬虫直接爬取,比如:quote.cngold.org/gjs/jjs.htm…



2、通过接口获取


例如nowapi中就有黄金数据信息接口,我们可以直接通过接口来获取:



二、提醒阈值设置


1、创建数据库



2、监控页面编写


简单编写一个页面用于添加和调整提醒内容。



三、修改配置信息


1、邮箱配置


这里我使用的qq邮箱作为发件账号,需要开启邮箱授权,获取授权码。


{
host: "smtp.qq.com", // 主机
secureConnection: true, // 使用 SSL
port: 465, // SMTP 端口
auth: {
user: "jyeontu@qq.com", // 自己用于发送邮件的账号
pass: "jyeontu", // 授权码(这个是假的,改成自己账号对应即可,获取方法: QQ邮箱-->设置-->账户-->POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务-->IMAP/SMTP开启 复制授权码)
}
}


  • (1)打开pc端qq邮箱,点击设置,再点击帐户




  • (2)往下拉 可开启POP3/SMTP服务 根据提示即可获取qq邮箱授权码




  • (3)将获取到的授权码复制到配置信息里即可



2、数据库配置


填写数据库对应的配置信息。


{
host: "localhost",
user: "root", //数据库账号
password: "jyeontu", //数据库密码
database: "test", //数据库名称
}

3、nowapi配置


免费开通后将AppKeySign替换成自己的就可以了。



{
AppKey: AppKey,
Sign: "Sign",
}

四、脚本功能编写


1、获取金价信息


我直接使用nowapi的免费试用套餐,配额是10 次/小时



const { nowapiConfig } = require("./config.js");
async function getGoldPrice() {
const result = await axios.get(
`https://sapi.k780.com/?app=finance.gold_price&goldid=1053&appkey=${nowapiConfig.AppKey}&sign=${nowapiConfig.Sign}&format=json`
);
return result.data.result.dtList["1053"];
}

获取到的数据如下:



2、获取消息提醒阈值


(1)连接数据库


使用填写好的数据库配置信息连接数据库


const mysql = require("mysql");
const { dbConfig } = require("./config.js");

const connection = mysql.createConnection(dbConfig);

function connectDatabase() {
return new Promise((resolve) => {
connection.connect((error) => {
if (error) throw error;
console.log("成功连接数据库!");
resolve("成功连接数据库!");
});
});
}

(2)查询数据


function mysqlQuery(sqlStr) {
return new Promise((resolve) => {
connection.query(sqlStr, (error, results) => {
if (error) throw error;
resolve(results);
});
});
}

async function getMessage() {
const sqlStr =
"select * from t_message where isShow = 1 and isActive = 1 and type = '金价监控';";
const res = await mysqlQuery(sqlStr);
return { ...res[0] };
}

获取到的数据如下:



3、发送提醒邮件


(1)创建邮件传输对象


使用填写好的邮箱配置信息,创建邮件传输对象


const nodemailer = require("nodemailer");
const { mail } = require("./config.js");

const smtpTransport = nodemailer.createTransport(mail);
const sendMail = (options) => {
return new Promise((resolve) => {
const mailOptions = {
from: mail.auth.user,
...options,
};
// 发送邮件
smtpTransport.sendMail(mailOptions, function (error, response) {
if (error) {
console.error("发送邮件失败:", error);
} else {
console.log("邮件发送成功");
}
smtpTransport.close(); // 发送完成关闭连接池
resolve(true);
});
});
};
module.exports = sendMail;

(2)阈值判断


判断获取到的金价信息是否超出阈值范围来决定是否发送邮件提醒


async function mail(messageInfo, goldInfo) {
let { minVal = -Infinity, maxVal = Infinity } = messageInfo;
let { buy_price } = goldInfo;
minVal = parseFloat(minVal);
maxVal = parseFloat(maxVal);
buy_price = parseFloat(buy_price);
if (minVal < buy_price && maxVal > buy_price) {
return;
}
const mailOptions = {
to: messageInfo.mail.replaceAll("、", ","), // 接收人列表,多人用','隔开
subject: "金价监控",
text: `当前金价为${buy_price.toFixed(2)}`,
};
await sendMail(mailOptions);
}

五、定时执行脚本


可以使用corn编写一个定时任务来定时执行脚本即可。



  • * * * * * *分别对应:秒、分钟、小时、日、月、星期。

  • 每个字段可以是具体的值、范围、通配符(*表示每一个)或一些特殊的表达式。


例如:


0 0 * * *:每天午夜 0 点执行。
0 30 9 * * 1-5:周一到周五上午 9:30 执行。

你可以根据自己的需求设置合适的 cron 表达式来定时执行特定的任务。


六、效果展示


如果金价不在我们设置的阈值内时,我们就会收到邮件告知当前金价:





七、脚本使用


1、源码下载


git clone https://gitee.com/zheng_yongtao/node-scripting-tool.git


  • 源码已经上传到gitee仓库


gitee.com/zheng_yongt…



  • 具体目录如下:



2、依赖下载


npm install

3、配置数据填写



这里的配置信息需要修改为你自己的信息,数据库、gitee仓库、nowapi配置。


4、脚本运行


node index.js

更多脚本


该脚本仓库里还有很多有趣的脚本工具,有兴趣的也可以看看其他的:gitee.com/zheng_yongt…




🌟觉得有帮助的可以点个star~


🖊有什么问题或错误可以指出,欢迎pr~


📬有什么想要实现的工具或想法可以联系我~




公众号


关注公众号『前端也能这么有趣』,获取更多有趣内容。


说在后面



🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。



作者:JYeontu
来源:juejin.cn/post/7437006854122815497
收起阅读 »

程序员设计不出精美的 UI 界面?让 V0 来帮你

web
大家好,我是双越,也是 wangEditor 作者。 今年我致力于开发一个 Node 全栈 AIGC 知识库 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线,大家可以去注册试用。 本文分享一下前端实用的 AI 工具 v0.dev 以及我在 划...
继续阅读 »

大家好,我是双越,也是 wangEditor 作者。



今年我致力于开发一个 Node 全栈 AIGC 知识库 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线,大家可以去注册试用。



本文分享一下前端实用的 AI 工具 v0.dev 以及我在 划水AI 中的实际应用经验,非常推荐这款工具。


不同 AI 工具写代码


ChatGPT 不好直接写代码


去年 ChatGPT 发布,但它一直是一个聊天工具,直接让它来写代码,用一问一答的形式,体验其实并不是非常友好。


可以让它来生成一些单一的代码或工具,例如 生成一个 nodejs 发送 Email 的函数 。然后我们把生成的代码复制粘贴过来,自己调整一下。


它可以作为一个导师或助理,指导你如何写代码,但它没法直接帮你写,尤其是在一个项目环境中。


image.png


PS. 这里只是说 ChatGPT 这种问答方式不适合直接写代码,但 ChatGPT 背后的 LLM 却未后面各种 AI 写代码工具提供了支持。


Cursor 非专业程序员


Cursor 其实去年我就试用过,它算是 AI 工具 + VSCode ,付费试用。没办法,AI 接口服务现在都是收费的。


前段时间 Cursor 突然在社区中很火爆,国内外都有看过它的宣传资料,我记得看过一个国外的 8 岁小女孩,用 Cursor 写 AI 聊天工具的视频,非常有意思,我全程看完了。


image.png


Cursor 可能会更加针对于非专业编程人员,去做一些简单的 demo ,主要体验编程的逻辑和过程,不用关心其中的 bug 。


例如,对于公司的 PM UI 人员,或者创业公司的老板。它真的可以产生价值,所以它也可以收费。


Copilot 针对专业程序员


我们是专业程序员,我更加推荐 Copilot ,直接在 vscode 安装插件即可。


我一直在使用 Copilot ,而且我现在都感觉自己有点依赖它了,每次写代码的时候都会停顿下来等待它帮我生成。


在一些比较明确的问题上,它的生成是非常精准的,可以大大节省人力,提高效率。


image.png


如果你遇到 Copilot 收费的问题,可以试试 Amazon CodeWhisper ,同样的功能,目前是免费的,未来不知道是否收费。


UI 很重要!!!


对于一个前端人员,有 UI 设计稿让他去还原开发这并不难,但你让他从 0 设计一个精美的 UI 页面,这有点困难。别说精美,能做到 UI 的基本美观就已经很不容易了。


举个例子,这是我偶遇一个笔记软件,这个 UI 真的是一言难尽:左上角无端的空白,左侧不对齐,icon 间距过大,字号不统一,tab 间距过小 …… 这种比较随性的 UI 设计,让人看了就没有任何试用的欲望。


image.png


可以在对比看一下 划水AI 的 UI 界面,看颜色、字号、艰巨、icon 等这些基础的 UI ,会否更加舒适一些?专业一些?


image.png


PS. 无意攻击谁(所以打了马赛克),只是做一个对比,强调 UI 的重要性。


V0 专业生成 UI 代码


V0 也是专业写代码的,不过它更加专注于一个方向 —— 生成 UI 代码 ,能做到基本的美观、舒适、甚至专业。


给一个指令 a home page like notion.com 生成了右侧的 UI 界面,我觉得已经非常不错了。要让我自己设计,我可设计不出来。


image.png


这一点对于很多人来说都是极具价值的,例如中小公司、创业公司的前端人员,他们负责开发 UI 但是没有专业的 UI 设计师,或者说他们开发的是一些 toB 的产品,也不需要招聘一个专职的 UI 设计师。


你可以直接拷贝 React 代码,也可以使用 npx 命令一键将代码转移到你自己的项目中。


image.png


它甚至还会考虑到响应式布局和黑白主题,这一点很惊艳


image.png


再让 V0 生成一个登录页,看看能做到啥效果。在首页输入指令 A login form like Github login page


image.png


等待 1-2 分钟,生成了如下效果,我个人还是挺满意的。如果让我自己写,我还得去翻阅一些 UI 组件库文档,看 form 表单怎么写,怎么对齐,宽度多少合适 …… 光写 UI 也得搞半天。


image.png


划水AI 中“我的首页” 就是 V0 生成的,虽然这个页面很简洁,但是我个人对 UI 要求很高,没有工具帮助,我无法短时间做到满意。


image.png


最后


任何行业和领域,看它是否成熟、是否能发展壮大,一个很重要的特点就是:是否有庞大的细分领域。例如现代医学、现代制造业、计算机领域…… 专业细分及其周密,大家各司其职,整个领域才能欣欣向荣。


AI 领域也是一样,AI 编程将是一个细分领域,再往下还有更多细分领域,像针对 UI 的、针对数据库的、针对云服务的,未来会有更多这方面的发展。


作者:前端双越老师
来源:juejin.cn/post/7438647233219903542
收起阅读 »

只写后台管理的前端要怎么提升自己

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。 写优雅的代码 一道面试题 大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单...
继续阅读 »

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。


写优雅的代码


一道面试题


大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。


原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb,而我要展示成 KBMB 等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):


function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;

while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024;
unitIndex++;
}

return `${kb.toFixed(2)} ${units[unitIndex]}`;
}

而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:


function formatSizeUnits(kb) {
var result = '';

if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}

return result;
}

虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。


如何提升代码质量


想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。


还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。


还是上面的问题,看看 GPT 给的答案


// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。

/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/

function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);

// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}

// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);

// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}

// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB

还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)


我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。


学会封装


一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?


你说,没时间,没必要,复制粘贴反而更快。


那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。


而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。


关注业务


对于前端业务重要吗?


相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。


但是就我找工作的经验,业务非常重要!


如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。


一场面试


还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。



  • 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”

  • 我:“好嘞!”


等到面试的时候:



  • 前端ld:“你知道xxx吗?(业务名词)”

  • 我:“我……”

  • 前端ld:“那xxxx呢?(业务名词)”

  • 我:“不……”

  • 前端ld:“那xxxxx呢??(业务名词)”

  • 我:“造……”


然后我就挂了………………


如何了解业务



  1. 每次接需求的时候,都要了解需求背景,并主动去理解


    我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么 cluster controller topic broker partition…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。


  2. 每次做完一个需求,都需要了解结果


    有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?


  3. 理解需求,并主动去优化


    产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?


    产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。


    其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。



关注源码


说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。


除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。


那说什么,后台管理就这些啊?!


如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?


可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点


至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?



讲一下 Axios 源码中,拦截器是怎么实现的?


Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。


在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含 fulfilledrejected 函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。


以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:


class InterceptorManager {
constructor() {
this.handlers = []; // 存储拦截器的数组
}

use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1; // 返回拦截器的ID
}

eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 移除拦截器
}
}

forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
}
}

在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过 forEach 方法将拦截器中的 fulfilledrejected 函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。


axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的 .then.catch 执行之前,插入自定义的逻辑。


请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。



前端基建


当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。


技术选型


技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表单配置化方案,比如Formily?


对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……


image.png

不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)


Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。


React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。


总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。


开发规范


这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlintstylelintprettiercommitlint 等。


前端监控


干了这么多年前端,前端监控我是……一点没做过。


image.png

前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。


对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。


对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerrorwindow.addEventListener('unhandledrejection', ...) 去分别捕获同步和异步错误,然后通过错误信息和 sourceMap 来定位到源码。


对于性能监控,我们可以通过 window.performancePerformanceObserver 等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。


最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon 还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。


CI/CD


持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。


场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。


这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline 、 Stage 和 Job 分别是什么,怎么配置,如何在不同环境配置不同工作流等。


了解技术动态


这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。


比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。


还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……


虽然不可能学完每一项新技术,但是可以多去了解下。


总结


写了这么多,可能有人会问,如果能回到过去,你会怎么做。


啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。


image.png

作者:我不吃饼干
来源:juejin.cn/post/7360528073631318027
收起阅读 »

用 vue 给女主播写了个工具,榜一大哥爱上了她,她爱上了我

web
用 vue 写了个直播助手,榜一大哥爱上了她,她爱上了我 这是一个什么样的程序?这是一个使用 sys-shim/vue3/vite 开发的一个 windows 程序。用于向网站注入自己的代码以实现一些自动化功能。 sys-shim 是什么?它是一个我开发的个人...
继续阅读 »

用 vue 写了个直播助手,榜一大哥爱上了她,她爱上了我


这是一个什么样的程序?这是一个使用 sys-shim/vue3/vite 开发的一个 windows 程序。用于向网站注入自己的代码以实现一些自动化功能。


sys-shim 是什么?它是一个我开发的个人工具,力求前端人员无需了解其他语言的情况下快速制作轻量的 windows 程序,详情请移步 electron 和 tauri 都不想用,那就自己写个想用的吧


为什么要开发这样的程序


虽然已经过去了很久,但那天的场景还是历历在目。


那天是在周五晚上 23 点过,大楼的中央空调都关了,我搓了搓手,看着还未完成的工作,想了想再不回去公车就没了,到家的话饭店也关门了。


然后按了一下显示器的电源按钮,让电脑继续工作着,准备回家吃饭后远程继续工作。


在大楼电梯遇到一个长得很挺好看的女生,由于这一层我们公司,然后看样子好像是直播部门的同事,虽然平时也都不怎么遇见,更没什么交集,只是公司偶尔让大家去主播间刷下人气,有点印象,猜想应该是直播部门的吧啦吧啦**蕾


虽然是同事,却不熟悉,想打个招呼都不知道说啥,有点尴尬。然后我索性无所是事刷微信列表去了,虽然微信列表里一条消息也没有。。。


突然她给我来了句:“小哥哥你是我们公司的吧,你们平时下班都这么晚的吗?”一边哈气搓手。


我礼貌性笑了一下:“嗯,不是每天都这么晚。”,然后继续低头无所是事的刷微信列表。


大约一两秒之后,她说:“哦”。然后再停顿一会,好像又找到了什么话题:“那你们最近工作也很忙吗?但是我前两几天也没基本没遇到我们公司这么晚下班的人”。


这句话听起来好像传达了很多信息。但时间可不允许我慢慢思考每个信息应该如何正确应对,就像领导给的项目开发时间根据来不及完善好每一个细节一样。


我只能粗略回答:“没有特别忙,只是有时候我喜欢弄些小工具啥的,一不小心就已很晚了”。我心里想:感觉有点像面试,没有说公司不好,又显得自己爱学习,是一个能沉浸于思考的人,应该没问题吧。


“真好,看得出来你还十分热爱你这份职业。能够愿意花自己的时候去研究它们。”听语气好像是有一点羡慕我,感觉还有一点就是她不太喜欢她现在的工作。是呀,我也经常在想,做直播的那些人,有多少是喜欢整蛊自己,取悦别人,有多少是喜欢见人就哥哥好,哥哥帅,哥哥真的好可爱?


“只是觉得,能有一些工具,能帮助减少些重复劳动,就挺好”。


“对对对,小哥哥你说得太对了,就是因为有工具,减少了很多像机器一样的工作,人才可以去做更多有意义的,不像是机器那样的事情。”


当她说这句话的时候,我想,有一种不知道是不是错觉的错觉,真是个有想法的人!还有,这难道是在夸我们这些做工具的人吗?但是她说这句时的微笑,一下子就让人感到了她的热情和礼貌。


我心想,竟然这么有亲和力,很想有愿意继续沟通的想法。对!不然人家怎么能做主播?要换我上去做主播,绝对场也冷了,人也散了。


我一边告诉自己,因为能做主播,所以她本身就很有亲和力,所以你感觉她很热情,是因为这个热情是固有属性,并不是对于你热情。


一边竟开始好奇,这么漂亮又有亲和力的妹子,谁的下属?忍心让她上班这么晚?谁的女朋友?忍心让她上班这么晚?


好奇心害死猫。我竟然还是问出那句话:


“为什么你这么晚才下班呢?”


“最近销售量有点下滑,我想保住我销售额前一至少前二名的位置。”听到这句话的时候,我有点惊讶。我靠,居然是销冠。然后我不温不火的说到:“是啊,快过年了,得拿年终奖。”


“不是,就是想着让成绩保持着,马上快一年了。”尴尬,人家只是想保持成绩。是我肤浅了。等等!保持快一年?没记错的话她好像也才在公司直播一年不到吧!这就是传说中的入职即巅峰吗?我突然觉得我好菜!好想快点自觉走开,奈何地铁还没到!


“原来是销冠,这么厉害!我以为是年底为了冲年终奖,是我肤浅了~”我简单表达一下敬意和歉意。有颜值有能力,突然人与人之间的距离一下就拉开了。


“没有没有!钱也重要,钱也重要!”她噗呲一笑。然后用期盼的眼神看着我,“对了,你喜欢研究小工具来着,你有没有知道哪种可以在直播时做一些辅助的小工具?我网上找了好多,都是只能用在抖音斗鱼这些大公司的辅助工具,我们公司的这个直播平台的一直没有找到。哎呀!好烦~”


完犊子了,这题我不会。看着她好像是工具花了很久没有找到,焦急得好像就要跺脚的样子,我只感觉头皮发麻,要掉头发了!怎么办?怎么办?哪里有这种工具?


但话题还是要接着啊!我开始答非所问:“到没关注过这方面的小工具,但我听说现在有些自动直播的工具,可以克隆人像和声音二十四小时直播。”


“不需要不需要,我不需要这么高端的工具,而且那些自动播的很缺少粉丝互动的。我只要可以帮我定时上下架商品啥的就好。”


我心想,这不搞个脚本 setInterval 一下就行?虽然但是要做得方便他们使用的形式还是得折腾下。我们这个直播平台又不是大平台,网上肯定也没有现成的,不过要做一个没什么难度。


我回答说:“那我帮你找找。”


“谢谢谢谢小哥哥!你人真好!”看着她一边开心的笑着一边双手拜托的样子,我既感觉完犊子了入坑了,又恨不得现在就给她做一个出来!


车来了。她转头看了一下,然后又转过头来问我“小哥哥可以加下你微信吗?你有消息的话随随时通知我,我都在的。”


我:“行的。”


她:“我加你我加你~”


我竟然一下子没有找到我的微信二维码名片在哪,确实,从来就没有其他女生加过我,没什么经验倒也正常,是吧?她又转头看了看停车的车,我知道她是她的车,可她还告诉我没事的慢慢来。


她加上了我的微信,然后蹦上滴滴滴快要关门的列车,在窗口笑着向我挥手告别。在转角那一刻她指了指手机,示意我看微信。


“我叫李蕾^_^”。


“收到”。


alt


功能设计


在上一节为什么要开发这样的程序花费了一定量的与技术无关的笔墨,可能有些读者会反感。对此我表示:你以为呢?接到一个项目哪那么容易?手动狗头。


在功能方面,主要考虑以下特性:


开发时方便


不方便肯定会影响开发进度啦。热更新热部署啥的,如果没有这些开发体验那可是一点都不快乐~


使用时点开就能用


解压、下一步下一步安装了都不要了。


多设备下多平台下多配置支持


如果不做设备隔离,万一主播把这软件发给别人用,岂不是乱套了。多平台的考虑是因为反正都是注入脚本,就统一实现。多配置主要实现每个配置是不同的浏览器配置和数据隔离。


便于更新


减少文件发来发去、版本混乱等问题。


便于风控


如果改天主播说这软件不想用了,那我远程关闭就行。


看下总体界面


alt


一个设备支持多个主配置,每个主配置可以绑定密钥进行验证。


主配置验证通过之后,才是平台配置,平台配置表示系统已支持自动化的平台,例如疼训筷手这些平台。这些每个平台对应自己的 logo、自动化脚本文件和状态。


自动化脚本文件在开发过程中可以修改,用户侧不可见,直接使用即可。


每个平台下有多个配置,比如疼训这个平台下,创建配置A作为账号A的自动化程序,配置B作为账号B的自动化程序。因为每个配置启动的是不同的浏览器实例,所以疼训理论上不会认为多个账号在同一浏览器下交叉使用。反正我司的平台肯定不会认为~


然后配置下有一些通用的功能:例如智能客服可以按关键字进行文字或语音回复。


例如假设你配置了一个关键字列表为


keys = [`小*姐姐`, `漂亮`]
reply = [`谢谢大哥`, `大哥你好呀`, `你也好帅`]

当你进入直播间,发了一句小*姐姐真漂亮时,就可能会自动收到小*姐姐的语音谢谢大哥, 你也好帅


在场控助手这边,根据场控需求,直播间可以按指定规则进行自动发言,自动高亮评论(就是某个评论或系统设定的内容以很抢眼的形式展示在屏幕上),这是防止直播间被粉丝门把话题逐渐带偏的操作方法之一。


商品助手这边,有一些按指定规则、时间或顺序等配置展示商品的功能。


技术选型



  • 使用 vue3/vite 进行界面开发。这部分众所周知是热更新的,并且可以在浏览器中进行调试。

  • 使用 sys-shim 提供的 js api 进行浏览器窗口创建、读写操作系统的文件。当创建浏览器窗口后,需要关闭窗口。

  • 使用 mockm 进行接口开发,自动实现多设备、平台、配置的 crud 逻辑支持。


在 vue3 进行界面开发的过程中,这个过程可以在浏览器里面也可以 sys-shim 的 app 容器中。因为界面与 sys-shim 进行通信主要是通过 websocket 。前端调用某个函数,例如打开计算器,然后这个函数在内部构造成 websocket 的消息体传给 sys-shim 去调用操作系统的 api 打开计算器。就像是前端调用后端提供的 api 让后端调用数据库查询数据,返回数据给前端。


在界面完成之后,把界面部署在服务器上,这样如果有更新的话,就像普通的前端项目一样上传 dist 中内容在服务器上即可。发给主播的 app 读取服务器内容进行界面展示和功能调用。


计划安排



  • 周五加加班,用两小时完成数据模型、API实现

  • 周六完成主要功能界面、交互开发

  • 周日上午进行体验完善、发布测试


开发过程


由于我只是个做前端的,并且只是个实习生。所以用到的技术都很简单,下面是具体实现:


数据模型、API实现


由于是多设备、多平台、多配置,所以数据模型如下:


const db = util.libObj.mockjs.mock({
// 设备
'device|3-5': [
{
'id|+1': 1,
电脑名: `@cname`,
},
],
// 主配置
'config|10': [
{
'id|+1': 1,
deviceId() {
const max = 3
return this.id % max || 3
},
名称: `@ctitle`,
卡密: `@uuid`,
激活时间: `@date`,
过期时间: `@date`,
},
],
// 平台
platform: [
{
id: 1,
封面: [
{
label: `@ctitle`,
value: `@image().jpg`,
},
],
网址: `https://example.com/`,
状态: `可使用`,
脚本文件: [
{
label: `@ctitle().js`,
value: `@url().js`,
},
],
名称: `豆印`,
},
],
'devicePlatformConfig|1-3': [
{
'id|+1': 1,
名称: `默认`,
deviceId() {
const max = 3
return this.id % max || 3
},
platformId() {
const max = 3
return this.id % max || 3
},
configId() {
const max = 3
return this.id % max || 3
},
数据目录() {
return `data/${this.id}`
},
// 功能配置
action: {
智能客服: {
文字回复: {
频率: `@integer(1, 5)-@integer(6, 10)`,
启用: `@boolean`,
'配置|1-5': [
{
关键词: `@ctitle`,
回复: `@ctitle`,
},
],
},
// ... 省略更多配置
},
// ... 省略更多配置
},
},
],
}),

观察上面的数据模型, 例如主配置中有一个 deviceId,由于这个字段是以驼峰后缀的 Id 结尾,所以会自动与 device 表进行关联。


platform 这张表由于没有与其他表有关联关系,所以无需添加含有 ...Id 的字段。


devicePlatformConfig 平台设备配置这张表,是某设备创建的针对于某一主配置下的某平台下的某一配置,所以会有 deviceId / platformId / configId


这样如何要查某设备下的所有平台的配置,直接 /devicePlatformConfig?deviceId=查某设备ID 即可。


由于上面这些表声明关联关系之后,模拟数据和接口都是自动生成的,所以这一块并没有其他步骤。


在 api 层面,有一个需要处理的小地方,就是类似于登录(token/用户标识)的功能。由于这个程序并不需要登录功能,所以使用设备ID作为用户标记。


const api = {
async 'use /'(req, res, next) {
// 不用自动注入用户信息的接口, 一般是系统接口, 例如公用字典
const publicList = [`/platform`]
const defaultObj =
!publicList.includes(req.path) &&
Object.entries({ ...req.headers }).reduce((acc, [key, value]) => {
const [, name] = key.match(/^default-(.*)$/) || []
if (name) {
const k2 = name.replace(/-([a-z])/g, (match, group) => group.toUpperCase())
acc[k2] = value
}
return acc
}, {})
if (req.method !== `GET`) {
req.body = {
...defaultObj,
...req.body,
}
}
req.query = {
...defaultObj,
...req.query,
}
next()
},
}

在后端 api 入口上,我们添加了一个拦截器,含有 default- 开头的键会被当成接口的默认参数。如果传设备 id 就相当于给每个接口带上设备标记,后面这个设备创建和修改、查询配置都会被限定在改设备下,实现了类似某用户只能或修改查看某用户的数据的功能。对于之前提到的公用数据,例如 /platform 这个接口的数据是所有用户都能看到,那直接配置到上面的 publicList 中即可。


前端的请求拦截器是这样的:


http.interceptors.request.use(
(options) => {
options.headers[`default-device-id`] = globalThis.userId
return options
},
(error) => {
Promise.reject(error)
},
)

什么?并不严谨?啊对对对!


界面实现:首先做一个浏览器


由于只会一些简单的跑在浏览器里的 js/css ,所以我们要先做一个浏览器来显示我们的软件界面。


经常用 google chrome,用习惯了,并且听说它还不错。所以打算做一个和它差不多的浏览器。


它封装了 chromium 作为内核,那我们也封装 chromium 吧。


微软听说大家都想要做个基于 chromium 的的界面渲染程序,于是微软就给我们做好了,叫 microsoft-edge/webview2


听说大家都在用这个渲染引擎,那么微软干脆把它内置于操作系统中,目前 win10/win11 都有,win7/8 也可以在程序内自动在线安装或引用安装程序离线安装。


不知不觉的浏览器就做好了。


如何使用这个做好的浏览器


由于只会 js ,所以目前我使用 js 创建这个 webview 实例是这样的:


const hwnd = await hook.openUrl({
url: platformInfo.value.网址,
preloadScript,
userDataDir: row.数据目录 || `default`,
})

可以看到,上面的 js 方法支持传入一个网址、预加载脚本和数据目录。


在这个方法的内部,我们通过构造一个 aardio 代码片段来创建 winform 窗口嵌入 webview 实例。


至于要构造什么 aardio 片段,是 aardio 已经做好相关示例了。复制粘贴就能跑,需要传参的地方,底层是使用 ipc 或 rpc 进行通信的。


ipc 是进程之前通知,可以简单的理解为一个基于事件的发布订阅程序。


rpc 是远程调用,可以简单理解为我们前端经常调用的 api。服务端封装好的 api,暴露给前端,直接调用就好了。


aardio示意片段


var winform = win.form({text: `sys-shim-app`}) // 创建一个 windows 窗口
var wbPage = web.view(winform, arg.userDataDir, arg.browserArguments) // 使用指定配置启动一个浏览器示例
wbPage.external = { // 向浏览器注入全局变量
wsUrl: global.G.wsUrl;
}
wbPage.preloadScript(arg.preloadScript) // 向浏览器注入 js 脚本
wbPage.go(arg.url) // 使用创建的浏览器打开指定 url
winform.show() // 显示窗口

有了上面的代码,已经可以做很多事情了。因为向浏览器注入了一个全局变量 wsUrl,这是服务端的接口地址。然后在注入的脚本里去连接这个接口地址。


脚本由于是先于 url 被加载的,所以首先可以对页面上的 fetch 或者页面元素这些进行监听,实现拦截或代理。另外 webview 也提供了 cdp 层面实现的数据监听。


功能实现:让宿主与实现分离


这里的宿主是指除开 注入自定义脚本 的所有功能。根据之前的设计,网站地址是用户配置的,脚本也是用户上传的。所以一切都是用户行为,与平台无关?


啊对对对就这样!


把自动化这块功能分离出去,让其他人写(我不会!手动狗头)。然后我们在程序里为现有功能做一个事件发布。当用户开启了某个功能,脚本可以知道,并可以得到对应配置的值,然后去做对应功能的事。


const keyList = Object.keys(flatObj(getBase()))
keyList.forEach((key) => {
watch(
() => {
return deepGet(devicePlatformConfig.value, key)
},
(newVal, oldVal) => {
keyByValueUpdate(key, newVal, oldVal)
},
{
immediate: true,
},
)
})


getBase 是一个配置的基础结构对象。把这个对象扁平化,就能等到每个对象的 key,使用 vue 的 watch 监听每个 key 的变化,变化后分别发布 [key, 当前值, 占值, 整个配置对象]


这样在自动化脚本那边只需要订阅一下他关心的 key 即可。


例如:当 场控助手.直播间发言.频率 从 2 变成 6 。


alt


ws.on(`action.场控助手.直播间发言.频率`, (...arg) => {
console.log(`变化了`, ...arg)
})

好了,接下来的内容就是在群里 v50 找人写写 js 模拟事件点击、dom监听啥的了(具体自动化脚本略,你懂的~手动狗头)。


alt


测试过程


总算赶在了周一完成了功能,终于可以进行测试啦~


alt


她同事进行功能测试的时候,提出了一些修改意见(还好是自己写的,不然真改不动一点),然后有个比较折腾的是原来我的配置窗口和平台直播页面是分别在不同的 windows 窗口下的,可以相互独立进行拖拽、最小化等控制,因为想着独立开来的话配置窗口就不会挡住直播页面的窗口了。


没想到她希望配置窗口可以悬浮在直播平台的页面上,并且可以展开折叠拖动。这对于之前设计的架构有一些差异,修改花了点时间。


alt


alt


alt


最终结果


alt


我很满意,手动狗头。


相关内容



声明:本文仅作为 sys-shim 的程序开发技术交流,本人没有也不提供可以自动化操作某直播平台的脚本。


作者:四叶草会开花
来源:juejin.cn/post/7448951076685119529
收起阅读 »

BOE(京东方)“向新2025”年终媒体智享会首站落地上海 六大维度创新开启产业发展新篇章

12月17日,BOE(京东方)以“向新2025”为主题的年终媒体智享会在上海启动。正值BOE(京东方)新三十年的开局之年,活动全面回顾了2024年BOE(京东方)在各领域所取得的领先成果,深度解读了六大维度的“向新”发展格局,同时详细剖析了BOE(京东方)在智...
继续阅读 »

12月17日,BOE(京东方)以“向新2025”为主题的年终媒体智享会在上海启动。正值BOE(京东方)新三十年的开局之年,活动全面回顾了2024年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(京东方)还推出“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座零碳工厂,以绿色产品、制造与运营践行低碳路径;“Innovation+”方面,BOE(京东方)凭借全部为自主创新的9万件专利的行业佳绩,以及技术策源地、技术公益池等举措,携手产业上下游伙伴协同创新;“Community+”方面,BOE(京东方)在教育、医疗、环境等公益领域持续投入,积极履行社会责任,例如,在“照亮成长路”公益项目中,BOE(京东方)十年间在偏远地区建设的智慧教室已经突破120所。

BOE(京东方)智能制造:铸就行业新典范

BOE(京东方)智能制造在引领标准、数字化变革、AI+制造和可持续发展四个方面,树立了全球智能制造卓越标杆,并引领产业迈向智能化、绿色化新时代。在引领标准方面,BOE(京东方)已建立起遍布全球的智能制造体系,包括18条半导体显示生产线和6大全球智能终端服务平台,并荣膺全球智能制造最高荣誉——世界经济论坛“灯塔工厂”。为应对布局全球的产供销业务体系,BOE(京东方)已构建起设供产销集成管理系统,可实现业财一体的全生命周期智能决策;在数字化变革方面,BOE(京东方)正致力于打造“一个、数字化、可视的京东方”,包括流程、组织、IT、数据四大管理要素,通过建立一个基于流程的、端到端的、高效的数字化管理体系为智能制造赋能;在AI+制造方面,通过系统化运用AI、大数据等技术,BOE(京东方)结合生产制造痛点难点问题,聚焦效率领先、品质卓越,务实高效地执行AI+制造规划。在品质把控方面,BOE(京东方)打造的IDM质检平台(Intelligence defect management)是面向业务人员开发的系统,功能覆盖工业质检全场景及AI建模全流程,引入大模型标注、判定技术,打通场景、工序、代际壁垒,极大提升了人机协同下的复判效率和判定准确率,在确保产品高质量的同时实现成本的有效控制;在可持续发展方面,BOE(京东方)始终秉承“以绿色科技推动生产发展理念”,旗下16家工厂获得国家级“绿色工厂”称号,以绿色制造助力产业可持续升维发展。

“向新2025”年终媒体智享会,是BOE(京东方)2024创新营销的收官之作和全新实践,系统深化了大众对BOE(京东方)品牌和技术创新实力的认知与理解。近年来,BOE(京东方)通过多种创意独具的品牌破圈推广,包括“你好BOE”系列品牌线下活动、技术科普综艺《BOE解忧实验室》等生动鲜活地传递出BOE(京东方)以创新科技赋能美好生活的理念,为企业业务增长提供了强大动力,也为科技企业品牌推广打造了全新范式。据了解,BOE(京东方)该“向新2025”主题系列活动还将于12月20日和12月27日分别落地成都和深圳。

面向未来,BOE(京东方)将胸怀“Best on Earth”宏伟愿景,坚持“屏之物联”战略引领,持续推动显示技术和物联网、AI等前沿技术的深度融合。从提升产品视觉体验到优化产业生态协同,从升级智能制造体系到践行社会责任担当,BOE(京东方)将砥砺奋进、创新不辍,为全球用户呈献超凡科技体验,领航全球产业创新发展的新篇章。

收起阅读 »

BOE(京东方)北京京东方医院主体结构开工 打造医工融合创新典范

12月12日,BOE(京东方)旗下北京京东方医院主体结构正式开工。北京京东方医院是2024年北京市“3个100”重点工程项目,定位为BOE(京东方)智慧物联网医院总院,位于房山区京东方生命科技产业基地,总占地面积约152亩, 总床位1500张,其中一期建设10...
继续阅读 »

12月12日,BOE(京东方)旗下北京京东方医院主体结构正式开工。北京京东方医院是2024年北京市“3个100”重点工程项目,定位为BOE(京东方)智慧物联网医院总院,位于房山区京东方生命科技产业基地,总占地面积约152亩, 总床位1500张,其中一期建设1000床,预计2026年建成开诊。北京京东方医院的建设将打造分级诊疗体系区域样板,为大众提供优质便捷高效的医疗服务,同时积极构建医工融合的产业集群,为区域医疗产业的智慧转型注入强劲动力。

京东方科技集团党委书记、副董事长冯强在致辞中表示,智慧医工业务作为BOE(京东方)在大健康领域的重要战略布局,历经十年发展,形成了以健康管理为核心,医工产品为牵引、数字医院为支撑的健康物联生态系统,并按照医工融合的发展理念,在房山布局了集“医教研产用”于一体的生命科技产业基地,北京京东方医院正是基地的核心支撑平台。医院以三级综合医院为基础,结合BOE(京东方)在显示、物联网、智慧医工等方面的核心优势,按照“补缺、合作、差异化”的原则,着力打造“技术领先、数字驱动、模式创新”的BOE(京东方)智慧物联网医院总院。

北京京东方医院将聚焦重症康复、急诊急救等重点学科,积极引进顶尖技术,推动诊疗技术向精准医学方向发展,做卓越医疗的践行者,并充分利用物联网和人工智能技术,建立数字孪生医院,做智慧医疗的引领者。同时,建立开放创新的平台化体系,汇聚专家资源,建立核心能力,将服务体系延伸到社区和家庭,做新型服务模式的创建者。作为生命科技产业基地的核心支撑,北京京东方医院将持续与全国一流高校、医疗机构、创新企业等有机协同,共同构建“从临床来,到临床去”的创新转化体系,推动生命科技产业新质生产力发展。

多年来,BOE(京东方)不断探索未来医疗健康产业的高潜发展方向,通过科技与医学融合创新,打通了“防治养”的全链条,实现了“医”为“工”提供需求来源和临床转化,“工”为“医”的技术提升持续赋能。目前,BOE(京东方)已在北京、合肥、成都、苏州四地建设并运营5家数字医院。面向未来,BOE(京东方)将坚持以“屏之物联”战略为牵引,积极推动医疗健康产业的智慧转型,以创新驱动和科技赋能开启医工融合发展的崭新篇章。

收起阅读 »

大学生编程第一人,一人抵一城!

就在上个月底,领域内知名的自动驾驶公司小马智行在纳斯达克成功挂牌上市,成为自动驾驶领域新焦点,这个新闻相信不少同学都看到了。 提到小马智行这个公司,可能有些同学还不是很了解。 但是提到小马智行公司的CTO,那相信不少同学就耳熟能详了。 没错,他就是那个神一...
继续阅读 »

就在上个月底,领域内知名的自动驾驶公司小马智行在纳斯达克成功挂牌上市,成为自动驾驶领域新焦点,这个新闻相信不少同学都看到了。



提到小马智行这个公司,可能有些同学还不是很了解。



但是提到小马智行公司的CTO,那相信不少同学就耳熟能详了。


没错,他就是那个神一般的男人,有着「中国大学生计算机编程第一人」称号,并且江湖人尊称为「楼教主」的计算机编程大牛:


楼天城博士



2016年底,楼天城和他的合伙人创立了小马智行,致力于提供安全、先进、可靠的全栈式自动驾驶技术。



不过在此之前,楼天城就以比赛ID “ACRush” 为计算机编程界所熟知。


楼教主毕业于清华,曾连续11年蝉联TopCoder中国区冠军,并且两次获得谷歌全球编程挑战赛冠军。他的职业生涯开始于谷歌,并且在创立小马智行之前,也曾是百度历史上最年轻的T10工程师。


2024年,楼天城以60亿元人民币财富登上《2024年·胡润百富榜》第875位。



从某一程度上也实现了靠写代码,登上了胡润财富榜!


看到这里,相信大家也都好奇这位编程大佬的的成长经历和成神之路。


究竟是怎样一个环境和经历才能造就今天的楼教主呢,今天这篇文章我们就来好好聊一聊。


0x01


1986年,楼天城出生于一个知识分子家庭,父母均为教育工作者,这样的家庭环境为他提供了良好的学习氛围。


尽管童年时期的他并未直接接触到计算机编程,但楼天城却展现出了对数学和逻辑的浓厚兴趣。


楼天城的理科思维十分敏捷,甚至在踢球时,也会计算战术中的各种概率,展现了他对细节的极致追求,这也为他日后的编程之路奠定了坚实的基础。


楼天城高中毕业于杭十四中。


众所周知,这是一所具有悠久历史的百年名校,位于美丽的西子湖畔,同时也是浙江省一级重点中学。



在进入杭十四中后,一次偶然的机会,楼天城加入了学校的计算机兴趣小组,从此开启了与编程的不解之缘。


在短短三个月内,他凭借自学,就在全国青少年信息学联赛(浙江赛区)中脱颖而出,荣获一等奖,这标志着他在编程领域的初步成功,也点燃了他对计算机和编程的巨大热情。


而这,只是他成绩的开端。


0x02


高中时期的楼天城由于竞赛成绩十分优秀,因此也成功保送进入清华大学计算机系,并在后来加入由图灵奖得主姚期智院士领导的清华计算机科学实验班(俗称“姚班”),并成为姚班的首届学生之一。



姚期智先生是世界著名的计算机科学家,唯一的图灵奖华人得奖者;而姚班则是中国乃至全球顶尖的计算机科学人才培养基地。


在这里,楼天城不仅系统学习了计算机科学的基础理论,更是参加了各类编程竞赛,成为校园内的传奇人物。


本科期间,楼天城以其深厚的编程功底和卓越的思维能力在各类竞赛中大放异彩,经常以一人单挑一个队,并且曾多次带领团队获得国内外编程竞赛的冠军,在ACM等编程竞赛界可谓是无人不知、无人不晓,因此也获得了“楼教主”的称号。


本科毕业以后,楼天城选择继续留校读博,并且继续师从姚期智院士。


在这个阶段,楼天城接受了更为严格的学术训练和前沿的科研指导。


在博士阶段,楼天城前期做的偏向theoretical computer science(理论计算机科学),后期偏向Social Network。


这个阶段的积累,为他日后的科研和创业之路打下了坚实的基础。


从高中到本科到读研,回顾楼天城的竞赛经历,可以说堪称传奇。
从高中开始,他几乎每一年都能拿到有关数学或者计算机方面的奖。


而且楼天城的竞赛经历几乎囊括了国内外所有重要的编程赛事,每一次参赛都留下了令人瞩目的成绩。


从高中时期的全国青少年信息学竞赛,到国际信息学奥林匹克竞赛(IOI)金牌,再到Google全球编程挑战赛冠军,楼天城用一次次胜利证明了自己的实力。


尤其是TopCoder,楼天城曾连续11年蝉联TopCoder中国区冠军,并且两次获得谷歌全球编程挑战赛冠军。


这些竞赛经历不仅锻炼了楼天城的编程能力和团队协作能力,更为他日后的职业发展奠定了坚实的基础。


0x03


毕业后的楼天城,职业生涯开始于谷歌,曾在谷歌总部从事社交网络和机器学习相关问题的研究。



2016年,楼天城受邀加入了百度美国研发中心,并且与当时的百度自动驾驶部门首席架构师彭军一起参与百度无人车和自动驾驶技术的研发。



在百度的工作期间,楼天城致力于自动驾驶相关技术的研究,同时他凭借卓越的技术实力和丰富的经验,也成为了百度历史上最年轻的T10工程师。
这段经历不仅加深了他对自动驾驶技术的理解,也让他看到了自动驾驶技术改变未来出行的巨大潜力,更是为即将到来的创业埋下了伏笔。


在百度工作了一段时间后,楼天城选择了从百度离职,并且与同时离职百度的首席架构师彭军一起创业,创立了自动驾驶创业公司小马智行。


至此,小马智行正式诞生



公司成立以后,彭军担任小马智行首席执行官,负责公司总体事务,而热爱技术和编程的楼天城则担任小马智行首席技术官,主要负责技术研发。


而且小马智行的初创团队成员不少都是楼天城曾经在竞赛中认识的朋友,或者是因其名气而慕名加入的。


楼天城作为公司的CTO,亲自参与了自动驾驶相关技术的设计与测试,带领团队攻克了一个个技术难题,推动小马智行的自动驾驶技术不断向前发展。



小马智行自成立之日起,就以其技术实力和前瞻性市场布局迅速成为了自动驾驶领域的佼佼者。而经过这几年的发展,如今的小马智行已经成为自动驾驶领域的领军企业之一。



0x04


回顾楼天城一路走来的成长经历:


从中学时期对编程的兴趣出露,到大学成为一个顶尖算法竞赛选手,再到研究生期间从事理论计算机等研究,最后再到走向业界与创业。


虽说这个过程充满了传奇色彩,但其中也不乏兴趣务实长期主义,以及坚持


楼教主曾说:


要学习认识你自己,了解什么样的事情会让自己开心,爱好就像一个多臂老虎机,要每个臂多拉几下才能知道自己的兴趣所在。


的确,像楼教主这样,在前行的路上找到自己所热爱的事业,并为之全力奔赴,这是一件何其幸运的事情。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



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

87年,37岁,脱下孔乙己的长衫,我开了家一人公司。

你好, 我是微笑哥。在职场做了 10 年的程序员,从 Java 开发做到研发副总,离职前带领着100+的研发队伍。19年开启自由职业,全世界一边旅行一边工作,2020年回西安创业,换了3个客厅2个办公楼走到今天。这中间,真的是脱了几层皮,从一名少年成为现在的沧...
继续阅读 »

你好, 我是微笑哥。
在职场做了 10 年的程序员,从 Java 开发做到研发副总,离职前带领着100+的研发队伍。
19年开启自由职业,全世界一边旅行一边工作,2020年回西安创业,换了3个客厅2个办公楼走到今天。
这中间,真的是脱了几层皮,从一名少年成为现在的沧桑大叔。借着这个日子,和大家聊聊我的经历。


1


毕业即失业

去流水线感悟人生


我大学学的是信息与科学,是一个没有对口就业的专业,我的同学各凭本事分布在各行各业。

大学玩了4年,到大四我慌了,我没背景也没能力,真不知道未来怎么走,只能疯狂去面试。

当时在宝鸡上的大学,大四上半年全部时间都去西安面试,2个月时间面试了几十上百家,最后入职了一家炒股软件公司。

说是储备干部,其实就是销售,去各个证券交易所里面找人要电话号码,然后邀请去公司做活动买软件。

这不是我想要的工作,年前快放寒假的时候辞职了,没有底薪只有提成,一分钱没有拿到回学校了。

临近过年,更着急了。

那个时候老大在比亚迪工厂,于是想去深圳看看有没有就业机会,于是坐了20多个小时火车来到深圳。

求职无果后,入职了比亚迪流水线车间,车间里面做了一周时间,差点和车间主任干起来了,工作是给硬纸板打孔很危险,墙上到处是事故照片,没有指头的那种,想了想还是辞了。

我在富士康工厂,周边环境

临近过年总不能空手回家吧,又去了富士康观澜的物流部,不说有多辛苦好歹扎扎实实工作了2个月。

人生真的很神奇,我去的那个厂子就是富士康当年十几跳的第一跳,我刚去就有工友给我说前几天有人跳楼了。

那是09年,我人生第二次在外地过春节;第一次是06年被我兄弟骗到传销组织安徽阜阳,被限制自由了28天。

而最夸张的是,我有个很铁的朋友,妹妹被骗到传销组织中,中间我竟然还请了2周假,想办法把他妹妹从传销组织搞了出来,不过那就是另外一个故事了。

总感觉大四那一年,经历了很多事情,很混乱很懵懂也很迷茫,就像青春少年本身就是迷茫一样

我在流水线挥汗如雨的时候,我就只想了一件事情,我一个大学生要在这流水线干一辈子吗,我这大学不是白读了。

于是3月份春暖花开的时候,又回陕西了。



2


误入程序员


人生有时候就是需要一个契机,刚经历的时候没觉得有啥,当把这些都串起来的时候,就成为了人生。

我在大三的时候,突然感觉大学白读了,不学点啥好像又对不起这4年,上课感觉毫无意义工作又不用(我们是数学专业,天天玩高数、微积分)

刚好听说有数学建模竞赛,我就报名了,玩命的学了3个月,我是小组负责编程的那位也是组长,把C语言玩的贼溜。

没想到几个月后也获奖了,好像是省级二等奖,上图左下方那个人就是我,在学校宾馆小组3天3夜写一份论文出来。

让我对程序有了启蒙,毕业的时候也不是没有找过程序员岗位,但都是简历投递出去没有人回复的状态。

但也一直有关注,知道程序员有培训这么一回事,也知道程序员工资蛮高的,但有一定的门槛。

从深圳回来后,我下定了决心学软件开发,计划做一名程序员。

本来去咨询培训C语言、C++的,结果跑了一圈都说 Java 好就业,就最后选择了培训 Java。

找我姐借了1万元(我大学四年,学费也是借的)。

去银行取钱的那一天,我抱着1万元在柜台业务员的注视下,数了好多遍,那是我第一次见这么钱(09年)

培训班的事情就不说了,从刚开始的二指禅敲键盘,以及对 Java 一无所知,不过培训班氛围不错都是大四学生,有不会的大家都相互帮助。

培训班期间某次团建

我在编程这方面还是有点天赋的,很快到毕业的时候,已经给同班其他人讲编程了。



3


入职2周就被辞退了。


培训班会推荐几个公司,我是第一批推荐名单里的,很快就拿到入职的 Offer 了。

对比了一下,选择当时李嘉诚儿子的一家公司,是港企。

确实不一样,入职先培训一个月再上岗,那一批入职了18个人,其中就有我们培训中的5个人。

当时心态还是学生状态, 做出成果之后也不主动汇报,一个人坐在角落里面瞎捣鼓。

2周后,我被辞退了。

给我姐打电话的时候,我姐说,关键18个人里面仅仅辞退了2个人,就有你


可能当时也不服气吧,憋着一股劲投了几十家公司,最后被华为的外包公司录取了,人力外包直接到华为公司上班。

一个月工资,1800元。

当时华为比较正规,平时加班有加班费,周末加班双倍工资,于是我疯狂加班,一个月工资3000+。

外包是学不到东西的,虽然领导对我都很不错,入职几个月竟然又搞起了C++编程。

第一份工作,小巴组集体去团建

我一想花1万学的Java不就白费了,而且 Java 未来的就业更广阔一点,我不能就这样走弯了。

9个月后,离职,又找了一家港资,做电信业务,工资4000。

在这家公司,我遇到了人生最好的几个朋友,Jerry、鸽子、不会笑,我们四个人一起入职。

Flyever户外组织,秦岭某个地方

在这家公司和鸽子、不会笑,还创建了一个户外组织 Flyever,其实就是组织一群年轻人去山里玩。

几乎每周都有活动,不是吃饭喝酒唱歌通宵,就是半夜4、5起床去爬山游水,度过了人生中最快乐的一个阶段。

也和Jerry、鸽子、不会笑建立起来了革命友谊,我们四个后来在创业的路上有过多次交集。

在这家公司工作了1年半,从一个初级Java工程师开始做起,走的时候已经可以独立负责一个项目了。

当时又有一个契机

我妈要在老家盖房,我给了2万还不够,又借了亲戚大概十几万,我想了想我这4000的工资。

除去日常花销,要还多少年呀?

听说我们一个同事去了北京工资1万,我同事说那个人技术水平还没有我高,说者无心听者有意。

当年 Jeryy 也要去北漂,他想创业,我说那一起吧。

一起就一起。



4


北漂7年。


他比我早一周到北京,找了一家做 OA 的公司。

那个时候,北京的分钟寺还没拆,一个巨大无比的城中村,我当时拿了3000元,租房一个月1000。

刚去北京第二天,在鸟巢留念

我用了一周时间,拿到了4个Offer。

印象中北京的风是真大,能把我吹上天,当时还用的是小米1,一接电话就死机,耽搁了我好多事。

最高的一家给了1.3万,不过我当时目标很明确,只进互联网公司,1万的月薪入职了一家第三方支付公司。

其实我北漂7年,几乎可以说就在这一家公司干了。


2013年,那真是一个波澜壮阔的时代,也是移动互联网的时代,更是万众创业的时候。

我们公司就在中关村创业一条街附近,当时还叫海淀图书街,因为创业氛围太过浓烈,后面才改名叫做创业街。

Jeryy想创业,我们几乎跑遍了这条街的所有咖啡馆,车库咖啡、3W咖啡等等,周末点上一本咖啡坐上一整天。

一起想创业的点子,一起开发程序做Demo,写商业策划书去融资,刚去的1年,我几乎没有任何空余时间。

失败是必然的。

Flyever,被我带到了北京

组建了一个叫做陕西人在北京的

继续组织爬山、吃饭、唱歌、喝酒

不过,我工作的那家支付公司1年后迎来了一个契机。

当时互联网金融大行其道,我们作为第三方支付公司,董事长旗下还有不少其它业务需要融资。

于是决定要做一个互联网借贷平台,当时称P2P,我们的模式是B2P,企业向用户借钱,给用户利息。

刚开始是一个百度过来的高管负责,也是清华大学毕业的,结果干了半年后,这名高管自己创业了。

团队也被带走了,技术开发就剩了一个人。


我的领导的领导的领导,临危受命要把这项目接下来,但是找不到合适的人选来负责落地。

我们那一批来不少人,但大部分技术高手1年后都离职了,我其实也刚好找到了外部的工作。

领导说让我接手,我说可以呀,得涨工资,领导说行,那就我干了。

领导说,可以让我在部门调两个人过去,我选择了一个实习生,一名年轻有潜力的程序员。

就这样把这个项目接了起来。


一个月后项目上线,当天融资了1000万;但没过多久,一天的融资能力就变成了好几个亿。

项目成了,从一个项目组变成集团的一个子公司。

3年时间,

我就从一个项目组长,变成了技术经理、部门经理(测试、运维都管),再到公司的技术负责人。

2015年,北戴河团建
生命不息,奋斗不止!

互联网金融真是一个疯狂的年代,是我人生中最忙的一个阶段,也是我成长最为迅速的一个阶段

早上第一个来公司,晚上也几乎是最晚离开,23点之前没有离开过公司,有一次下班出车祸也就隔了一天又去上班了。

因为所有的事儿,都是必须我来扛。


16年,

也就是那个时候,我觉得应该把我段人生经历记录下来,特别是在互联网金融公司工作的那些事。

太好玩了。

高并发、大数据、微服务、分布式、黑客攻击、黑客勒索等等,我们又用的是市面上最新的技术栈,遇到了很多很多问题,也解决了很多很多问题。

我想要有一个窗口和技术人交流。

就到博客园写文章,没想到写一篇火上一篇,1年的时间,我就成为博客园推荐博客排名前10。

老版博客园的推荐博客列表

也写了一些开源项目,Github关注数量中国排名前50。

1年后,开通了个人公众号纯洁的微笑。


2017年,因为母公司要上市,我们被卖给了海淀国资委,公司的风格一下子就变了。

于是,我离职又重新去了母公司。

从这个互联网金融公司离开时

团队瞒着我,送了一个全体签名的短袖


从一名架构师开始做起,负责公司新支付系统的大数据平台建设,处理每天几十亿交易数据。

半年后,连跳3级被提拔为研发副总,当时研发事业部已经有超过100+的研发人员。

我只负责一个事情,把公司研发的第四代收单平台,推动上线迁移替换旧的收单系统。

中间又是遇到了 N 多的事情,除了技术问题之后,在高管的位置上自然有其它是非在其中。

有时候,你不站队也是一种站队。


18年,萌生去意,但老领导们提拔我不容易,至少要有知遇之恩,又咬着牙坚持了1年。

2018年,上海出差

19年,研发中心出现一次事故,借此就离开了。

19年离职的时候,我的副业自媒体收入早已经超过了主业,收入这方面倒也不是很在乎。

只是那个时候,也有别的互联网公司开出年薪百万工资挖我去做技术总监,当然了也是和支付相关的。

那我就面临人生一个最重要的选择,继续在北京互联网公司做高管,还是尝试一个人做点事情。

其实也没太多纠结,因为上班确实上够了。

从09年开始培训到19年离职,我的每一份工作和上一份工作都是连着的,10年时间没休息过。

HR劝我的时候,我说你觉得我现在创业风险大,还是几年后创业风险大,HR说应该是几年后吧。

我说,那就对了。



5


自由职业


离职之后,我就成为自由职业了。

那个时候,我还不敢对外说是创业,毕竟在我的印象中,创业应该是一件蛮大的事情了。

那个时候,我只能养活自己。

在离职之前,公司在美国有一个业务,需要派别人过去,我当时也申请了,顺势办理了美国10年签证。

于是,离职第一件事,搬家。

从三环边搬到五环旁,先改变自己的居住环境,毕竟自由职业要在家里上班的。

第二件事情,就是到处溜达,当然也包括美国。

现实自驾草原天路,再是北海道那一带,然后就是环京大环线,还在深圳附近自驾来半个月。

感觉还是不过瘾,又去美国旅游了一个月,从东海岸到西西海岸,从黄石公园到约书亚树国家公园。

2019年,纽约时代广场

所以,自由职业的头一年,大概就是不断的旅游,不断的折腾自媒体事业。


不过,从美国回来之后,让我意识到了一件事。

好像出国旅游花不了多少钱,既然是这样,何不计划一下环游世界,我从来都是一个说干就干的人。

要环游世界,那就要把英语学好,也要把北京的房子退了(一个月租金6500,几乎一半时间都空着)。

当时的计划是这样的。

第一步,把北京房子退了,东西都打包回老家。

第二步,在菲律宾报了一个语言学校,3个月先把英语给学扎实了,并且要拿到托福证书。

第三步,西安去亚洲,再去新西兰澳洲,最后环游欧洲,欧洲走完了再说后面的人生计划。


如果不是当初的那一场疫情,还不知道现在我在世界的哪个角落呆着,不知道从事着什么样的工作?

谋事在人,成事在天!



6


回到西安,创业


2019年,春节。

行李均已经邮寄到了老家,我从北京回到西安,准备在西安待上几天见见老朋友回家过春节。

当时菲律宾签证已经搞定,菲律宾当地的学校也联系好了,就连定金也给学校交了。

同时日本、新西兰的签证也办理好了。

一切静等在老家过完春节,开启自己环游世界的人生。


我在西安计划住5天,到了第3天就从新闻上传出,武汉那边好像出了一点事情,而且有蔓延的趋势。

到第3天,我看趋势不太对,没有坐班车,就打车直接回老家了。

在老家整整呆了6个月,这应该是我毕业以后,在老家呆过最久的一段时间了。

在老家的年轻人,也都陆陆续续回城市工作了,村里面的老大爷老大哥问我,你怎么还不走。

我是闲不住的人,既然环球旅游无望了,那就踏踏实实创业。

到西安后,就申请创建了公司。

在淘宝上,花了 600 元买了一个很大的黑色桌子,放在了客厅,作为办公桌,我坐这边,不会笑青年坐对面。

就这样开启创业了。


1月后,又搬到了不会笑青年家的客厅。

2020年,不会笑青年家的客厅

在他们小区可以打乒乓球,于是每天下午3点,无论夏日还是寒冬,我们都准时下去和老大爷抢场地打乒乓球。

1年后(2022年),不会笑青年要结婚,又搬到我新装修的房子客厅办公,顺势又把团队从2个人扩展到4个人。

公司变成了4个人。

集团活动从乒乓球又变成了羽毛球,于是4个人每天下午3点,又准时去小区打羽毛球。

这一打又是1年。


到了2023年,AI爆发。

我们觉得这是未来的大趋势,公司的整体发展方向全部转向AI,于是创建了 AI 俱乐部星球。

AI俱乐部用户已高达800人,AI俱乐部社群的推出

代表公司全部转向AI创业,后面的业务都和AI有关系。

2023年7月,又把公司从西安东北角搬到了西南角的高新区,终于有了一个相对正式的办公室。

搬家有两方面的原因:

一方面,不会笑青年当时住高新,来回通勤的压力很大;另一方面,想找一个办公楼方便后续招聘扩张。


搬家1个月后(2023年9月),掩体来西安旅游。

带来了AI变现训练营这个项目,我们当时测试了一下,一个月的时间80%的人可以月入过万。

那还有什么说的,全力 All in 继续搞!

又1年后,这个项目越做越大,团队规模又翻了一倍,从最初的4个人变成了8个人。

原来的办公司又坐不下了。

2023年,第一次搬到写字楼办公,在这个小办公室里面,加了N多次班。

今年7月,又重新租下了现在的办公室,也就是视频开头我拍摄的那个办公环境,可以容纳12人。

现在的这个办公室,才真正有点像一家公司的样子,在这之前我们都是草根游击队。

未来,公司能够发展多大,能够赚多少钱?已经不是我第一关心的事情了,更关心的是有没有更享受创业的这个过程。

从来没有想过要做多大的一个公司,一直想的是怎么做一个小而美的公司,有激情有奋斗有冲劲但又不是那么忙。

当然了,现在还差很远,还在既苦逼又享受的那个阶段。


人必有痴,而后有成,痴于物,而成于心。

09年到24年,

眨眼间,已经过去了15年,最大的感慨就是,人如果真要做成一件事,是不容易的。

特别对于我们这种出身贫穷的普通人。

每一步都不能错,错了就没有回头路能走,能够帮你的人永远只有你自己而已。

当然除了下苦功夫努力,也需要机缘更需要运气和贵人,才能让努力可以开花结果。

我特别喜欢一段话,作为这篇文章的结尾:

北冥有鱼,其名为鲲。鲲之大,不知其几千里也;化而为鸟,其名为鹏。鹏之背,不知其几千里也,怒而飞,其翼若垂天之云


作者:纯洁的微笑
来源:mp.weixin.qq.com/s/TBlJdOfzDCYDZ9NQVcTneQ
收起阅读 »

狂肝两周,写了一个"本地化"的桌面端工具:超级待办

web
事情是这样的,上一年写了一个基于uTools的插件,超级待办,目前日活300+,插件累计下载量24000+,有很多用户期望能够开发独立版本且数据可控(在本地保存),所以在插件版的基础上,优化UI,交互,新增相关功能,开发了一个桌面端版本(win+mac)的超级...
继续阅读 »

事情是这样的,上一年写了一个基于uTools的插件,超级待办,目前日活300+,插件累计下载量24000+,有很多用户期望能够开发独立版本且数据可控(在本地保存),所以在插件版的基础上,优化UI,交互,新增相关功能,开发了一个桌面端版本(win+mac)的超级待办。


1.gif


应用特色:


一、快速记录待办


二、所有数据保存在本地


很多人在记了一两条就放弃了,很难坚持下来,所以先把待办记下来,后续使用拖拽的形式快速修改待办状态,在此场景下,设计了多个快速记录待办的方式:


一、使用快捷键Ctrl+Space快捷键(可自定义)快速呼出应用,自动聚焦输入待办


二、剪切板转待办,应用会记录剪切板记录,你可以查看记录并转为待办


该应用共分为任务主面板、日历、列表、随心记(富文本)、分类、剪切板等模块。


任务主面板模块


在这里根据完成状态把任务分成未开始,进行中,已完成三个框体,不同状态之间支持拖拽,排序等快速操作;并且根据任务的紧急状态把每个任务分为紧急,高,中,低四个状态,方便标记任务,区分优先级;支持关联随心记,编辑,修改,删除,归档等操作。
image.png


日历模块


在这里根据每个任务的时间,分布在日历视图上,日历包含节假日等信息,能够查看任务分布及完成情况,支持关键字搜索,编辑任务,删除任务等操作。


image.png


列表模块


把任务汇总为列表模式,你可以在这里取消归档,按照任务状态进行筛选,关键字搜索,分类搜索,查看关联的随心记,删除,编辑等操作,并能直观的查看你所记录的待办事项。
image.png


随心记


这里使用了富文本编辑器,支持上传图片,方便记录待办所关联的一些信息,例如:我完成了一个模块的开发,可以把具体的注意事项关联写到随心记里,方便日后查看。不仅如此,你可以记录你想记录的任何文本。


image.png


分类


在这里你可以去维护应用的分类信息,在创建任务及随心记时,可以进行关联,方便归类管理。虽然模块名是分类,但在使用过程中,你可以把分类定义为项目名,日期归总等等。
image.png


剪切板


我们在应用后台,实时记录剪切板内容并保存到本地,你可以在这里把剪切板某个内容转为待办,也可以根据关键词进行搜索,同时可以一键删除所有,删除单个内容等。
image.png


设置


如果你觉得不希望每次都出现很大的框体,只希望在桌面上出现一个还未完成的任务面板并进行操作,你可以在设置中开启待办事项面板,开启后,你可以固定到桌面的任意位置。我们默认保存该面板的位置信息,在你重启电脑后,面板依旧在你设置的位置。


同时你可以在这里设置是否开机自启,是否显示快速添加待办的面板,启动时是否只显示一个输入框还是整个应用,以及呼出应用的快捷键,怎么使用,任你发挥。


为了极少的收集用户信息,我们采用微信绑定许可码(一机一码)形式,当应用安装后,可以按照操作申请试用许可码,每个微信支持绑定多个机器。
image.png


在公测期间,一共收获了300个用户,并提了很多宝贵的建议,后续迭代会持续的进行优化更新~


最后,如果你也有记录待办的需求,不妨体验一下呀~


作者:探路
来源:juejin.cn/post/7423583639081664564
收起阅读 »

极狐 GitLab 双重风波,Git 私服该如何选择?

极狐 GitLab 的双重风波 (一)间谍风波 前两天,极狐 GitLab 陷入了员工实名举报公司高管为美国间谍的漩涡之中。这一事件犹如一颗重磅炸弹,在业界引起了轩然大波。尽管目前尚未被实锤,但此消息一经传出,便迅速吸引了众多目光,也让极狐 GitLab 的企...
继续阅读 »

极狐 GitLab 的双重风波


(一)间谍风波


前两天,极狐 GitLab 陷入了员工实名举报公司高管为美国间谍的漩涡之中。这一事件犹如一颗重磅炸弹,在业界引起了轩然大波。尽管目前尚未被实锤,但此消息一经传出,便迅速吸引了众多目光,也让极狐 GitLab 的企业形象蒙上了一层阴影。这一事件不仅引发了内部员工的震动,也使得外界对其公司的信任度产生了动摇,其后续发展仍有待进一步观察。


(二)绝户网计划


就在今天早上,极狐 GitLab 的 “绝户网计划” 也浮出水面。据爆料,极狐 GitLab 要求销售人员在与使用 GitLab CE(社区版)免费版的用户交流时,引导用户明确表达正在使用免费版,并将此作为证据存档,以便未来可能的发函或起诉。 从其告知函来看,极狐 GitLab 强调其核心产品 GitLab 受法律保护,指出用户涉嫌未经授权使用软件,违反相关法律法规。公司内部对此计划也存在分歧,部分销售和技术同事反对,认为这会得罪潜在客户,影响长期生意,但公司高层却决定推行,寄希望于小部分害怕被起诉的用户付费来提升今年业绩。此计划引发了广泛争议,因为 GitLab 的免费版在全球范围内被大量程序员使用,且一直以来被认为是可商用的,这一举措无疑打破了以往的认知,让许多用户感到不满和担忧。



告知函内容如下:



GitLab 替代品分析


巧合的是,前不久我刚好对 GitLab 私服替代品进行了一波调研,同时成功将 GitLab 仓库迁移到新的仓库中,这次正好分享一下替代品的优缺点。


(一)Gitea



  • 优点

    • 轻量级:资源占用较低,对于硬件配置要求不高,适合小型团队或个人开发者在有限资源环境下搭建 Git 私服。

    • 功能较为完善:能够满足基本的代码托管、版本控制、分支管理等常见需求,支持多种主流操作系统的部署。

    • 社区活跃:有大量的开发者参与社区贡献,遇到问题时能够在社区中较快地获取解决方案和技术支持。



  • 缺点

    • 在大规模团队协作和复杂项目管理场景下,一些高级功能可能相对薄弱,例如在代码审查流程的精细化管理方面不如一些大型商业 Git 工具。

    • 与一些大型企业级工具相比,其集成能力可能稍逊一筹,在与其他企业内部系统如 CI/CD 平台、项目管理软件等的深度整合上存在一定局限性。

    • Gitea 的域名和商标在社区不知情、未经社区批准的情况下被转让给一家营利性公司,有一定开源风险。




(二)Gogs



  • 优点

    • 易于安装和使用:安装过程简单,即使是技术基础相对薄弱的用户也能快速上手搭建自己的 Git 私服。

    • 性能表现不错:在处理中等规模的代码仓库和团队协作时,能够保持较为稳定的运行速度和响应效率。

    • 界面简洁:对于注重简洁操作界面的用户来说,Gogs 的界面设计较为友好,易于操作和管理。



  • 缺点

    • 功能扩展性相对有限:虽然基本功能齐全,但在面对一些特殊需求或新兴技术场景时,可能难以通过插件或扩展机制快速实现功能增强。

    • 社区规模和活跃度不如一些头部的 Git 工具,这可能导致在长期发展过程中,功能更新和问题修复的速度相对较慢。




(三)OneDev



  • 优点

    • 强大的项目管理功能:除了基本的 Git 代码托管功能外,OneDev 在项目管理方面表现出色,提供了丰富的项目进度跟踪、任务分配、团队协作等功能,适合以项目为导向的团队使用。

    • 支持多语言:能够很好地适应不同语言环境下的开发团队需求,方便国际化团队协作。

    • 可定制性强:用户可以根据自己团队的特定需求,对 OneDev 的功能和界面进行一定程度的定制,以提高工作效率。



  • 缺点

    • 学习成本相对较高:由于其功能丰富且较为复杂,新用户需要花费一定时间来熟悉和掌握其操作流程和功能配置。

    • 部署相对复杂:相比一些轻量级的 Git 私服工具,OneDev 的部署过程需要更多的配置和环境依赖,对于运维人员的技术要求较高。




(四)GitBucket



  • 优点

    • 与 GitHub 风格相似:对于熟悉 GitHub 操作的用户来说,GitBucket 的界面和操作方式具有较高的相似度,降低了用户的迁移成本。

    • 支持多种数据库:可以灵活选择数据库类型,如 MySQL、PostgreSQL 等,方便根据现有技术架构进行整合。

    • 插件丰富:提供了大量的插件来扩展其功能,例如代码质量检测、代码统计等插件,能够满足不同团队的多样化需求。



  • 缺点

    • 性能优化方面可能存在不足:在处理大规模代码库和高并发请求时,可能会出现性能瓶颈,需要进行额外的性能调优工作。

    • 社区文档相对不够完善:在一些复杂功能的使用和问题排查上,由于社区文档的不全面,可能会给用户带来一定困扰。




(五)Gitblit



  • 优点

    • 专注于 Git 核心功能:对 Git 的核心功能支持得非常稳定和高效,如代码托管、分支管理、权限管理等,适合那些只需要基本 Git 功能且追求稳定性的团队。

    • 轻量级且资源占用少:在硬件资源有限的情况下,能够稳定运行,不会对服务器资源造成过大压力。

    • 安全性能较高:提供了较为完善的权限管理和安全机制,能够有效保护代码仓库的安全。



  • 缺点

    • 功能相对单一:缺乏一些现代 Git 工具所具备的高级项目管理和团队协作功能,如敏捷项目管理工具集成等。

    • 用户界面相对简陋:在美观度和交互体验上不如一些新兴的 Git 私服工具,可能会影响用户的使用感受。




Forgejo 的选择理由


经过个人调研和综合考量,最终选择了 Forgejo 替代 GitLab。Forgejo 是 Gitea 的一个硬分叉,它继承了 Gitea 的所有优点,如轻量级、功能完善、社区活跃等。同时,Forgejo 还具有自身独特的优势,其界面美观,给用户带来了良好的视觉体验;部署简单,降低了迁移成本和技术门槛,即使是非专业运维人员也能轻松上手;加载速度快,能够提高团队成员的工作效率,减少等待时间。


在迁移项目时,可以参考我之前写的迁移教程:



综上所述,个人认为 Forgejo 在应对极狐 GitLab 近期风波所带来的不确定性时,是一个较为理想的 Git 私服替代品。


相关资料



作者:子洋
来源:juejin.cn/post/7446578471901626420
收起阅读 »

面试了一个45岁的程序员,他要月薪2万,我同意了。结果面试完送他到电梯口,他说14薪月薪1.8万也行。

电梯口最后的那句话,让我整整失眠一夜。震惊!昨天的一场面试,让这个面试官陷入深深的自我反思。作为一个互联网公司的技术总监,8年来面试过上千名候选人,但这次经历让面试官久久不能平静,甚至彻夜难眠...在BAT大厂干了12年技术管理,面试过形形色色的程序员。有意气...
继续阅读 »

电梯口最后的那句话,让我整整失眠一夜。


震惊!昨天的一场面试,让这个面试官陷入深深的自我反思。作为一个互联网公司的技术总监,8年来面试过上千名候选人,但这次经历让面试官久久不能平静,甚至彻夜难眠...


在BAT大厂干了12年技术管理,面试过形形色色的程序员。有意气风发的应届生,有经验丰富的大牛,有从外企跳槽的专家,但从没有一次面试像这次一样,让我感受如此复杂,如此揪心。


昨天下午3点,一个穿着深色格子衬衫的中年人准时出现在会议室门口。他个子不高,微微有些谢顶,背着一个看起来有些年头的双肩包。包的肩带有些磨损,但被主人仔细地缝补过。


"您好,我是来面试Java开发岗位的。"他的声音透着一丝紧张,手里紧握着一份打印得整整齐齐的简历。


看了看简历,我心里一惊 —— 45岁,20年开发经验,科班出身,Java后端出身,精通分布式架构,曾经在几个知名公司待过,技术栈相当扎实。"这么资深为什么还在找工作?这个年龄段找工作会遇到什么困难?"我心里冒出一连串疑问。


面试正式开始。我们聊了他最近在研究的技术领域,他说起SpringCloud微服务架构时,眼睛里闪烁着光芒:"最近我在研究服务网格,看到社区有个问题,还提了个PR..."


当我抛出一个个技术难题,他的回答让我眼前一亮。Spring源码分析、JVM调优、分布式架构设计、性能优化经验...每个答案都精准到位,不仅能说出理论基础,更难得的是能结合实际场景,说出各种坑的实践经验。这绝对是一位技术大牛!


面试中他提到一个有趣的细节:"有次生产环境遇到一个诡异的Bug,年轻同事们试了好多方案都没解决。我凭着多年经验,想起12年前遇到过类似问题,最后找到根源是JVM的一个默认参数设置有问题..."


就在我准备问收入预期时,他的手机响了。他有些慌乱地掏出手机,看了一眼赶紧按掉:"对不起,是我女儿的电话,她今天考试..."我注意到他手机屏幕上的裂痕和掉漆的边框。


"您期望的薪资是多少?"问到这个问题时,他略显犹豫,目光有些闪躲。


"如果可以的话...希望月薪2万。"他的声音有点发抖,"我知道可能比市场价低一些..."


2万?我愣住了。以他的技术水平和经验,在深圳互联网公司要个35k-40k一点问题都没有。难道是我听错了?记得上个月刚招的一个3年经验的小伙子,谈价时底气十足地要价25k,最后我们也给了。


"您确定只要2万吗?以您的能力和经验..."


"是的,我觉得够用了。"他打断了我的话,语速突然变快,"主要是想找一个稳定的工作环境。现在这个行业...您也知道...我的年龄...其实能有工作机会我就很感激了。"


看着他局促不安的样子,我决定不再讨价还价:"好的,2万没问题,我们这边非常认可您的能力。"


他的眼睛一下子亮了起来,露出欣慰的笑容,甚至有些激动:"真的非常感谢!我一定会好好干,加班什么的完全没问题,我身体很好的,每天都在坚持跑步..."


面试结束后,按惯例我送他到电梯口。看得出来他心情不错,脚步都轻快了许多。


就在电梯门快关上的瞬间,他突然回头说了一句话:"其实...如果是14薪的话,月薪1.8万也可以...我就是希望能有个稳定的岗位..."


那一刻,我的心狠狠地揪了一下。电梯门缓缓关上,我却久久站在原地。


回到办公室,我的脑海里一直回荡着他说的那句话。打开电脑,我忍不住搜索了一下"中年程序员"这个话题。一篇篇文章像刀子一样戳着我的心:"35岁程序员职业危机"、"互联网公司青睐年轻人"、"中年程序员如何自救"...


原来在这个年轻人主导的互联网行业,45岁居然已经被贴上"高龄"的标签。这个数字像一道无形的门槛,挡住了多少技术大牛的职业发展之路?


想起面试时的细节:问到加班时,他特别强调自己的抗压能力,说周末随叫随到,身体素质一点问题都没有。一个45岁的技术大牛,却要用"随叫随到"来证明自己的价值...这句话背后,是多少无奈和妥协?


这个行业太狂热地追逐年轻,以至于忽视了经验的珍贵。 那些深夜里背后的debug经历、重重坑里趟出来的实践经验、危机时刻的临危不乱,都是年轻人用996都换不来的宝贵财富。


我开始回想他说过的话:"现在的新技术学起来也很快,我每天都在看技术博客,Github上的开源项目也一直在关注..."这句话背后,是多少个加班后的深夜自学时光?是多少个被质疑"老了学不动"后的倔强证明?


突然想起去年一个深夜,生产环境出现重大事故,年轻员工们手忙脚乱,是一个45岁的老程序员站了出来,凭着多年经验在半小时内定位到问题。那一刻,我就明白了什么叫"经验的价值"。


我立刻给HR打了电话:"这位候选人非常合适,请按2万月薪给offer,年终奖至少3个月。对了,入职后把他安排到王工组,让他带带新人。"


与其花20万培养一个年轻人踩坑,不如用同样的钱留住一个踩过无数坑的技术专家。 这不仅是对团队负责,更是对整个行业的价值观负责。


今天早上,我收到了他愉快的入职确认。想起昨天电梯口那一幕,我暗暗发誓:一定要让团队里的每个人,不分年龄,都能被公平对待,都能实现自己的价值。


也许有一天,当我也到了45岁,也会面临同样的困境。但我希望到那时,整个行业都能多一些包容,少一些偏见。技术,永远不该被年龄定义。


朋友们,你们觉得技术行业真的存在"年龄歧视"吗?在你心目中,技术人的黄金年龄是多少?欢迎在评论区分享你的想法和经历。


PS:今天我特意找物业,把他的停车位安排在了地下车库最方便的位置。这或许是我能做的,最微小但也最真诚的关照了。


PPS:刚才和CTO聊了这事,我们决定在公司推动一个"经验导师"计划,专门邀请一些资深程序员来做技术分享。毕竟,程序员的价值,不应该用年龄来衡量。


为什么他愿意主动降薪?


他的降薪行为背后,藏着许多深层次的职场逻辑。


1. 现实的自知之明


45岁,对于程序员来说已经算是“高龄”。在互联网行业,35岁被称为“职场分水岭”,他显然很清楚自己的处境。


他主动降低薪资,是在用“性价比”消除我的顾虑,同时也为自己争取更多的机会。


2. 对稳定的渴求


从简历上看,她的职业生涯很稳定,前一份工作待了十几年。这种“稳定感”不仅是他的一种职场标签,也是他的核心诉求。对于45岁的他来说,找一份能长期发展的工作,远比短期的高薪重要。


3. 一种不显山露水的智慧


他看似是退让,实际上是在“示弱”中展现了自己的诚意。他让招聘方觉得,他不是一个只谈薪资、不顾团队的人,而是愿意为公司利益妥协的合作者。这样的沟通方式,比硬碰硬的谈判更容易让人接受。


高龄程序员的职场困境与价值


他的故事,让我重新思考了“高龄程序员”在职场中的价值和困境。


1. 困境:行业的无情现实


  • 技术更新速度快,高龄程序员容易被贴上“跟不上时代”的标签。

  • 用人成本高,很多企业更愿意选择年轻且“便宜”的新人。

  • 体力和精力相比年轻人有劣势,难以适应高强度加班文化。


2. 价值:丰富经验不可替代


  • 高龄程序员的优势在于对复杂系统的理解能力,这种能力并不是通过短时间学习能掌握的。

  • 他们往往有很强的团队协作能力,能起到“传帮带”的作用。

  • 稳定性和责任感是他们的职业标签,尤其适合对稳定性要求高的岗位。


这次面试经历让我明白了两个道理:


  1. 职场不是简单的年龄比较,而是价值的体现。
    他虽然45岁,但他的经验和态度是团队里稀缺的宝藏。年轻人的速度和创造力,和老员工的稳定与深度,是互补关系,而不是对立关系。


  2. 谈判不是赢输,而是双赢。
    他的降薪行为,并不是单纯的退让,而是一种职场智慧。他通过主动示弱,换来了更多的机会,也打消了用人方的顾虑。


朋友们,你们对高龄程序员怎么看?你是否愿意为团队引入一个经验丰富但年龄偏大的同事?欢迎在评论区分享你的看法!


作者:小乐

来源:编程技术圈

收起阅读 »