threejs做特效:实现物体的发光效果-EffectComposer详解!
简介与效果概览
各位大佬给个赞,感谢呢!
threejs的开发中,实现物体发光效果是一个常见需求,比如实现楼体的等待照明
要想实现这样的效果,我们只需要了解一个效果合成器概念:EffectComposer。
效果合成器能够合成各种花里胡哨的效果,好比是一个做特效的AE,本教程,我们将使用它来实现一个简单的发光效果。
如图,这是我们将导入的一个模型
.
我们要给他赋予灵魂,实现下面的发光效果
顺带的,我们要实现物体的自动旋转、一个简单的性能监视器、一个发光参数调节的面板
技术方案
原生html框架搭建
借助threejs实现一个物体发光效果非常简单,首先我们使用html搭建一个简单的开发框架
参考官方起步文档:three.js中文网
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js物体发光效果</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<link type="text/css" rel="stylesheet" href="./main.css" />
<style>
#info>* {
max-width: 650px;
margin-left: auto;
margin-right: auto;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
</script>
</body>
</html>
上述代码中,我们采用type="importmap"
的方式引入了threejs开发 的一些核心依赖,"three"是开发的最基本依赖;在Three.js中,"addons" 通常指的是一些附加功能或扩展模块,它们提供了额外的功能,可以用于增强或扩展Three.js的基本功能。
在type="module"
中,我们引入了threejs的一些基础依赖,OrbitControls
轨道控制器和GLTFLoader
模型加载器。
实现模型的加载
我们将下载好的模型放在文件根目录
http://www.yanhuangxueyuan.com/threejs/examples/models/gltf/PrimaryIonDrive.glb
基于threejs的基础知识,我们先实现模型的加载与渲染
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
init()
function init() {
const container = document.getElementById("container");
// WebGL渲染器
// antialias是否执行抗锯齿。默认为false.
renderer = new THREE.WebGLRenderer({ antialias: true });
// 设置设备像素比。通常用于避免HiDPI设备上绘图模糊
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置色调映射 这个属性用于在普通计算机显示器或者移动设备屏幕等低动态范围介质上,模拟、逼近高动态范围(HDR)效果。
renderer.toneMapping = THREE.ReinhardToneMapping;
container.appendChild(renderer.domElement);
// 创建新的场景对象。
const scene = new THREE.Scene();
// 创建透视相机
camera = new THREE.PerspectiveCamera(
40,
window.innerWidth / window.innerHeight,
1,
100
);
camera.position.set(-5, 2.5, -3.5);
scene.add(camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.maxPolarAngle = Math.PI * 0.5;
controls.minDistance = 3;
controls.maxDistance = 8;
// 添加了一个环境光
scene.add(new THREE.AmbientLight(0xcccccc));
// 创建了一个点光源
const pointLight = new THREE.PointLight(0xffffff, 100);
camera.add(pointLight);
// 模型加载
new GLTFLoader().load("./PrimaryIonDrive.glb", function (gltf) {
const model = gltf.scene;
scene.add(model);
const clip = gltf.animations[0];
renderer.render(scene, camera);
});
}
</script>
现在,我们的页面中就有了下面的场景
接下来,我们实现模型的发光效果添加。
模型发光效果添加
实现模型的发光效果,实际是EffectComposer效果合成器实现的。
官方定义:用于在three.js中实现后期处理效果。该类管理了产生最终视觉效果的后期处理过程链。 后期处理过程根据它们添加/插入的顺序来执行,最后一个过程会被自动渲染到屏幕上。
简单来说,EffectComposer效果合成器只是一个工具,它可以将多种效果集成,进行渲染。我们来看一个伪代码:
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
// 创建效果合成器
composer = new EffectComposer(renderer);
composer.addPass(发光效果);
composer.addPass(光晕效果);
composer.addPass(玻璃磨砂效果
// 渲染
composer.render();
它的实现过程大致如上述代码。要实现发光效果,我们需要先熟悉三个Pass。
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
- RenderPass: 渲染通道是用于传递渲染结果的对象。RenderPass是EffectComposer中的一个通道,用于将场景渲染到纹理上。(固定代码,相当于混合效果的开始)
- UnrealBloomPass: 这是一个用于实现逼真的辉光效果的通道。它模拟了逼真的辉光,使得场景中的亮部分在渲染后产生耀眼的辉光效果。(不同效果有不同的pass)
- OutputPass: OutputPass是EffectComposer中的一个通道,用于将最终渲染结果输出到屏幕上。(固定代码,相当于混合效果的结束)
现在,我们完整的实现发光效果
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
let camera;
let composer, renderer;
const params = {
threshold: 0,
strength: 1,
radius: 0,
exposure: 1,
};
init();
function init() {
const container = document.getElementById("container");
// WebGL渲染器
// antialias是否执行抗锯齿。默认为false.
renderer = new THREE.WebGLRenderer({ antialias: true });
// 设置设备像素比。通常用于避免HiDPI设备上绘图模糊
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置色调映射 这个属性用于在普通计算机显示器或者移动设备屏幕等低动态范围介质上,模拟、逼近高动态范围(HDR)效果。
renderer.toneMapping = THREE.ReinhardToneMapping;
container.appendChild(renderer.domElement);
// 创建新的场景对象。
const scene = new THREE.Scene();
// 创建透视相机
camera = new THREE.PerspectiveCamera(
40,
window.innerWidth / window.innerHeight,
1,
100
);
camera.position.set(-5, 2.5, -3.5);
scene.add(camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.maxPolarAngle = Math.PI * 0.5;
controls.minDistance = 3;
controls.maxDistance = 8;
// 添加了一个环境光
scene.add(new THREE.AmbientLight(0xcccccc));
// 创建了一个点光源
const pointLight = new THREE.PointLight(0xffffff, 100);
camera.add(pointLight);
// 创建了一个RenderPass对象,用于将场景渲染到纹理上。
const renderScene = new RenderPass(scene, camera);
// 创建了一个UnrealBloomPass对象,用于实现辉光效果。≈
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5,
0.4,
0.85
);
// 设置发光参数,阈值、强度和半径。
bloomPass.threshold = params.threshold;
bloomPass.strength = params.strength;
bloomPass.radius = params.radius;
// 创建了一个OutputPass对象,用于将最终渲染结果输出到屏幕上。
const outputPass = new OutputPass();
// 创建了一个EffectComposer对象,并将RenderPass、UnrealBloomPass和OutputPass添加到渲染通道中。
composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
composer.addPass(outputPass);
// 模型加载
new GLTFLoader().load("./PrimaryIonDrive.glb", function (gltf) {
const model = gltf.scene;
scene.add(model);
const clip = gltf.animations[0];
animate();
});
}
function animate() {
requestAnimationFrame(animate);
// 通过调用 render 方法,将场景渲染到屏幕上。
composer.render();
}
</script>
现在,我们就实现发光的基本效果了!
实现物体的自动旋转动画
现在,我们实现一下物体自身的旋转动画
AnimationMixer是three中的动画合成器,使用AnimationMixer可以解析到模型中的动画数据
// 模型加载
new GLTFLoader().load("./PrimaryIonDrive.glb", function (gltf) {
const model = gltf.scene;
scene.add(model);
//创建了THREE.AnimationMixer 对象,用于管理模型的动画。
mixer = new THREE.AnimationMixer(model);
//从加载的glTF模型文件中获取动画数据。
//这里假设模型文件包含动画数据,通过 gltf.animations[0] 获取第一个动画片段。
const clip = gltf.animations[0];
// 使用 mixer.clipAction(clip) 创建了一个动画操作(AnimationAction),并立即播放该动画
mixer.clipAction(clip.optimize()).play();
animate();
});
实现动画更新
let clock;
clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
//使用了 clock 对象的 getDelta() 方法来获取上一次调用后经过的时间,即时间间隔(delta)。
const delta = clock.getDelta();
//根据上一次更新以来经过的时间间隔来更新动画。
//这个方法会自动调整动画的播放速度,使得动画看起来更加平滑,不受帧率的影响
mixer.update(delta);
// 通过调用 render 方法,将场景渲染到屏幕上。
composer.render();
}
完整代码
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
let camera, stats;
let composer, renderer, mixer, clock;
const params = {
threshold: 0,
strength: 1,
radius: 0,
exposure: 1,
};
init();
function init() {
const container = document.getElementById("container");
clock = new THREE.Clock();
// WebGL渲染器
// antialias是否执行抗锯齿。默认为false.
renderer = new THREE.WebGLRenderer({ antialias: true });
// .....
// 模型加载
new GLTFLoader().load("./PrimaryIonDrive.glb", function (gltf) {
const model = gltf.scene;
scene.add(model);
mixer = new THREE.AnimationMixer(model);
const clip = gltf.animations[0];
mixer.clipAction(clip.optimize()).play();
animate();
});
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
mixer.update(delta);
// 通过调用 render 方法,将场景渲染到屏幕上。
composer.render();
}
</script>
优化屏幕缩放逻辑
init{
// ....
window.addEventListener("resize", onWindowResize);
}
function onWindowResize() {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
composer.setSize(width, height);
}
添加参数调节面板
在Three.js中,GUI是一个用于创建用户界面(UI)控件的库。具体来说,GUI库允许你在Three.js应用程序中创建交互式的图形用户界面元素,例如滑块、复选框、按钮等,这些元素可以用于控制场景中的对象、相机、光源等参数。
我们借助这个工具实现如下发光效果调试面板
import { GUI } from "three/addons/libs/lil-gui.module.min.js";
init{
// ....
// 创建一个GUI实例
const gui = new GUI();
// 创建一个名为"bloom"的文件夹,用于容纳调整泛光效果的参数
const bloomFolder = gui.addFolder("bloom");
// 在"bloom"文件夹中添加一个滑块控件,用于调整泛光效果的阈值参数
bloomFolder
.add(params, "threshold", 0.0, 1.0)
.onChange(function (value) {
bloomPass.threshold = Number(value);
});
// 在"bloom"文件夹中添加另一个滑块控件,用于调整泛光效果的强度参数
bloomFolder
.add(params, "strength", 0.0, 3.0)
.onChange(function (value) {
bloomPass.strength = Number(value);
});
// 在根容器中添加一个滑块控件,用于调整泛光效果的半径参数
gui
.add(params, "radius", 0.0, 1.0)
.step(0.01)
.onChange(function (value) {
bloomPass.radius = Number(value);
});
// 创建一个名为"tone mapping"的文件夹,用于容纳调整色调映射效果的参数
const toneMappingFolder = gui.addFolder("tone mapping");
// 在"tone mapping"文件夹中添加一个滑块控件,用于调整曝光度参数
toneMappingFolder
.add(params, "exposure", 0.1, 2)
.onChange(function (value) {
renderer.toneMappingExposure = Math.pow(value, 4.0);
});
window.addEventListener("resize", onWindowResize);
}
添加性能监视器
import Stats from "three/addons/libs/stats.module.js";
init{
stats = new Stats();
container.appendChild(stats.dom);
// ...
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
mixer.update(delta);
stats.update();
// 通过调用 render 方法,将场景渲染到屏幕上。
composer.render();
}
在Three.js中,Stats是一个性能监视器,用于跟踪帧速率(FPS)、内存使用量和渲染时间等信息。
完整demo代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js物体发光效果</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<link type="text/css" rel="stylesheet" href="./main.css" />
<style>
#info>* {
max-width: 650px;
margin-left: auto;
margin-right: auto;
}
</style>
</head>
<body>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import Stats from "three/addons/libs/stats.module.js";
import { GUI } from "three/addons/libs/lil-gui.module.min.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
let camera, stats;
let composer, renderer, mixer, clock;
const params = {
threshold: 0,
strength: 1,
radius: 0,
exposure: 1,
};
init();
function init() {
const container = document.getElementById("container");
stats = new Stats();
container.appendChild(stats.dom);
clock = new THREE.Clock();
// WebGL渲染器
// antialias是否执行抗锯齿。默认为false.
renderer = new THREE.WebGLRenderer({ antialias: true });
// 设置设备像素比。通常用于避免HiDPI设备上绘图模糊
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置色调映射 这个属性用于在普通计算机显示器或者移动设备屏幕等低动态范围介质上,模拟、逼近高动态范围(HDR)效果。
renderer.toneMapping = THREE.ReinhardToneMapping;
container.appendChild(renderer.domElement);
// 创建新的场景对象。
const scene = new THREE.Scene();
// 创建透视相机
camera = new THREE.PerspectiveCamera(
40,
window.innerWidth / window.innerHeight,
1,
100
);
camera.position.set(-5, 2.5, -3.5);
scene.add(camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.maxPolarAngle = Math.PI * 0.5;
controls.minDistance = 3;
controls.maxDistance = 8;
// 添加了一个环境光
scene.add(new THREE.AmbientLight(0xcccccc));
// 创建了一个点光源
const pointLight = new THREE.PointLight(0xffffff, 100);
camera.add(pointLight);
// 创建了一个RenderPass对象,用于将场景渲染到纹理上。
const renderScene = new RenderPass(scene, camera);
// 创建了一个UnrealBloomPass对象,用于实现辉光效果。≈
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5,
0.4,
0.85
);
// 设置发光参数,阈值、强度和半径。
bloomPass.threshold = params.threshold;
bloomPass.strength = params.strength;
bloomPass.radius = params.radius;
// 创建了一个OutputPass对象,用于将最终渲染结果输出到屏幕上。
const outputPass = new OutputPass();
// 创建了一个EffectComposer对象,并将RenderPass、UnrealBloomPass和OutputPass添加到渲染通道中。
composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
composer.addPass(outputPass);
// 模型加载
new GLTFLoader().load("./PrimaryIonDrive.glb", function (gltf) {
const model = gltf.scene;
scene.add(model);
mixer = new THREE.AnimationMixer(model);
const clip = gltf.animations[0];
mixer.clipAction(clip.optimize()).play();
animate();
});
const gui = new GUI();
const bloomFolder = gui.addFolder("bloom");
bloomFolder
.add(params, "threshold", 0.0, 1.0)
.onChange(function (value) {
bloomPass.threshold = Number(value);
});
bloomFolder
.add(params, "strength", 0.0, 3.0)
.onChange(function (value) {
bloomPass.strength = Number(value);
});
gui
.add(params, "radius", 0.0, 1.0)
.step(0.01)
.onChange(function (value) {
bloomPass.radius = Number(value);
});
const toneMappingFolder = gui.addFolder("tone mapping");
toneMappingFolder
.add(params, "exposure", 0.1, 2)
.onChange(function (value) {
renderer.toneMappingExposure = Math.pow(value, 4.0);
});
window.addEventListener("resize", onWindowResize);
}
function onWindowResize() {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
composer.setSize(width, height);
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
mixer.update(delta);
stats.update();
// 通过调用 render 方法,将场景渲染到屏幕上。
composer.render();
}
</script>
</body>
</html>
main.css
body {
margin: 0;
background-color: #000;
color: #fff;
font-family: Monospace;
font-size: 13px;
line-height: 24px;
overscroll-behavior: none;
}
a {
color: #ff0;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
cursor: pointer;
text-transform: uppercase;
}
#info {
position: absolute;
top: 0px;
width: 100%;
padding: 10px;
box-sizing: border-box;
text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
z-index: 1; /* TODO Solve this in HTML */
}
a, button, input, select {
pointer-events: auto;
}
.lil-gui {
z-index: 2 !important; /* TODO Solve this in HTML */
}
@media all and ( max-width: 640px ) {
.lil-gui.root {
right: auto;
top: auto;
max-height: 50%;
max-width: 80%;
bottom: 0;
left: 0;
}
}
#overlay {
position: absolute;
font-size: 16px;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: rgba(0,0,0,0.7);
}
#overlay button {
background: transparent;
border: 0;
border: 1px solid rgb(255, 255, 255);
border-radius: 4px;
color: #ffffff;
padding: 12px 18px;
text-transform: uppercase;
cursor: pointer;
}
#notSupported {
width: 50%;
margin: auto;
background-color: #f00;
margin-top: 20px;
padding: 10px;
}
总结
通过本教程,我想现在你对效果合成器一定有了更深入的了解,现在,我们在看看官网的定义:
用于在three.js中实现后期处理效果。该类管理了产生最终视觉效果的后期处理过程链。 后期处理过程根据它们添加/插入的顺序来执行,最后一个过程会被自动渲染到屏幕上
结合代码,我想现在理解其它非常容易
<script type="module">
import * as THREE from "three";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
function init() {
// 1【渲染开始】创建了一个RenderPass对象,用于将场景渲染到纹理上。
const renderScene = new RenderPass(scene, camera);
// 2【需要合成的中间特效】创建了一个UnrealBloomPass对象,用于实现辉光效果。≈
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5,
0.4,
0.85
);
// 【特效设置】设置发光参数,阈值、强度和半径。
bloomPass.threshold = params.threshold;
bloomPass.strength = params.strength;
bloomPass.radius = params.radius;
// 3【效果输出】创建了一个OutputPass对象,用于将最终渲染结果输出到屏幕上。
const outputPass = new OutputPass();
// 4【特效合并】创建了一个EffectComposer对象,并将RenderPass、UnrealBloomPass和OutputPass添加到渲染通道中。
composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
composer.addPass(outputPass);
}
function animate() {
requestAnimationFrame(animate);
// 5【渲染特效】通过调用 render 方法,将场景渲染到屏幕上。
composer.render();
}
</script>
来源:juejin.cn/post/7355055084822446095
go的生态真的一言难尽
前言
标题党了,原生go很好用,只不过我习惯了java封装大法。最近在看golang,因为是javaer,所以突发奇想,不如开发一个类似于 Maven 或 Gradle 的构建工具来管理 Go 项目的依赖,众所周知,构建和发布是一个复杂的任务,但通过合理的设计和利用现有的工具与库,可以实现一个功能强大且灵活的工具。
正文分为两部分:项目本身和如何使用
一、项目本身
1. 项目需求分析
核心需求
- 依赖管理:
- 解析和下载 Go 项目的依赖。
- 支持依赖版本控制和冲突解决。
- 构建管理:
- 支持编译 Go 项目。
- 支持跨平台编译。
- 支持自定义构建选项。
- 发布管理:
- 打包构建结果。
- 支持发布到不同的平台(如 Docker Hub、GitHub Releases)。
- 任务管理:
- 支持定义和执行自定义任务(如运行测试、生成文档)。
- 插件系统:
- 支持扩展工具的功能。
可选需求
- 缓存机制:缓存依赖和构建结果以提升速度。
- 并行执行:支持并行下载和编译。
2. 技术选型
2.1 编程语言
- Go 语言:由于我们要构建的是 Go 项目的构建工具,选择 Go 语言本身作为开发语言是合理的。
2.2 依赖管理
- Go Modules:Go 自带的依赖管理工具已经很好地解决了依赖管理的问题,可以直接利用 Go Modules 来解析和管理依赖。
2.3 构建工具
- Go 标准库:Go 的标准库提供了强大的编译和构建功能(如
go build
,go install
等命令),可以通过调用这些命令或直接使用相关库来进行构建。
2.4 发布工具
- Docker:对于发布管理,可能需要集成 Docker 来构建和发布 Docker 镜像。
- upx:用于压缩可执行文件。
2.5 配置文件格式
- YAML 或 TOML:选择一种易于阅读和编写的配置文件格式,如 YAML 或 TOML。
3. 系统架构设计
3.1 模块划分
- 依赖管理模块:
- 负责解析和下载项目的依赖。
- 构建管理模块:
- 负责编译 Go 项目,支持跨平台编译和自定义构建选项。
- 发布管理模块:
- 负责将构建结果打包和发布到不同平台。
- 任务管理模块:
- 负责定义和执行自定义任务。
- 插件系统:
- 提供扩展点,允许用户编写插件来扩展工具的功能。
3.2 系统流程
- 初始化项目:读取配置文件,初始化项目环境。
- 依赖管理:解析依赖并下载。
- 构建项目:根据配置文件进行项目构建。
- 执行任务:执行用户定义的任务(如测试)。
- 发布项目:打包构建结果并发布到指定平台。
4. 模块详细设计与实现
4.1 依赖管理模块
4.1.1 设计
利用 Go Modules 现有的功能来管理依赖。可以通过 go list
命令来获取项目的依赖:
4.1.2 实现
package dependency
import (
"fmt"
"os/exec"
)
// ListDependencies 列出项目所有依赖
func ListDependencies() ([]byte, error) {
cmd := exec.Command("go", "list", "-m", "all")
return cmd.Output()
}
// DownloadDependencies 下载项目所有依赖
func DownloadDependencies() error {
cmd := exec.Command("go", "mod", "download")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("download failed: %s", output)
}
return nil
}
4.2 构建管理模块
4.2.1 设计
调用 Go 编译器进行构建,支持跨平台编译和自定义构建选项。
4.2.2 实现
package build
import (
"fmt"
"os/exec"
"runtime"
"path/filepath"
)
// BuildProject 构建项目
func BuildProject(outputDir string) error {
// 设置跨平台编译参数
var goos, goarch string
switch runtime.GOOS {
case "windows":
goos = "windows"
case "linux":
goos = "linux"
default:
goos = runtime.GOOS
}
goarch = "amd64"
output := filepath.Join(outputDir, "myapp")
cmd := exec.Command("go", "build", "-o", output, "-ldflags", "-X main.version=1.0.0")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("build failed: %s", output)
}
fmt.Println("Build successful")
return nil
}
4.3 发布管理模块
4.3.1 设计
打包构建结果并发布到不同平台。例如,构建 Docker 镜像并发布到 Docker Hub。
4.3.2 实现
package release
import (
"fmt"
"os/exec"
)
// BuildDockerImage 构建 Docker 镜像
func BuildDockerImage(tag string) error {
cmd := exec.Command("docker", "build", "-t", tag, ".")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("docker build failed: %s", output)
}
fmt.Println("Docker image built successfully")
return nil
}
// PushDockerImage 推送 Docker 镜像
func PushDockerImage(tag string) error {
cmd := exec.Command("docker", "push", tag)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("docker push failed: %s", output)
}
fmt.Println("Docker image pushed successfully")
return nil
}
5. 任务管理模块
允许用户定义和执行自定义任务:
package task
import (
"fmt"
"os/exec"
)
type Task func() error
func RunTask(name string, task Task) {
fmt.Println("Running task:", name)
err := task()
if err != nil {
fmt.Println("Task failed:", err)
return
}
fmt.Println("Task completed:", name)
}
func TestTask() error {
cmd := exec.Command("go", "test", "./...")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("tests failed: %s", output)
}
fmt.Println("Tests passed")
return nil
}
6. 插件系统
可以通过动态加载外部插件或使用 Go 插件机制来实现插件系统:
package plugin
import (
"fmt"
"plugin"
)
type Plugin interface {
Run() error
}
func LoadPlugin(path string) (Plugin, error) {
p, err := plugin.Open(path)
if err != nil {
return nil, err
}
symbol, err := p.Lookup("PluginImpl")
if err != nil {
return nil, err
}
impl, ok := symbol.(Plugin)
if !ok {
return nil, fmt.Errorf("unexpected type from module symbol")
}
return impl, nil
}
5. 示例配置文件
使用 YAML 作为配置文件格式,定义项目的构建和发布选项:
name: myapp
version: 1.0.0
build:
options:
- -ldflags
- "-X main.version=1.0.0"
release:
docker:
image: myapp:latest
tag: v1.0.0
tasks:
- name: test
command: go test ./...
6. 持续改进
后续我将持续改进工具的功能和性能,例如:
- 增加更多的构建和发布选项。
- 优化依赖管理和冲突解决算法。
- 提供更丰富的插件。
二、如何使用
1. 安装构建工具
我已经将构建工具发布到 GitHub 并提供了可执行文件,用户可以通过以下方式安装该工具。
1.1 使用安装脚本安装
我将提供一个简单的安装脚本,开发者可以通过 curl
或 wget
安装构建工具。
使用 curl
安装
curl -L https://github.com/yunmaoQu/GoForge/releases/download/v1.0.0/install.sh | bash
使用 wget
安装
wget -qO- https://github.com//yunmaoQu/GoForge/releases/download/v1.0.0/install.sh | bash
1.2 手动下载并安装
如果你不想使用自动安装脚本,可以直接从 GitHub Releases 页面手动下载适合你操作系统的二进制文件。
- 访问 GitHub Releases 页面。
- 下载适合你操作系统的二进制文件:
- Linux:
GoForge-linux-amd64
- macOS:
GoForge-darwin-amd64
- Windows:
GoForge-windows-amd64.exe
- Linux:
- 将下载的二进制文件移动到系统的 PATH 路径(如
/usr/local/bin/
),并确保文件有执行权限。
# 以 Linux 系统为例
mv GoForge-linux-amd64 /usr/local/bin/GoForge
chmod +x /usr/local/bin/GoForge
2. 创建 Go 项目并配置构建工具
2.1 初始化 Go 项目
假设你已经有一个 Go 项目或你想创建一个新的 Go 项目。首先,初始化 Go 模块:
mkdir my-go-project
cd my-go-project
go mod init github.com/myuser/my-go-project
2.2 创建 build.yaml
文件
在项目根目录下创建 build.yaml
文件,这个文件类似于 Maven 的 pom.xml
或 Gradle 的 build.gradle
,用于配置项目的依赖、构建任务和发布任务。
示例 build.yaml
project:
name: my-go-project
version: 1.0.0
dependencies:
- name: github.com/gin-gonic/gin
version: v1.7.7
- name: github.com/stretchr/testify
version: v1.7.0
build:
output: bin/my-go-app
commands:
- go build -o bin/my-go-app main.go
tasks:
clean:
command: rm -rf bin/
test:
command: go test ./...
build:
dependsOn:
- test
command: go build -o bin/my-go-app main.go
publish:
type: github
repo: myuser/my-go-project
token: $GITHUB_TOKEN
assets:
- bin/my-go-app
配置说明:
- project: 定义项目名称和版本。
- dependencies: 列出项目的依赖包及其版本号。
- build: 定义构建输出路径和构建命令。
- tasks: 用户可以定义自定义任务(如
clean
、test
、build
等),并可以配置任务依赖关系。 - publish: 定义发布到 GitHub 的配置,包括发布的仓库和需要发布的二进制文件。
3. 执行构建任务
构建工具允许你通过命令行执行各种任务,如构建、测试、清理、发布等。以下是一些常用的命令。
3.1 构建项目
执行以下命令来构建项目。该命令会根据 build.yaml
文件中定义的 build
任务进行构建,并生成二进制文件到指定的 output
目录。
GoForge build
构建过程会自动执行依赖任务(例如 test
任务),确保在构建之前所有测试通过。
3.2 运行测试
如果你想单独运行测试,可以使用以下命令:
GoForge test
这将执行 go test ./...
,并运行所有测试文件。
3.3 清理构建产物
如果你想删除构建生成的二进制文件等产物,可以运行 clean
任务:
GoForge clean
这会执行 rm -rf bin/
,清理 bin/
目录下的所有文件。
3.4 列出所有可用任务
如果你想查看所有可用的任务,可以运行:
GoForge tasks
这会列出 build.yaml
文件中定义的所有任务,并显示它们的依赖关系。
4. 依赖管理
构建工具会根据 build.yaml
中的 dependencies
部分来处理 Go 项目的依赖。
4.1 安装依赖
当执行构建任务时,工具会自动解析依赖并安装指定的第三方库(类似于 go mod tidy
)。
你也可以单独运行以下命令来手动处理依赖:
GoForge deps
4.2 更新依赖
如果你需要更新依赖版本,可以在 build.yaml
中手动更改依赖的版本号,然后运行 mybuild deps
来更新依赖。
5. 发布项目
构建工具提供了发布项目到 GitHub 等平台的功能。根据 build.yaml
中的 publish
配置,你可以将项目的构建产物发布到 GitHub Releases。
5.1 配置发布相关信息
确保你在 build.yaml
中正确配置了发布信息:
publish:
type: github
repo: myuser/my-go-project
token: $GITHUB_TOKEN
assets:
- bin/my-go-app
- type: 发布的目标平台(GitHub 等)。
- repo: GitHub 仓库路径。
- token: 需要设置环境变量
GITHUB_TOKEN
,用于认证 GitHub API。 - assets: 指定发布时需要上传的二进制文件。
5.2 发布项目
确保你已经完成构建,并且生成了二进制文件。然后,你可以执行以下命令来发布项目:
GoForge publish
这会将 bin/my-go-app
上传到 GitHub Releases,并生成一个新的发布版本。
5.3 测试发布(Dry Run)
如果你想在发布之前测试发布流程(不上传文件),可以使用 --dry-run
选项:
GoForge publish --dry-run
这会模拟发布过程,但不会实际上传文件。
6. 高级功能
6.1 增量构建
构建工具支持增量构建,如果你在 build.yaml
中启用了增量构建功能,工具会根据文件的修改时间戳或内容哈希来判断是否需要重新构建未被修改的部分。
build:
output: bin/my-go-app
incremental: true
commands:
- go build -o bin/my-go-app main.go
6.2 插件机制
你可以通过插件机制来扩展构建工具的功能。例如,你可以为工具增加自定义的任务逻辑,或在构建生命周期的不同阶段插入钩子。
在 build.yaml
中定义插件:
plugins:
- name: custom-task
path: plugins/custom-task.go
编写 custom-task.go
,并实现你需要的功能。
7. 调试和日志
如果你在使用时遇到了问题,可以通过以下方式启用调试模式,查看详细的日志输出:
GoForge --debug build
这会输出工具在执行任务时的每一步详细日志,帮助你定位问题。
总结
通过这个构建工具,你可以轻松管理 Go 项目的依赖、构建过程和发布任务。以下是使用步骤的简要总结:
- 安装构建工具:使用安装脚本或手动下载二进制文件。
- 配置项目:创建
build.yaml
文件,定义依赖、构建任务和发布任务。 - 执行任务:通过命令行执行构建、测试、清理等任务。
- 发布项目:将项目的构建产物发布到 GitHub 或其他平台。
来源:juejin.cn/post/7431545806085423158
白嫖微信OCR,实现批量提取图片中的文字
微信自带的OCR使用比较方便,且准确率较高,但是唯一不足的是需要手动截图之后再识别,无法批量操作,这里我们借助wechat-ocr这一开源工具,实现批量读取文件夹下的所有图片并提取文本的功能。下面介绍下操作步骤。
1. 安装wechat-ocr这个库
这里我们使用的是GoBot这一自动化工具(如对该软件感兴趣,可以关注公众号:RPA二师兄),他提供的可视化安装依赖的功能。打开依赖包管理的Tab页,在Python包名称处填写wechat-ocr,然后点击安装,就能完成wechat-ocr的安装,安装完成之后可以切换到管理已安装模块的Tab,可以看到已经成功安装。
2. 编写调用代码
这里我们直接给出代码,只需要创建一个代码流程,将我们给的代码复制进去就可以了。
from package import variables as glv #全局变量,例如glv['test']
from robot_base import log_util
import robot_basic
from robot_base import log_util
import os
import re
from wechat_ocr.ocr_manager import OcrManager, OCR_MAX_TASK_ID
def main(args):
#输入参数使用示例
# if args is :
# 输入参数1 = ""
#else:
# 输入参数1 = args.get("输入参数1", "")
log_util.Logger.info(args)
init_ocr_manger(args['微信安装目录'])
ocr_manager.DoOCRTask(args['待识别图片路径'])
while ocr_manager.m_task_id.qsize() != OCR_MAX_TASK_ID:
pass
global ocr_result
return ocr_result
ocr_result = {}
ocr_manager =
def ocr_result_callback(img_path:str, results:dict):
log_util.Logger.info(results)
ocr_result = results
def init_ocr_manger(wechat_dir):
wechat_dir = find_wechat_path(wechat_dir)
wechat_ocr_dir = find_wechatocr_exe()
global ocr_manager
if ocr_manager is :
ocr_manager = OcrManager(wechat_dir)
# 设置WeChatOcr目录
ocr_manager.SetExePath(wechat_ocr_dir)
# 设置微信所在路径
ocr_manager.SetUsrLibDir(wechat_dir)
# 设置ocr识别结果的回调函数
ocr_manager.SetOcrResultCallback(ocr_result_callback)
# 启动ocr服务
ocr_manager.StartWeChatOCR()
def find_wechat_path(wechat_dir):
# 定义匹配版本号文件夹的正则表达式
version_pattern = re.compile(r'\[\d+\.\d+\.\d+\.\d+\]')
path_temp = os.listdir(wechat_dir)
for temp in path_temp:
# 下载是正则匹配到[3.9.10.27]
# 使用正则表达式匹配版本号文件夹
if version_pattern.match(temp):
wechat_path = os.path.join(wechat_dir, temp)
if os.path.isdir(wechat_path):
return wechat_path
def find_wechatocr_exe():
# 获取APPDATA路径
appdata_path = os.getenv("APPDATA")
if not appdata_path:
print("APPDATA environment variable not found.")
return
# 定义WeChatOCR的基本路径
base_path = os.path.join(appdata_path, r"Tencent\WeChat\XPlugin\Plugins\WeChatOCR")
# 定义匹配版本号文件夹的正则表达式
version_pattern = re.compile(r'\d+')
try:
# 获取路径下的所有文件夹
path_temp = os.listdir(base_path)
except FileNotFoundError:
print(f"The path {base_path} does not exist.")
return
for temp in path_temp:
# 使用正则表达式匹配版本号文件夹
if version_pattern.match(temp):
wechatocr_path = os.path.join(base_path, temp, 'extracted', 'WeChatOCR.exe')
if os.path.isfile(wechatocr_path):
return wechatocr_path
# 如果没有找到匹配的文件夹,返回
return
然后点击流程参数,创建流程的输入参数
3. 调用OCR识别的方法,实现批量的文字提取
使用调用流程组件,填写对应的参数,即可实现图片文字的提取了。
来源:juejin.cn/post/7432193949765287962
uni-app的这个“地雷”坑,我踩了
距离上次的 uni-app-x 文章已有一月有余,在此期间笔者又“拥抱”了 uni-app,使用 uni-app 开发微信小程序。
与使用 uni-app-x 相比个人感觉 uni-app 在开发体验上更流畅,更舒服一些,这可能得益于 uni-app 相对成熟,且与标准的前端开发相差不大。至少在 IDE 的选择上比较自由,比如可以选择 VSCode 或者 WebStorm,笔者习惯了 Jetbrains 家的 IDE,自然选择了 WebStorm。
虽说 uni-app 相对成熟,但是笔者还是踩到了“地雷式”的巨坑,下面且听我娓娓道来。
附:配套代码。
什么样的坑
先描述下是什么样的坑。简单来说,我有一个动态的 style 样式,伪代码如下:
<view v-for="(c, ci) in 10" :key="ci" :style="{ height: `${50}px` }">
{{ c }}
</view>
理论上编译到小程序应该如下:
<view style="height: 50px">1</view>
但是,实际上编译后却是:
<view style="height: [object Object]">1</view>
最后导致样式没有生效。
着手排查
先网上搜索一番,基本上千篇一律的都是 uni-app 编程成微信小程序时 style 不支持对象的形式,需要在对象外包一层数组,需要做如下修改:
<view :style="[{ height: `${50}px` }]"></view>
但是,这种方式对我无效。
然后开始了漫长的排查之旅,对比之前的项目是使用的对象形式对动态 style 进行的赋值也没有遇到这样问题,最后各种尝试至深夜一点多也没有解决,浪费我大好的“青春”。
没有解决问题实在是不甘心啊,于是第二天上午继续排查,观察 git 提交记录,没有发现什么异常的代码,然后开始拉新分支一个一个的 commit 回滚代码,然后再把回滚的代码手敲一遍再一点点的编译调试对比,这真的是浪费时间与精力的一件事,最终也是功夫不负有心人,终于锁定了一个 commit 提交,在这个 commit 后出现了上述问题。
为什么要回滚代码?因为在之前的代码中都是以对象形式为动态 style 赋值的。
现在可以着重的“攻击”这个 commit 上的代码了,仿佛沉溺在水中马上就要浮出水面可以呼一口气。这个 commit 上的代码不是很多,其中就包含上述的伪代码。最后,经过仔细的审查这个 commit 上的代码也没有发现什么异常的代码逻辑,好像突然没有了力气又慢慢沉入了水底。
反正是经过了各种尝试,其中历程真是一把鼻涕一把泪,不提也罢。
也不知是脑子不好使还是最后的倔强,突发奇想的修改了上述伪代码中 v-for
语句中的 c
变量命名:
<view v-for="(a, ci) in 10" :key="ci" :style="{ height: `${50}px` }">
{{ a }}
</view>
妈耶,奇迹发生了,动态 style 编译后正常了,样式生效了。随后又测试了一些其他的命名,如:A,b,B,C,d,D,i,I
,这些都编译后正常,唯独命名为小写的 c
后,编译后不正常还是 [object Object]
的形式。
如果,现在,你迫不及待的去新建个 uni-app 项目来验证笔者所言是否属实,那么不好意思,你大概率不会踩到这个坑。
但是,如果你在动态 style 中再多加一个 css 属性,代码如下:
<view
v-for="(c, ci) in 5"
:key="ci"
:style="{
height: `${50}px`,
marginTop: `${10}px`,
}"
>
{{ c }}
</view>
那么你会发现第一个 height
属性生效了,然而新加的 marginTop
属性却是 [object Object]
。
如果你再多加几个属性,你会发现它们都生效了,唯独第二个属性是失效的。
如果你在这个问题 view
代码前面使用过 v-for
且使用过动态 style 且动态 style 中有字符串模板,那么你会发现问题 view
变正常了。
总结
本文记录了笔者排查 uni-app 动态 style 失效的心路历程,虽然问题得到了解决,但是没有深入研究产生此问题的本质原因,总结起来就是菜,还得多练。
深夜对着星空感叹,这种坑也能被我踩到,真是时也命也。
来源:juejin.cn/post/7416554802254364708
写了一个字典hook,瞬间让组员开发效率提高20%!!!
1、引言
在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react 开发一个小程序,所以就写一个字典 hook 方便大家开发。
2、实现过程
首先,字典接口返回的数据类型如下图所示:
其次,在没有实现字典 hook 之前,是这样使用 选择器 组件的:
const [unitOptions, setUnitOptions] = useState([])
useEffect(() => {
dictAppGetOptionsList(['DEV_TYPE']).then((res: any) => {
let _data = res.rows.map(item => {
return {
label: item.fullName,
value: item.id
}
})
setUnitOptions(_data)
})
}, [])
const popup = (
<PickSelect
defaultValue=""
open={unitOpen}
options={unitOptions}
onCancel={() => setUnitOpen(false)}
onClose={() => setUnitOpen(false)}
/>
)
每次都需要在页面组件中请求到字典数据提供给 PickSelect 组件的 options 属性,如果有多个 PickSelect 组件,那就需要请求多次接口,非常麻烦!!!!!
既然字典接口返回的数据格式是一样的,那能不能写一个 hook 接收不同属性,返回不同字典数据呢,而且还能 缓存 请求过的字典数据?
当然是可以的!!!
预想一下如何使用这个字典 hook?
const { list } = useDictionary('DEV_TYPE')
const { label } = useDictionary('DEV_TYPE', 1)
const { label } = useDictionary('DEV_TYPE', 1, '、')
从上面代码中可以看到,第一个参数接收字典名称,第二个参数接收字典对应的值,第三个参数接收分隔符,而且后两个参数是可选的,因此根据上面的用法来写我们的字典 hook 代码。
interface dictOptionsProps {
label: string | number;
value: string | number | boolean | object;
disabled?: boolean;
}
interface DictResponse {
value: string;
list: dictOptionsProps[];
getDictValue: (value: string) => string
}
let timer = null;
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {}; // 字典缓存
// 因为接收不同参数,很适合用函数重载
function useDictionary(type: string): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]); // 字典数组
const [dictValue, setDictValue] = useState(""); // 字典对应值
const init = () => {
if (!dict[type] || !dict[type].length) {
dict[type] = [];
types.push(type);
// 当多次调用hook时,获取所有参数,合成数组,再去请求,这样可以避免多次调用接口。
timer && clearTimeout(timer);
timer = setTimeout(() => {
dictAppGetOptionsList(types.slice()).then((res) => {
for (const key in dictRes.data) {
const dictList = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
dict[type] = dictList
setOptions(dictList) // 注意这里会有bug,后面有说明的
}
});
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
};
// 获取字典对应值的中文名称
const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];
const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
)
useEffect(() => init(), [])
useEffect(() => setDictValue(getLabel(value)), [options, value])
return { value: dictValue, list: options, getDictValue: getLabel };
}
初步的字典hook已经开发完成,在 Input 组件中添加 dict 属性,去试试看效果如何。
export interface IProps extends taroInputProps {
value?: any;
dict?: string; // 字典名称
}
const CnInput = ({
dict,
value,
...props
}: IProps) => {
const { value: _value } = dict ? useDictionary(dict, value) : { value };
return <Input value={_value} {...props} />
}
添加完成,然后去调用 Input 组件
<CnInput
readonly
dict="DEV_ACCES_TYPE"
value={formData?.accesType}
/>
<CnInput
readonly
dict="DEV_SOURCE"
value={formData?.devSource}
/>
没想到,翻车了
会发现,在一个页面组件中,多次调用 Input 组件,只有最后一个 Input 组件才会回显数据
这个bug是怎么出现的呢?原来是 setTimeout 搞的鬼,在 useDictionary hook 中,当多次调用 useDictionary hook 的时候,为了能拿到全部的 type 值,请求一次接口拿到所有字典的数据,就把字典接口放在 setTimeout 里,弄成异步的逻辑。但是每次调用都会清除上一次的 setTimeout,只保存了最后一次调用 useDictionary 的 setTimeout ,所以就会出现上面的bug了。
既然知道问题所在,那就知道怎么去解决了。
解决方案: 因为只有调用 setOptions 才会引起页面刷新,为了不让 setTimeout 清除掉 setOptions,就把 setOptions 添加到一个更新队列中,等字典接口数据回来再去执行更新队列就可以了。
let timer = null;
const queue = []; // 更新队列
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {};
function useDictionary2(type: string): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]);
const [dictValue, setDictValue] = useState("");
const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];
const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
);
const init = () => {
if (typeof type === "string") {
if (!dict[type] || !dict[type].length) {
dict[type] = [];
const item = {
key: type,
exeFunc: () => {
if (typeof type === "string") {
setOptions(dict[type]);
} else {
setOptions(type);
}
},
};
queue.push(item); // 把 setOptions 推到 更新队列(queue)中
types.push(type);
timer && clearTimeout(timer);
timer = setTimeout(async () => {
const params = types.slice();
types.length = 0;
try {
let dictRes = await dictAppGetOptionsList(params);
for (const key in dictRes.data) {
dict[key] = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
}
queue.forEach((item) => item.exeFunc()); // 接口回来了再执行更新队列
queue.length = 0; // 清空更新队列
} catch (error) {
queue.length = 0;
}
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
}
};
useEffect(() => init(), []);
useEffect(() => setDictValue(getLabel(value)), [options, value]);
return { value: dictValue, list: options, getDictValue: getLabel };
}
export default useDictionary;
修复完成,再去试试看~
不错不错,已经修复,嘿嘿~
这样就可以愉快的使用 字典 hook 啦,去改造一下 PickSelect 组件
export interface IProps extends PickerProps {
open: boolean;
dict?: string;
options?: dictOptionsProps[];
onClose: () => void;
}
const Base = ({
dict,
open = false,
options = [],
onClose = () => { },
...props
}: Partial<IProps>) => {
// 如果不传 dict ,就拿 options
const { list: _options } = dict ? useDictionary(dict) : { list: options };
return <Picker.Column>
{_options.map((item) => {
return (
<Picker.Option
value={item.value}
key={item.value as string | number}
>
{item.label}
</Picker.Option>
);
})}
</Picker.Column>
在页面组件调用 PickSelect 组件
效果:
这样就只需要传入 dict 值,就可以轻轻松松获取到字典数据啦。不用再手动去调用字典接口啦,省下来的时间又可以愉快的摸鱼咯,哈哈哈
最近也在写 vue3 的项目,用 vue3 也实现一个吧。
// 定时器
let timer = 0
const timeout = 10
// 字典类型缓存
const types: string[] = []
// 响应式的字典对象
const dict: Record<string, Ref<CnPage.OptionProps[]>> = {}
// 请求字典选项
function useDictionary(type: string): Ref<CnPage.OptionProps[]>
// 解析字典选项,可以传入已有字典解析
function useDictionary(
type: string | CnPage.OptionProps[],
value: number | string | Array<number | string>,
separator?: string
): ComputedRef<string>
function useDictionary(
type: string | CnPage.OptionProps[],
value?: number | string | Array<number | string>,
separator = ','
): Ref<CnPage.OptionProps[]> | ComputedRef<string> {
// 请求接口,获取字典
if (typeof type === 'string') {
if (!dict[type]) {
dict[type] = ref<CnPage.OptionProps[]>([])
if (type === 'UNIT_LIST') {
// 单位列表调单独接口获取
getUnitListDict()
} else if (type === 'UNIT_TYPE') {
// 单位类型调单独接口获取
getUnitTypeDict()
} else {
types.push(type)
}
}
// 等一下,人齐了才发车
timer && clearTimeout(timer)
timer = setTimeout(() => {
if (types.length === 0) return
const newTypes = types.slice()
types.length = 0
getDictionary(newTypes).then((res) => {
for (const key in res.data) {
dict[key].value = res.data[key].map((v) => ({
label: v.description,
value: v.subtype
}))
}
})
}, timeout)
}
const options = typeof type === 'string' ? dict[type] : ref(type)
const label = computed(() => {
if (value === undefined || value === null) return ''
const values = Array.isArray(value) ? value : [value]
const items = values.map(
(value) => {
if (typeof value === 'number') value = value.toString()
return options.value.find((v) => v.value === value) || { label: value }
}
)
return items.map((v) => v.label).join(separator)
})
return value === undefined ? options : label
}
export default useDictionary
感觉 vue3 更简单啊!
到此结束!如果有错误,欢迎大佬指正~
来源:juejin.cn/post/7377559533785022527
什么?Flutter 又要凉了? Flock 是什么东西?
今天突然看到这个消息,突然又有一种熟悉的味道,看来这个月 Flutter “又要凉一次了”:
起因 flutter foundation 决定 fork Flutter 并推出 Flock 分支用于自建维护,理由是:
foundation 推测 Flutter 团队的劳动力短缺,因为 Flutter 需要维护 Android、iOS、Mac、Window、Linux、Web 等平台,但是 Flutter团队的规模仅略有增加。
在 foundation 看来,保守估计全球至少有 100 万 Flutter 相关开发者,而 Flutter 团队的规模大概就只有 50+ 人,这个比例并不健康。
问题在于这个数据推测就很迷,没有数据来源的推测貌似全靠“我认为”。。。。
另外 foundation 做这个决定,还因为 Flutter 官方团队对其 6 个支持的平台中,有 3 个处于维护模式(Window、Mac、Linux),所以他们无法接受桌面端的现场,因为他们认为桌面端很可能是 Flutter 最大的未开发价值。
关于这点目前 PC 端支持确实缓慢,但也并没有完全停止,如果关注 PC issue 的应该看到, Mac 的 PlatformView 和 WebView 支持近期才初步落地。
而让 foundation 最无法忍受的是,issue 的处理还有 pr 的 merge 有时候甚至可能会积累数年之久。
事实上这点确实成立,因为 Flutter 在很多功能上都十分保守,同时 issue 量大且各平台需求多等原因,很多能力的支持时间跨度多比较长,例如 「Row/Column 即将支持 Flex.spacing」 、「宏编程支持」 、「支持 P3 色域」 等这些都是持续了很久才被 merge 的 feature 。
所以 Flutter 的另外一个支持途径是来自社区 PR,但是 foundation 表示 Flutter 的代码 Review 和审计工作缓慢,并且沟通困难,想法很难被认可等,让 foundation 无法和 Flutter 官方有效沟通。
总结起来,在 foundation 的角度是,Flutter 官方团队维护 Flutter 不够尽心尽力。
所以他们决定,创建 Flutter 分支,并称为 Flock:意寓位 “Flutter+”。
不过 foundation 表示,他们其实并不想也不打算分叉 Flutter 社区,Flock 将始终与 Flutter 保持同步。
Flock 的重点是添加重要的错误修复和全新的社区功能支持,例如 Flutter 团队不做的,或者短期不会实现:
并且 Flock 的目的是招募一个比 Flutter 团队大得多的 PR 审查团队,从而加快 PR 的审计和推进。
所以看起来貌似这是一件好事,那么为什么解读会是“崩盘”和“内斗”?大概还是 Flutter 到时间凉了,毕竟刚刚过完 Flutter 是十周年生日 ,凉一凉也挺好的。
来源:juejin.cn/post/7431032490284236839
BOE(京东方)2024年前三季度净利润三位数增长 “屏之物联”引领企业高质发展
10月30日,京东方科技集团股份有限公司(京东方A:000725;京东方B:200725)发布2024年第三季度报告,前三季度公司实现营业收入1437.32亿元,较去年同期增长13.61%;归属于上市公司股东净利润33.10亿元,同比大幅增长223.80%。其中,第三季度实现营业收入503.45亿元,较去年同期增长8.65%;归属于上市公司股东净利润10.26亿元,同比增长258.21%。BOE(京东方)凭借稳健的经营策略和行业领先的技术优势,在保持半导体显示产业龙头地位的同时,持续推动“1+4+N+生态链”在各个细分市场的深度布局与成果落地,不断深化“屏之物联”战略在多业态场景的转化应用。面向下一个三十年,BOE(京东方)积极推动构建产业发展“第N曲线”,打造新的业务增长极,持续激发产业生态活力。
不仅业绩表现亮眼,BOE(京东方)还不断加大在前沿技术领域和物联网转型方面的投入与探索,为全球显示及物联网产业的未来发展注入新的活力。第三季度,BOE(京东方)全球创新伙伴大会成功举办,全面展示公司在前沿技术领域的重要突破以及物联网转型创新成果,并重磅发布了企业创新发展的战略升维“第N曲线”理论。这一理论不仅承载着企业文化的深厚底蕴,更是对核心优势资源的深度拓展,在“第N曲线”理论指导下,BOE(京东方)已在玻璃基、钙钛矿等新兴领域重点布局,其中,钙钛矿光伏电池中试线从设备搬入到首批样品产出,历时仅38天,创造了行业新记录,这一突破性进展标志着BOE(京东方)在钙钛矿光伏产业化道路上迈出了重要一步,以卓越的实力和高效的速度着力打造“第N曲线”关键增长极,持续引领行业走向智能化、可持续化发展。
稳居半导体显示领域龙头地位,技术创新引领行业发展
2024年前三季度,BOE(京东方)凭借前瞻性的全球市场布局,持续稳固半导体显示领域的龙头地位,不仅专利申请量保持全球领先,更有自主研发的ADS Pro顶尖技术引领行业发展,在柔性AMOLED市场也持续突破,各类技术创新成果丰硕。据市场调研机构Omdia数据显示,BOE(京东方)显示屏整体出货量和五大主流应用领域液晶显示屏出货量稳居全球第一。在专利方面,BOE(京东方)累计自主专利申请已超9万件,其中发明专利超90%,海外专利超30%,技术与产品创新能力稳步提升。同时,BOE(京东方)持续展现强大的创新实力和市场影响力,BOE(京东方)自主研发的、独有的液晶显示领域顶流技术ADS Pro,不仅是目前全球出货量最高的主流液晶显示技术,也是应用最广的硬屏液晶显示技术。凭借高环境光对比度、全视角无色偏、高刷新率和动态画面优化等方面的卓越性能表现,ADS Pro技术成为客户高端旗舰产品的首选,市场出货量和客户采纳度遥遥领先,展现了液晶显示技术蓬勃的生命力,更是极大推动了全球显示产业的良性健康发展。在柔性显示领域,2024年前三季度,BOE(京东方)柔性AMOLED产品出货量进一步增加,荣耀Magic6系列搭载BOE(京东方)首发的OLED低功耗解决方案,开启柔性OLED低功耗全新时代,获得市场和客户的广泛赞誉;与OPPO一加客户联合发布全新2K+LTPO全能高端屏幕标志着柔性显示的又一次全面技术革新,凭借在画质、性能及护眼等多方面的显著提升,再次定义高端柔性OLED屏幕行业新标准。同时,BOE(京东方)加快AMOLED产业布局,投建的国内首条第8.6代AMOLED生产线从开工到封顶仅用183天,以科学、高效、高质的速度树立行业新标杆,推动OLED显示产业快速迈进中尺寸发展阶段。
“1+4+N”业务布局成果显著,打造多元化发展格局
在持续深耕显示行业的同时,BOE(京东方)始终坚持创新发展,“1+4+N+生态链”业务也在创新技术的赋能下展现出全新活力,各个细分市场成果显著。BOE(京东方)物联网创新业务在智慧终端和系统方案两大领域持续高速发展,智慧终端领域,BOE(京东方)正式发布“Smart GOAL”业务目标,致力于打造软硬融合、服务全球、一站式、高效敏捷、绿色低碳的智造体系,并在白板、拼接、电子价签(ESL)等细分市场出货量保持全球第一(数据来源:迪显、Omdia等);系统方案领域,BOE(京东方)持续深耕智慧园区、智慧金融等多个物联网细分场景,积极拓展人机交互协同新边界。在传感业务方面,BOE(京东方)光幕技术及MEMS传感等技术加速赋能奇瑞汽车,推进汽车智能化转型;发布国内首个《乘用车用电子染料液晶调光玻璃技术规范》团体标准,并在智慧视窗领域超额完成极氪首款标配车型调光窗的量产交付,实现订单量增长200%,开启光幕技术创新与应用的新篇章;同时还在工业传感器领域导入6家战略客户,未来将在项目合作及产品研发等方面开展广泛合作。在MLED业务方面, BOE(京东方)MLED珠海项目全面启动,标志着公司在MLED领域进一步深入布局,为全球MLED市场的拓展奠定了坚实基础。在智慧医工业务方面,BOE(京东方)强化科技与医学相结合,打通“防治养”全链条,持续推动医疗健康服务的智慧化升级,成都京东方智慧医养社区正式投入运营,创新医养融合模式,成为BOE(京东方)布局智慧医养领域的重要里程碑;合肥京东方医院加入胸部肿瘤长三角联盟,携手优质专家资源造福当地患者;BOE(京东方)健康研究院与山西省肿瘤医院合作开展NK细胞治疗膀胱癌的临床研究,助力医疗技术创新。
BOE(京东方)还以“N”业务为着力点,为不同行业提供软硬融合的整体解决方案,包括智慧车载、数字艺术、AI+、超高清显示、智慧能源等多个细分领域,打造业务增长新曲线。在车载领域,BOE(京东方)持续保持车载显示屏出货量及出货面积全球第一(数据来源:Omdia),智能座舱产品全面应用到长安汽车、吉利汽车、蔚来、理想等全球各大主流汽车品牌中。在数字艺术领域,艺云科技在裸眼3D显示技术等方面取得新突破,裸眼3D屏亮相国家博物馆,艺云数字艺术中心(王府井)、艺云数字艺术中心(宜宾)正式开馆,创新显示技术为多个领域增光添彩。在AI+领域,BOE(京东方)已将人工智能技术产品、服务、解决方案应用于制造、办公、医疗、零售等细分场景,依托自主研发的人工智能平台及衍生技术集,打造AI良率分析系统、AI显示知识问答系统、显示工业大模型等,大幅度提高生产效率。在超高清领域,BOE(京东方)中联超清通过8K超高清显示技术、超薄全贴合电子站牌和户外LCD广告屏等产品,赋能合肥高新区公交站升级、成都双流国际机场T1航站楼,助力交通出行智能化服务水平大幅提升。在绿色发展方面,BOE(京东方)能源业务在工业、商业、园区等多个场景下加速推进零碳综合能源服务,成功落地多个能源托管项目和碳资产管理项目,助力社会实现二氧化碳减排约33万吨。
值得一提的是,BOE(京东方)在全球化布局与品牌建设的道路上也迈出了更加坚实的步伐。“你好,BOE”、《BOE解忧实验室》两大营销IP持续大热,BOE(京东方)年度标志性品牌活动“你好,BOE”首站亮相海外,助力中国非物质文化遗产艺术展览落地法国巴黎,向世界展示中国科技的创新活力;在上海北外滩盛大启幕的“你好,BOE”SUPER O SPACE影像科技展以“艺术x科技”为主题为观众带来了一场视觉盛宴,成为BOE(京东方)“屏之物联”战略赋能万千应用场景的又一次生动展现;《BOE解忧实验室》奇遇发现季节目以全网4.58亿传播的辉煌成绩,成为2024年度硬核技术科普综艺及科技企业破圈营销典范。2024年是体育大年,在巴黎全球体育盛会举办期间,BOE(京东方)还与联合国教科文组织(UNESCO)在法国巴黎总部签订合作协议,成为首个支持联合国“科学十年”的中国科技企业,更助力中国击剑队出征巴黎,在科技、体育、文化等多个维度树立中国科技企业出海的全新范式。
在新技术、新消费、新场景的多重驱动下,2024年前三季度,BOE(京东方)保持了稳健的发展态势,不断创新前沿技术成果,丰富多元应用场景,为半导体显示产业高质升维发展注入源源不断的动能。未来,BOE(京东方)将继续秉承“屏之物联”战略,以稳健的经营、前瞻性的技术研发和持续应用创新,携手全球合作伙伴共同构建“Powered by BOE”产业价值创新生态,推动显示技术、物联网技术与数字技术的深度融合,为显示行业高质量发展贡献力量,共创智慧美好未来。
BOE(京东方)全新一代发光器件赋能iQOO 13 全面引领柔性显示行业性能新高度
10月30日,备受瞩目的iQOO最新旗舰机——被誉为“性能之光”的iQOO 13在深圳震撼发布。该款机型由BOE(京东方)独供6.82英寸超旗舰2K LTPO直屏,行业首发搭载全新一代Q10发光器件,在画面表现、护眼舒适度及性能功耗方面均达到行业领先水准,并以“直屏超窄边”的设计为用户呈现了前所未有的视觉体验,将直板手机的产品性能推向了全新高度。此次BOE(京东方)携手vivo旗下iQOO品牌联合打造旗舰新品,既体现了以“Powered by BOE”的生态携手合作伙伴联合创新的强大成果,还彰显了BOE(京东方)在柔性显示领域的强大技术优势和引领实力。
在画面表现方面,得益于BOE(京东方)全新发光器件,iQOO 13实现了屏幕性能的全面提升。该款机型全局峰值亮度提升12.5%,可达1800nit,局部峰值亮度更是达到4500nit,即使在强光照射下屏幕内容也清晰可见;在极端温度环境(-10℃至45℃)下,屏幕的偏色现象减少50%以上,确保用户无论身处何种复杂环境下,都能享受到始终一致的卓越屏幕观看体验。此外,iQOO 13屏幕还实现了15%的拖影减轻效果,搭配其高达144Hz的刷新率,无论是观看高清视频还是畅玩大型游戏,都能呈现流畅无阻、细腻入微的视觉效果。
在性能及功耗领域,借助BOE(京东方)领先的屏幕技术加持,iQOO 13的触控技术全新飞跃,实现了更为灵敏的触控体验,响应速度也达到了前所未有的快捷。在确保高度灵敏操作的同时,iQOO 13还通过BOE(京东方)全新迭代升级的Q10核心发光器件,成功将屏幕显示功耗降低10%,寿命提升33%,从而实现续航能力的全面跃升。这一系列改进不仅提升了能效比,更带来了更为精细化的表现,为用户带来更加出色的使用体验。
在护眼舒适度方面,作为首款将2K分辨率与类自然偏光完美结合的产品,iQOO 13的表现同样出色,搭载BOE(京东方)OLED圆偏振光护眼技术,此项技术能够通过模拟自然光线在进入人眼时在多个方向上的均匀分布特性,还原自然光所带来的舒适、健康体验,从而显著减轻长时间使用手机所带来的用眼疲劳问题。同时,iQOO 13还配备BOE(京东方)至高2592Hz全亮度高频PWM调光方案,在各种光线环境下都能确保屏幕的稳定性,可有效降低屏幕频闪对眼睛造成的潜在危害。
根据Omdia数据显示,截至2024年上半年,京东方柔性OLED出货量已连续多年稳居国内第一,全球第二,柔性OLED相关专利申请超3万件。目前,BOE(京东方)柔性显示终端解决方案已应用于多款国内外头部品牌的高端旗舰机型,并持续拓展至笔记本、车载、可穿戴等丰富场景。
作为领先的物联网创新企业,BOE(京东方)多年来始终秉持对技术的尊重和对创新的坚持,通过“Powered by BOE”的生态构建与合作伙伴联合创新,引领中国柔性OLED产业不断迈向新高度。未来,BOE(京东方)将秉持“屏之物联”战略,联动上下游生态链伙伴,共同探索更多柔性显示应用场景,为全球亿万用户带来屏联万物的美好“视”界。
收起阅读 »买了个mini主机当服务器
虽然有苹果的电脑,但是在安装一些软件的时候,总想着能不能有一个小型的服务器,免得各种设置导致 Mac 出现异常。整体上看了一些小型主机,也看过苹果的 Mac mini,但是发现它太贵了,大概要 3000 多,特别是如果要更高配置的话,价格会更高,甚至更贵。所以,我就考虑一些别的小型主机。也看了一些像 NUC 这些服务器,但是觉得还是太贵了。于是我自己去淘宝搜索,找到了这一款 N100 版的主机。
成本的话,由于有折扣,所以大概是 410 左右,然后自己加了个看上去不错的内存条花了 300 左右。硬盘的话我自己之前就有,所以总成本大概是 700 左右。大小的话,大概是一台手机横着和竖着的正方形大小,还带 Wi-Fi,虽然不太稳定。
一、系统的安装
系统我看是支持windows,还有现在Ubuntu,但是我这种选择的是centos stream 9, 10的话我也找过,但是发现很多软件还有不兼容。所以最终还是centos stream 9。
1、下载Ventoy软件
去Ventoy官网下载Ventoy软件(Download . Ventoy)如下图界面
2、制作启动盘
选择合适的版本以及平台下载好之后,进行解压,解压出来之后进入文件夹,如下图左边所示,双击打开Ventoy2Disk.exe,会出现下图右边的界面,选择好自己需要制作启动盘的U盘,然后点击安装等待安装成功即可顺利制作成功启动U盘。
3、centos安装
直接取官网,下载完放到u盘即可。
它的BIOS是按F7启动,直接加载即可。
之后就是正常的centos安装流程了。
二、连接wifi
因为是用作服务器的,所以并没有给它配置个专门的显示器,只要换个网络,就连不上新的wifi了,这里可以用网线连接路由器进行下面的操作即可。
在 CentOS 系统中,通过命令行连接 Wi-Fi 通常需要使用 nmcli(NetworkManager 命令行工具)来管理网络连接。nmcli 是 NetworkManager 的一个命令行接口,可以用于创建、修改、激活和停用网络连接。以下是如何使用 nmcli 命令行工具连接 Wi-Fi 的详细步骤。
步骤 1: 检查网络接口
首先,确认你的 Wi-Fi 网络接口是否被检测到,并且 NetworkManager 是否正在运行。
nmcli device status
输出示例:
DEVICE TYPE STATE CONNECTION
wlp3s0 wifi disconnected --
enp0s25 ethernet connected Wired connection 1
lo loopback unmanaged --
在这个示例中,wlp3s0 是 Wi-Fi 接口,它当前处于未连接状态。
步骤 2: 启用 Wi-Fi 网卡
如果你的 Wi-Fi 网卡是禁用状态,可以通过以下命令启用:
nmcli radio wifi on
验证 Wi-Fi 是否已启用:
nmcli radio
步骤 3: 扫描可用的 Wi-Fi 网络
使用 nmcli 扫描附近的 Wi-Fi 网络:
nmcli device wifi list
你将看到可用的 Wi-Fi 网络列表,每个网络都会显示 SSID(网络名称)、安全类型等信息。
步骤 4: 连接到 Wi-Fi 网络
使用 nmcli 命令连接到指定的 Wi-Fi 网络。例如,如果你的 Wi-Fi 网络名称(SSID)是 MyWiFiNetwork,并且密码是 password123,你可以使用以下命令连接:
nmcli device wifi connect 'xxxxxx' password 'xxxxx'
你应该会看到类似于以下输出,表明连接成功:
Device 'wlp3s0' successfully activated with 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.
步骤 5: 验证连接状态
验证网络连接状态:
nmcli connection show
查看当前连接的详细信息:
nmcli device show wlp3s0
三、VNC远程连接
桌面还是偶尔需要用一下的,虽然用的不多。
root@master:~# dnf install -y tigervnc-server
root@master:~# vncserver
bash: vncserver: command not found...
Install package 'tigervnc-server' to provide command 'vncserver'? [N/y] y
* Waiting in queue...
* Loading list of packages....
The following packages have to be installed:
dbus-x11-1:1.12.20-8.el9.x86_64 X11-requiring add-ons for D-BUS
tigervnc-license-1.14.0-3.el9.noarch License of TigerVNC suite
tigervnc-selinux-1.14.0-3.el9.noarch SELinux module for TigerVNC
tigervnc-server-1.14.0-3.el9.x86_64 A TigerVNC server
tigervnc-server-minimal-1.14.0-3.el9.x86_64 A minimal installation of TigerVNC server
Proceed with changes? [N/y] y
* Waiting in queue...
* Waiting for authentication...
* Waiting in queue...
* Downloading packages...
* Requesting data...
* Testing changes...
* Installing packages...
WARNING: vncserver has been replaced by a systemd unit and is now considered deprecated and removed in upstream.
Please read /usr/share/doc/tigervnc/HOWTO.md for more information.
You will require a password to access your desktops.
getpassword error: Inappropriate ioctl for device
Password:
之后在mac开启屏幕共享就可以了
四、docker 配置
docker安装我以为很简单,没想到这里是最难的一步了。安装完docker之后,总是报错:
Error response from daemon: Get "https://registry-1.docker.io/v2/": context deadline exceeded
即使改了mirrors也毫无作用
{
"registry-mirrors": [
"https://ylce84v9.mirror.aliyuncs.com"
]
}
看起来好像是docker每次pull镜像都要访问一次registry-1.docker.io,但是这个网址国内已经无法连接了,各种折腾,这里只贴一下代码吧,原理就就不讲了(懂得都懂)。
sslocal -c /etc/猫代理.json -d start
curl --socks5 127.0.0.1:1080 http://httpbin.org/ip
sudo yum -y install privoxy
vim /etc/systemd/system/docker.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:8118"
/etc/systemd/system/docker.service.d/https-proxy.conf
[Service]
Environment="HTTPS_PROXY=http://127.0.0.1:8118"
最后重启docker
systemctl start privoxy
systemctl enable privoxy
sudo systemctl daemon-reload
sudo systemctl restart docker
五、文件共享
sd卡好像读取不了,只能换个usb转换器
fdisk -l
mount /dev/sdb1 /mnt/usb/sd
在CentOS中设置文件共享,可以使用Samba服务。以下是配置Samba以共享文件的基本步骤:
- 安装Samba
sudo yum install samba samba-client samba-common
- 设置共享目录
编辑Samba配置文件
/etc/samba/smb.conf
,在文件末尾添加以下内容:
[shared]
path = /path/to/shared/directory
writable = yes
browseable = yes
guest ok = yes
- 设置Samba密码
为了允许访问,需要为用户设置一个Samba密码:
sudo smbpasswd -a your_username
- 重启Samba服务
sudo systemctl restart smb.service
sudo systemctl restart nmb.service
- 配置防火墙(如果已启用)
允许Samba通过防火墙:
sudo firewall-cmd --permanent --zone=public --add-service=samba
sudo firewall-cmd --reload
现在,您应该能够从网络上的其他计算机通过SMB/CIFS访问共享。在Windows中,你可以使用\\centos-ip\shared
,在Linux中,你可以使用smbclient //centos-ip/shared -U your_username
参考:
https://猫代理help.github.io/猫代理/linux.html
来源:juejin.cn/post/7430460789067055154
开发小同学的骚操作,还好被我发现了
大家好,我是程序员鱼皮。今天给朋友们还原一个我们团队真实的开发场景。
开发现场
最近我们编程导航网站要开发 用户私信
功能,第一期要做的需求很简单:
- 能让两个用户之间 1 对 1 单独发送消息
- 用户能够查看到消息记录
- 用户能够实时收到消息通知
这其实是一个双向实时通讯的场景,显然可以使用 WebSocket 技术来实现。
团队的后端开发小 c 拿到需求后就去调研了,最后打算采用 Spring Boot Starter 快速整合 Websocket 来实现,接受前端某个用户传来的消息后,转发到接受消息的用户的会话,并在数据库中记录,便于用户查看历史。
小 c 的代码写得还是不错的,用了一些设计模式(像策略模式、工厂模式)对代码进行了一些抽象封装。虽然在我看来对目前的需求来说稍微有点过度设计,但开发同学有自己的理由和想法,表示尊重~
前端同学小 L 也很快完成了开发,并且通过了产品的验收。
看似这个需求就圆满地完成了,但直到我阅读前端同学的代码时,才发现了一个 “坑”。
这是前端同学小 L 提交的私信功能代码,看到这里我就已经发现问题了,朋友们能注意到么?
解释一下,小 L 引入了一个 nanoid
库,这个库的作用是生成唯一 id。看到这里,我本能地感到疑惑:为什么要引入这个库?为什么前端要生成唯一 id?
难道。。。是作为私信消息的 id?
果不其然,通过这个库在前端给每个消息生成了一个唯一 id,然后发送给后端。
后端开发的同学可能会想:一般情况下不都是后端利用数据库的自增来生成唯一 id 并返回给前端嘛,怎么需要让前端来生成呢?
这里小 L 的解释是,在本地创建消息的时候,需要有一个 id 来追踪状态,不会出现消息没有 id 的情况。
首先,这么做的确 能够满足需求 ,所以我还是通过了代码审查;但严格意义上来说,让前端来生成唯一 id 其实不够优雅,可能会有一些问题。
前端生成 id 的问题
1)ID 冲突:同时使用系统的前端用户可能是非常多的,每个用户都是一个客户端,多个前端实例可能会生成相同的 ID,导致数据覆盖或混乱。
2)不够安全:切记,前端是没有办法保证安全性的!因为攻击者可以篡改或伪造请求中的数据,比如构造一个已存在的 id,导致原本的数据被覆盖掉,从而破坏数据的一致性。
要做这件事成本非常低,甚至不需要网络攻击方面的知识,打开 F12 浏览器控制台,重放个请求就行实现:
3)时间戳问题:某些生成 id 的算法是依赖时间戳的,比如当前时间不同,生成的 id 就不同。但是如果前端不同用户的电脑时间不一致,就可能会生成重复 id 或无效 id。比如用户 A 电脑是 9 点时生成了 id = 06030901,另一个用户 B 电脑时间比 A 慢了一个小时,现在是 8 点,等用户 B 电脑时间为 9 点的时候,可能又生成了重复 id = 06030901,导致数据冲突。这也被称为 “分布式系统中的全局时钟问题”。
明确前后端职责
虽然 Nanoid 这个库不依赖时间戳来生成 id,不会受到设备时钟不同步的影响,也不会因为时间戳重复而导致 ID 冲突。根据我查阅的资料,生成大约 10 ^ 9 个 ID 后,重复的可能性大约是 10 ^ -17,几乎可以忽略不计。但一般情况下,我个人会更建议将业务逻辑统一放到后端实现,这么做的好处有很多:
- 后端更容易保证数据的安全性,可以对数据先进行校验再生成 id
- 前端尽量避免进行复杂的计算,而是交给后端,可以提升整体的性能
- 职责分离,前端专注于页面展示,后端专注于业务,而不是双方都要维护一套业务逻辑
我举个典型的例子,比如前端下拉框内要展示一些可选项。由于选项的数量并不多,前端当然可以自己维护这些数据(一般叫做枚举值),但后端也会用到这些枚举值,双方都写一套枚举值,就很容易出现不一致的情况。推荐的做法是,让后端返回枚举值给前端,前端不用重复编写。
所以一般情况下,对于 id 的生成,建议统一交给后端实现,可以用雪花算法根据时间戳生成,也可以利用数据库主键生成自增 id 或 UUID,具体需求具体分析吧~
来源:juejin.cn/post/7376148503087169562
前端大佬都在用的useForm究竟有多强?
大家好,今天我要和大家分享一个超级实用的功能 - alovajs 的 useForm。老实说,当我第一次接触到这个功能时,我简直惊呆了!以前处理表单提交总是让我头疼不已,写了一堆重复的代码还容易出错。但现在有了 useForm,一切都变得如此简单和优雅。让我来告诉你它是如何改变我的开发体验的!
alovajs 简介
首先,让我介绍一下 alovajs。它不仅仅是一个普通的请求工具,而是一个能大大简化我们 API 集成流程的新一代利器。与 react-query 和 swr 这些库不同,alovajs 提供了针对各种请求场景的完整解决方案。
它有 15+ 个"请求策略",每个策略都包含状态化数据、特定事件和 actions。 这意味着我们只需要很少的代码就能实现特定场景下的请求。我记得第一次使用时,我惊讶地发现原来复杂的请求逻辑可以如此简洁!
如果你想了解更多关于 alovajs 的信息,强烈推荐你去官网看看: alova.js.org。相信我,你会发现一个全新的世界!
useForm 的神奇用法
现在,让我们一起深入了解 useForm 的具体用法。每次我使用这些功能时,都会感叹它的设计有多么巧妙。
基本用法
useForm 的基本用法非常简单,看看这段代码:
const {
loading: submiting,
form,
send: submit,
onSuccess,
onError,
onComplete
} = useForm(
formData => {
return formSubmit(formData);
},
{
initialForm: {
name: '',
cls: '1'
}
}
);
只需要这么几行代码,我们就能获得表单状态、数据、提交函数等所有需要的东西。 第一次看到这个时,我简直不敢相信自己的眼睛!
自动重置表单
还记得以前每次提交表单后都要手动重置吗?那种繁琐的感觉简直让人抓狂。但是 useForm 为我们提供了一个优雅的解决方案:
useForm(submitData, {
resetAfterSubmiting: true
});
设置这个参数为 true,表单就会在提交后自动重置。 当我发现这个功能时,我感觉自己省了好几年的寿命!
表单草稿
你有没有遇到过这种情况:正在填写一个长表单,突然被打断,等回来时发现数据全没了?那种沮丧的感觉我再清楚不过了。但是 useForm 的表单草稿功能彻底解决了这个问题:
useForm(submitData, {
store: true
});
开启这个功能后,即使刷新页面也能恢复表单数据。 我第一次使用这个功能时,简直感动得想哭!
多页面表单
对于那些需要分步骤填写的复杂表单,useForm 也有令人惊叹的解决方案:
// 组件A
const { form, send } = useForm(submitData, {
initialForm: { /*...*/ },
id: 'testForm'
});
// 组件B、C
const { form, send } = useForm(submitData, {
id: 'testForm'
});
通过设置相同的 id,我们可以在不同组件间共享表单数据。 这个功能让我在处理复杂表单时不再手忙脚乱,简直是多页面表单的福音!
条件筛选
useForm 还可以用于数据筛选,这个功能让我在开发搜索功能时如虎添翼:
const { send: searchData } = useForm(queryCity, {
initialForm: { cityName: '' },
immediate: true
});
设置 immediate 为 true,就能在初始化时就开始查询数据。 这对于需要立即显示结果的场景非常有用,大大提升了用户体验。
看完这些用法,你是不是也和我一样,被 useForm 的强大所折服?它不仅简化了我们的代码,还为我们考虑了各种常见的表单场景。使用 useForm,我感觉自己可以更专注于业务逻辑,而不是被繁琐的表单处理所困扰。
那么,你有没有在项目中遇到过类似的表单处理问题?useForm 是否解决了你的痛点?我真的很好奇你的想法和经验!如果你觉得这篇文章对你有帮助,别忘了点个赞哦!让我们一起探讨,一起进步!
来源:juejin.cn/post/7425193631583305780
老板:不是吧,这坨屎都给你优化好了,给你涨500工资!!
前言
最近负责了项目的一个大迭代,然后目前基本的功能都是实现了,也上了生产。但是呢,大佬们可以先看下面这张图,cpu占用率100%,真的卡了不得了哈哈哈,用户根本没有一点使用体验。还有就是首屏加载,我靠说实话,真的贼夸张,首屏加载要十来秒,打开控制台一看,一个js资源加载就要七八秒。本来呢,我在这个迭代中我应该是负责开发需求的那个底层苦力码农,而这种性能优化这种活应该是组长架构师来干的,我这种小菜鸡应该是拿个小本本偷偷记笔记的,但是组长离职跳槽了,哥们摇身一变变成了项目负责人哈哈哈了。所以就有了这篇文章,和大家分享记录一下,毕业几个月的菜鸡的性能优化思路和手段,也希望大佬们给指点一下。
先和大家说一下。这个页面主要有两个问题 卡顿 和 首屏加载,本来这篇文章是打算把我优化这两个问题的思路和方法都一起分享给大家的,但是我码完卡顿的思路和方法后发现写的有点多。所以这篇文章就只介绍我优化卡顿的思路和方法,首屏加载我会另外发一篇文章。
卡顿
这个页面卡顿呢,主要是由于这个表格的原因,很多人应该会想着表格为什么会卡顿啊,但是我这个表格是真的牛逼,大家可以看我这篇文章 “不是吧,刚毕业几个月的前端,就写这么复杂的表格??”,顺便给我装一下杯,这篇文章上了前端热榜第一(还是断层霸榜哦)(手动狗头)。
言归正传,为了一些盆友们不想看那篇文章,我给大家总结一下(最好看一下嘿嘿嘿),这个表格整体就是由三个表格合成为一个表格的,所以这个页面相当于有三个表格。因为它是一个整体的,所以我就需要去监听这个三个表格滚动事件去保证它表现为一个表格,其实就是保证他们滚动同步,以及信息栏浮层正确的计算位置,有点啰嗦了哈哈哈。
其实可以看到,很明显的卡顿。而且,这还是最普通的时候,为什么说普通呢,因为这个项目是金融方面的,所以这些数据都是需要实时更新的,我录制的这个动图是没有进行数据更新的。然后这个表格是一共是有四百来条数据,四百来条实时更新,这也就是为什么cpu占用率百分百的主要原因。再加之为了实现三个表格表现为一个表格,不得不给每一个表格都监听滚动事件,去改变剩下两个表格滚动条,然后改变滚动条也会触发滚动事件,也就是说滚动一下,触发了三个函数,啥意思呢,就比如说我本来只用执行1行代码,现在会执行3行代码(如果看不明白,去上面那边文章的demo跑一下就知道了)。所以,我们就可以知道主要的卡顿原因了。
卡顿原因
看到这盆友们应该知道为什么卡顿了,如果还不知道,那罚你再重新看一遍咯。其实真可以去看一下那篇文章,那篇文章很好的阐述了这个表格为什么会这么复杂。
卡顿原因:
- 大量数据需要实时更新
- 三个表格滚动事件让工作代码量变成了三倍
优化效果
不行,得先学资本家给大家画个饼,不然搞得我好像在诈骗一样,可以看下面这两张动态图,我只能说吃了二十盒德芙也没有这么丝滑。虽然滚轮滚动速度是有差别,可能会造成误差,但是这两区别也太大,丝滑了不止一点点,肉眼都可以看的出来。
优化前
优化后
在看数据实时更新的前后对比动图,优化前的动图可以看到,cpu占有率基本都是100%,偶尔会跳去99%。但是看优化后的图,虽然也会有飙到100的cpu占有率,但是只是某一个瞬间。这肯定就高下立判了,吾与城北徐公孰美,肯定是吾美啊!
优化前
优化后
优化思路与方法
如何呢,少侠?是不是还不错!
前面已经说过了两个原因导致卡顿,我们只要解决这两个原因自然就会好起来了,也不是解决,只能说是优化它,因为在网络,数据大量更新,以及用户频繁操作等等其他原因,还是会特别卡。
如何优化三个表格的滚动事件
对于这三个表格,核心是一次滚动事件会触发三次滚动函数,而且三个事件函数其实都是大差不差的,都是去改变其余两个表格的上下滚动高度或者左右滚动宽度,换句话说,这个滚动事件的主要目的其实就是获取当前这个表格滚动了多少。那我们偷换一下概念,原本的是滚动事件去改变其他两个表格的滚动高度,不如把他变成滚动了多少去改变其他两个表格的滚动高度。懵了吧,少年哈哈哈哈!看下修改后的代码你就能细评这句话了,代码是vue3写法,而且并不全,大家知道我在干嘛就行。
修改前的js代码
const leftO = document.querySelector("#left")
const middleO = document.querySelector("#middle")
const rightO = document.querySelector("#right")
leftO.addEventListener("scroll", (e) => {
const top = e.target.scrollTop
const left = e.target.scrollLeft
middleO.scrollTop = e.target.scrollTop
rightO.scrollTop = e.target.scrollTop
rightO.scrollLeft = left
},true)
middleO.addEventListener("scroll", (e) => {
const top = e.target.scrollTop
leftO.scrollTop = e.target.scrollTop
rightO.scrollTop = e.target.scrollTop
},true)
rightO.addEventListener("scroll", (e) => {
const left = e.target.scrollLeft
const top = e.target.scrollTop
leftO.scrollTop = e.target.scrollTop
middleO.scrollTop = e.target.scrollTop
leftO.scrollLeft = left
},true)
修改后的js代码
const leftO = document.querySelector("#left")
const middleO = document.querySelector("#middle")
const rightO = document.querySelector("#right")
const top = ref(0)
const left = ref(0)
// 这个是判断哪个表格进行滚动了
const flag = ref("")
leftO.addEventListener("scroll", (e) => {
// 记录top和left
top.value = e.target.scrollTop
left.value = e.target.scrollLeft
flag.value = 'left'
}, true)
middleO.addEventListener("scroll", (e) => {
// 记录top
top.value = e.target.scrollTop
flag.value = 'middle'
}, true)
rightO.addEventListener("scroll", (e) => {
// 记录top和left
top.value = e.target.scrollTop
left.value = e.target.scrollLeft
flag.value = 'right'
}, true)
// 监听top去进行滚动
watch(() => top.value, (newV) => {
// 当前滚动就不进行设置滚动条了
flag.value!=="left" && (leftO.scrollTop = newV)
flag.value!=="middle" && (middleO.scrollTop = newV)
flag.value!=="right" && (rightO.scrollTop = newV)
})
// 监听left去进行滚动
watch(() => left.value, (newV) => {
// 当前滚动就不进行设置滚动条了
flag.value!=="left" && (leftO.scrollleft = newV)
flag.value!=="right" && (rightO.scrollleft= newV)
})
看完了吧,我简单的总结下我都干了啥,其实就是将三个滚动事件所造成的影响全部作用于变量,再通过watch
去监听变量是否变化再去作用于表格,而不是直接作用于表格。换句来说,从之前的监听三个滚动事件去滚动表格变成监听一个滚动高度变量去滚动表格,自然代码工作量从原来的三倍变回了原来的一倍。其实和发布订阅是有异曲同工之妙,三个发布者通知一个订阅者
。如此简单的一个事,为啥我要啰里吧嗦逼逼这么多,其实就是想让大家体会待入一下那种恍然大悟妙不可言的高潮感,而不是坐享其成的麻痹感。
如何优化大量数据实时更新
前面说过这是一个金融项目的页面,所以他是需要实时更新的。但是这个表格大概有四百来条数据,一条数据有二十一列,也就是可能会有八千多个数据需要更新。这肯定导致页面很卡,甚至是页面崩溃。那咋办呢,俗话说的好啊,只要思想不滑坡,办法总比困难多!
我们不妨想一想,四百来条数据都要实时更新吗?对,这并不需要!我们只要实现了类似于图片懒加载的效果,啥意思呢?就是比如当前我们屏幕只能看到二十条数据,我们只要实时更新的当前这二十条就行了,在滚动的时候屏幕又显示了另外二十条,我们在实时更新这二十条数据。不就洒洒水的优化了好几倍的性能吗。
我先和大家先说一下,我这边实现这个实时更新是通过websocket去实现的,前端将需要实时更新的数据id代码,发送给服务端,服务端就会一直推送相关的更新数据。然后我接下来就用subscribe代表去给通知服务端需要更新哪些数据id,unsubscribe代表去通知服务的不用继续更新数据,来给大家讲一下整体一个思路。
首先,我们需要去维护好一个数组,什么数组呢。就是在可视窗口的所有数据的id数组,有了这个数组我们就可以写出下面的一个逻辑,只要是在可视窗口的数据id数组发生了变化,就把之前的数据推送取消,在重新开启当前这二十条的数据推送
。
// idArr为当前在可视窗口数据id数组
function updateSubscribe(idArr){
// 取消之前二十条的数据推送
unsubscribe()
// 开启当前这二十条的数据推送
subscribe(idArr)
}
所以,现在问题就变成如何维护好这个数组了!这个是在用户滚动
的时候会发生变化,所以我们还是要监听滚动事件,虽然我们之前已经做了上面的表格滚动优化操作,我这边还是给大家用滚动事件去演示demo。言归正传,我们要获取到这个数组,就要知道有哪些数据的dom是在可视窗口中的!这里我的方法还是比较笨的,我感觉应该是有更好的方法去获取的。大家可以复制下面这个demo跑一下,打开控制台看一下打印的数组。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}
.box {
width: 400px;
height: 600px;
margin: 0 auto;
margin-top: 150px;
border: 1px solid red;
overflow-y: scroll;
overflow-x: hidden;
}
.item {
width: 400px;
height: 100px;
/* background-color: beige; */
border: 1px solid rgb(42, 165, 42);
text-align: center;
}
</style>
</head>
<body>
<div class="box" id="box">
<div class="item" id="1">
1
</div>
<div class="item" id="2">
2
</div>
<div class="item" id="3">
3
</div>
<div class="item" id="4">
4
</div>
<div class="item" id="5">
5
</div>
<div class="item" id="6">
6
</div>
<div class="item" id="7">
7
</div>
<div class="item" id="8">
8
</div>
<div class="item" id="9">
9
</div>
<div class="item" id="10">
10
</div>
<div class="item" id="11">
11
</div>
<div class="item" id="12">
12
</div>
<div class="item" id="13">
13
</div>
<div class="item" id="14">
14
</div>
<div class="item" id="15">
15
</div>
<div class="item" id="16">
16
</div>
<div class="item" id="17">
17
</div>
</div>
</body>
<script>
const oBOX = document.querySelector("#box")
oBOX.addEventListener('scroll', () => {
console.log(findIDArr())
})
const findIDArr = () => {
const domList = document.querySelectorAll(".item")
// 过滤在视口的dom
const visibleDom = Array.prototype.filter.call(domList, dom => isVisible(dom))
const idArr = Array.prototype.map.call(visibleDom, (dom) => dom.id)
return idArr
}
// 是否在可视区域内
const isVisible = element => {
const bounding = element.getBoundingClientRect()
// 判断元素是否在可见视口中
const isVisible =
bounding.top >= 0 && bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight)
return isVisible
}
</script>
</html>
这段代码其实还是很好理解的,我就给大家提两个地方比较难搞的地方。
id的获取方式
我们这里是先在每个div手动的绑定了id,然后在通过是拿到dom的实例对象,进而去获取到它的id。而在我们实际的开发工作中,基本都是使用组件的,然后是数据驱动视图的。就比如el-table,给他绑定好一个数据列表,就可以渲染出一个列表。也就是说,这一行的dom和这一行绑定的数据是两个东西,我们所获取的dom不一定就能拿到id,所以怎么获取到每一行的id也是一个问题,反正核心就是将dom和数据id联系起来,这就需要大家具体问题具体分析解决了。
如何判断是否在可视区域
判断是否在可视区域主要是通过getBoundingClientRect
函数,这个函数是可以获取一个元素的六个属性,分别是上面(下面)的这几个属性,然后就可以根据这些字段去判断是否在可视区域。
- width: 元素的宽度
- height: 元素的高度
- x: 元素左上角相对于视口的横坐标
- y: 元素左上角相对于视口的纵坐标
- top: 元素顶部距离窗口的距离
- left: 元素左侧距离窗口左侧的距离
- bottom: 元素底部距离窗口顶部的距离 (等于 y + height)
- right: 元素右侧距离窗口右侧的距离(等于 x + width)
进一步优化
除了上面这些,我还做一个优化,啥优化呢?就是在vue中因为是响应式驱动,只要数据一发生变化就会触发视图更新,但是如果变化的太频繁,也会特别卡,所以我就添加了一个节流,让他一秒更新一次,但是这个优化其实是有一丢丢问题的。为什么呢,比如以一秒为一个时间跨度,他本来是在0.5秒更新的,但是我现在把他变成了在1秒更新,在某种意义上他就并不实时了。但是做了这个操作,性能肯定是比之前好得多,这就涉及到一个平衡了,毕竟鱼和熊掌不可兼得嘛。因为保密协议巴拉巴拉的,我就给大家写了个伪代码。
// 表格绑定的值
const tableData = ref([])
// 表格原始值
const tableRow = toRaw(tableData.value)
// 定时器变量
let timer
// 更新函数
const updateData = (resData) => {
// resData是websocket服务端推送的一个数据更新的数组,我们假设resData这个数据结构是下面这样
// [{
// id: "",
// data: {}
// },
// {
// id: "",
// data: {}
// }]
resData.forEach(item => {
// 更新的id
const Id = item.id
// 先去找tableRow原始值中找到对应的数据
const dataItem = tableRow.findIndex(row => row.id == Id)
// 更新tableRow原始值数据
dataItem[data] = item.data
})
if(!timer){
timer = setTimeout(()=>{
// 这个时候才去更新tableData再去更新视图
tableData.value = [...tableRow]
timer = null
},1000)
}
}
我大概的讲一下这段代码在干嘛。假设我这个表格绑定的值是tableData
,我用vue3的toRaw
方法,将这个拷贝一份形成一个没有响应式的值为tableRow
。这里提一嘴,toRaw
这个方法并不是深拷贝,他只是丧失了响应式了,改变tableRow
的值,tableData
也会发生变化但是不会更新视图。updateData
大家可以看成封装好的更新方法。传入的参数为服务端推送的数据,它是一个全是对象的数组。这段代码的核心就是服务端推送的数据先去更新tableRow的值,再利用节流实现一秒更新一次tableData的值。
toRaW
这里再给大家分享一个知识,大家可以看到我去更新的tableData
的值的时候是新创建了一个数组,然后用...扩展运算符
去浅拷贝。这是因为如果直接用toRaw后的对象去赋值给响应式的的对象,这个对象也会丧失响应式。但是如果只是某一个属性单独赋值是不会丧失响应式的
单独属性赋值
import { reactive, toRaw } from 'vue';
const state = reactive({ count: 0 });
const rawState = toRaw(state);
// 将原始对象的属性值赋给响应式对象的属性
state.count = rawState.count;
const increment = () => {
state.count++;
};
increment();
console.log(state.count); // 响应式更新,输出1
整个对象赋值
import { reactive, toRaw } from 'vue';
const state = reactive({ count: 0 });
const rawState = toRaw(state);
// 错误地用原始对象替换响应式对象
state = rawState;
// 这会导致错误,因为不能重新赋值响应式对象本身,并且响应式关联被破坏
并不是深拷贝
import { reactive, toRaw } from 'vue';
const nestedObj = reactive({
a: 1,
b: {
c: 2
}
});
const rawObj = toRaw(nestedObj);
// 修改原始对象的属性
rawObj.a = 10;
console.log(nestedObj.a); // 输出10,说明不是深拷贝,因为修改原始对象影响了响应式对象
总结
其实整体来看,并没有做一些高大上的操作,但是性能确实好了很多很多。去年面试的时候被问到性能优化总是会很慌张,因为我一直觉得的性能优化特别牛逼,我也确实没有做过什么性能优化的操作,只能背一些八股文,什么防抖节流,图片懒加载,虚拟列表......然后我想表达啥呢,因为我觉得肯定很多人面试的时候很怕被问到性能优化,特别是现在在准备秋招春招啥的,因为我也刚毕业三四个月,我包有体会的。所以我想告诉大家的意思的,性能优化并没有这么高端,只要是能让你的项目变好的,都是性能优化。实在不行,你就好好看哥们的写的东西,你就说这个表格是你写,反正面试不就是糊弄面试官的吗,自信!
来源:juejin.cn/post/7430026536215281698
6个月时间,我放弃一人企业又去打工了
本来没有心情写的,但是跟读者承诺过三个月后再汇报……
因为被欠薪,在IT圈子规规矩矩上班12年,年龄34岁的我,决定先不找工作。今年4月份打算在家里自己搞些事情,并且还发布了一篇《IT男的一人企业》以明志。随后,“TF男孩”修改昵称为“IT男的一人企业”,斗志昂扬地高喊自由之歌:打工一眼看到头,创业灵活又自由……
7月份,过去三个月了,我写了一篇总结《那个不找工作在家干一人企业的程序员,现在怎么样了?》,汇报了我的收入和近况。其实那三个月,已经是巅峰。对于我来说,一个月挣1万多,已经是到顶了。
到8月份时,收入减至一千,9月时,低至几百。让我放弃的主要原因没有别的,就是收入。本身我的太太不支持我辞职,即便我有很多理由,比如这个行业可能是青春饭,要早考虑以后,早死早投胎,等人家失业,我已经探索出新路子了。而她的想法却是既然是青春饭,那就趁着青春多吃几年,等到吃不动再探索。
为了实现自己的想法,我还专门回老家,问父母要了些钱,这些钱等同原来两个月的工资。这样,我才有了底气去放心做一人企业。
后来的事情,如上文总结里所说,其实还可以。凭借我多年写文章的影响力,我主打一个廉价可行AI技术方案。就是写很多小老板想实现的,但是他们问自己的技术部,技术部说不可能实现。一般小企业技术能力一般,另外也懒惰。而这事在我这里成本只需要三五千。我一般会推荐他们看我的教程文章,如果抄作业他们都懒得抄。老板们就会找我做。成本低,他们也愿意试错,因为亏不了多少。而如果成了,他们的业务可能会插上飞翔的翅膀。我提供的AI方案,也确是可行,是自己验证过的适合小企业的低成本(尽量CPU)、开源、可商用、可本地化部署的项目。
8月、9月两个月孩子放暑假,我是专职带娃。没想到,带娃后根本没时间。我买了两个沙漏用于时间可视化。我跟孩子约定,她自己玩一个沙漏时间,然后我陪她一个沙漏的时间,类似于番茄工作法。这样,我就有些时间做自己想做的。但是,孩子虽然表面答应,她还是跟你捣乱。只能孩子睡觉时做些事情。
也是这个时候,家庭矛盾开始激化。主要还是没钱。但是却体现在我地拖得不干净,饭做得不好吃,为什么孩子又在看动画片……没办法,受不了鸡飞狗跳,提高厨艺根本没用,获取持续稳定的收入,是解决一切问题的关键。
9月中期开始找工作,很快就找到了。现在找AI算法岗位工作并不难。但凡招人的企业,就算是三五个人的小公司,它也要招一个AI工程师,为的就是盘活老业务,讲好智能故事。
我入职的是一个500多人的传统企业,成立20多年了,这里IT技术十来人。除了上线前后,不加班,当然工资也不高。
另外,上家公司的欠薪至今也没有追回来。去年9月开始欠薪,今年年初仲裁判决公司支付我拖欠的工资。然后公司不服仲裁判决,向法院起诉,我已收到传票。看到公司的起诉状我也无语了,他请求法院判定不支付工资xx.1元,应支付工资是xx.2元。改了一个数,还给我涨了钱,继续走一审二审。一个周期又是半年起步。目前公司面临几百例强制执行和几十个限制高消费,都是劳动纠纷的,已经启动破产。而根源则是老板盲目扩张,欠薪几千万了,还未停止大规模招人。他总想着自己振臂一呼,大量资金会涌入。
从去年9月到今年9月,我已经一年没有工资收入了。这也是经济压力大的原因。换哪个普通家庭都受不了。这么看,打工是有风险的。但是,打工的风险可控。最多损失1~2个月工资。当然,这得是你足够机灵的情况下。上家公司,被欠八个月工资的也有,甚至有员工私人带款给公司花。里面的操作不细说。
不打工的风险我也试过。就是上面写的,可能持续没有收入,是消耗存量等待增量。
我以前觉得,对于个人的事业,可能只有拼进全力才能稍微有点效果。比如我要验证一件自己的想法,上班时可能需要半年。但是当我全职去做时,可能半个月就验证出来了。看得鸡汤多了些,说什么行业竞争很激烈,时间就是机会,人家专职团队都干不成,凭啥你业余时间搞就能成功。所以要全身心投入创业。
而我的老领导说,如果你有想法,上着班也能把一个事业干成。如果你没有想法,就算专职干也没啥用。有些事情是需要孵化周期的,还有些事情是需要等待的。长期策略更适合普通人。我感觉还是他的话比较温和与现实。
事情就是这么个事情。昵称又改回了“TF男孩”。TF是TensorFlow的简称,因为我学AI是从tensorflow开始。以后也没有啥一人企业了。我乐意变回那个普通的男孩。
来源:juejin.cn/post/7424915312166600755
离职后的这半年,我前所未有的觉得这世界是值得的
大家好,我是一名前端开发工程师,属于是没有赶上互联网红利,但赶上了房价飞涨时代的 95 后社畜。2024 年 3 月份我做了个决定,即使已经失业半年、负收入 10w+ 的如今的我,也毫不后悔的决定:辞职感受下这个世界。
为什么要辞职,一是因为各种社会、家庭层面的处境对个人身心的伤害已经达到了不可逆转的程度,传播互联网负面情绪的话我也不想多说了,经历过的朋友懂得都懂,总结来说就是,在当前处境和环境下,已经没有办法感受到任何的快乐了,只剩焦虑、压抑,只能自救;二是我觉得人这一辈子,怎么也得来一次难以忘怀、回忆起来能回甘的经历吧!然而在我的计划中,不辞职的话,做不到。
3 月
在 3 月份,我去考了个摩托车驾-照,考完后购买了一辆摩托车 DL250,便宜质量也好,开始着手准备摩旅。
4 月份正式离职后,我的初步计划是先在杭州的周边上路骑骑练下车技,直接跑长途还是很危险的,这在我后面真的去摩旅时候感受颇深,差点交代了。
4 月
4.19 号我正式离职,在杭州的出租屋里狠狠地休息了一个星期,每天睡到自然醒,无聊了就打打游戏,或者骑着摩托车去周边玩,真的非常非常舒服。
不过在五一之前,我家里人打电话跟我说我母亲生病了,糖尿病引发的炎症,比较严重,花了 2w+ 住院费,也是从这个时候才知道我父母都没有交医保(更别说社保),他们也没有正式、稳定的工作,也没有一分钱存款,于是我立马打电话给老家的亲戚让一个表姐帮忙去交了农村医保。所有这些都是我一个人扛,还有个亲哥时不时问我借钱。
说实话,我不是很理解我的父母为什么在外打工那么多年,一分钱都存不下来的,因为我从小比较懂事,没让他们操过什么心,也没花过什么大钱。虽然从农村出来不是很容易,但和周围的相同条件的亲戚对比,我只能理解为我父母真的爱玩,没有存钱的概念。
我可能也继承了他们的基因吧?才敢这样任性的离职。过去几年努力地想去改变这个处境,发现根本没用,还把自己搞得心力交瘁,现在想想不如让自己活开心些吧。
5 月
母亲出院后,我回到杭州和摩友去骑了千岛湖,还有周边的一些山啊路啊,累计差不多跑了 2000 多公里,于是我开始确立我的摩旅计划,路线是杭州-海南岛-云南-成都-拉萨,后面实际跑的时候,因为云南之前去过,时间又太赶,就没去云南了。
6 月
在摩友的帮助下,给摩托车简单进行了一些改装,主要加了大容量的三箱和防雨的驮包,也配备了一些路上需要的药品、装备,就一个人出发了。
从杭州到海南这部分旅行,我也是简单记录了一下,视频我上传了 B 站,有兴趣的朋友可以看看:
拯救焦虑的29岁,考摩托车驾-照,裸辞,买车,向着自由,出发。
摩托车确实是危险的,毕竟肉包铁,即使大部分情况我已经开的很慢,但是仍然会遇到下大雨路滑、小汽车别我、大货车擦肩而过这种危险情况,有一次在过福建的某个隧道时,那时候下着大雨,刚进隧道口就轮胎打滑,对向来车是连续的大货车,打滑之后摩托车不受控制,径直朝向对向车道冲过去,那两秒钟其实我觉得已经完蛋了,倒是没有影视剧中的人生画面闪回,但是真的会在那个瞬间非常绝望,还好我的手还是强行在对龙头进行扳正,奇迹般地扳回来且稳定住了。
过了隧道惊魂未定,找了个路边小店蹲在地上大口喘气,雨水打湿了全身加上心情无法平复,我全身都是抖的,眼泪也止不住流,不是害怕,是那种久违地从人类身体发出的求生本能让我控制不住情绪的肆意发泄。
在国道开久了人也会变得很麻木,因为没什么风景,路况也是好的坏的各式各样,我现在回看自己的记录视频,有的雨天我既然能在窄路开到 100+ 码,真的很吓人,一旦摔车就是与世长辞了。
不过路上的一切不好的遭遇,在克服之后,都会被给予惊喜,到达海南岛之后,我第一次感觉到什么叫精神自由,沿着海边骑行吹着自由的风,到达一个好看的地方就停车喝水观景,玩沙子,没有工作的烦扰,没有任何让自己感受到压力的事情,就像回到了小时候无忧无虑玩泥巴的日子,非常惬意。
在完成海南环岛之后,我随即就赶往成都,与前公司被裁的前同事碰面了。我们在成都玩了三天左右,主要去看了一直想看的大熊猫🐼!
之后我们在 6.15 号开始从成都的 318 起始点出发,那一天的心情很激动,感觉自己终于要做一件不太一样的事,见不一样的风景了。
小时候在农村,读书后在小镇,大学又没什么经济能力去旅行,见识到的事物都非常有限,但是这一切遗憾在川藏线上彻底被弥补了。从开始进入高原地貌,一路上的风景真的美到我哭!很多时候我头盔下面都是情不自禁地笑着的,发自内心的那种笑,那种快乐的感觉,我已经很久很久很久没有了。
同样地,这段经历我也以视频的方式记录了下来,有兴趣的朋友可以观看:
以前只敢想想,现在勇敢向前踏出了一步,暂时放下了工作,用摩托跑完了318
到拉萨了!
花了 150 大洋买的奖牌,当做证明也顺便做慈善了:)
后面到拉萨之后我和朋友分开了,他去自驾新疆,我转头走 109 国道,也就是青藏线,这条线真的巨壮美,独自一人行驶在这条路,会感觉和自然融合在了一起,一切都很飘渺,感觉自己特别渺小。不过这条线路因为冻土层和大货车非常非常多的原因,路已经凹凸不平了,许多炮弹坑,稍微骑快点就会飞起来。
这条线还会经过青海湖,我发誓青海湖真的是我看到过最震撼的景色了,绿色和蓝色的完美融合,真的非常非常美,以后还要再去!
拍到了自己的人生照片:
经历了接近一个半月的在外漂泊,我到了西宁,感觉有点累了,我就找了个顺丰把摩托车拖运了,我自己就坐飞机回家了。
这一段经历对我来说非常宝贵,遇到的有趣的人和事,遭遇的磨难,见到的美景我无法大篇幅细说,但是每次回想起这段记忆我都会由衷地感觉到快乐,感觉自己真的像个人一样活着。
这次旅行还给了我感知快乐和美的能力,回到家后,我看那些原来觉得并不怎么样的风景,现在觉得都很美,而且我很容易因为生活中的小确幸感到快乐,这种能力很重要。
7 月
回到家大概 7 月中旬。
这两个多月的经历,我的身体和心态都调整的不错了,但还不是很想找工作,感觉放下内心的很多执念后,生活还是很轻松的,就想着在家里好好陪陪母亲吧,上班那几年除了过年都没怎么回家。
在家里没什么事,但是后面工作的技能还是要继续学习的,之前工作经历是第一家公司用的 React 16,后面公司用的是 Vue3,对 React 有些生疏,我就完整地看了下 React 18 的文档,感觉变化也不是很大。
8、9 月
虽然放下了许多执念,对于社会评价(房子、结婚、孩子)也没有像之前一样过于在乎了,但还是要生活的,也要有一定积蓄应对未来风险,所以这段时间在准备面试,写简历、整理项目、看看技术知识点、刷刷 leetcode。
也上线了一个比较有意义的网站,写了一个让前端开发者更方便进行 TypeScript 类型体操的网站,名字是 TypeRoom 类型小屋,题源是基于 antfu 大佬的 type-challenges。
目前 Type Challenges 官方提供了三种刷题方式
- 通过 TypeScript Playground 方式,利用 TypeScript 官方在线环境来刷题。
- 克隆 type-challenges 项目到本地进行刷题。
- 安装 vscode 插件来刷题。
这几种方式其实都很方便,不过都在题目的可读性上有一定的不足,还对开发者有一定的工具负担、IDE 负担。
针对这个问题,也是建立 TypeRoom 的第一个主要原因之一,就是提供直接在浏览器端就能刷题的在线环境,并且从技术和布局设计上让题目描述和答题区域区分开来,更为直观和清晰。不需要额外再做任何事,打开一个网址即可直接开始刷题,并且你的答题记录会存储到云端。
欢迎大家来刷题,网址:typeroom.cn
因为个人维护,还有很多题目没翻译,很多题解没写,也还有很多功能没做,有兴趣一起参与的朋友可以联系我哦,让我一起造福社区!
同时也介绍下技术栈吧:
前端主要使用 Vue3 + Pinia + TypeScript,服务端一开始是 Koa2 的,后面用 Nest 重写了,所以现在服务端为 Nest + Mysql + TypeORM。
另外,作为期待了四年,每一个预告片都看好多遍的《黑神话·悟空》的铁粉,玩了四周目,白金了。
现在
现在是 10 月份了,准备开始投简历找工作了,目前元气满满,不急不躁,对工作没有排斥感了,甚至想想工作还蛮好的,可能是闲久了吧,哈哈哈,人就是贱~
最后
其实大多数我们活得很累,都是背负的东西太多了,而这些大多数其实并不一定要接受的,发挥主观能动性,让自己活得开心些最重要,加油啊,各位,感谢你看到这里,祝你快乐!
这是我的 github profile,上面有我的各种联系方式,想交个朋友的可以加我~❤️
来源:juejin.cn/post/7424902549256224804
HR的骚操作,真的是卧龙凤雏!
现在基本已经对通过面试找工作不抱啥希望了。
有时候面试官和我聊的,还没有前面hr小姐姐和我聊的多,我一听开场白就基本知道就是拿我走个过场,没戏!
现在的面试流程都是人事先和你聊半天,没什么硬伤大坑才会放你去见面试官。
二零一几年那会可不是这样,第一次的详聊都是直接业务层,业务的人觉得你ok,你再和人事沟通,定个薪资就完了。
13年的时候我在一家外企,三千的月薪。当时我一个小目标就是月薪过五千。
可别笑,13年的月薪五千,那还是能勉强算上一个小白领的。
我就老琢磨着升职加薪。但眼下的公司规模小,人员基本不扩增,不流通,我就想跳槽了。
当时我同时面了AB两家外资游戏公司。都过了业务层的面试,只剩和人事定薪资。
我给A公司报价5500,给B公司报价6000,因为我知道B公司刚来国内开拓业务,属于扩张期。
这时候,A公司HR的骚操作就来了,她说:“嗯,5500嘛,有难度,但不是不可能,我可以帮你跟老板争取。”
然后又问我:“你已经从现在的公司里面离职了吗?”
我说:“还没呢,我想先把下家定了。”
她就说:“哎呀,那有点难办,你得先从现在这家公司离职,我得确保我帮你争取下来后,你不会鸽我,不然我没法和老板交代,要不你先把那边离职办了吧。”
我说:“那我再考虑考虑吧。”
然后没过两天,我收到了B公司的offer。人家都没还价,直接定了6000,我就开始走离职手续。
这时候A公司的HR又出来问我,你从现在的公司离职了吗?
我说离了,她说你给我看看离职证明,我就拍照给她看离职证明。
然后她连假装让我等一会儿,说自己去问一下老板的戏都不演了,直接秒回说:“我帮你问了老板了,老板说不行,5500给不了,最多给到4500,要不你先入职呢,后面有机会提加薪的。”
瞬间给我恶心的,怎么会有这么恶心的人事!先把你忽悠离职,然后翻脸不认人,可劲往下砍价,为了公司的KPI,自己做人的脸都不要了。
我当时就觉得这样的人真傻,就算我认了4500的杀价入了职,我把和她的对话记录公司群里一发,老板会怎么看她,同事会怎么看她。
咱做人得有底线呀,你用这种脏办法帮公司省那几百块钱,还把自己的名声信誉搭进去了,真的值得吗?
后来我在入职B公司差不多半年后,传来了A公司解散倒闭的消息,我心里还暗爽了一把,幸亏当年没进那个火坑。
但半年后,我所在的B公司也解散了。
2013年那是一个手游刚兴起的疯狂年代,数不清的大小公司起家,创业,失败,解散,换批核心班子,再起家,再失败,浮浮沉沉,我也成了疯狂年代下的沧海一粟。
来源:juejin.cn/post/7426685644230213643
37 岁程序员被裁日记
37 岁被裁员,老婆即将临盆,求职却毫无音讯,我经历了人生中最艰难的时刻。我将这段时间每天发生的故事写进了日记,既是对未来生活的警醒,也希望能给面临相似困境的同伴们带来一些启示——无论多么艰难,绝不能放弃希望。
2024.8.27 (背景)
我在 2021 年九月底入职了某外企(以下简称 CP),合同三年。入职后,才知道这家企业有两个“潜规则”:
- Senior 岗(我就是这个岗)如果三年内不能晋升,原则上就滚蛋
- 每年大约有 10% ~ 15%的 PIP(绩效优化)比率
我躲过了三年的 PIP,但是没机会晋升。按惯例,三年合同到期前,公司会提前一个月通知我不再续约。只是这个是潜规则,所以我一直心存侥幸,盼着已经是 8 月底了,如果没有通知我,就可以躲过这一难。
但是——真的是卡着点——今天我还是被老板通知不续约了,他的话很平静:“有一个对你不是很好的消息要告诉你,director 想换一个更好的前端……”。
其实从去年年底开始我就意识到了不对劲,但是一直心存幻想,想在最后的 promotion 的机会里再拼一次;即便没有晋升成功,也想让老板感觉到我有点价值——至少能保住饭碗。然后上半年一个人徒手搭建了新的 web 技术栈,一举减少了大几十天的人力成本;部门上线新项目,45%工作是前端相关,我带着四五个不大会写 react 的后端同事紧赶慢赶完成了交付;这期间我参加了公司的 hackathon 项目,还获得了优胜;另外还在部门里分享里数次 Lightning talks,还有两个专利的输出。总以为这些努力至少不会让老板 nominate 我的时候太觉难堪吧。可是,nomination 还是与我无关,他的回复是“你没达到我的 bar,我是不会 nominate 你的;你如果觉得有续约风险,你应该一早让 director 来 nominate 你”。冷冰冰的,没有一丝三年共事的情分。确实是我太幼稚了,我以为我和老板的关系是:我给你干活,你帮我实现职业目标;但是这个老板的想法是,我是公司派来监督你的人,你不合格,我就替公司换人。
后来我提前问他续约的事,他信誓旦旦地说“现在招个 L6-2 的前端不容易(公司政策只招比我级别高的人),我会同意让你续约的”,我又问他这个谁说了算,他说他说了算。但是我一点都不信。过了一个月我们部门 L6-1 的前端 HC 赫然在目(L6-1 需要 VP 特批才能要到 HC);但是他依旧装着什么都不说,非得等到 8 月底最后一次 1 on 1 的机会(每两周我和他有一次 1 on 1),他才告诉我:“有一个对你不是很好的消息要告诉你,director 想换一个更好的前端……”
他一直知道我的困难: 外面行情很差、我年纪也不小了、老婆即将临盆。但是最后的谈话,至始至终都在甩锅给上级;切记切记,不要相信老板的说辞,不要对公司抱有幻想,更不要心存侥幸。和他的谈话结束后,我就与几个要好的同事做了简短的告别;之后一个月基本不会去厂里了,毕竟已经没有意义了。
2024.8.28
早上还是有一些同事找我处理业务上的事,我私信回了它们,不再参与代码了。他们虽然有点震惊,最后也给了我祝福。一些同事很讲义气的,给我推了 HR 和猎头。
- 早上,我联系了一个猎头,告诉我蚂蚁国际有 HC,但是我还没准备中文版的简历,也没开始背八股,所以暂时没投递。
- 中午,我联系了途虎的 HR,我直言不讳地问了是否卡年龄。我感觉她犹豫了,37 岁却是太大了。
- 下午,我和 AWX 的前同事喝了杯咖啡。他是这里的后端开发,帮我询问了前端的面试流程。听说 leetcode 难度可能到 hard;而且很难躲过我最讨厌的八股文。对于项目面,他的经验是去别的厂里多历练历练,能把项目吹得滴水不漏才好。
然后回家的时候,CP HR 通知我可以讨论赔偿的事宜了。真的是太感人了:昨天老板通知,今天 HR 就来了,太急了吧。我又联系了之前被赶走的同事,他告诉我公司还是很抠的:是个小 n,不是大 N。哎……
2024.8.29
今天早上陪老婆去做了产检,孩子的预产期是十月初,而我的工作合同将在 9 月 29 日正式结束。希望宝宝能带来好运,出生时爸爸已经为他准备好了奶粉钱。顺带一提,我推迟了和法务部门第二天的会面,关于赔偿的问题暂时不急,还是找些时间专心学习吧。
午饭后,我更新了一版简历并发送给了途虎的 HR,但遗憾的是一天都没有收到回复。下午,隔壁邻居开始装修,噪音让我无法静心学习。于是,我选择去了社区图书馆,这里不仅可以免费看书,还有空调,虽然没有网络,但说实话,是一个避暑的好去处。
图书馆里主要是一些做作业的学生以及一位白发苍苍的老奶奶,她用放大镜整整看了一下午的报纸。而我,也趁机读完了一整本《图解 HTTP》。不过,为了确保面试顺利,还得专门背诵一些关于 HTTP 的面试题。
2024.8.30
早上,我给蚂蚁国际的猎头发了简历,时间不等人,无论如何我也得硬着头皮上了。她给了我几道历年真题,但都是大路货,没什么用。
下午的时候,base 在韩国的一个同事来问我现状,推荐我可以看看 Booking 和 AWX。Booking 我知道已经没有 HC 了,AWX 还想再准备一下。虽然没什么帮助,但是哥们这么老远还特地来关心我,还是很讲义气的,感动……
2024.8.31
今天礼拜六,对于找工作的人来说,没有双休日。我又去了社区图书馆,这里每天都开放。道听途说某些公司的前端可能会有 UI 设计面,我看完了《用户体验要素》。不过,囫囵吞枣,应该对面试起不了太大作用。说实在这个阶段最难的是:不知道自己缺什么;感觉什么都要准备,但是什么都不够。
老婆比我更加焦虑了,大着肚子,看她也没睡好。然后催着我快点给 AWX 投简历。说实在,它们家是现在我能发现难得有我这种 HC 的外企了,第一面就是它们家很担心凉凉了。
2024.9.01
今天又是学习的一天。早上第一件事是背六级单词;每天 20 个单词,累计打卡第 254 天。在 CP 这些年,最大的收获还是上英语课——Speexx。两次半年的口语培训,让我有了很大的成长;虽然和 Native English speaker 相比还是有本质差距的,但是对国内有英语要求的公司,这些年我不再那么心虚了。
中午时分,老婆在刷 boss 直聘的时候,看到 Paypal 放出了 HC。虽然这样的公司一次可能就放一两个 HC 吧,但还是让我阴霾的心闪现出一丝光亮:“金九银十”要来了?我马上托同事帮我打听内部情况——他老婆正好在 paypal 当前端,应该周一才能知道详情。他又提醒我可以海投国内大厂,比如美团、字节、腾讯什么的,不要总是想着小而美的外企。他去过很多公司,身经百战;而我九年就跳过一次槽,心中还有”面试恐惧症“。他的话一下把我点醒了——我现在的工作就是“面试”,没什么可患得患失了。
2024.9.02
早上本来要陪老婆去产检的,但是被她厉声喝止了:让我专心在家改简历——把 AWX 的简历交了。这种糟糕的心态,不可避免地传递到了老婆身上了。于是我又改了版简历,给 AWX 的师兄交了过去;他帮我指出了几处拼写错误——幸好师兄心善,真的是太丢脸了。不到一个小时,HR 就加我微信了。我想缓缓,就把 HR 的电话面约到了第二天。(真没出息,竟然三年没面过试,HR 面都紧张成这样了)
下午的时候,又一个同事来找我;他也快到期了。从我这里得到的消息,让他心里瞬间凉了一截。看样子大家都是差不多的情况:到了这个岁数,能不能延续职业生涯,是绝大多数人的心头病了。
2024.9.03
早上十点和 AWX 的 HR 视频了半小时。他们家没有 behavioral 面,就是随便介绍一下自己的期望薪资,以及面试流程。
- 一面 coding:leetcode,手撸 promise,实现 UI 组件都有可能(跟没说一样)
- 二面设计面:应该不是市面上正统的系统设计面
- 三面 Line manager:聊项目
- 四面 VP:看他心情,可能是人生面吧
其实没啥有价值的信息,并没有缩小我的准备范围。我还特地花了 79 块钱开通了 leetcode 会员,就是为了看看真题;结果就五题,亏大了。
吃完晚饭,四个同事几乎同时向问我打听现状,并表示有需要可以帮助内推他们之前的公司。感觉他们应该是有个小群正好说到我的事情吧。虽然暂时帮不到什么忙,但还是很感激的。说实在,我们的老板工作中已经有点去人格化了,但是他手下的人还都是温情默默的。
2024.9.04
今天,新前端正式加入公司。本来预计他是两周后才上班;可能真的是缺人手吧,催着他提前入职了。爽文小说里“裁员裁到大动脉”的剧情现实中说很难发生;我离开的一个礼拜里,部门里也仅仅发生了一个 minor 的 incident。可能对于老板来说,他又一次成功地实现了“以旧换新”。至于同行,也没有恨意,祝愿他在新公司一切顺利;并期望自己也能成功找到下一份工作。至于不开心的事,就让它随风而去吧;收下伤感,继续背八股了。
2024.9.05
今天终于参加了第一次面试——zoom 面,这两天刷了十几道 leetcode 题。但是面试的时候,前五十分钟在聊项目,最后十分才做了到算法题;跟预期差距也太大了。说实在聊项目的细节我还有所欠缺,本想 coding 和设计面后再恶补一下,没想到第一面就来了。复盘一下我自己的缺陷:
- 讲项目的时候,应该分享个画板,给面试官更好的体验
- 微前端这块写到了简历里,但是八股知识不够扎实
- UI 这块,design system 得预习一下
当然,上述缺陷的本质问题是面试经历太少了,还没把自己的各个方面都武装过一遍。算了,放平心态,即便是挂了,至少也攒到了些经验。
2024.9.06
一夜无眠,还在想昨天的那道算法题。虽然不难,但是我漏掉了负数的判断,不知道面试官有没有注意到。这种心态真的很糟糕,一直提醒自己:这仅仅是一次面试而已,不能影响后面的准备。早上十点,我主动问了 HR 一面结果;等了一会儿,她回答我可以准备二面了。一下子心态平了很多。
之后我看了会儿阮一峰的博客,他的博客有个《谁在招人》的板块,但很可惜没有什么理想的公司。不过,他介绍了一个神奇网站,叫轻松游牧:一个远程工作聚合网站,每天从网上收集支持国内远程的岗位。我听说有一种面向国外的远程外包,也是通过类似网站招人的;每年给个固定的包工费,但是价位会比国内的外包高很多。再过段时间如果还是找不到工作的话,得试试这个模式了。
2024.9.07
礼拜六继续图书馆走起,坐在我傍边的依旧是同一帮爷爷奶奶们——看年纪他们应该都有八十好几了;他们真的是全年无休,人手一个放大镜,一直沉静在自己的书海里。上海确实是一个不一般的地方,这种人文的气质很是让我这种“乡下人”鼓舞的。下午看完了《微前端实战》——豆瓣 8.8 分,应该是评分太少,有点虚高了。不过,读完还是让我开阔了许多眼界,它顺便还提到了点 design system,与我上一次面试所欠缺的知识点不谋而合;希望下一轮面试能帮到我一点吧。回忆起这三年,我还是买了了挺多书的(公司每季 200 元图书福利),但可惜真正读完的没几本;现在临走了,却逼着我看了好几本,真是有点好笑的。
2024.9.08
今日 leetcode:977,1589(之后把每天 leetcode 题也记一下)
早上我一般都是先背英语单词,然后两道 leetcode。今天,我特意试了一下 leetcode 里 Paypal 原题:超过 1/4 是 hard 题;即便是 medium 题,也是包着算法外衣的数学题。很好奇,这个公司真的这么卷算法吗?
下午主要在看设计题,说实在前端系统设计很小众,网上的资料也就《News Feed》和《autocomplete》两题。我三年前进 CP 厂的时候面过一次设计题——《前端的 web log》,那时答得稀巴烂。这次比之前多看了些资料,只能说稍微了解了些套路;但从来没有实战过,说实在比之 coding 面更加没有信心。
2024.9.09
今日 leetcode:2181, 1969
不知道大家在网上是怎么找面经的,我之前主要在看准网找留言。但是今天登录看准网后,出了个弹窗,意思大致是它们家要关门了。哎,真是让人唏嘘不已;猜测写这个弹窗的前端小哥应该也已经被裁了吧。后来,我又去看了一亩三分地。这里的面经谈到这家设计题主要是 OOD 的题目,和传统的 system design 不同。所以白天我一直在油管上看某些高频题目的设计视频。但心里也明白,这些都是盲猜;很可能是浪费一天宝贵时间。说实在,面试也是信息检索的一种考验,要是又靠谱的信息渠道,面试成功率就能高很多了。
2024.9.10
今日 leetcode:2554、2559
下午 2 点二面。果然每次面试内容和预期的都不一样。所谓的设计面,根本和设计无关;事实上是聊项目+coding。项目面,前几天看的《微前端实战》倒是给了我点解答问题的灵感;但是 coding 表现很不好,时间还剩下十几分钟,我怕来不及,竟然选择纯口嗨——给面试官口头解释如何实现。有点后悔了,面试结束我自己又试了一下,其实这道题对我来说时间是充裕的。哎,还是太紧张了,策略错误……所幸,一个小时后,HR 邀我两天后三面;谢谢面试官网开一面。
2024.9.11
今日 leetcode:1999、2380
今天主要还是集中精力准备第二天的面试,虽然被告知是项目面,但是我心里还是没底——之前的两面都是 surprise。不过事已至此,我也只能全力总结项目了(再让我背八股,我也来不及了)。二面的经验让我想到了一个面试策略:就是事先把项目的架构图画好,面试的时候直接对着架构图讲业务;有点类似于 UI 设计人员在面试时会展示自己作品集的那个意思。我面经不足,不知道大家是不是都这么干的。
2024.9.12
今日 leetcode:2524、2576
上午 11 点 Line Manager 面,他让我聊聊个人最得意的项目;昨天画的架构图倒是派上了点用场。但是涉及到更深的问题解决方案,我还是没回答上来。老板一般想知道候选人的技术边界在哪里,被问懵也很正常。只是我个人有点焦虑,体感很差。面试结束后一直放不下心;HR 没联系我,我也心虚不敢主动问;真想“给个痛快”算了。
上午有面试,老婆没跟我说自己一个人去了产检;然后怕影响到我,一直到面试结束才告知:羊水有点少,医生让她下午去住院。我听完脑子一懵,很懊悔最近让老婆担惊受怕了。吃完午饭我就载她去了住院部,医生说先观察两天,如果羊水继续减少,娃娃必须提前出生。下午两边的妈妈都来了,老婆让我先回家继续学习,家里的事情不要但心,瞬间泪奔了。
2024.9.13
今日 leetcode:256、265
又是一夜未面,妻子、孩子、工作,反复地在脑中回荡。早上我向 HR 问了结果,但是她迟迟不回,只能干等着。谁叫我在“求职”,而不是在"应聘“呢?中午时分终于得到 HR 回复,可以去第四面了;但是大老板中秋后才有空——这一等又得是一个礼拜。最近我又投了几家,但是都没人鸟我;说实在,在这个时间点,它是我唯一的希望了。
之前问我签约事宜的同事好像成功续约了;我也很替他高兴。他告诉我,他直线老板还是很有人情味的:看到他三年期将至就 nominate 他了。怎么说呢,每级老板确实都有流动率指标,但事实上也有卡 bug 的方法的:比如正好在 promotion cycle 里,一般都能续约;虽然晋升渺茫,但是至少能保住饭碗。只可惜我没这个运气。
2024.9.14
今日 leetcode:2390、2056
老婆还在住院,所幸今天测羊水,比之前高了一点;但是还需要观察,至少住院 4 天才能回家。她住在了 40 一天的五人病房里,旁边娃娃晚上一直哭闹,她也睡不好觉。我很心疼她,想让她换到 4000 的单人病房里。但是她执意不肯,说“4000 一晚,我什么时候生不知道的,这得花多少钱?我的事你别操心了。”有妻如此,夫复何求?
我继续回家学习;上一次天天学习的时光应该是 2012 年考研的那段时间吧。年纪大了,我倒是比年轻时更静得下心了;但是身体是真扛不住了,尤其是肩颈,已经疼得我彻夜难免。颈椎好坏有个简单的测试:看你下巴能不能碰到喉结。我试图测了一下,结果脖子直接抽筋了。老婆很担心我,对我说:“即便你找到了工作,身体还能承受得住吗?三年后四十岁,再碰一次裁员,我们又该怎么办?”今天全网都在讨论延迟退休,我们都吃到了多工作三年的福利;三年又三年,每次都只能走一步算一步吧。
2024.9.15
今日 leetcode:2183、2848
今天是中秋假期第一天,我依旧保持着日常学习的节奏。但是,早上就有点偏头痛了;我一直有偏头痛的老毛病,每个月都得疼一次,尤其是焦虑的时候更明显。所幸今天没有面试,不然赶上面试当天真的是太灾难了。最近两边的妈妈帮了大忙,一个烧饭,一个照顾老婆,让我得以安心学习。在最困难的时候家里人还是最坚实的后盾,我真的很感激他们。下午稍微恢复了一点,我又在 boss 直聘里看到一家又 AI 又汽车又金融的外企有 HC;稍许翻了一下一亩三分地,感觉不差,遂 ping 了一下对方,但是 HR 应该也在过中秋节吧。还是希望全世界优秀的公司都能入驻国内,给我们这样的普通人带来更多的机会吧。
2024.9.16
今日 leetcode:1148、1473
今天好消息是老婆出院了:羊水恢复到了正常水平线,可能是前段时间喝水太少导致的。但是娃娃胎位不正,大概率要提前剖腹产;他现在才五斤左右,稍显轻了点,希望能抓紧最后一个礼拜努力长点身体。一切都开始变得好起来了,愿老婆和娃娃都能健康平安。
最近一直在做 leetcode 上 P 家的题目,真的是太难了——上来就三维动态规划。说实在我年轻时也不见得做得出来,更何况现在呢?帮我内推的同事告诉我,我的简历还在筛选阶段;这个岗位要求比较高,可能前面有几百个人排着队。我又看了一下他们家招聘页,只有这个岗位有 HC,根本没有其他 low 一点的选择了。
下午,在 Linkedin 上看到某四大有 HC,但是马上又在朋友圈看到他们家今天大裁员;简直了,这真的是一个魔幻的时代。
2024.9.17
今日 leetcode:146、157
今天是中秋佳节,网络上热议着月饼销量大减的现象。由于我个人对月饼并无太大喜好,因此购买月饼的念头从未在我脑海中浮现。然而,下午与老婆漫步时,我们恰巧路过一家香气扑鼻的烘焙店。我突然意识到,尽管与老婆共度了这么多时光,我却未曾询问过她是否有品尝月饼的愿望。在我的印象中,她一直是个对食物颇为挑剔的人。今年,她仅仅吃了点单位食堂提供的月饼。于是,我轻声问道:“要不要买点月饼尝尝?”她微笑着回答:“这里的月饼口感挺不错的,不过明天就打三折了,我们到时候再来买吧。”她的话让我瞬间意识到,这两天我或许对她的关心有所疏忽。我们携手走过了七个年头,我依稀记得七年前的她,还是个充满青春活力、略带中二气质的大学毕业生。而如今,我们都已踏入中年,面对生活的种种变迁,我们不得不学会更加成熟、稳重地应对。
2024.9.18
今日 leetcode:282、2332
明天就是四面了,说实在大老板面其实也准备不了什么。听 HR 说要英文自我介绍,想了一下好像之前也没像样准备过,就打算今天花一些时间背一下。Kimi 倒是挺好用的,把简历 pdf 上传给它,让它生成英文自我介绍,一下就出来了。我把自己背的内容录下来听了一下,这个重音和停顿还是非常的 Chinglish;英语还是差太多了,希望明天能顺利一点吧。很多人说以后有同声传译软件了就不需要学英语了;但是我觉得英语作为一种删选机制还是会长期存在的。所以无论如何都不能放弃英语学习,尤其是我们非 native English speaker 更是要坚持终身学习。
2024.9.19
今日 leetcode:163、2414
今天总算迎来了终面。1:40,HR 小姐姐带我参观了一下公司,公司里每人一张升降桌还是很有范儿的。闲逛的时候碰到了我两个师兄,年纪大了能遇到熟人真的是很激动的。2 点正式开面,大老板很 nice,全程都没有发表“重要讲话”。和他交流,一下子就能感觉得出他不是那种长期脱离产线的高管,因为很容易和我形成开发上的共鸣。当然,Behavior 面还是必备流程。我以前对这类 BQ 有点抵触,像有些国内厂就是要问你“卷不卷”,也听说北美有些企业会问“喜欢吃草还是吃肉”——吃肉的才是狼。但是后来渐渐意识到,BQ 面也许真的很有必要,比如,网上有“粉红”和“美分”之争,价值观差太多放在一起会很难受。只可惜,国内求职者很多就是要碗饭吃,不可能像某些帝国主义的应聘者那样能挑三拣四。
面试结束后,我走回了家,很累但是躺在床上一点也睡不着觉。我能做的都已尽力了,一切未知就看天命了。
2024.9.20
今日 leetcode:29、170
早上起来得到消息:offer 还在 pending 中;反馈是我用的技术栈太老了……有点震惊,我用的是 next 14 + shadcn/ui,这个怎么再新点呢?不过,回过头来一想,面试的时候我一直在强调我们的技术债务如何苦大仇深:一二三四五,等对方 buy in 了痛点,再提出使用特定技术解决这些遗产问题(日常 design doc 惯性)。面试时候就显得前摆太长了,之后我又没反复强调我解决的方案是最新的技术栈(之前老板不在乎技术新不新);导致别人只记住 CP 技术栈落后这件事了。这种点,不常面试的我真的是完全没有准备到,太痛了。大家平日里无论如何要多去了解业界动态,试试水,不然这类经验不可能凭空获得的。
2024.9.21
今日 leetcode:243
下午和 HR 谈了 offer 的事,很感谢 HR 小姐姐在周末加班帮我搞定 offer。这段时间真的很累,但能在 last day 之前拿到 offer 也是奇迹了。AWX 还是很够意思的,在这个年景给到了我心里价位。只可惜在 CP 三年没能 promotion,title 太低了;因此一开始就只能面 senior 岗,之后再怎么努力也很难在级别上有所突破了。这个倒不怪人家,行业规矩放在那。
我们到了一定年龄后,大家选择工作时不能再简单地看钱了:有些工作可能钱暂时多一点,但是 scope 太小了,发展空间受限,几年后就会反噬。就业市场上对每个年龄段的要求是不一样的,若在特定年龄段没有突破特定限制,那以后就很难了。我们程序员某种意义上需要了解更大的世界,比如人脉、行业动态、市场趋势等,这些都不是简单码代码能实现的。
即使身处最冷的寒冬,我知道自己的内心深处,有一个不可战胜的夏天。 -- 加缪,著名法国小说家
2024.9.22
今日 leetcode:246
早上七点起床去做了一次体检,算是把 CP 仅有的一点福利也给用完了。我最新的体重是是 137 斤,记得去年这时候是 150 的样子,这段时间我也几乎没有运动就是单纯地降体重了。期间在刷手机的时候听说一个消息:某个前同事去了发发奇;今年被 CP 收购了,然后也不出意外地被裁了。一切的行事风格都是那么商业,倒不是说有什么对错,只是没有一丝温情罢了。
2024.9.23
今日 leetcode:359、1014
今天是老婆的生日,阳光明媚,气温 23 摄氏度;难得的好日子,所以我们决定去徐汇的网红街吃顿大餐。午饭的时候,邮箱收到了 AWX 的 offer。无疑,这一天是我漫长时光中最值得欢欣鼓舞的时刻。
自去年年末开始,我便察觉到了形势的异常:部门只有 4 个 L5,上海韩国各两个;其中韩国那俩,一个刚从 4 升到 5,一个被 nominate 了,所以年度 performance 的 PIP 指标大概率在上海这边产生;而且即便我过了第一道坎,还有续约这第二道坎。从那时起,我就开始焦虑了,只能每天晚上回家后刷 leetcode 来减轻一些不安。所幸去年有两同事跑路了,正好吃掉了当年的 PIP 名额——让我没有在三月份速死,给了我一段较长时间的缓冲期。若非如此,我真难想象如何在短短三个礼拜内找到新工作。想想真的是后怕,那段时间即便有小幅的涨薪,即便 CP 股票一直在上扬,我都没有一丝喜悦之情;我甚至觉得自己因工作丧失了最基本的人格感知,一切理因欣喜的事都被我当做了回光返照,甚至连老婆怀孕的消息都让我感到压力倍增。如今,这一切终于结束了……
2024.9.24
今日 leetcode: 252
早上陪老婆去产检,羊水又降了,娃娃也像爸爸一样正经历者人生的磨难。医生建议再住一次院,但是考虑到之前也遇到了同样的问题;在住院部根本休息不好,我们决定先租个胎心仪,回家观察,两天后复查羊水。现在只能走一步算一步,娃娃才 5 斤半,略显瘦小,能在娘胎里待一天是一天。
本来约了 HR 两天后聊赔偿的事,但是和复查冲突了,所以我调整了会面时间至今天下午。有点小意外,是“小 n+1”,可能是我之前听错了成了“小 n”吧;但是确实也不高,在外企里依旧是地板流。我提到了娃娃可能在 last day 之前出生,陪产假能否适当赔偿,但被断然拒绝;另外,我有一笔 Q3 激励的 RSU 在 last day 之后“一天”才发放,这笔钱是否能到账,HR 表示让我自己联系美国的 stock 组,她不负责这块。
晚上,我找到了 CP 前员工的 PIP 离职交流群,询问了群主是否能拿到 RSU;他表示,没戏,我这个情况只能 n+1 走人——赔偿就是踩着法律线的地板流。很难想象这家企业贴的 HC 标的是阿里 P8、P9、P10,但是遣散费就这德行。
2024.9.25
今日 leetcode: 266
昨天我邮件联系了美国的 stock 组,今天终于回复了;他们表示无能为力:这是一开始在合约了规定的条款。看来我只能死心了。随后,我与昨天的群主闲聊了几句,他又抖了点黑料;我也不想传谣,但若这些消息属实,那确实令人感到心寒——始作俑者其无后乎?下午,我与一位新入职的同事交谈了片刻。他坦言:在入职之前就知晓了这里的 PIP 政策;不过即便如此,他还是会来的,因为他也是被之前的公司裁员了,好几个月才找到下家,不能考虑太多了。没办法,经济如此,国内互联网已是明日黄花;现在是买方市场,每个人的命运都已转向。
2024.9.26
今日 leetcode: 2535
昨晚,娃娃的胎心再次出现异常,情况紧急,我们不得不连夜办理了住院手续。吸取上次四人间的拥挤与不便的教训,我决心为老婆选择一个稍好一些的住院环境。然而,我未曾料到红房子的床位竟如此紧张,不仅单人间、双人间已全部满员,连四人间的加床也一张不剩。无奈之下,我们只得在过道上安顿下来——这一次的艰辛,远比上次更甚。
面对这样的困境,我心中五味杂陈,却也只能眼睁睁看着老婆承受这份苦难。今晨,我们及时联系了门诊医生,经过综合考虑,决定于 30 号进行剖腹产手术。
然而,手术前的这段时间,老婆仍需在医院等待,期盼着能排到一个稍微好一些的病房。想到她至少还要在这样的环境中度过五天,我实在是心疼不已。这段时间对我们来说无疑是巨大的考验,一切的一切只愿换来新生命的安全到来。
2024.9.27
今日 leetcode: 293
今天是我在上家公司的 last day,我重走了一遍上班路,在地铁上背单词,然后十点多踏入公司。离职手续进行很简单,不到十一点就已经全部办妥。我穿梭在公司的每一层楼,与那些熟悉的面孔一一道别。
中午时分,我和组里的小伙伴们聚在一起吃了最后的散伙饭。尽管这是告别的时刻,但大家依然保持着往日的激情,餐桌上热烈地讨论着技术话题。我本打算为这顿饭买单,以表达我的感激之情,然而同事们却抢先一步结了账。这让我心中不禁涌起一股暖流,同时也夹杂着一丝歉意:大家平日里都过着节俭的生活,一顿午餐通常只需三十几块,而今天这顿饭的人均消费却近两百元。
饭后,我们像往常一样漫步至 Manner,我点了一份多年未变的“小澳白”。只是这次,我没有带上自己的杯子,无法再享受那 5 块钱的优惠了。这个小小的变化,似乎也在提醒着我,今天的一切都与往日不同了。
最后,我们在欢笑中一一握手告别。这一别,只能再会江湖了……
2024.9.28
今日 leetcode: 270
等啊等,自费病房始终空不出来。这两天,老婆只能屈身于四人病房的加床之上。倒是我丈母娘挺乐观的,她含笑说道:“这孩子似乎天生带有财运。他的爸爸费尽心思,终于为他找到了奶粉钱;而他的妈妈,虽然历经艰辛却没能花了他的钱;现在,就连股票也呈现上涨趋势……”对我来说,吃些苦头并无大碍,我只祈求他们母子能够平安健康。
晚上,又一位前同事联系我,他正处于 PIP 阶段,自称已快承受不住。我只能不住得给他灌鸡汤,为他打气。说实话,作为旁观者,我们很难提供具体且实用的帮助。我所能给的,只是一些宽泛的建议,譬如那句老话,“好死不如赖活着”。我与他分享了亲身经历,并阐明一个观点:坚持下去,只是为了给自己争取更多的缓冲时间,以便找到新的工作机会,从而摆脱现状;而并非为了“适应这里的 PUA”。他和几个月前的我如出一辙,总是患得患失,害怕寻找新工作。但其实,最艰难的部分并非找工作本身,而是下定决心,勇敢迈出那第一步。一旦跨出那一步,找工作便只剩下概率乘以时间的期望值问题了。
2024.9.29
今日 leetcode: 346
今天,老婆终于住进了 LDR 病房;临行前,普通病房的床友们都为她加油助威,仪式感拉得满满的,只有经历过才能体会到生育的不易吧。我们加了一个增值服务——爸爸陪护入产房;然后几个护士轮番教我手术室注意事项,有七步洗手法、如何抱孩子、以及最难的戴无菌手套。她们还分享了一些准爸爸的糗事——在手术室里晕血了,然后医生护士们还得先照顾那位爸爸。不知道我明天会不会成为她们下一段趣事。老婆倒是一切正常,身体也没有异样,中午我们还一起去散步去喝了杯 M-stand。进产房前的一切物件,早就被娃娃妈妈准备的一应俱全,我们只等着新生命的到来了。
2024.9.30
今日leetcode:1064
早晨 8 点 52 分,娃娃一声啼哭,宣誓着新生命的降临;我颤抖着双手戴上了无菌手套,为他剪下了脐带。然后,回到手术台旁,紧紧握住老婆的手,告诉她:“儿子 6 斤半,黄金体重”。娃娃妈含着泪说:“长得磕碜吗?”(这颜控……)娃娃出生时,全身红紫色,很小的一只;但眼角很宽,皮肤光洁,一看就是帅小伙。手术很成功,二十分钟后就结束了;母子随后被转移到了观察室里。娃娃出生后缺少安全感,所以最好有肌肤接触;医生要我解开衣襟,让娃娃趴在了我身上。人类幼崽还是很有趣的:出厂设置极简——啥都不会,只保留了一个本能——吸奶;这好大儿竟在老父亲胸口边爬边种草莓。一个小时后,我们回到了病房,老婆还需要休息一会儿;大家便离开了病房,开始围着娃娃看。这时候,娃娃全身的红紫色已然退去,一双小手白皙粉嫩,面色清秀很像爸爸,看样子注定要迷倒万千少女了。全家人都乐呵呵地围着娃娃,连他竖个兰花指都能逗乐奶奶;外婆更是把娃娃拉粑粑的片段都给录了下来分享给全家人。最最重要的是,他出生后 A 股竟然当天涨了 8 个点,大家都啧啧称赞这娃娃自带财运。新的篇章从此开始……
来源:juejin.cn/post/7430031817254944805
One vs Taro vs Uniapp:跨平台三巨头对决,谁能成为你的终极开发利器?
随着移动端和Web应用的多样化发展,跨平台开发已经成为越来越多开发者的选择。写一套代码,运行在多个平台上,能大大提升开发效率、节省时间。那么,问题来了:在众多的跨平台框架中,究竟该选择哪个?今天在 GitHub 上看到了一个新的多端框架,ONE,号称可以统一全平台
索性,我们就来聊聊三个热门框架——Taro、One和Uniapp,看看它们各自的优势和适用场景,帮你找到最适合的跨平台解决方案。
为什么选择Taro、One和Uniapp?
这三者都是当前跨平台开发领域的主力军,但它们各自的定位和优势略有不同。Taro,由京东旗下的凹凸实验室推出,基于React,特别擅长小程序和H5的跨平台开发,国内开发者使用率很高;One,作为一款新兴的React框架,专注于Web、移动端和桌面端的跨平台开发,且具备本地优先的数据同步特性;Uniapp,由DCloud开发,基于Vue,主打“一次开发,多端适配”,在国内的小程序开发中占有一席之地。
接下来,我们从多个维度对比一下它们,看看哪个框架更适合你的项目需求。
平台覆盖范围对比
Taro的最大特点是对小程序支持非常全面,不仅支持微信小程序,还兼容支付宝、百度、字节跳动等多种小程序平台。此外,它还支持H5和React Native开发,因此如果你需要同时开发多个小程序和移动端App,Taro是一个非常合适的选择。
One在平台覆盖上更加广泛,它不仅支持Web、iOS、Android,还支持桌面应用程序的开发。然而,One目前并不支持小程序开发,所以如果你项目的重点是小程序,One可能不适合你。
Uniapp则也是小程序开发的强者,支持包括微信、支付宝、钉钉在内的多个小程序平台。同时,Uniapp还支持H5、iOS、Android,甚至可以打包为App、桌面应用,几乎覆盖了所有主流平台。对于那些需要开发多端应用,尤其是小程序的开发者来说,Uniapp可以说是一个“全能型选手”。
总结:如果你的项目主要涉及小程序开发,Taro和Uniapp更胜一筹,Taro在React生态下表现优异,Uniapp则在Vue生态中一骑绝尘;而如果你的项目重心是跨Web、移动端和桌面应用,One的优势更为明显。
技术栈对比——React vs Vue
框架选择的背后,往往与技术栈密不可分。对于大部分开发者来说,选择技术栈往往决定了上手的难度和开发的舒适度。
Taro基于React,提供了类似React的开发体验。对于习惯React的开发者来说,Taro非常友好,语法、组件化思路与React保持一致,你可以毫无缝隙地把已有的React经验直接应用到Taro项目中。
One同样基于React,但它做到了更深层次的跨平台统一,支持Web、移动端和桌面端的无缝切换,并且主打本地优先的数据处理,避免了频繁的API调用和复杂的同步逻辑。如果你习惯了React,并且希望进一步简化跨平台开发中的数据处理,One会是一个非常强大的工具。
Uniapp则基于Vue,对于喜欢Vue的开发者来说,Uniapp的上手难度很低,而且Uniapp的语法风格与Vue保持高度一致,你可以直接复用已有的Vue项目中的代码和经验。
总结:喜欢React的开发者可以考虑Taro或One,两者在跨平台能力上各有侧重;而如果你偏好Vue,那么Uniapp无疑是更理想的选择。
跨平台代码复用率对比
在跨平台开发中,代码复用率是开发者最关心的问题。Taro、One和Uniapp在这方面的表现都有各自的亮点。
Taro的代码复用率相对高,尤其是在小程序和H5应用中,大部分代码可以共享。但如果涉及到React Native,你仍然需要做一些针对平台的适配工作。
One则走得更远,它通过React和本地优先的数据处理模式,最大程度地减少了跨平台开发中的代码分歧。你可以只写一套代码,就能让应用无缝运行在Web、移动端和桌面端,并且无需为离线数据同步操心,这让One的代码复用率和开发效率非常出色。
Uniapp在代码复用率上表现也非常不错,它支持“一次开发,多端适配”,通过Vue语法几乎可以覆盖所有平台。只需要根据不同平台的差异做少量适配,便能确保项目在多端无缝运行。
总结:如果你希望最大化代码复用率,One在Web、移动和桌面端的表现最优;而如果你需要同时兼顾小程序和H5、App开发,Taro和Uniapp都可以满足需求。
性能对比
Taro和Uniapp在小程序和H5上的性能表现都非常优秀,接近原生体验。在React Native和App开发中,Taro的性能也相对稳定。
One则主打性能无缝衔接,尤其是本地优先的特性让它在处理大量数据时能表现得更加流畅。相比Taro和Uniapp,One的Web和桌面端性能更为出色,移动端的性能也接近原生。
总结:在小程序领域,Taro和Uniapp表现优秀;而在处理跨平台的Web、移动和桌面应用时,One的性能表现更胜一筹。
代码示例——如何选择适合的框架
让我们通过一个简单的代码示例,看看Taro、One和Uniapp在实际开发中的差异。
Taro 代码示例:
import { Component } from '@tarojs/taro';
import { View, Button } from '@tarojs/components';
class TodoApp extends Component {
state = {
todos: []
};
addTodo = () => {
this.setState({ todos: [...this.state.todos, '新任务'] });
};
render() {
return (
<View>
<Button onClick={this.addTodo}>添加任务</Button>
<View>
{this.state.todos.map((todo, index) => (
<View key={index}>{todo}</View>
))}
</View>
</View>
);
}
}
One 代码示例:
import { useLocalStore } from 'one-stack';
function TodoApp() {
const [todos, setTodos] = useLocalStore('todos', []);
function addTodo() {
setTodos([...todos, '新任务']);
}
return (
<div>
<button onClick={addTodo}>添加任务</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</div>
);
}
Uniapp 代码示例:
<template>
<view>
<button @click="addTodo">添加任务</button>
<view v-for="(todo, index) in todos" :key="index">{{ todo }}</view>
</view>
</template>
<script>
export default {
data() {
return {
todos: []
};
},
methods: {
addTodo() {
this.todos.push('新任务');
}
}
};
</script>
可以看到,Taro和Uniapp在小程序和多端开发上拥有强大的兼容性,而One则在Web和桌面应用中拥有更广泛的适用场景。不同的框架在开发体验上虽然有所不同,但总体而言,它们都能够较好地实现跨平台开发的目标。
生态与社区支持
选择一个框架,不仅要看它本身的功能,还要看其背后的生态和社区支持,因为这些决定了在遇到问题时能否快速找到解决方案,以及框架的未来发展潜力。
Taro依托于京东的支持,经过多年的迭代更新,拥有一个非常活跃的社区。你可以在社区中找到丰富的插件、第三方组件库和详细的教程文档。如果你在小程序开发中遇到问题,基本上都能通过Taro的社区找到解决方案。
One虽然是一个新兴的框架,但它的开发团队对React社区有着深厚的积累。因为基于React,它可以无缝利用React的生态,包括丰富的第三方库、开发工具和强大的社区支持。不过,作为一个新框架,One的社区规模还不如Taro和Uniapp庞大,但由于其独特的跨平台能力,未来的生态成长潜力不容小觑。
Uniapp的社区在国内极其庞大,DCloud团队也在持续更新Uniapp的功能和插件库。它的文档详细而完善,社区中也有大量的开发者分享经验,解决实际开发中的问题,尤其是在小程序开发领域,Uniapp几乎拥有无可匹敌的生态优势。
总结:如果你注重社区和生态的完善性,Taro和Uniapp的社区非常活跃,拥有丰富的插件和第三方支持;而如果你追求跨平台开发的前沿技术,One虽然较新,但凭借React的生态也有着很强的社区支持潜力。
结论:如何选择适合你的跨平台开发框架?
在Taro、One和Uniapp三者之间,选择最适合的框架取决于你的项目需求和技术栈。
- • 如果你以小程序开发为核心,并且希望使用React进行开发,那么Taro是你的最佳选择,尤其是当你还需要兼顾H5和移动端应用时,Taro的表现也非常出色。
- • 如果你的项目涉及Web、移动端和桌面端的统一开发,并且你希望有更好的代码复用率和数据同步机制,那么One会是一个颠覆性的选择,它通过本地优先的设计,解决了许多跨平台开发中的数据同步问题,提升了开发效率。
- • 如果你更习惯Vue,并且需要覆盖从小程序到H5、App等多个平台,Uniapp无疑是一个全能的选手。它在国内有着广泛的应用,特别是在小程序开发中拥有明显优势。
最终,选择哪一个框架,还是要根据你团队的技术栈、项目需求以及你对跨平台性能和代码复用率的要求做出判断。无论是Taro、One还是Uniapp,它们都能为你的跨平台开发提供强大的支持。
希望这篇文章能帮你理清思路,让你在框架选择上不再迷茫。如果你还在犹豫,不妨亲自试用一下这三个框架,结合实际开发需求和团队技术背景,相信你一定能找到那个“最合拍”的开发工具。
你觉得这三个框架哪个更适合你的项目呢?有任何问题或者经验分享,欢迎在评论区留言,我们一起讨论交流!
来源:juejin.cn/post/7420971044158193664
每一个失业的前端er都必须有一个稳定盈利的独立开发项目
如题,最近非常焦虑,因为考试临近了,所以只好来祸害一下网友了
俺从2023年离职,经历了考研,独立开发,remote,好几段经历
首先是考研,去年考的其实还行,但还是复试被刷,至今被刷原因未知,盲猜是因为本科是民办三本吧
然后remote就是找了个美国的区块链公司,但是因为四月份我忙着搞调剂,过程十分煎熬,根本无心顾暇remote那边天天开会的节奏,所以只能离职,当然啦,最终也没调剂上
这都不是重点,重点是独立开发
从我离职到现在,也快两年了,聪明的人已经发现了,整个互联网技术栈这两年可以说毫无变化,新的端没有,新的框架没有,新的红利也没有,新的独角兽公司也没有
道理很简单,因为现在是僧多粥少的时代,每个人手机上就固定几个app,而且都是存量状态(不需要推翻重来,只需要shi山跳舞)
与此同时,还有若干小公司不断倒闭
懂了吧,现在是需求没了,业务没了,招聘的公司没了
独立开发就只不过是,没有业务,我们自己发现制造业务罢了
但是呢,会更难,因为,资本虽然是傻逼,但它们也不是完全没脑子,如果轻易能成功,他们就不需要跑路了
现实就是,我朋友圈有很多独立开发的,推特上也有很多,但能做到稳定盈利的人,几乎为0
有的是卖小册,有的是搞博客,还有开公司做面试辅导的,也有外包接活的,也有收费技术咨询的
这些统统都是噶韭菜——因为我说的很清楚了,现在是业务没了,是需求没了,但凡不制造需求的,都是瞎扯
——所以我把c站卖了,c站转让前日活5w,但是动漫行业实在太卷了,各种各样的竞品,让我自己都不想看番,更别提服务给他人看了
之前在携程,我的老板和我说,你就当独立创业,携程三万人就是你的第一批客户,我觉得老板说的没错,就是比起b端,我更喜欢c端的用户
所以毫无疑问,我不可能再回去写前端框架了,纯粹浪费时间,浪费我的❤
唉,说了这么多,总而言之,言而总之
回到题目,那就是,每个人失业的前端er都必须有一个稳定盈利的独立开发项目
我也在开新坑了,敬请期待~
来源:juejin.cn/post/7426258631161528335
在老的Node.js服务器里“加点Rust”,我的服务性能飙升近 80%
你有没有遇到过这样的情况?服务器跑着跑着就卡了,明明只是一些普通的操作,却让资源“飚红”,甚至快撑不住了。特别是当你用JavaScript或者Python这些脚本语言写的服务器,遇到CPU密集型任务时,性能瓶颈似乎更是无可避免。这时候,是不是觉得有点力不从心?
今天,我们安利一个解决方案——Rust!一种速度快、效率高的编程语言。它有点像是给你的Node.js或者Python服务器加了“肌肉”,尤其适合处理高强度的运算任务。下面,我就给大家讲讲如何一步步把Rust“融入”到现有的服务器里,用简单的策略大幅度提升性能。
引入Rust的三步策略
在这个策略中,我们从“0”开始,逐步引入Rust,分别通过Rust CLI工具和Wasm模块来提升服务器的性能。总的原则是:每一步都不搞大改动,让你的老服务器既能“焕发新生”,又能保持现有的代码框架。
第0步:从Node.js服务器开始
假设我们现在有一个Node.js服务器,用来生成二维码。这个需求其实并不复杂,但在高并发的情况下,这样的CPU密集型任务会让JavaScript显得吃力。
const express = require('express');
const generateQrCode = require('./generate-qr.js');
const app = express();
app.get('/qrcode', async (req, res) => {
const { text } = req.query;
if (!text) {
return res.status(400).send('missing "text" query param');
}
if (text.length > 512) {
return res.status(400).send('text must be <= 512 bytes');
}
try {
const qrCode = await generateQrCode(text);
res.setHeader('Content-Type', 'image/png');
res.send(qrCode);
} catch (err) {
res.status(500).send('failed generating QR code');
}
});
app.listen(42069, '127.0.0.1');
基准测试:在纯Node.js的情况下,这个服务每秒能处理1464个请求,内存占用也不小。虽然勉强能跑起来,但一旦用户多了,可能会明显感觉到卡顿。
第1步:引入Rust CLI工具,效率提升近80%
这里的策略是保留Node.js的框架不变,把处理二维码生成的那段代码用Rust写成一个独立的命令行工具(CLI)。在Node.js中,我们直接调用这个CLI工具,分担高强度的计算工作。
/** qr_lib/lib.rs **/
use qrcode::{QrCode, EcLevel};
use image::Luma;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
pub type StdErr = Box<dyn std::error::Error>;
pub fn generate_qr_code(text: &str) -> Result<Vec<u8>, StdErr> {
let qr = QrCode::with_error_correction_level(text, EcLevel::L)?;
let img_buf = qr.render::u8>>()
.min_dimensions(200, 200)
.build();
let mut encoded_buf = Vec::with_capacity(512);
let encoder = PngEncoder::new_with_quality(
&mut encoded_buf,
// these options were chosen since
// they offered the best balance
// between speed and compression
// during testing
CompressionType::Default,
FilterType::NoFilter,
);
img_buf.write_with_encoder(encoder)?;
Ok(encoded_buf)
}
效果:重写后,我们的处理性能直接飙升到了每秒2572个请求!这是一个显著的提升,更让人欣慰的是,内存占用也跟着降了下来。Rust的高效编译和内存管理,确实比JavaScript强太多了。
实现步骤:
- 首先,用Rust编写二维码生成的核心逻辑代码。
- 将这段Rust代码编译成一个可执行的CLI工具。
- 在Node.js代码中,通过子进程调用CLI工具,直接拿到生成的结果。
在Node.js中调用Rust CLI工具的代码示例如下:
const { exec } = require('child_process');
exec('./qr_generator_cli', (error, stdout, stderr) => {
if (error) {
console.error(`执行出错: ${error}`);
return;
}
console.log(`生成的二维码数据: ${stdout}`);
});
这个方法就像是给Node.js加了一个“外挂”,而且几乎不需要改动现有代码。也就是说,你可以在不动大框架的情况下,得到Rust的性能优势。
第2步:编译Rust到WebAssembly(Wasm),性能提升再进一步
在第1步中,我们通过CLI工具调用了Rust,但依旧会产生一定的通信开销。所以,接下来,我们可以进一步优化,将Rust代码编译成WebAssembly(Wasm)模块,并在Node.js中直接调用它。这样,整个过程就在内存中运行,不用通过子进程调用CLI,速度进一步提升。
效果:使用Wasm后,处理性能再上升到了每秒2978个请求,而内存使用依旧维持在较低水平。
实现步骤:
- 将Rust代码编译为Wasm模块。可以使用
wasm-pack
这样的工具来帮助生成。
- 将Rust代码编译为Wasm模块。可以使用
- 在Node.js中,通过
wasm-bindgen
等工具直接加载并调用Wasm模块。
- 在Node.js中,通过
Node.js中加载Wasm模块的代码示例如下:
const fs = require('fs');
const wasmBuffer = fs.readFileSync('./qr_generator_bg.wasm');
WebAssembly.instantiate(wasmBuffer).then(wasmModule => {
const qrGenerator = wasmModule.instance.exports.qr_generate;
console.log(qrGenerator('Hello, Rust with Wasm!'));
});
这种方法让我们完全绕过了CLI的通信环节,直接把Rust的性能用在Node.js中。这不仅提升了效率,还让代码更加紧凑,减少了延迟。
思考
通过以上三步策略,我们可以在不完全推翻现有代码的前提下,逐步引入Rust,极大地提升服务器的性能。这个过程既适用于Node.js,也可以推广到其他语言和环境中。
为什么这个方法特别值得尝试呢?首先,它成本低。你不需要重写整个系统,只需要对瓶颈部分进行改进。其次,效果明显,尤其是对那些经常“吃力”的功能。最后,这个方法是可扩展的,你可以根据实际情况,灵活选择用CLI还是Wasm的方式来引入Rust。
所以,如果你的服务器正被性能问题困扰,不妨试试这个三步引Rust法。正如一位资深开发者所说:“Rust不仅让你的服务器跑得更快,还让代码变得更加优雅。”
来源:juejin.cn/post/7431091997114843151
将B站作为曲库,实现一个畅听全网歌曲的音乐客户端
仓库地址
前言
在很久之前做了一个能够免费听周杰伦歌曲的网页,经历了各种歌曲源失效的问题之后,换了一种实现思路,将B站作为曲库,开发一个应用,这样只要B站不倒,就可以一直白嫖歌曲了。
实现思路
- B 站上有很多的音乐视频,相当于一种超级全的音乐聚合曲库(索尼直接将 B 站当做网盘,传了 15w 个视频)
- 对这些视频进行收集制作成歌单
- 无需登录即可完整播放,无广告
- 使用 SocialSisterYi 整理的 B 站接口文档,直接就可以获取和搜索 B 站视频数据
功能
- 播放器
- 基础功能(播放,暂停,上一首,下一首)
- 播放列表
- 单曲循环,列表循环,随机播放
- 进度拖动
- 计时播放
- 搜索
- 名称关键字搜索
- 歌单
- 歌单同步
- 歌单广场(由用户贡献分享自己的歌单)
技术栈
- Flutter
缺陷
- 没有 IOS 版本(上架太贵了)
- 没有歌词
- 云同步配置麻烦一些,(隐私与便利不可兼得)
UI
警告
此项目仅供个人学习使用,请勿用于商业用途,否则后果自负。
鸣谢致敬
- SocialSisterYi 感谢这个库的作者和相关贡献者
- 感谢广大 B 站网友们提供的视频资源
来源:juejin.cn/post/7414129923633905675
THREE.JS——让你的logo切割出高级感
灵感图
每次都根据灵感图写代码,我都快成灵感大王了,本文较长,跨度较大,效果较好,请耐心看完,本文阶段代码有tag可以分部查看
前言
这是B站的一段视频,用3D渲染的方式表达各个大厂的logo如何制作出来的,其中提取出一小段,用于本文的灵感,就是这个图的切割效果,下文不包含激光的圆圈和工作平台,只有切割的光线、切割效果和分离动画,灵感图中切割的部分是超过logo的,如果有UI设计师,可以让设计师给提供分段的svg,我孤军奋战没有那么些资源,文中的点位都是从logo的svg文件获取的,场景创建就不赘述了,以前的文章也讲过很多次,那么我们开始吧
准备工作
- threejs
- ts
- vite
找一个这个小鸟的svg文件。
将svg文件的点位获取出来并将svg加入到场景中
渲染svg
// 加载模型
const loadModel = async () => {
svgLoader.load('./svg/logo.svg', (data) => {
const material = new THREE.MeshBasicMaterial({
color: '#000',
});
for (const path of data.paths) {
const shapes = SVGLoader.createShapes(path);
for (const shape of shapes) {
const geometry = new THREE.ShapeGeometry(shape);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh)
}
}
renderer.setAnimationLoop(render)
})
}
loadModel()
渲染结果
svg加载出来后的shape
就是组成当前logo的所有关键点位信息,接下来要做的是将这个logo以正确的角度放置在场景,再将这些关键点位生成激光运动路径,比如一个圆弧,是一个贝塞尔曲线,有两个定点,几个手柄,通过不同的角度组成曲线,而我们要做的是一条布满点位的曲线作为运动路径
获取曲线点位
这里用到的api是# CubicBezierCurve
贝塞尔曲线的基类Curve对象提供的方法getPoints
.getPoints ( divisions : Integer ) : Array
divisions -- 要将曲线划分为的分段数。默认是 5.
为了更方便的查看我们创建的点位,我们将生成的点位信息创建一个cube
// 加载模型
const loadModel = async () => {
...
for (const curve of shape.curves) {
/*
* .getPoints ( divisions : Integer ) : Array
* divisions -- 要将曲线划分为的分段数。默认是 5.
*/
const points = curve.getPoints(100);
console.log(points);
for (const v2 of points) {
const geometry = new THREE.BoxGeometry(10, 10, 10);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(v2.x, v2.y, 0)
scene.add(cube);
}
}
...
}
}
renderer.setAnimationLoop(render)
})
}
loadModel()
从图中可以看出,现在cube已经绕着logo围成一圈了,但是有一个现象,就是路径长的地方cube比较稀疏,而路径比较短的曲线cube比较密集,上面代码创建的关键点位信息都是以100的数量创建,所以会导致这种情况,曲线的疏密程度决定将来激光的行走速度,为了保证不管多长的路径,他们的行走速度是一样的,那么我们需要动态计算一下到底该以多少个点位来生成这条路径
...
const length = curve.getLength ();
const points = curve.getPoints(Math.floor(length/10));
...
在遍历curve的时候,通过getLength
获取曲线的长度,根据长度的不同,决定分段的点位数量,这样就保证了点位之间的距离是一样的,将来激光行走的速度也是可以控制成一样的,速度一样,距离越短,越先完成,当然你想让所有激光都同时完成,那是不需要让分割的点位分布均匀的。
提取点位信息
由于之前我们获取到了所有的点位信息,那么是不要加载原有的svg生成的logo,所以我们现在要将获取到的分割点,改为vector3,并缩小一下logo,这样方便以后操作
// 新建一个二维数组用于收集组成logo的点位信息
// 用于计算box3的点位合集
let divisionPoints: THREE.Vector2[] = []
// 用于计算box3的点位合集
let divisionPoints: THREE.Vector3[] = []
// 将遍历贝塞尔曲线的地方再改造一下
let list: THREE.Vector3[] = []
/*
* .getPoints ( divisions : Integer ) : Array
* divisions -- 要将曲线划分为的分段数。默认是 5.
*/
const length = curve.getLength();
const points = curve.getPoints(Math.floor(length / 20));
for (const v2 of points) {
// logo 太大了,缩小一下,这里不建议用scale缩svg,直接缩向量
v2.divideScalar(20)
const v3 = new THREE.Vector3(v2.x, 0, v2.y)
list.push(v3)
divisionPoints.push(v2)
}
paths.push(list)
制作底板并将logo和底板统一放在视图中心
在此之前需要先定义几个变量,用于之后的使用
const logoSize = new THREE.Vector2()
const logoCenter = new THREE.Vector2()
// 底板厚度
const floorHeight = 3
let floor: THREE.Mesh | null
// 底板比logo的扩张尺寸
let floorOffset = 8
根据点位信息收集logo 的信息
根据之前收集的点位信息创建出底板和logo
const handlePaths = () => {
const box2 = new THREE.Box2();
box2.setFromPoints(divisionPoints)
box2.getSize(logoSize)
box2.getCenter(logoCenter)
createFloor()
}
创建地板和logo
const createFloor = () => {
const floorSize = logoSize.clone().addScalar(floorOffset)
const geometry = new THREE.BoxGeometry(floorSize.width, floorHeight, floorSize.height);
const material = new THREE.MeshLambertMaterial({ color: 0x6ac3f7 });
floor = new THREE.Mesh(geometry, material);
scene.add(floor);
createLine()
}
const createLine = () => {
const material = new THREE.LineBasicMaterial({
color: 0x0000ff
});
const points: THREE.Vector3[] = [];
divisionPoints.forEach(point => {
points.push(new THREE.Vector3(point.x, floorHeight, point.y))
})
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, material);
const linePos = logoSize.clone().divideScalar(-2)
line.position.set(linePos.x, 0, linePos.y)
scene.add(line);
}
我们之前加载的svg已经没有用了,只是为了提供点位信息,所以需要再根据整理后的点位信息创建一个logo的Line
对象
效果图
绘制激光
创建4(可自定)条激光,起点从底板上方30的位置,结束于logo,然后结束的点位随着logo的点位进行改变,从而实现激光运动的效果,提前先确定一下激光起点,
判断起点
由于激光数量可以自定,那么我们需要自定义一个激光的数量,当前用的数量是10,而要配置不同数量的激光,位置就需要有一定的规则,下面代码是创建了一个圆弧,以激光数量为基础,在圆弧上获取相应的点位,这样不管多少个激光,都可以从这个圆弧上取起点位置,圆弧的半径是以logo为基础向内缩进的,而结束点,目前定在底板的下面。
// 激光组
const buiGr0up = new THREE.Gr0up()
// 激光起点相对于logo缩进的位置
const buiDivide = 3
// 决定激光起点距离场景中心的距离
const buiOffsetH = 30
// 决定有几条激光
const buiCount = 10
const createBui = () => {
// 创建一个圆弧,将来如果有很多激光,那么起点就从圆弧的点位上取
var R = Math.min(...logoSize.toArray()) / buiDivide; //圆弧半径
var N = buiCount * 10; // 根据激光的条数生成圆弧上的点位数量
// 批量生成圆弧上的顶点数据
const vertices: number[] = []
for (var i = 0; i < N; i++) {
var angle = 2 * Math.PI / N * i;
var x = R * Math.sin(angle);
var y = R * Math.cos(angle);
vertices.push(x, buiOffsetH, y)
}
// 创建圆弧的辅助线
initArc(vertices)
for (let i = 0; i < buiCount; i++) {
const startPoint = new THREE.Vector3().fromArray(vertices, i * buiCount * 3)
const endPoint = new THREE.Vector3()
endPoint.copy(startPoint.clone().setY(-floorHeight))
// 创建cube辅助块
const color = new THREE.Color(Math.random() * 0xffffff)
initCube(startPoint, color)
initCube(endPoint, color)
}
}
效果图
每两个相同的颜色就是当前激光一条激光的两段
line2
下面该创建激光biu~
,原理上是一条可控制宽度的线,虽然threejs中的线条材质提供的linewidth来控制线宽,但是属性下面有说明,无论怎么设置,线宽始终是1,所以我们要用另一种表现形式:Line2
.linewidth : Float
控制线宽。默认值为 1。
由于OpenGL Core Profile与 大多数平台上WebGL渲染器的限制,无论如何设置该值,线宽始终为1。
import { Line2 } from "three/examples/jsm/lines/Line2.js";
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
...
const createLine2 = (linePoints: number[]) => {
const geometry = new LineGeometry();
geometry.setPositions(linePoints);
const matLine = new LineMaterial({
linewidth: 0.002, // 可以调整线宽
dashed: true,
opacity: 0.5,
color: 0x4cb2f8,
vertexColors: false, // 是否使用顶点颜色
});
let biu = new Line2(geometry, matLine);
biuGr0up.add(biu);
}
调用initBiu~
createLine2([...startPoint.toArray(),...endPoint.toArray()])
效果图
准备工作大致就到此结束了,接下来要实现的效果是激光运动
、激光发光
、logo切割
。
激光效果
首先先把激光的数量改为4,再将之前收集到的logo坐标点位分成四份,每根激光负责切割其中一份,切割的过程就是将激光的endpoint进行改变。
激光运动
计算激光结束点位置
在创建好激光后调用biuAnimate
方法,这个方法更新了激光的结束点,遍历之前从svg上获取的点位信息,将这些点位以激光的数量等分,再将这些点位信息作为Line2的顶点信息,通过setInterval的形式更新到激光的Line2
const biuAnimate = () => {
console.log('paths', paths, divisionPoints);
// biuCount
// todo 这里要改成points这样的 每次切割完 收缩一下激光,再伸展出来
const allPoints = [...divisionPoints]
const len = Math.ceil(allPoints.length / biuCount)
for (let i = 0; i < biuCount; i++) {
const s = (i - 1) * len
const points = allPoints.splice(0, len);
const biu = biuGr0up.children[i] as Line2;
const biuStartPoint = biu.userData.startPoint
let j = 0;
const interval = setInterval(() => {
if (j < points.length) {
const point = points[j]
const attrPosition = [...biuStartPoint.toArray(), ...new THREE.Vector3(point.x, floorHeight/2, point.y).add(getlogoPos()).toArray()]
uploadBiuLine(biu, attrPosition)
j++
} else {
clearInterval(interval)
}
}, 100)
}
}
// 更新激光信息
const uploadBiuLine = (line2: Line2, attrPosition) => {
const geometry = new LineGeometry();
line2.geometry.setPositions(attrPosition);
}
效果图
根据激光经过的路径绘制logo
首先隐藏掉原有的logo,以每一条激光为维度,创建一个THREE.Line
,这样我们就有了4条曲线,在每次激光经过的点作为这条曲线的节点,去更新BufferGeometry
。
创建激光的部分代码
for (let i = 0; i < biuCount; i++) {
...
// 创建线段
const line = createLine()
scene.add(line)
const interval = setInterval(() => {
if (j < points.length) {
const point = points[j]
const endArray = new THREE.Vector3(point.x, floorHeight / 2, point.y).add(getlogoPos()).toArray()
const attrPosition = [...biuStartPoint.toArray(), ...endArray]
...
// 获取原有的点位信息
const logoLinePointArray = [...(line.geometry.attributes['position']?.array||[])];
logoLinePointArray.push(...endArray)
// 更新线段
line.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(logoLinePointArray), 3))
j++
} else {
clearInterval(interval)
}
}, 100)
}
从图中可以看到,每根曲线之间的衔接做的并不是很到位,所以稍微改造一下代码,将上一根线的最后一个点位给到当前的线,
const points = allPoints.splice(0, len);
// allPoints是截取到上一轮点位的其余点位,所以第一个就是当前激光相邻的第一个点
if(i<biuCount-1) {
points.push(allPoints[0])
} else {
//最后一条曲线需要加的点是第一条线的第一个点
points.push(divisionPoints[0])
}
logo分离
激光切割完毕后,logo和底板将分离,之前想用的是threeBSP
进行布尔运算进行裁切,但是对于复杂的logo使用布尔运算去裁切太消耗资源了,简单的几何形状可以。
创建裁切的多余部分
创建裁切的过程其实就是新增和删除的过程,新增一个logo和多余部分,再将原有的底板删除掉
这里多余的部分使用shape的孔洞,底板尺寸生成的形状作为主体,logo作为孔洞,结合起来后,将得到的shape进行挤压
创建logo和多余部分的几何体
在外部创建logo和多余部分的shape
// 用于创建logo挤压模型的形状Shape
const logoShape = new THREE.Shape()
// 用于创建多余部分的挤压模型形状
const moreShape = new THREE.Shape()
loadModel
方法新增代码,用于收集logoShape的点位信息
// 加载模型
const loadModel = async () => {
...
for (let i = 0; i < points.length - 1; i++) {
const v2 = points[i]
if (v2.x !== 0 && v2.x && v2.y !== 0 && v2.y) {
// logo 太大了,缩小一下,这里不建议用scale缩svg,直接缩向量,后面依赖向量的元素都需要重新绘制
v2.divideScalar(20)
const v3 = new THREE.Vector3(v2.x, 0, v2.y)
list.push(v3)
divisionPoints.push(v2)
if (i === 0) {
logoShape.moveTo(v2.x, v2.y)
} else {
logoShape.lineTo(v2.x, v2.y)
}
}
}
...
}
createFloor
方法创建moreMesh多余部分的挤压几何体
const createFloor = () => {
const floorSize = logoSize.clone().addScalar(floorOffset)
const geometry = new THREE.BoxGeometry(floorSize.width, floorHeight, floorSize.height);
floor = new THREE.Mesh(geometry, logoMaterial);
// scene.add(floor);
moreShape.moveTo(floorSize.x / 2, floorSize.y / 2);
moreShape.lineTo(-floorSize.x / 2, floorSize.y / 2);
moreShape.lineTo(-floorSize.x / 2, -floorSize.y / 2);
moreShape.lineTo(floorSize.x / 2, -floorSize.y / 2);
const path = new THREE.Path()
const logoPos = new THREE.Vector3(logoCenter.x, floorHeight / 2, logoCenter.y).negate()
// logo实例
logoMesh = createLogoMesh(logoShape)
logoMesh.position.copy(logoPos.clone().setY(floorHeight))
logoMesh.material = new THREE.MeshLambertMaterial({ color: 0xff0000, side: THREE.DoubleSide });
scene.add(logoMesh);
// 孔洞path
divisionPoints.forEach((point, i) => {
point.add(logoCenter.clone().negate())
if (i === 0) {
path.moveTo(point.x, point.y);
} else {
path.lineTo(point.x, point.y);
}
})
// 多余部分添加孔洞
moreShape.holes.push(path)
// 多余部分实例
moreMesh = createLogoMesh(moreShape)
// moreMesh.visible = false
scene.add(moreMesh)
}
经过以上的改造,画面总共分为三个主要部分,激光、多余部分、logo。
大概效果就是这样的,再加上动画,让激光有收起和展开,再加上切割完以后,多余部分的动画,那这篇教程基本上就完事儿了,下面优化的部分就不一一展示了,可以看最终的效果动图,也可以从gitee上将代码下载下来自行运行
推特logo
抖音 logo
github logo
动图比较大,可以保存在本地查看
项目地址
来源:juejin.cn/post/7337169269951283235
BOE(京东方)首度全面解读ADS Pro液晶显示技术众多“真像” 倡导以创新推动产业高价值增长
10月28日,BOE(京东方)“真像 只有一个”ADS Pro技术品鉴会在上海举行。BOE(京东方)通过打造“光影显真”、“万像归真”、“竞速见真”三大场景互动区,以及生动鲜活的实例和现场体验,揭示了众多“真像”,解读了行业误区以及消费者认知偏差,不仅全面展示了ADS Pro技术在高环境光对比度、全视角无色偏、高刷新率和动态画面优化等方面的卓越性能表现,以及液晶显示技术蓬勃的生命力,更是极大推动了全球显示产业的良性健康发展。活动现场,BOE(京东方)高级副总裁、首席技术官刘志强,京东集团副总裁、京东零售家电家居生活事业群总裁李帅等出席并发表致辞,并在行业嘉宾、媒体伙伴的见证下,共同发起“产业高价值增长倡议”,标志着中国显示产业开启从价格竞争走向价值竞争的高质发展新时代。
BOE(京东方)高级副总裁、首席技术官刘志强表示,人类对真相的探究永无止境,而显示技术的“真像”也需要还原最真实的色彩和场景。作为中国大陆液晶显示产业的先行者和领导者,BOE(京东方)在市场规模、出货量、技术、应用等方面遥遥领先,如今,有屏的地方就有京东方,如何选好屏,也成为当下消费者最关注的议题之一。作为三大技术品牌之一,BOE(京东方)自主研发的ADS Pro是应用最广的主流液晶显示技术,搭载ADS Pro技术的产品目前全球出货量最高。BOE(京东方)通过不断技术创新,依托ADS Pro技术的超高环境光对比度、超广视角、超高刷新率等优势,不断迭代升级并推出包括BD Cell、UB Cell、以及ADS Pro+Mini LED背光等创新显示解决方案,引领显示行业技术发展潮流,带领中国屏幕走向全球。
京东集团副总裁、京东零售家电家居生活事业群总裁李帅表示,作为BOE(京东方)自主研发的高端显示技术,ADS Pro在高对比度、更广色域、超广全视角、超低反射率等方面的技术特性,极大程度满足了用户对于高端电视的消费需求,今年618期间,ADS Pro电视在京东的成交额同比增长超过3倍。京东与BOE(京东方)共同打造了全域内容营销矩阵,通过整合京东站内外内容,用好内容积攒产品口碑。未来,“双京”将持续强强联手,让更多人了解ADS Pro技术、选购ADS Pro技术赋能的高端电视,让更好的视听体验走进千家万户。
在品鉴会现场,BOE(京东方)带领行业探寻了一系列ADS Pro的技术真相:
真相一:在环境光对比度方面,通常传统液晶显示技术所呈现的对比度多数用暗室对比度进行测试,脱离用户真实使用场景的数值是毫无意义的。在真实应用场景中,室内常规照明情况下的环境光对比度对用户更有意义,也是决定用户真实体验的关键指标,BOE(京东方)创新升级环境光对比度(ACR)这一更加适配当前屏幕使用场景的测试指标,更准确地表征人眼真实感知的对比度。作为中国唯一拥有自主知识产权的高端液晶显示技术,BOE(京东方)ADS Pro技术对比度测试标准从用户体验出发,在近似真实的使用场景下进行数据测试,ACR数值高达1400:1,与其他同类技术相比具有显著优势。同时,通过像素结构优化、新材料开发、表面处理等多种减反技术,大幅降低了显示屏幕光线反射率,达到远超常规显示技术的超高环境光对比度,实现更高的光线适应性和更佳的画质表现。在BOE(京东方)ACR技术的加持下,能够让消费者在观看屏幕时无需受到环境光干扰。
真相二:在广视角方面,传统测量标准下,几乎所有产品都能达到所谓的“广视角”,但实际观看效果有很大区别,“色偏视角”才能更客观反馈广视角显示效果。大屏观看时,产品色偏问题显得尤为突出。色偏是指屏幕在侧视角观看时,亮度/色彩的变化与正视角观看时的差异,色偏视角能真实呈现色彩的“本真”。在所有显示技术中,ADS Pro在广视角观看时显示效果最能够达到真实还原,实现接近180°的超广视角,且全视角无色偏、无褪色,让消费者实现家庭观影处处都是“C位”,这也成为ADS Pro技术的另一大重要优势。
真相三:高端LCD显示技术依然是大屏电视产品的主流,并实现了媲美OLED的显示效果。基于BOE(京东方)ADS Pro技术进一步升级的高端LCD解决方案UB Cell,所呈现的完美画质可以媲美OLED,甚至超越它的显示效果,这是LCD显示技术领域发展的重要里程碑。BOE(京东方)UB Cell技术在感知画质、信赖性、能耗等方面相较于其他技术更具优势。由于采用了多种减反技术,UB Cell显示屏的表面反射率大幅降低,使其环境光对比度远高于市场旗舰机型中应用其他技术的产品,从而极大提升屏幕的亮态亮度和暗态亮度的比值,让画面显示的暗态部分更暗、亮态部分更亮,画质更加细腻逼真。同时,BOE(京东方)通过开发新型光学相位补偿技术,实现了超宽视角,使得UB Cell技术的大视角色彩表现能力进一步提升。此外,借助ADS Pro技术的特有像素结构,配合宽频液晶材料体系和驱动算法,可以全灰阶满足G-sync 标准,完全无屏闪,更护眼的同时也让显示画面更稳定更流畅。
真相四:ADS Pro广视角和高刷的优势,结合Mini LED在HDR和极致暗态画质的优异表现,让二者做到最完美的优势互补,这样的组合才是画质提升的最佳拍档!BOE(京东方)采用高光效LED+高对比度ADS Pro面板+画质增强算法方案,相比其他显示技术有更出众的表现,不仅实现更宽的亮度范围,还进一部拓展了高亮度灰阶,扩充暗场景灰阶层次感。此外,随着刷新率的不断提升,通过ADS Pro+Mini LED实现分区动态差黑,可以极大提升高速运动画面清晰度,显著减少卡顿、延迟等现象,树立电竞显示的性能画质新标杆。目前,ADS Pro+Mini LED解决方案已成为全品类产品的应用趋势。
真相五:作为目前全球领先的主流液晶显示技术,BOE(京东方)ADS Pro是唯一可以覆盖从手机、平板电脑、笔记本、显示器到电视所有产品类型的技术。ADS Pro技术在大屏上的优势更加明显,并且通过专业级高端画质、极致的视觉享受及一系列健康护眼技术,为各行业客户打开新的增长空间。目前ADS Pro技术在显示器、笔记本、平板电脑、大屏高端电视等领域市场份额逐年攀升,已成为全球各大一线终端品牌高端机型的首选。群智咨询总经理李亚琴表示,五年后,LCD面板出货面积较当前水平将达到两位数增幅。在用户需求和技术升级的双重驱动下,ADS/IPS技术在IT市场将位居绝对主流地位并逐年提升份额;尤其在电竞领域,用户对高分辨率、高刷新率、快速响应时间、高色域、更大尺寸屏幕等显示性能提升有很高的期待。
当前,显示无处不在的时代已经到来,显示技术的持续迭代升级,背后的“真像”是中国科技力量在全球发挥着越来越重要的价值。作为全球半导体显示行业龙头企业,BOE(京东方)携手行业伙伴共同发起倡议,呼吁行业各界合作伙伴打破内卷,以技术升维不断提升用户体验,从聚焦价格的“零和博弈”走向聚焦价值的“融合共生”,开辟新技术、新赛道、新模式,共同发展高科技、高效益、高质量的新质生产力!未来,以BOE(京东方)为代表的中国科技企业也将持续创新,为消费带来更真实、更智能、更时尚、更节能的显示技术和产品,引领中国屏幕走向全球,为产业高质升维发展注入源源不断的新动能。
收起阅读 »如何优雅的将MultipartFile和File互转
我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。
前言
首先来区别一下MultipartFile和File:
- MultipartFile是 Spring 框架的一部分,File是 Java 标准库的一部分。
- MultipartFile主要用于接收上传的文件,File主要用于操作系统文件。
MultipartFile转换为File
使用 transferTo
这是一种最简单的方法,使用MultipartFile自带的transferTo 方法将MultipartFile转换为File,这里通过上传表单文件,将MultipartFile转换为File格式,然后输出到特定的路径,具体写法如下。
使用 FileOutputStream
这是最常用的一种方法,使用 FileOutputStream 可以将字节写入文件。具体写法如下。
使用 Java NIO
Java NIO 提供了文件复制的方法。具体写法如下。
File装换为MultipartFile
从File转换为MultipartFile 通常在测试或模拟场景中使用,生产环境一般不这么用,这里只介绍一种最常用的方法。
使用 MockMultipartFile
在转换之前先确保引入了spring-test 依赖(以Maven举例)
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>version</version>
<scope>test</scope>
</dependency>
通过获得File文件的名称、mime类型以及内容将其转换为MultipartFile格式。具体写法如下。
更多文章干货,推荐公众号【程序员老J】
来源:juejin.cn/post/7295559402475667492
一起读本书吧~《认知觉醒》,潜意识里有你要的答案
读后感
书中改变我的话:
- 佼佼者更愿意做高耗能的事——消除模糊,制造清晰。
- 先用感性选择,再用理性思考,或许是一个更好的策略,尤其是在做哪些重大选择时。
- 针对当下的时间,保持觉知,审视第一反应,产生明确的主张;针对全天的日程,保持清醒,时刻明确下一步要做的事情;针对长远的目标,保持思考,想清楚长远意义和内在动机。
第二章书摘
- “学霸”和普通同学之间的差异不仅体现在勤奋的程度上,还体现在努力的模式上:谁更愿意做高耗能的事——消除模糊,制造清晰。
- 多数人为了逃避真正的思考,愿意做任何事。
- 记住,任何痛苦事件都不会自动消失,哪怕再小的事情也是如此。要想不受其困扰,唯一的办法就是正视他、看清它、拆解它、化解它,不给它进入潜意识的机会,不给它变模糊的机会,即使已经进入潜意识,也要想办法将它挖出来。
- 恐惧就是一个欺软怕硬的货色,你躲避它,它就张牙舞爪,你正视它,它就原形毕露。一旦把它看的清清楚楚,情绪就会慢慢从潜意识中消散,你的生活将会舒畅无比。
- 认知清晰,情绪平和,最终还要行动坚定。很多人把行动力不足的原因归结为环境干扰或是意志力弱,其实,行动力不足的真正原因是选择模糊。
- 潜意识的感性总能帮我们发现什么是真正适合自己的,从而引导精力投入,快速提升自己。
- 先用感性选择,再用理性思考,或许是一个更好的策略,尤其是在做哪些重大选择时。诚如洪兰教授的建议:小事听从你的脑,大事听从你的心。
- 梦境。梦境是潜意识传递信息的一种方式,它可能是内心最真实想法的展示,也可能是灵感的启发。
- 针对当下的时间,保持觉知,审视第一反应,产生明确的主张;针对全天的日程,保持清醒,时刻明确下一步要做的事情;针对长远的目标,保持思考,想清楚长远意义和内在动机。
针对书摘1:如此道理,当你面对不了解的事物时,唯有抽丝剥茧、不断细化,并且直面它,你才能清晰的了解到事物的全貌,否则就是管中窥豹,同样,这也是消除焦虑的最好方式。
针对书摘2:无论多么庞大的任务,最怕的就是任务分解,分解它的过程也是你直面和了解它的过程,在拆解的过程中你将对它越来越清晰,在此过程中,会消除你因为对其不了解而产生的焦虑,同样也是让自己的工作具象化、透明化的过程。
针对书摘5:你迟迟不肯行动,也许是担心自己做不好(低期望值),或许是觉得这件事没有意义(价值感不足),也可能是自己每次在行动时总三心二意(高冲动),也可能最终日期很遥远,当下没有一定要做这件事的压力,但是无论什么原因,都请把握这件事自己可以把握的部分,让事情本身对自己变得有意义、有助于自己成长,做自己当下应该做的事。
针对书摘6:兴趣是最好的老师,在前行的路上保持好奇心。
针对书摘7:潜意识往往不会骗自己,且更符合自己的内心最真实的想法,首先选择你喜欢的,选择后再经过自己理性的思考,得到最终结果。
针对书摘8:明确当下的任务(当下目标),保持清晰的思路做事(短期目标),坚持长期人生主义。
每一次克制自己,就意味着比以前更强大。——高尔基
来源:juejin.cn/post/7430801077455798299
微信的消息订阅,就是小程序有通知,可以直接发到你的微信上
给客户做了一个信息发布的小程序,今天客户提要求说希望用户发布信息了以后,他能收到信息,然后即时给用户审核,并且要免费,我就想到了微信的订阅消息。之前做过一次,但是忘了,这次记录一下,还是有一些坑的。
一 先申请消息模版
先去微信公众平台,申请消息模版
在uni-app 里面下载这个插件uni-subscribemsg
我的原则就是有插件用插件,别自己造轮子。而且这个插件文档很好
根据文档定义一个message.js 云函数
这个其实文档里面都有现成的代码,但我还是贴一下自己的吧。
'use strict';
const uidObj = require('uni-id');
const {
Controller
} = require('uni-cloud-router');
// 引入uni-subscribemsg公共模块
const UniSubscribemsg = require('uni-subscribemsg');
// 初始化实例
let uniSubscribemsg = new UniSubscribemsg({
dcloudAppid: "填你的应用id",
provider: "weixin-mp",
});
module.exports = class messagesController extends Controller {
// 发送消息
async send() {
let response = { code: 1, msg: '发送消息失败', datas: {} };
const {
openid,
data,
} = this.ctx.data;
// 发送订阅消息
let resdata = await uniSubscribemsg.sendSubscribeMessage({
touser: openid,// 就是用户的微信id,决定发给他
template_id: "填你刚刚申请的消息模版id",
page: "pages/tabbar/home", // 小程序页面地址
miniprogram_state: "developer", // 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版
lang: "zh_CN",
data: {
thing1: {
value: "信息审核通知"// 消息标题
},
thing2: {
value: '你有新的内容需要审核' // 消息内容
},
number3: {
value: 1 // 未读数量
},
thing4: {
value: '管理员' // 发送人
},
time7: {
value: data.time // 发送时间
}
}
});
response.code = 0;
response.msg = '发送消息成功';
response.datas = resdata;
return response;
}
}
四 让用户主动订阅消息
微信为了防止打扰用户,需要用户订阅消息,并且每次订阅只能发送一次,不过我取巧,在用户操作按钮上偷偷加订阅方法,让用户一直订阅,我就可以一直发
// 订阅
dingYue() {
uni.requestSubscribeMessage({
tmplIds: ["消息模版id"], // 改成你的小程序订阅消息模板id
success: (res) => {
if (res['消息模版id'] == 'accept') {
}
}
});
},
五 讲一下坑
我安装了那个uni-app 的消息插件,但是一直报错找不到那个模块。原来是unicloud 云函数要主动关联公共模块,什么意思呢,直接上图。
来源:juejin.cn/post/7430353222685048859
mysql到底是join性能好,还是in一下更快呢?
大家好呀,我是楼仔。
今天发现一篇很有意思的文章,使用 mysql 查询时,是使用 join 好,还是直接 in 更好,这个大家工作时经常遇到。
为了方便大家查看,文章我重新进行了排版。
我没有直接用作者的结论,感觉可能会误导读者,而是根据实验结果,给出我自己的建议。
不 BB,上目录:
01 背景
事情是这样的,去年入职的新公司,之后在代码 review 的时候被提出说,不要写 join,join 耗性能还是慢来着,当时也是真的没有多想,那就写 in 好了。
最近发现 in 的数据量过大的时候会导致 sql 慢,甚至 sql 太长,直接报错了。
这次来浅究一下,到底是 in 好还是 join 好,仅目前认知探寻,有不对之处欢迎指正。
以下实验仅在本机电脑试验。
02 表结构
2.1 用户表
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`gender` smallint DEFAULT NULL COMMENT '性别',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `mobile` (`mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1005 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
2.2 订单表
CREATE TABLE `order` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`price` decimal(18,2) NOT NULL,
`user_id` int NOT NULL,
`product_id` int NOT NULL,
`status` smallint NOT NULL DEFAULT '0' COMMENT '订单状态',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `product_id` (`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=202 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
03 千条数据情况
数据量:用户表插一千条随机生成的数据,订单表插一百条随机数据
要求:查下所有的订单以及订单对应的用户
耗时衡量指标:多表连接查询成本 = 一次驱动表成本 + 从驱动表查出的记录数 * 一次被驱动表的成本
3.1 join
select order.id, price, user.name from order join user on order.user_id = user.id;
3.2 in
select id,price,user_id from order;
select name from user where id in (8, 11, 20, 32, 49, 58, 64, 67, 97, 105, 113, 118, 129, 173, 179, 181, 210, 213, 215, 216, 224, 243, 244, 251, 280, 309, 319, 321, 336, 342, 344, 349, 353, 358, 363, 367, 374, 377, 380, 417, 418, 420, 435, 447, 449, 452, 454, 459, 461, 472, 480, 487, 498, 499, 515, 525, 525, 531, 564, 566, 580, 584, 586, 592, 595, 610, 633, 635, 640, 652, 658, 668, 674, 685, 687, 701, 718, 720, 733, 739, 745, 751, 758, 770, 771, 780, 806, 834, 841, 856, 856, 857, 858, 882, 934, 942, 983, 989, 994, 995);
其中 in 的是order查出来的所有用户 id。
如此看来,分开查和 join 查的成本并没有相差许多。
3.3 并发场景
主要用php原生写了脚本,用ab进行10个同时的请求,看下时间,进行比较。
> ab -n 100 -c 10 // 执行脚本
下面是 join 查询的执行脚本:
$mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}
$result = $mysqli->query('select order.id, price, user.`name` from `order` join user on order.user_id = user.id;');
$orders = $result->fetch_all(MYSQLI_ASSOC);
var_dump($orders);
$mysqli->close();
下面是 in 查询的执行脚本:
$mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}
$result = $mysqli->query('select `id`,price,user_id from `order`');
$orders = $result->fetch_all(MYSQLI_ASSOC);
$userIds = implode(',', array_column($orders, 'user_id')); // 获取订单中的用户id
$result = $mysqli->query("select `id`,`name` from `user` where id in ({$userIds})");
$users = $result->fetch_all(MYSQLI_ASSOC);// 获取这些用户的姓名
// 将id做数组键
$userRes = [];
foreach ($users as $user) {
$userRes[$user['id']] = $user['name'];
}
$res = [];
// 整合数据
foreach ($orders as $order) {
$current = [];
$current['id'] = $order['id'];
$current['price'] = $order['price'];
$current['name'] = $userRes[$order['user_id']] ?: '';
$res[] = $current;
}
var_dump($res);
// 关闭mysql连接
$mysqli->close();
看时间的话,明显 join 更快一些。
04 万条数据情况
user表现在10000条数据,order表10000条试下。
4.1 join
4.2 in
order 耗时:
user 耗时:
4.3 并发场景
join 耗时:
in 耗时:
数据量达到万级别,非并发场景,in 更快,并发场景 join 更快。
05 十万条数据情况
随机插入后user表十万条数据,order表一百万条试下。
5.1 join
5.2 in
order 耗时:
user 耗时:
order查出来的结果过长了...
5.3 并发场景
join 耗时:
in 耗时:
数据量达到十万/百万级别,非并发场景,in 过长,并发场景 join 更快。
06 总结
实验结论:
- 数据量不到万级别,join 和 in 差不多;
- 数据量达到万级别,非并发场景,in 更快,并发场景 join 更快;
- 数据量达到十万/百万级别,非并发场景,in 过长,并发场景 join 更快。
下面是楼仔给出的一些建议。
当数据量比较小时,建议用 in,虽然两者的性能差不多,但是 join 会增加 sql 的复杂度,后续再变更,会非常麻烦。
当数据量比较大时,建议用 join,主要还是出于查询性能的考虑。
不过使用 join 时,小表驱动大表,一定要建立索引,join 的表最好不要超过 3 个,否则性能会非常差,还会大大增加 sql 的复杂度,非常不利于后续功能扩展。
最后,把楼仔的座右铭送给你:我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。
原创好文:
来源:juejin.cn/post/7306322677039218724
自研一套带双向认证的Android通用网络库
当前,许多网络库基于Retrofit或OkHttp开发,但实际项目中常需要定制化,并且需要添加类似双向认证等安全功能。这意味着每个项目都可能需要二次开发。那么,有没有一种通用的封装方式,可以满足大多数项目需求?本文将介绍一种通用化的封装方法,帮助你用最少的代码量开发出自己的网络库。
源码及涉及思路参考:如何开发一款安全高效的Android网络库(详细教程)
框架简介
FlexNet 网络库是基于 Square 公司开源的 Retrofit 网络框架进行封装的。Retrofit 底层采用 OkHttp 实现,但相比于 OkHttp,Retrofit 更加便捷易用,尤其适合用于 RESTful API 格式的请求。
在网络库内部,我们实现了双向认证功能。在初始化时,您可以选择是否开启双向认证,框架会自动切换相应的 URL,而业务方无需关注与服务端认证的具体细节。
接入方式
1. 本地aar依赖
下载aar到本地(下载地址见文末),copy到app的libs目录下,如图:
implementation(files("libs/flex-net.aar"))
然后sync只会即可
2. 通过Maven远程依赖
FlexNet目前已上传Maven,可通过Maven的方式引入,在app的build.gradle中加入以下依赖:
implementation("com.max.android:flex-net:3.0.0")
sync之后即可拉到Flex-Net
快速上手
网络库中默认打开了双向认证,并根据双向认证开关配置了相应的 baseUrl,大多数场景下只需要控制双向认证开关,其余配置走默认即可。
初始化
在发起网络请求之前(建议在Application
的onCreate()
中),调用:
fun initialize(
app: Application,
logEnable: Boolean = BuildConfig.LOG_DEBUG,
sslParams: SSLParams? = null,
)
- application: Application类型,传入当前App的Application实例;
- logEnable: Boolean类型,网络日志开关,会发打印Http的Request和Resonpse信息,可能涉及敏感数据,release包慎用;(仅限网络请求日志,和双向认证的日志不同)
- sslParams: 双向认证相关参数,可选,为空则关闭双向认证。具体描述见下文。
当App需要双向认证功能时,需要在initialize()
方法中传递sslParams参数,所有双向认证相关的参数都放在sslParams当中,传此参数默认打开双向认证。
SSLParams的定义如下:
data class SSLParams(
/** App 是否在白名单之中。默认不在 */
val inWhiteList: Boolean = false,
/** 双向认证日志开关,可能涉及隐私,release版本慎开。默认关 */
val logSwitch: Boolean = true,
/** 是否开启双向认证。默认开 */
val enable: Boolean = true,
/** 双向认证回调。默认null */
val callback: MutualAuthCallback = null,
)
- inWhiteList: App是否在白名单中,默认不在
- logSwitch: 双向认证日志开关,可能涉及隐私,release版本慎开。默认关,注意这里仅针对双向认证日志,与
initialize()
方法中的logEnable
不同 - callback : 监听初始化结果回调,true表示成功,反之失败。可选参数,默认为null,仅
enableMutualAuth
为true时有效
在调用了initialize
之后就完成了初始化工作,内部包含了双向认证、网络状态、本地网络缓存等等功能,所有的网络请求都需要在初始化之后发起。
初始化示例代码:
FlexNetManger.initialize(this,
logEnable = true,
SSLParams {
Timber.i("Mutual auth result : $it")
})
PS *: *部分App在启动的时候获取不到证书,所以这里会失败。如果失败了后续可以在合适的时机通过MutualAuthenticate.isSSLReady()
来检查是否认证成功,然后通过MutualAuthenticate.suspendBuildSSL()
来主动触发双向认证,成功之后方可开始网络请求。具体可参见文档“配置项”的内容。
双向认证失败及其相关问题,可参考双向认证文档 : [双向认证])
定义数据 Model
在请求之前需要根据接口协议的字段定义对应的数据Model,用来做Request或者Response的body。
比如我们需要通过UserId获取对应用户的UserName
定义 Request 数据 Model
后端请求接口参数如下:
{
"userId" : "123456"
}
那么根据参数定义一个UserNameReq类:
data class UserNameReq(
/** 用户id */
var userId: String
)
定义 Response 数据 Model
后端返回数据如下:
{
"userName" : "MC"
}
对应定义一个UserNameRsp:
data class UserNameRsp(
/** 用户id */
var userId: String
)
编写 Http 接口
接口类必须继承自IServerAPI:
interface UserApi: IServerApi
然后在IServerApi的实现类中,每个接口需要用注解的形式标注 Http 方法,通过参数传入 http 请求的 url:
interface UserApi: IServerApi {
/** 获取用户ID */
@POST("api/cloudxcar/atmos/v1/getName")
suspend fun getUserName(@Body request: UserNameReq): ResponseEntity
}
这里需要注意的是,我们的UserNameRsp需要用ResponseEntity封装一层,看一下ResponseEntity的内容:
sealed class ResponseEntity<T>(val body: T?, val code: Int, val msg: String)
有3个参数:
- body: 消息体,即UserNameReq。仅成功时有效
- code : 返回码,这里要分多种情况描述。
- Http错误:此时code为Http错误码
- 其他异常:code对应错误原因,后面会附上映射表
- 请求成功:区分网络数据和缓存数据
- msg : 错误信息
可调用ResponseEntity.isSuccessful()
来判断是否请求成功,然后通过ResponseEntity.body
获取数据,返回的是一个根据服务端返回的 Json 解析而来的UserNameRsp实体类。
如果请求失败,则从ResponseEntity.msg
和ResponseEntity.code
中获取失败ma失败码和失败提示
创建网络请求Repo
继承自BaseRepo,泛型参数为步骤3中创建的IserverApi实现类:
class VersionRepo : BaseRepo<VersionAPI>
- 其中需要有1个必覆写的变量:
- baseUrl: 网络接口的baseUrl
- 两个可选项:
- mutualAuthSwitch: 双向认证开关,此开关仅针对当前 baseUrl 生效。默认开
- interceptorList: 需要设置的拦截器列表
- 一个必覆写的方法:
- createRepository(): 创建当前网络仓库
完整的Repo类内容如下:
class UserRepo: BaseRepo<UserApi>() {
// 必填
override val baseUrl = "https://juejin.cn/editor/drafts/7379502040140218422"
// 必填
override fun createRepository(): VersionAPI =
MutualAuthenticate.getServerApi(baseUrl, mutualAuthSwitch, interceptorList)
// 可选:双向认证开关,仅针对当前repo生效
override val mutualAuthSwitch = true
// 可选:Http拦截器
override val interceptorList: List? = listOf(HeaderInterceptor())
// 请求接口
suspend fun getUserName(): ResponseEntity{
return mRepo.upgradeVersion(UserNameReq("123456"))
}
}
注: 其中拦截器的设置interceptorList,如果声明的时候提示错误,可以尝试加上完整的类型声明:
interceptorList: List?
5 发起网络请求
最后就可以在业务代码中通过Repo类完成网络请求的调用了:
lifecycleScope.launch {
val entity= UserRepo().getUserName()
Timber.i("Get responseEntity: $entity")
if (entity.isSuccessful()) {
val result = entity.body
Timber.i("Get user name result: $result")
} else {
val code = entity.code
val msg = entity.msg
Timber.i("Get user name failed: code->$code; msg->$msg")
}
}
到这里,就可以发起一次基础的网络请求接口了。
依赖项
双向认证
目前引入的双向认证版本为1.6.0,如果需要切换版本,或者编译出现依赖冲突,可以尝试使用exclude的方式自行依赖。当然也请自行确保功能正常。
日志库
implementation("com.jakewharton.timber:timber:4.7.0")
组件库中的日志库。FlexNet推荐宿主使用Timber进行日志输出,但是需要宿主App在初始化FlexNet之前对Timber做plant操作。
网络请求内核
// Net
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
底层网络请求目前依赖OkHttp完成。
本地持久化
implementation("com.tencent:mmkv:1.2.14")
网络库中的本地存储,主要用于保存网络缓存,目前采用MMKV-1.2.14版本,同样如果有冲突,或者需要另换版本,可通过exclude实现。
Gson
api(core.network.retrofit.gson) {
exclude(module = "okio")
exclude(module = "okhttp")
}
依赖Gson,用于做数据结构和Json的相互转化
错误码对照表
CODE_SUCCESS | 10000 | 请求成功,数据来源网络 |
---|---|---|
CODE_SUCCESS_CACHE | 10001 | 返回成功,数据来源于本地缓存 |
CODE_SUCCESS_BODY_NULL | 10002 | 请求成功,但消息体为空 |
CODE_ERROR_UNKNOWN | -200 | 未知错误 |
CODE_ERROR_UNKNOWN_HOST | -201 | host解析失败,无网络也属于其中 |
CODE_ERROR_NO_NETWORK | -202 | 无网络 |
日志管理
从FlexNet 2.0.5开始,对接入方使用的日志库不再限制(2.0.5以下必须用Timber,否则无日志输出)。可以通过以下接口来设置日志监视器:
setLogMonitor(log: ILog)
设置之后所有的网络日志都会回调给ILog,即可由接入方自行决定如何处理日志数据。
如果没有设置LogMonitor
,则会使用Timber
或者Android
原生Log
来进行日志输出。当宿主App的Timber挂载优先于FlexNet的初始化,则会采用Timber做日志输出,反之使用Android Log。
文件下载
网络库内置了下载功能,可配置下载链接和下载目录。注意外部存储地址需要自行申请系统权限。
1 构建下载器
使用Downloader.builder()
来构建你的下载器,Builder需要传入以下参数:
- url:待下载文件的url
- filePath:下载文件路径
- listener:下载状态回调。可选参数,空则无回调
示例代码如下:
Downloader.Builder("https://juejin.cn/editor/drafts/7379502040140218422.zip",
File(requireContext().filesDir, "MC").absolutePath)
2 回调监听
builder()
最后一个参数,可传入下载监听器接口DownloadListener
,内部有3个方法需要实现:
- onFinish(file: File): 下载完成,返回下载完成的文件对象
- onProgress( progress : Int, downloadedLengthKb: Long, totalLengthKb: Long): 下载进度回调,回传进度百分比、已下载的大小、总大小
- onFailed(errMsg: String?): 下载失败,回调失败信息
示例代码如下:
val downloader = Downloader.Builder("https://juejin.cn/editor/drafts/7379502040140218422.zip",
File(Environment.getExternalStorageDirectory(), "MC").absolutePath,
object : DownloadListener {
override fun onFinish(file: File) {
Timber.e("下载的文件地址为:${file.absolutePath}".trimIndent())
}
override fun onProgress(
progress: Int,
downloadedLengthKb: Long,
totalLengthKb: Long,
) {
runOnUiThread {
textView.text =
"文件文件下载进度:${progress}% \n\n已下载:%${downloadedLengthKb}KB | 总长:${totalLengthKb}KB"
}
}
override fun onFailed(errMsg: String?) {
Timber.e("Download Failed: $errMsg")
}
}).build()
PS : 这里要注意,FlexNet会在业务方调用下载的线程返回下载回调,所以绝大部分时候回调是发生在子线程,此时如果有线程敏感的功能(比如刷新UI),需要自行处理线程切换。
3 触发下载
通过Builder.build()
创建 Downloader 下载器,最后调用Downloader.download()
方法即可开始下载。
和Http Request一样,download()
是一个suspend方法,需要在协程中使用:
lifecycleScope.launch(Dispatchers.IO) {
downloader.download()
}
整体架构
设置配置项
1. 设置双向认证开关
在初始化的时候控制双向认证开关:
fun init(context: Application, needMutualAuth: Boolean = true)
方法内部会根据开关值来切换不同的后端服务器,但是有些App不能过早的获取证书,这样会有双向认证失败的风险,FlexNet同时支持懒汉式的主动双向认证
2. 主动双向认证接口
在确定拿到证书,或者确定可以双向认证的时机,可随时发起双向认证请求:
MutualAuthenticate.suspendBuildSSL()
可通过
MutualAuthenticate.isSSLReady()
接口来检查当前双向认证是否成功。
主动触发示例代码如下:
MutualAuthenticate.suspendBuildSSL {
if (it) {
Toast.makeText(context, "双向认证成功,可以开始访问加密资源", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "双向认证失败", Toast.LENGTH_SHORT).show()
}
}
3. 数据缓存
在前面发起请求调用httpRequest
顶层函数的时候,可以传入一个可选参数cacheKey
,这个key不为空则网络库会在本地保存当前请求的返回数据。Key作为缓存的唯一标识,在无网络或请求失败的时候,会通知调用方错误,并返回缓存的数据。
缓存部分流程如下:
4. 错误及异常处理
在发起请求的顶层函数 httpRequest
中,有两个参数用来提供给调用方处理错误和异常。
首先区分一下错误和异常:
错误通常是发起了网络请求,且网络请求有响应,只是由于接口地址或者参数等等原因导致服务端解析失败,最终返回错误码及错误信息。
而异常是指在发起网络请求的过程中出现了 Exception,导致整个网络请求流程被中断,所以当异常发生的时候,网络库是不会返回错误码和错误信息的,只能返回异常信息供调用方定位问题。
回调的使用方式很简单,只需要在httpRequest
中传入两个回调:fail
和error
,下面分别看看二者的处理方式:
1. 错误处理
fai的定义如下:
fail: (response: ResponseEntity) -> Unit = {
onFail(it)
}
传入的回调有一个 ResponseEntity 参数,这是网络请求返回的响应实体,内部包含errorCode
和errorMessage
,不传则默认打印这两个字段,可以在 Logcat 中通过Tag:Http Request
**过滤出来。
2. 异常处理
error的定义如下:
error: (e: Exception) -> Unit = {
onError(it)
} ,
回调函数只有一个 Exeption 对象,和前面的定义相符,在异常的时候将异常返回供调用方定位问题。不传网络库默认打印异常,可以在 Logcat 中通过Tag:Http Request
**过滤出来。
扩展接口:发起请求并处理返回结果
网络库定义了一个顶层函数用来发起请求并接收返回结果或处理异常:
fun
- block: 实际请求体,必填。可以传入步骤 4 中实现的接口
- fail: 请求错误回调,非必填。用来处理服务端返回的请求错误,会携带错误码及错误信息
- error: 请求异常回调,非必填。用来处理请求中发生的异常,此时没有response返回
- cacheKey: 数据缓存唯一标识,非必填
httpRequest 中的泛型 T 就是接入步骤2定义的 Response 实体,正常返回会在方法内部自动解析出 UserNameRsp
,到此就完成了一次网络请求。
以上是基本的使用方式,涵盖了安全、数据请求、缓存、异常处理等功能,可以适应于多种项目场景。应大家的建议,后续会完善几篇文章拆解具体的原理及开发思路,从源码的角度教你如何从0开发一套完善的网络库
大家如果想了解设计思路及框架原理,可以参考:源码及涉及思路参考:如何开发一款安全高效的Android网络库(详细教程)
需要体验的同学可以在评论区留下联系方式,我给你发送aar以及源码。有问题欢迎随时探讨
来源:juejin.cn/post/7379521155286941708
sleep 和 wait深度对比!
在计算机编程中,特别是在多线程或并发编程中,sleep
和 wait
是两个非常常见的函数,但它们有不同的用途和工作机制,这篇文章我们将详细地讨论 sleep
和 wait
的区别,包括它们的内部工作原理、应用场景以及详细的示例代码,以帮助更全面地理解它们。
sleep
工作机制
- 暂停当前线程:
sleep
方法暂停当前执行的线程一段指定的时间,时间结束后线程再恢复执行。 - 不会释放锁: 即使线程在
sleep
状态下持有锁,它也不会释放。它依然占用着该锁,其他线程无法获得该锁。 - 线程状态转换:
sleep
方法会使线程从运行(RUNNING)状态转换为计时等待(TIMED_WAITING)状态。 - 静态方法: 它是
Thread
类的静态方法,调用时通过Thread.sleep
访问。
应用场景
- 限流: 控制任务执行的频率,防止线程过度占用CPU资源。
- 定时任务: 在某个循环中,定时执行某些任务。
示例代码
public class SleepExample extends Thread {
public void run() {
try {
System.out.println("Thread going to sleep for 2 seconds.");
Thread.sleep(2000); // 睡眠 2 秒
System.out.println("Thread woke up after sleeping.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SleepExample thread = new SleepExample();
thread.start();
}
}
wait
工作机制
- 释放锁并等待通知:
wait
方法使当前线程等待,直到其他线程调用当前对象的notify
或notifyAll
方法。调用wait
时,线程会释放它持有的锁。 - 必须在同步块或同步方法中使用:
wait
方法必须在同步块或同步方法中调用,否则会抛出IllegalMonitorStateException
。 - 线程状态转换:
wait
方法会使线程从运行(RUNNING)状态转换为等待(WAITING)状态。 - 对象方法: 它是
Object
类的方法,所以任何对象都可以调用。
应用场景
- 线程间通信: 多个线程协同工作时,一个线程等待某个条件满足后,再被其他线程通知继续执行。
- 生产者-消费者模型: 经常用于实现生产者-消费者模式中的同步。
示例代码
public class WaitNotifyExample {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 等待线程
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread waiting for the lock to be released.");
lock.wait(); // 进入等待状态并释放锁
System.out.println("Thread resumed after lock released.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 通知线程
Thread notifyingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("Notifying other threads.");
lock.notify(); // 通知其他等待该锁的线程
System.out.println("Notified waiting thread.");
}
});
waitingThread.start();
Thread.sleep(1000); // 确保 waitingThread 先持有锁并进入等待状态
notifyingThread.start();
}
}
sleep 和 wait的对比
特性 | sleep | wait |
---|---|---|
释放锁 | 否 | 是 |
需要在同步块或方法中 | 否 | 是 |
属于 | Thread 类 | Object 类 |
引发异常 | InterruptedException | InterruptedException 引发机制相同 |
作用范围 | 当前调用的线程 | 当前拥有锁的线程 |
线程状态改变 | 变为计时等待(TIMED_WAITING) | 变为等待(WAITING) |
典型应用场景 | 暂停线程的一段时间,用于控制节奏或定时操作 | 线程间通信,生产者-消费者模型等 |
总结
本文,我们分析了sleep
和 wait
,sleep
用于暂停当前线程一段指定时间,但仍保持锁,这常用来控制执行节奏或定时操作。wait
使线程释放锁并进入等待状态,直到通过 notify/notifyAll 被唤醒,需在同步块中使用,适用于线程间通信如生产者-消费者模型。
来源:juejin.cn/post/7420718386953355279
springboot + minio + kkfile实现文件预览(不暴露minio地址)
前言
之前我写过一片文章【springboot + minio + kkfile实现文件预览】,该文章介绍了如何使用kkfile预览文件,但是文章中介绍的方案,会暴露minio的地址,实际的预览地址如下:
http://kkfile-server/onlinePreview?url=base64UrlEncode(minio生成的文件预览地址)
但是大多数情况下,minio服务的地址是不允许暴露的,所有我们对其进行优化,依然使用kkfile预览文件,但是我们使用文件流的方式,并且在下载接口上校验用户认证的有效性,在保证不暴露minio地址的前提下,还加入了token认证,提高了安全性,话不多说,直接上代码。
一、文件上传
上传服务
public void uploadFile(MultipartFile file) throws Exception {
String fileName = System.currentTimeMillis() + "-" + file.getOriginalFilename();
PutObjectArgs args = PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(fileName).stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
client.putObject(args);
}
封装接口
@PostMapping("upload")
public RestResult upload(MultipartFile file) {
try {
sysFileService.uploadFile(file);
} catch (Exception e) {
log.error("上传文件失败", e);
return RestResult.fail(e.getMessage());
}
}
二、文件下载
下载服务
public void download(String filename, HttpServletResponse response) throws ServiceException {
try {
InputStream inputStream = client.getObject(GetObjectArgs.builder().bucket(minioConfig.getBucketName()).object(filename).build());
// 设置响应头信息,告诉前端浏览器下载文件
response.setContentType("application/octet-stream;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
// 获取输出流进行写入数据
OutputStream outputStream = response.getOutputStream();
// 将输入流复制到输出流
byte[] buffer = new byte[4096];
int bytesRead = -1;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 关闭流资源
inputStream.close();
outputStream.close();
} catch (Exception e) {
log.error("文件下载失败:" + e.getMessage());
throw new ServiceException("文件下载失败");
}
}
封装接口
@ApiOperation("文件下载")
@GetMapping("/download/{token}/{filename}")
public void getDownload(@PathVariable("token") String token, @PathVariable("filename") String filename, HttpServletResponse response) {
tokenUtils.validateToken(token);
sysFileService.download(filename, response);
}
上面的接口有两个地方需要注意
- @GetMapping("/download/{token}/{filename}")中filename参数必须放在最后
- tokenUtils.validateToken(token);
该接口要在拦截器中放行,验证token在代码逻辑中,这里根据项目中实际场景去实现。该地址为kkfile请求获取文件流的地址,所以需要放开鉴权
三、文件预览地址获取
文件预览地址生成服务(该服务只是获取token并拼接到文件下载地址中,不对token做验证,因为该服务的接口在请求进入前要做校验)
public String getPreviewUrl(String filename) throws UnsupportedEncodingException {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
if (request ==null || StringUtils.isBlank(request.getHeader(TokenConstants.AUTHENTICATION))) {
throw new ServiceException("未获取到有效token");
}
String previewUrl = filePreviewUrl + FileUploadUtils.base64UrlEncode(fileDownloadUrl + "/" + token + "/" + filename);
return previewUrl + "&fullfilename=" + URLEncoder.encode(filename, "UTF-8");
}
FileUploadUtils中的base64UrlEncode方法
public static String base64UrlEncode(String url) throws UnsupportedEncodingException {
String base64Url = Base64.getEncoder().encodeToString(url.getBytes(StandardCharsets.UTF_8));
return URLEncoder.encode(base64Url, "UTF-8");
}
封装接口,获取文件预览地址
@GetMapping("/getPreviewUrl")
public RestResult<String> getPreviewUrl(String filename) throws UnsupportedEncodingException {
return RestResult.ok(sysFileService.getPreviewUrl(filename));
}
测试
假设
- 文件服务地址为:http://file-server
- kkfile服务地址为:http://kkfile-server
- 文件名称为:xxxx.docx
最后生成的文件预览地址为:
http://kkfile-server/onlinePreview?url=aHR0cDovLzE3Mi4xNi41MC4y....&fullfilename=xxxx.docx
其中aHR0cDovLzE3Mi4xNi41MC4y....为:
FileUploadUtils.base64UrlEncode("http://file-server" + "/" + token + "/" + filename);
截图为证
来源:juejin.cn/post/7424338056918761498
Java已死,大模型才是未来?
引言
在数字技术的浪潮中,编程语言始终扮演着至关重要的角色。Java,自1995年诞生以来,便以其跨平台的特性和丰富的生态系统,成为了全球范围内开发者们最为青睐的编程语言之一
然而,随着技术的不断进步和新兴语言的崛起,近年来,“Java已死”的论调开始不绝于耳。尤其是在大模型技术迅猛发展的今天,Java的地位似乎更加岌岌可危。然而,事实真的如此吗?Java的春天,真的已经渐行渐远了吗?本文将从多个维度深入探讨Java的现状、大模型技术的影响,以及Java与大模型融合的可能性,为读者提供一个更为全面和深入的视角。
Java的辉煌历史与稳健地位
Java,作为Sun Microsystems在1995年推出的编程语言,一经问世便凭借其独特的跨平台特性和丰富的生态系统,迅速在全球范围内赢得了广泛的认可和应用。从最初的Java Applet,到后来的Java Web开发、Java EE企业级应用,再到如今的Android应用开发、大数据处理等领域,Java都展现出了其强大的生命力和广泛的应用前景。
在最新的TIOBE编程语言排行榜上,Java长期位居前列,这足以证明其在开发界的重要地位。而在中国这个拥有庞大IT市场的国家中,Java更是受到了广泛的关注和追捧。无论是大型企业还是初创公司,Java都成为了其首选的开发语言之一。这背后,是Java的跨平台特性、丰富的库和框架、强大的社区支持等多方面的优势所共同铸就的。
然而,随着技术的不断进步和新兴语言的崛起,Java也面临着一些挑战和质疑。
一些人认为,Java的语法过于繁琐、性能不够优越、新兴语言如Python、Go等更加轻便灵活。这些观点在一定程度上反映了Java在某些方面的不足和局限性。
但是,我们也不能忽视Java在企业级应用、Web开发、大数据处理等领域的深厚积累和广泛应用。这些领域对Java的稳定性和可靠性有着极高的要求,而Java正是凭借其在这方面的优势,赢得了众多企业和开发者的青睐。
大模型技术的崛起与影响
近年来,随着人工智能和机器学习技术的飞速发展,大模型技术逐渐成为了人工智能领域的一大热点,可谓是百家争鸣。大模型技术通过构建庞大的神经网络模型,实现对海量数据的深度学习和处理,从而在各种应用场景中取得了令人瞩目的成果。
在自然语言处理领域,大模型技术通过训练庞大的语言模型,实现了对自然语言的深入理解和生成。这使得机器能够更加智能地处理人类的语言信息,从而实现更加自然和流畅的人机交互。在图像处理领域,大模型技术也展现出了强大的能力。通过训练庞大的卷积神经网络模型,机器能够实现对图像的精准识别和分析,从而在各种应用场景中发挥出巨大的作用。
大模型技术的崛起对软件开发产生了深远的影响。
首先,大模型技术为开发者提供了更加高级别的抽象和智能化解决方案。这使得开发者能够更加专注于核心业务逻辑的实现,而无需过多关注底层技术的细节。其次,大模型技术降低了AI应用的开发门槛。传统的AI应用开发需要深厚的数学和编程基础,而大模型技术则通过提供易于使用的工具和框架,使得开发者能够更加方便地构建和部署AI应用。最后,大模型技术推动了软件开发的智能化升级。从需求分析、设计到开发、测试和维护等各个环节都在经历着智能化的变革,这使得软件开发过程更加高效和智能。
Java与大模型的融合与变革
在大模型技术崛起的背景下,Java作为一种成熟且广泛应用的编程语言,自然也在探索与大模型技术的融合之路。事实上,Java与大模型的融合已经取得了不少进展和成果。
首先,Java社区对于大模型技术的支持和探索已经初见成效。一些开源项目和框架在Java环境中实现了深度学习和大模型技术的支持,如Deeplearning4j、ND4J等。这些项目和框架为Java开发者提供了丰富的工具和资源,使得他们能够更加方便地构建和部署基于大模型的应用。
其次,Java自身的特性和优势也为其与大模型的融合提供了有力的支持。Java作为一种面向对象的语言,具有强大的抽象能力和封装性,这使得它能够更好地处理大模型中的复杂数据结构和算法。同时,Java的跨平台特性也使得基于Java的大模型应用能够在不同的操作系统和硬件平台上运行,从而提高了应用的兼容性和可移植性。
最后,Java与大模型的融合也推动了软件开发的智能化升级。在需求分析阶段,大模型技术可以通过对海量数据的学习和分析,帮助开发者更加准确地把握用户需求和市场趋势。在设计阶段,大模型技术可以通过对已有设计的分析和优化,提高设计的合理性和效率。在开发阶段,大模型技术可以为开发者提供智能化的编程辅助和错误检查功能,从而提高开发效率和代码质量。在测试和维护阶段,大模型技术可以通过对应用的持续监控和分析,及时发现和修复潜在的问题和缺陷。
未来趋势与展望
随着AI和机器学习技术的不断发展,大模型技术将在未来继续发挥重要的作用。而Java作为一种成熟且广泛应用的编程语言,也将继续在大模型时代发挥其独特的优势和作用。
首先,Java将继续优化其性能和语法,提高开发者的开发效率和代码质量。同时,Java还将加强对大模型技术的支持和整合,为开发者提供更加全面和强大的工具和框架。
其次,Java将与更多新兴技术进行融合和创新。例如,随着云计算和边缘计算的兴起,Java将加强与这些技术的融合,推动云计算和边缘计算应用的发展。此外,Java还将与物联网、区块链等新兴技术进行深度融合,开拓新的应用领域和市场空间。
最后,Java将继续发挥其在企业级应用、Web开发、大数据处理等领域的优势,为各行各业提供更加稳定、可靠、安全的解决方案。同时,Java也将积极拥抱开源文化和社区文化,与全球开发者共同推动Java生态系统的繁荣和发展。
总之,Java作为一种历久弥新的编程语言巨头,将在大模型时代继续发挥其独特的优势和作用。通过与大模型技术的深度融合与创新,Java将引领编程世界的潮流,为各行各业带来更加智能化和自动化的解决方案。让我们共同期待Java在未来的辉煌!
写在最后
我不禁要感慨Java这一编程语言的深厚底蕴和持久魅力。它不仅是一段技术史,更是无数开发者智慧与汗水的结晶。在大模型时代,Java也会以其独特的稳定性和可靠性,持续为各行各业提供着坚实的支撑。正如历史的河流永不停息,Java也在不断地进化与创新,与新兴技术深度融合,共同推动着科技发展的浪潮。让我们携手前行,继续书写Java的辉煌篇章,为构建更加智能、更加美好的未来贡献力量。
来源:juejin.cn/post/7419967609451675700
Android串口开发入门
最近的开发项目有涉及到Android串口开发,所以比较好奇安卓项目中是如何读取串口数据的,官方给了一个代码示例,但是发现官方的示例比较久了,没有使用CMake,下载后运行老是报错,踩坑,最后使用CMake的方式终于运行成功,在此记录一下,源码:gitee.com/hluck/hello…
目录结构
1.创建一个HelloWord项目
2.引入jni和so库
将jni文件夹和jniLibs文件夹复制到main目录下:
3.修改gradle
由于此时Android studio编译时,不会去编译加载CMakeLists.txt,所以要告诉他在哪加载:
android {
...
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt" // 指定 CMakeLists.txt 文件路径
// 其他 CMake 选项
}
}
}
4.加载动态库,编译native方法
官方示例中有两个类是关于打开和关闭串口api的:
1.SeriaPort
其中加载动态库,打开和关闭串口的native方法在SerialPort类中:
这两个native方法对应的是jni文件下的SerialPort.c文件中,如果你的SerialPort类所在包名和我的不一样,记得修改一下这个文件,值得一提的是,open方法中的第一个参数是串口地址,第二个参数是波特率,第三个参数是打开串口时的操作模式,0表示默认,当调用读写操作时,如果串口没有准备好数据,程序会阻塞等待,直到有数据可以读取或写入。
2.FileDescriptor
上面的open方法会返回一个FileDescriptor实例,通过这个实例获取写入和读取串口数据的流。
5.读取或写入串口数据
在Application类中保存一个SerialPort实例,这样就能通过获取SerialPort实例来读写串口数据了。
参考文章
来源:juejin.cn/post/7381347654743326746
作为前端开发,这些年跟设计师的斗智斗勇
我无意中在知乎上看到这样一个话题, 让我不得不有感而发。
因为曾经的我也是被设计师给虐惨了。
我是工作了 8 年的老前端了, 如果算上实习那就有 9 年了。
我做过 C 端应用, 做过 B 端应用, 做过 SaaS 应用, 我经历的所有设计师, 都不接受 0.5px 的像素偏差。 可以说是对像素偏差 0 容忍。
所以想作为前端工程师, 来来给大家聊聊我的日常工作中是怎么跟设计师斗智斗勇的。
1. 给设计师设置门槛
这个设置门槛意思很简单, 你不能拿着电脑, 指着我屏幕就说, 这这这不行, 那那那不行的。
你得走系统工单, 一个样式一个工单。 得注明, 哪儿样式不对齐, 差多少像素, 预期重新验收的时间, 走查报告, 样式走查进度等等。如果没有系统工单的流程, 搞一个复杂的文档丢给设计同学也行, 让设计师同学把每一个问题都尽可能的详细记录下来。
还要明确设计走查验收时间, 定稿的设计稿件, 非特殊原因不允许修改等方式, 增加设计师自身的成本。
和设计师合作完成一个项目, 完成之后大家都分蛋糕, 自己改样式是有成本投入, 设计师别人是零成本投入, 哪儿那行? 所以这个就是门槛的来源。
2. 告诉设计师我修改某一些样式的成本, 这个成本超过了预期, 需要设计师额外承担成本付出
举一个简单的例子哈。 设计是要求做 移动端 和 PC 端兼容, 我预估做移动端和PC 端兼容, 需要 5 天时间。 已经拍板定下来了。 做到了一半, 设计说, 我的移动端, 要适配 小屏幕手机和大屏幕手机以及 平板 拥有独立的样式展现。 这个成本是预估之外的额外成本, 可能需要多加 5 天时间。 那么这个 5 天时间, 是需要设计师去向项目经理申请的, 项目经理如果同意, 多给我加五天我就做。(其实相当于转移矛盾)
3. 给出设计师无法拒绝的理由
就说到了静态和动态的问题了。 比如设计师给了一个版本的设计, 是没有数据情况下的设计, 但是前端数据加载出来了, 渲染出来的结果, 跟设计师预期的不一样。 而且设计师自己也没有给设计稿。 这个就直接专业碾压就行了, 黑话直接就来,比如:我的架构能力已经做好了, 实在是改不了了, 否则的推翻了重新做; 你这个 1 PX 的像素偏差, 真的有必要吗,你如何论证你这部分必要性;它是一个共性问题,以前的需求都是这样子的, 如果你这次改了,那之前的那些样式也都改吗,否则是不一致的;..........
这个方向, 就是主要针对一些可有可无的样式调整。如果修改成本较大, 而且设计稿本身就模棱两可的, 就可以使用专业知识碾压。
4. 设计稿变动要周知
其实很多时候在开发过程中是, 会出现一个情况, 设计师改了设计稿(在没有跟前端同学商量的情况下改了设计稿)。这个我相信是很多前端同学最头疼的事情之一。
我之前合作过一个设计师, 很喜欢临时改设计稿,但是改了又不周知,让我跟着后面反复改, 最后项目导致了一定时间的延期。 最后项目复盘的时候, 我就直接跳出来喷这个情况,有理有据,让 leader 们去核算这部分成本了。
其实一定要达成一个一致意见, 甚至可以在做项目之前就商量好, 如果遇到设计稿变更, 导致开发工作要返工的情况, 总计返工超过 0.5 日的情况, 要提出需求变更,不通过需求变更的设计稿变更, 一律不接受。
5. 提升自己专业能力
最后这个没啥好说的, 吃这个饭, 就的接受这个设定;尽量想办法还原设计师的设计稿即可。
曾经我也常被设计师折磨得体无完肤, 甚至想过要转行算了。 想想, 后端同学还不是一样被数据、稳定性、服务器运维等问题折磨得体无完肤。 各行都有各行的难处, 吃这个饭就得接受这个设定。
提升自己专业能力, 只会有利无害, 就比如我现在也能算是半个像素眼。
来源:juejin.cn/post/7429981053039312934
回顾我转前端的这一年。。
✊不积跬步,无以至千里;不积小流,无以成江海
从去年年尾意外发生,突然“找工作”,23年10月24号注册掘金写下第一篇博客,到今天刚好一年。这一年经历了写毕业论文、从0做项目找实习,答辩,刷算法,准备秋招,最终拿到四个大厂offer。也算是天糊开局,完美收官。
说实话最开始并不是以进入大厂作为target来规划今年的秋招的,因此也还算走了一些“弯路”。但也许offer真的和运气是相关的,索性殊途同归了吧。当然9本给我了折腾的底气,但我相信就算是非92,一年时间(其实是用不了的)好好规划,是一定能够在秋招季收获一个好的结果的。
回顾这一年,做的最正确的决定之一,就是坚持写博客。虽然也是写到差不多第十个月才感受到什么是“写博客输出”,但没有前面十个月的坚持,也不会有后几个月的蜕变。所以!没有一点努力是白费的!就像我每一篇文章的quota:’✊不积跬步,无以至千里;不积小流,无以成江海‘。种下一棵树最好的时间是十年前,其次是现在。所以看到这篇文章的你,不妨从现在开始尝试写下自己的第一个博客文章~~
这一年来,除去整理其他公司的前端笔试题这种博客,应该写了差不多70篇左右的博客。先介绍一下博客,也大概是我的学习过程的缩影。
前三十篇左右的博客更像是知识点的罗列复习。其实我并不是第一次接触前端,但曾经系统学过的知识点已经不够熟悉了,我坚信好记性不如烂笔头,所以连查带写带输出,完成了前端知识点的第一轮复习。
之后十篇左右的博客记录了三个月跟着网上视频学项目的时候遇到的问题,比如场景,或者问题解法等等。一方面是觉得这些点比较有趣,另一方面也是为了面试的时候和面试官沟通的话不用干巴巴讲,有博客+动图能够尽量完整的将信息传递。(毕竟考虑别人的【用户体验】也是前端工程师要在意的事)
再然后到了找实习之前,为了面试写了一系列的面试复习笔记,大概十多篇。都是实习时候会被问到的各种前端知识点,应该是比较全和具有代表性的。这个时候开始试图自己归纳、总结、整理输出成自己的想法。“自己的感悟”开始有一点雏形。也是这个时候明显觉得自己的前端知识上了一个台阶,能够理解知识点之间的关系并可以串联,能初步构建自己的前端知识网络。
之后就是实习的感悟。其实实习的时候因为太忙(实习任务、新城市适应、生活变动、学校杂事、回学校答辩),并没有写什么博客。反而是要准备离开实习公司,开始罗列自己的收获的时候,把自己日常的工作记录摘抄一些“精品”而有趣的点,写了几篇博客。也是这个时候我自我反思觉得不坚持写博客真的时间会“溜走”,还是记下来更有安全感,坚定了要坚持写下去的信念。
后面就是到了秋招,其实这个阶段大框架上的知识点已经很少有哪里需要查漏补缺了。但和一些有经验的大佬沟通,觉得似乎“沉淀和思考”还可以做的更好。所以花了相当一部分精力修正或者说串联之前的博客,以及酝酿了几篇非常好的博客。一篇博客光写就要写两天的那种。虽然数据一般,但我相信是金子总会发光哈哈。基本这个阶段走完,自己前端知识点网络已经构建的比较全面了,很多问题都已经有了自己的见解,不止仅仅是那种看几个博客看几个视频的见解,而是真的自己也知道怎么回事,什么原理,要怎么样去用这种。
综上所述,自己也完成了从罗列博客,到写博客,再到输出博客的蜕变。还是对自己的这一年很满意的。
对于看到这篇文章的朋友,并且想自己找工作能有好结果的,我能给出的一些建议:
- 坚持做一件事,写博客只是其中一件。每个工作日github push代码也可以是一件事。量变到质变的积累,要先有量才可以,没有人是天才。
- 实习经历(有效,不是混日子那种)大于项目经历。实际工作场景中能够获得的成长是个人项目没法获得的。
- 如果因为某些原因没办法有实习,那么只能通过疯狂背“八股”来展示自己的’优秀‘基础。但别气馁,没实习的人很多的。
- leetcode代码是不能逃掉的一个待办项。比起我那时极度痛苦疯狂恶补,不如给自己三个月,每天都刷一点点。leetcode对每个人都很公平,只能靠量,几乎不可能速成。
- 如果从大一就开始为前端做准备,那不同大厂间的不同业务线实习是最优解。这能帮助你更好的了解不同大厂/不同业务的风格,能帮助你找到自己想要的到底是什么。当然,如果你找到的第一个就发现是自己所爱,那恭喜你,持续在这里实习吧!
- 如果再给我一个月,也许我会静下心来好好研究一下react源码/fiber架构这类的。尽管关于这些网上有长视频讲的很好,但想真的有自己的理解,唯有自己沉下心来好好研究,所有视频资料都不过是辅助工具。当然,更好的理解方式是在工作中慢慢感悟。
- 找工作是一场马拉松,没到最后,万物皆有可能,心态要稳。我一周收获了三个offer,我相信如果我肯多约几个面试,也许能创造更多的不可能。
很高兴你能看到最后,回首这一年,虽然很苦,但是真的很开心。希望你回顾自己的一年,也会觉得很开心~
有些什么问题也可以在评论区留言,打破零回复哈哈
来源:juejin.cn/post/7429321661491462155
WebGL实现soul星球效果
WebGL实现soul星球效果
最近在研究webGL,觉得soul app的星球挺有意思的,于是就实现了一下,中间涉及的细节和知识点挺多的,写篇博客分享一下
soul原版
WebGL实现的
主要技术要点
1.使用黄金分割数螺旋分配
使小球在球表面均匀分布
使用不同的goldenRatio可以得到非常多分布效果,采用黄金分割数在视觉上最匀称、舒服
const goldenRatio = (1 + Math.sqrt(5)) / 2
const y = 1 - (i / (numPoints - 1)) * 2
const radiusAtY = Math.sqrt(1 - y * y)
const theta = (2 * Math.PI * i) / goldenRatio
const x = Math.cos(theta) * radiusAtY
const z = Math.sin(theta) * radiusAtY
2.自由转动
因为要解决万向锁的问题,所以不能使用rotateX
、rotateY
、rotateZ
来旋转,应当使用四元数THREE.Quaternion
3.背面小球变暗
这里通过内部放置了一个半透明的黑色小球来实现
// 创建半透明球体
const sphereGeometry = new THREE.SphereGeometry(4.85, 16, 16)
为了使小球从正面转动的背面的过程中可以平滑的变暗,这里还需要把半透明小球的边沿处理成高斯模糊
,具体实现就是使用GLSL的插值函数smoothstep
fragmentShader: `
uniform vec3 color;
uniform float opacity;
varying vec3 vNormal;
void main() {
float alpha = opacity * smoothstep(0.5, 1.0, vNormal.z);
gl_FragColor = vec4(color, alpha);
}
但是需要注意的是需要关闭小球的深度测试
,否则会遮挡小球
side: THREE.FrontSide,
depthWrite: false,
4.使用THREE.Sprite
创建小球标签
5.标签位置计算
for (let i = 0; i < numPoints; i++) {
const y = 1 - (i / (numPoints - 1)) * 2
const radiusAtY = Math.sqrt(1 - y * y)
const theta = (2 * Math.PI * i) / goldenRatio
const x = Math.cos(theta) * radiusAtY
const z = Math.sin(theta) * radiusAtY
const smallBallMaterial = new THREE.MeshBasicMaterial({
color: getRandomBrightColor(),
depthWrite: true,
depthTest: true,
side: THREE.FrontSide,
})
const smallBall = new THREE.Mesh(smallBallGeometry, smallBallMaterial)
smallBall.position.set(x * radius, y * radius, z * radius)
6.超出长度的标签采用贴图采样位移
来实现跑马灯效果
7.滚动阻尼,鼠标转动球体之后速度能衰减到转动旋转的速率
8.自动旋转需要保持上一次滚动的方向
9.使用射线拾取
来实现点击交互
完整代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>3D 半透明球体与可交互小球</title>
<style>
body {
margin: 0;
background-color: black;
touch-action: none;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js'
// 创建场景
const scene = new THREE.Scene()
// 创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
)
camera.position.set(0, 0, 14)
camera.lookAt(0, 0, 0)
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setClearColor(0x000000, 0)
document.body.appendChild(renderer.domElement)
// 创建半透明球体
const sphereGeometry = new THREE.SphereGeometry(4.85, 16, 16)
const sphereMaterial = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(0x000000) },
opacity: { value: 0.8 },
},
vertexShader: `
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform vec3 color;
uniform float opacity;
varying vec3 vNormal;
void main() {
float alpha = opacity * smoothstep(0.5, 1.0, vNormal.z);
gl_FragColor = vec4(color, alpha);
}
`,
transparent: true,
side: THREE.FrontSide,
depthWrite: false,
})
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
scene.add(sphere)
// 创建小球体和标签数组
const smallBallGeometry = new THREE.SphereGeometry(0.15, 16, 16)
const smallBalls = []
const labelSprites = []
const radius = 5
const numPoints = 88
const goldenRatio = (1 + Math.sqrt(5)) / 2
const maxWidth = 160
const textSpeed = 0.002
// 创建射线投射器
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
function createTextTexture(text, parameters = {}) {
const {
fontSize = 24,
fontFace = 'PingFang SC, Microsoft YaHei, Noto Sans, Arial, sans-serif',
textColor = 'white',
backgroundColor = 'rgba(0,0,0,0)',
maxWidth = 160,
} = parameters
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.font = `${fontSize}px ${fontFace}`
const textMetrics = context.measureText(text)
const textWidth = Math.ceil(textMetrics.width)
const textHeight = fontSize * 1.2
const needMarquee = textWidth > maxWidth
let canvasWidth = maxWidth
if (needMarquee) {
canvasWidth = textWidth + 60
}
canvas.width = canvasWidth
canvas.height = textHeight
context.font = `${fontSize}px ${fontFace}`
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = backgroundColor
context.fillRect(0, 0, canvas.width, canvas.height)
context.fillStyle = textColor
context.textAlign = needMarquee ? 'left' : 'center'
context.textBaseline = 'middle'
if (needMarquee) {
context.fillText(text, 0, canvas.height / 2)
} else {
context.fillText(text, maxWidth / 2, canvas.height / 2)
}
const texture = new THREE.CanvasTexture(canvas)
texture.needsUpdate = true
if (needMarquee) {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.ClampToEdgeWrapping
texture.repeat.x = maxWidth / canvas.width
} else {
texture.wrapS = THREE.ClampToEdgeWrapping
texture.wrapT = THREE.ClampToEdgeWrapping
}
texture.minFilter = THREE.LinearFilter
texture.magFilter = THREE.LinearFilter
texture.generateMipmaps = false
return { texture, needMarquee, HWRate: textHeight / maxWidth }
}
for (let i = 0; i < numPoints; i++) {
const y = 1 - (i / (numPoints - 1)) * 2
const radiusAtY = Math.sqrt(1 - y * y)
const theta = (2 * Math.PI * i) / goldenRatio
const x = Math.cos(theta) * radiusAtY
const z = Math.sin(theta) * radiusAtY
const smallBallMaterial = new THREE.MeshBasicMaterial({
color: getRandomBrightColor(),
depthWrite: true,
depthTest: true,
side: THREE.FrontSide,
})
const smallBall = new THREE.Mesh(smallBallGeometry, smallBallMaterial)
smallBall.position.set(x * radius, y * radius, z * radius)
sphere.add(smallBall)
smallBalls.push(smallBall)
const labelText = getRandomNickname()
const { texture, needMarquee, HWRate } = createTextTexture(labelText, {
fontSize: 28,
fontFace: 'PingFang SC, Microsoft YaHei, Noto Sans, Arial, sans-serif',
textColor: '#bbbbbb',
maxWidth: maxWidth,
})
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: true,
depthTest: true,
blending: THREE.NormalBlending,
})
const sprite = new THREE.Sprite(spriteMaterial)
sprite.scale.set(1, HWRate, 1)
labelSprites.push({ sprite, smallBall, texture, needMarquee, labelText })
scene.add(sprite)
}
// 添加灯光
const light = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(light)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
directionalLight.position.set(5, 5, 5)
scene.add(directionalLight)
// 定义自动旋转速度和轴
const autoRotationSpeed = 0.0005
let autoRotationAxis = new THREE.Vector3(0, 1, 0).normalize()
let currentAngularVelocity = autoRotationAxis.clone().multiplyScalar(autoRotationSpeed)
let isDragging = false
let previousMousePosition = { x: 0, y: 0 }
let lastDragDelta = { x: 0, y: 0 }
const decayRate = 0.92
const increaseRate = 1.02
// 鼠标事件处理
const onMouseDown = (event) => {
isDragging = true
previousMousePosition = {
x: event.clientX,
y: event.clientY,
}
}
const onMouseMove = (event) => {
if (isDragging) {
const deltaX = event.clientX - previousMousePosition.x
const deltaY = event.clientY - previousMousePosition.y
lastDragDelta = { x: deltaX, y: deltaY }
const rotationFactor = 0.005
const angleY = deltaX * rotationFactor
const angleX = deltaY * rotationFactor
const quaternionY = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
angleY
)
const quaternionX = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0),
angleX
)
const deltaQuat = new THREE.Quaternion().multiplyQuaternions(quaternionY, quaternionX)
sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)
const dragRotationAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
const dragRotationSpeed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * rotationFactor
if (dragRotationAxis.length() > 0) {
currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
}
previousMousePosition = {
x: event.clientX,
y: event.clientY,
}
}
}
const onMouseUp = () => {
if (isDragging) {
isDragging = false
const deltaX = lastDragDelta.x
const deltaY = lastDragDelta.y
if (deltaX !== 0 || deltaY !== 0) {
const newAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
if (newAxis.length() > 0) {
autoRotationAxis.copy(newAxis)
}
const dragSpeed = currentAngularVelocity.length()
if (dragSpeed > autoRotationSpeed) {
// 维持当前旋转速度
} else {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}
}
}
// 触摸事件处理
const onTouchStart = (event) => {
isDragging = true
const touch = event.touches[0]
previousMousePosition = {
x: touch.clientX,
y: touch.clientY,
}
}
const onTouchMove = (event) => {
event.preventDefault()
if (isDragging) {
const touch = event.touches[0]
const deltaX = touch.clientX - previousMousePosition.x
const deltaY = touch.clientY - previousMousePosition.y
lastDragDelta = { x: deltaX, y: deltaY }
const rotationFactor = 0.002
const angleY = deltaX * rotationFactor
const angleX = deltaY * rotationFactor
const quaternionY = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
angleY
)
const quaternionX = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0),
angleX
)
const deltaQuat = new THREE.Quaternion().multiplyQuaternions(quaternionY, quaternionX)
sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)
const dragRotationAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
const dragRotationSpeed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * rotationFactor
if (dragRotationAxis.length() > 0) {
currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
}
previousMousePosition = {
x: touch.clientX,
y: touch.clientY,
}
}
}
const onTouchEnd = (event) => {
if (isDragging) {
isDragging = false
const deltaX = lastDragDelta.x
const deltaY = lastDragDelta.y
if (deltaX !== 0 || deltaY !== 0) {
const newAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
if (newAxis.length() > 0) {
autoRotationAxis.copy(newAxis)
}
const dragSpeed = currentAngularVelocity.length()
if (dragSpeed > autoRotationSpeed) {
// 维持当前旋转速度
} else {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}
}
// 检查点击事件
if (event.changedTouches.length > 0) {
const touch = event.changedTouches[0]
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1
mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1
checkIntersection()
}
}
// 事件监听
window.addEventListener('mousedown', onMouseDown)
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
window.addEventListener('touchstart', onTouchStart)
window.addEventListener('touchmove', onTouchMove)
window.addEventListener('touchend', onTouchEnd)
document.addEventListener('gesturestart', function (e) {
e.preventDefault()
})
// 添加点击事件监听
window.addEventListener('click', onMouseClick)
// 处理窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
function onMouseClick(event) {
event.preventDefault()
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
console.log(event.clientX, mouse.x, mouse.y)
checkIntersection()
}
function checkIntersection() {
raycaster.setFromCamera(mouse, camera)
const intersects = raycaster.intersectObjects(smallBalls)
if (intersects.length > 0) {
const intersectedBall = intersects[0].object
const index = smallBalls.indexOf(intersectedBall)
if (index !== -1) {
const labelInfo = labelSprites[index]
showLabelInfo(labelInfo)
}
}
}
function showLabelInfo(labelInfo) {
alert(`点击的小球标签:${labelInfo.labelText}`)
}
// 动画循环
function animate() {
requestAnimationFrame(animate)
if (!isDragging) {
const deltaQuat = new THREE.Quaternion().setFromEuler(
new THREE.Euler(
currentAngularVelocity.x,
currentAngularVelocity.y,
currentAngularVelocity.z,
'XYZ'
)
)
sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)
const currentSpeed = currentAngularVelocity.length()
if (currentSpeed > autoRotationSpeed) {
currentAngularVelocity.multiplyScalar(decayRate)
if (currentAngularVelocity.length() < autoRotationSpeed) {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
} else if (currentSpeed < autoRotationSpeed) {
currentAngularVelocity.multiplyScalar(increaseRate)
if (currentAngularVelocity.length() > autoRotationSpeed) {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
} else {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}
// 更新标签的位置和跑马灯效果
labelSprites.forEach(({ sprite, smallBall, texture, needMarquee }) => {
smallBall.updateMatrixWorld()
const smallBallWorldPos = new THREE.Vector3()
smallBall.getWorldPosition(smallBallWorldPos)
const upOffset = new THREE.Vector3(0, 0.3, 0)
sprite.position.copy(smallBallWorldPos).add(upOffset)
if (needMarquee) {
texture.offset.x += textSpeed
if (texture.offset.x > 1) {
texture.offset.x = 0
}
}
})
renderer.render(scene, camera)
}
animate()
function getRandomBrightColor() {
const hue = Math.floor(Math.random() * 360)
const saturation = Math.floor(Math.random() * 40 + 10)
const lightness = Math.floor(Math.random() * 40 + 40)
const rgb = hslToRgb(hue, saturation, lightness)
return (rgb.r << 16) | (rgb.g << 8) | rgb.b
}
function hslToRgb(h, s, l) {
s /= 100
l /= 100
const c = (1 - Math.abs(2 * l - 1)) * s
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
const m = l - c / 2
let r, g, b
if (h >= 0 && h < 60) {
r = c
g = x
b = 0
} else if (h >= 60 && h < 120) {
r = x
g = c
b = 0
} else if (h >= 120 && h < 180) {
r = 0
g = c
b = x
} else if (h >= 180 && h < 240) {
r = 0
g = x
b = c
} else if (h >= 240 && h < 300) {
r = x
g = 0
b = c
} else {
r = c
g = 0
b = x
}
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255),
}
}
function getRandomNickname() {
const adjectives = [
'Cool',
'Crazy',
'Mysterious',
'Happy',
'Silly',
'Brave',
'Smart',
'Swift',
'Fierce',
'Gentle',
]
const nouns = [
'Tiger',
'Lion',
'Dragon',
'Wizard',
'Ninja',
'Pirate',
'Hero',
'Ghost',
'Phantom',
'Knight',
]
const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)]
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]
const nickname = `${randomAdjective} ${randomNoun}`
if (nickname.length < 2) {
return getRandomNickname()
} else if (nickname.length > 22) {
return nickname.slice(0, 22)
}
return nickname
}
</script>
</body>
</html>
来源:juejin.cn/post/7425249244850913280
当前端遇到了自动驾驶
这是一篇用ThreeJS开发自动驾驶点云标注系统的实战记录,也是《THREEJS无师自通》的第一篇。通常情况,一个系列文章开篇应该是Quick Start或者Guide之类的简单口水文,而我选择以此开篇,最主要的原因还是因为这次经历足够有趣。
公众号|沐洒(ID:musama2018)
前端开发,大家熟啊,有很多亲(bi)切(shi)的称谓,诸如“切图仔”,“Bug路由器”。自动驾驶,大家更熟了吧,最近几年但凡新能源汽车,谁要是不说自己搭配点自动驾驶(或辅助驾驶)功能,都不好意思拿出来卖。那么,当前端和自动驾驶碰到了一起,会发生什么有意思的事呢?
有点云标注相关背景的可以跳过背景普及,直接看方案。
背景
去年9月,我们业务因为某些原因(商业机密)开始接触自动驾驶领域的数据处理,经过仔细一系列调研和盘算,我们最终决定从零开始,独立自研一套自动驾驶点云数据标注系统。你可能要说了,自动驾驶我知道啊,但是“点云”是个啥?呐,就是这玩意儿:
点云的学术定义比较复杂,大家可以自行搜索学习,这里我简单贴一个引述:
点云是指目标表面特性的海量点集合。
根据激光测量原理得到的点云,包括三维坐标(XYZ)和激光反射强度(Intensity)。
根据摄影测量原理得到的点云,包括三维坐标(XYZ)和颜色信息(RGB)。
结合激光测量和摄影测量原理得到点云,包括三维坐标(XYZ)、激光反射强度(Intensity)和颜色信息(RGB)。
在获取物体表面每个采样点的空间坐标后,得到的是一个点的集合,称之为“点云”(Point Cloud)。
看不懂?没事,不重要,你只需要知道,我们周围的世界,都是点构成的,而点云只不过是用一些仪器(比如激光雷达),对真实世界进行了采样(且只对部分属性进行采样)。
好了,假设你已经知道“点云”是啥了,但你心里肯定还有十万个为什么:
你不是说自动驾驶么?前端呢?这仨有啥关联么?这东西自研成本很高么?
别急,容我慢慢解释,先快速普及一下啥叫“数据标注”:
人工智能数据标注是对文本、视频、图像等元数据进行标注的过程,标记好的数据将用于训练机器学习的模型。常见的数据标注类型有文本标注、语义分割和图像视频标注。
这些经标注的训练数据集可用于训练自动驾驶、聊天机器人、翻译系统、智能客服和搜索引擎等人工智能应用场景之中
假设你懒得看,或者看不懂,我再给你翻译翻译,什么叫数据标注:
一个婴儿来到这个世界,你在它面前放两张卡片,一张红色,一张绿色,你问它,这是什么颜色,它必然是不知道的(我们假设它能听懂并理解你的话)。只有当你一遍又一遍的,不断的告诉它,这是红色,这是绿色,它才会记住。等下次你带它过马路时,它就能准确地识别出红绿灯,并在你面前大声喊出来 “红色(的灯)!”没错,你应该猜到了,那两张卡片本身没有标签(元数据),是你给它们“打上了标”(分别标注了红色和绿色),然后把这个“结构化的数据”,“喂养”给你的宝宝,久而久之,这个宝宝就学会了分辨世间万物,成为一个“智人”。
(图片来源于网络)
你的“喂养”,就是人工;宝宝的成长,就是智能。人工智能(AI,Artificial Intelligence),就是数据喂养的成果,没有数据标注,就没有人工智能。
从这个意义上聊,你和我,都是别人(父母,老师,朋友…)用成千上万的标注数据喂养出来的AI。
扯远了,收!我们说回自动驾驶。
大家都知道现在自动驾驶很火啊,那自动驾驶的“智能”是怎么训练的呢?当然是算法工程师用模型训练出来的啦,而自动驾驶模型需要喂养的数据,就是点云。仪器扫描回来的点云数据里,仅仅只是包含了所有点的基本信息(位置,颜色,激光强度等),模型怎么知道这个点是人身上采的,还是出租车上采的呢?!
(图片来源于网络)
于是这些点就需要被加工(标注),被我们用一系列手段(包括人工和机器)给点赋予更多的信息,区分出每一个点的含义(语义分割)。在自动驾驶领域的点云标注里,我们需要通过2D+3D工具,把物体识别出来。本文重点讲3D的部分。可以先看下3D框的效果:
(图中黄色高亮的点,就是被3D框圈中的点云)
挑战
以往我们较为常见的数据标注,主要集中在文本,图片,视频等类型,例如文本翻译,音频转写,图片分类等等,涉及的工具基本上都是传统web开发知识可以搞定的,而点云标注则完全不同,点云需要作为3D数据渲染到立体空间内,这就需要使用到3D渲染引擎。我们使用的是ThreeJS,这是一个基于WebGL封装的3D引擎。
写了10年的web前端代码,能有机会把玩一下3D技术,还真是挺令人兴奋的。于是我们吭哧吭哧把基本的3D拉框功能做出来了,效果是这样的:
(3D拉框 - 人工调整边缘:2倍速录制)
动图是我加了2倍速的效果,真实情况是,要标出图上这辆小汽车,我需要先拉出一个大概的2D矩形区域,然后在三视图上不断的人工调整边缘细节,确保把应该纳入的点都框进去(未框入的点呈白色,框体垂直方向未框入则呈现蓝色,框入的呈现黄色)
看起来好像也还行?
no,no,no!你知道一份完整的点云标注任务需要标多少个框么?也不吓唬大家,保守点,一般情况一份连续帧平均20帧左右,每帧里要标注的框体保守点,取100个吧,而这一份连续帧标注,必须同一个标注员完成,那么20帧至少有2000个框体需要标注!
按照上面实现的这种人工调节边缘的方式来拉框,一个框需要22秒(GIF共11秒,2倍速),熟练工可能能在10秒内调整完成。那么2000个框体,单纯只是拉框这一件小事,不包括其他工序(打标等),就需要耗费20000秒,约等于5.5小时!
这是什么概念?通常情况标注员都是坐班制,平均一天有效工作时长不超过6小时,也就是说,一个标注员,在工位上一动不动,大气都不敢喘一下的工作一天,就只能把一条点云数据标完,哦不对,仅仅只是拉完框!没错,只是拉框而已。
这种低效的重复性工作,哪个组织受得了?怎么办呢?
方法比较容易想,不就是引入自动化能力么,实现自动边缘检测,嗯,想想倒是挺简单的,问题是怎么实现呢?
以下进入干货区,友情提示:货很干,注意补水。
方案
点云分类
基本思路就是进行边缘探测:
找出三个坐标轴(XYZ)方向上的框体边缘点,计算出边缘点之间的距离,调整框体的长宽高,进而将框体贴合到边缘点。
边缘的定义:
某方向上的同值坐标点数大于某个设定值(可配置,默认3,三者为众)
找出边缘点的核心算法:
遍历框体内的点,分别将XYZ方向的坐标值存入数组,加权,排序,取第一个满足边缘定义的点,作为该方向极限值。
进行边缘判定之前,我们得先找出存在于框体内的点,这就涉及到第一个核心问题:点云和3D框的相对位置判断。
为了更好的管理与框体“强相关”的点云,我们先对点云进行一个基本分类:
从俯视图看,把3D图降维成2D图,立方体则看作矩形,如下图:
则点与框的相对位置可以降维等效为:
第一类(点在立方体内)
点在矩形内,且点的Z值在立方体[Zmin, Zmax]范围内
第二类(点在立方体垂直方向)
点在矩形内,且Z值在立方体[Zmin, Zmax]范围外
第三类(点在立方体周围)
点在延展矩形(向外延展N个距离)内,且不属于第二类。
我们先按这个思路实现一版代码:
// 判断点是否位于框体XY平面区域内
function isPointInXYPlane(gap: IGap, distance = 0) {
const { gapL, gapR, gapB, gapU } = gap;
// 在框体XY平面区域内
return gapL > - distance && gapR < distance && gapU < distance && gapB > - distance;
}
// 在框体垂直方向上下边界内
function isPointInVerticalBoundry(up: number, bottom: number, z: number) {
return z >= bottom && z <= up;
}
// 位于框体XY平面向外延伸NEAR_DISTANCE距离的区域内
if (isPointInXYPlane(posInfo.gap, NEAR_DISTANCE)) {
const isInVerticalBoundry = isPointInVerticalBoundry(posInfo.up, posInfo.bottom, posInfo.z);
// 位于框体XY平面区域内
if (isPointInXYPlane(posInfo.gap)) {
// 在框体内
if (isInVerticalBoundry) {
isInside = true;
} else {
// 在框体外的垂直方向上
isVertical = true;
}
}
// 在框体上下边界内
if (isInVerticalBoundry) {
isNearBy = true;
}
}
通过以上逻辑,我们就拿到了与框体“相关”的点云(正确与否先按下不表,后面会说),我们先存起来,后面做极值寻找(即边缘检测)时候使用。
第一版效果
看起来好像还行,基本实现了贴合,但是……我们旋转一下看看:
好家伙,旋转后框体边界没更新!所以点云高亮也没变化。
这个问题其实也好理解,我们在处理边界的时候,只采用position和scale计算,并没有使用rotation属性,所以当框体的旋转分量发生变化,我们计算边界时没有及时调整,程序就会认为框体此时仍然留在原地未动呢。
我们来优化一下。我先尝试用三角函数来计算旋转后的新坐标点,类似这样
折腾了很久的三角函数,有点变化了,但是效果却成了这样:
已经接近真相了,只需要把待判定点放到三角函数判定公式里,就可以知道该点是否在旋转后的框体内了,不过到这里我突然意识到问题被我搞复杂了,是不是可以有更简单的方法来判定矩形内部点呢?
我们回到最初的问题:判断一个点,与一个立方体的相对位置
对这个原始问题进行逻辑拆解,可以拆为3个子问题:
- 如何判断一个点位于立方体内部?
- 如何判断一个点位于立方体的垂直方向(排除体内点)?
- 如何判断一个点位于立方体的周围(排除垂直方向点)?
关于问题1,第一反应还是立体几何,而且我笃定这是个非常成熟的几何问题,没必要自己硬憋。于是我就上网搜索:How to determine a point is inside or outside a cube? 结果如下:
上面是stackoverflow上大神给的两种数学方法,一看就知道能解,奈何我看图是看懂了,公式没有完全吸收透,于是最终没有采纳(尽量不干不求甚解的事,写成代码就要求自己得是真的懂)
于是我进一步思考:
几种数学方法确实都很虎,但我是不是把问题搞复杂了?能不能没事踩踩别人的肩膀呢?
看看ThreeJS 是否有相应的API……果然有:
这不正好就是我想要的效果么?踏破铁鞋无觅处,得来全不费功夫啊!
直接拿来用,搞定!
但问题来了,人家是怎么做到的呢?带着这个疑问,我开始翻相关源码。
首先看到containsPoint,其实就和我们用的方法是一样的:
// https://github.com/mrdoob/three.js/blob/4503ef10b81a00f5c6c64fe9a856881ee31fe6a3/src/math/Box3.js#L243
containsPoint( point ) {
return point.x < this.min.x || point.x > this.max.x ||
point.y < this.min.y || point.y > this.max.y ||
point.z < this.min.z || point.z > this.max.z ? false : true;
}
而核心问题还是得想办法计算出box.min和box.max,那ThreeJS是怎么计算的呢?继续看:
// https://github.com/mrdoob/three.js/blob/4503ef10b81a00f5c6c64fe9a856881ee31fe6a3/src/core/BufferGeometry.js#L290
computeBoundingBox() {
// ..... 省略部分代码 ....
const position = this.attributes.position;
if ( position !== undefined ) {
this.boundingBox.setFromBufferAttribute(position);
}
// ..... 省略部分代码 ....
}
看起来boundingBox的属性来自于attributes.position,这个position就是box在世界坐标里的具体位置,是我们在创建box时候设定的。再继续深挖下setFromBufferAttribute:
// https://github.com/mrdoob/three.js/blob/4503ef10b81a00f5c6c64fe9a856881ee31fe6a3/src/math/Box3.js#L56
setFromBufferAttribute( attribute ) {
// ..... 省略部分代码 ....
for ( let i = 0, l = attribute.count; i < l; i ++ ) {
const x = attribute.getX( i );
const y = attribute.getY( i );
const z = attribute.getZ( i );
if ( x < minX ) minX = x;
if ( y < minY ) minY = y;
if ( z < minZ ) minZ = z;
if ( x > maxX ) maxX = x;
if ( y > maxY ) maxY = y;
if ( z > maxZ ) maxZ = z;
}
this.min.set( minX, minY, minZ );
this.max.set( maxX, maxY, maxZ );
return this;
}
平平无奇啊这代码,几乎和我们自己写的边界判定代码一模一样啊,也没引入rotation变量,那到底怎么是在哪处理的旋转分量呢?
关键点在这里:
我尝试给你解释下:
在调用containsPoint之前,我们使用box的转换矩阵,对point使用了一次矩阵逆变换,从而把point的坐标系转换到了box的坐标系,而这个转换矩阵,是一个Matrix4(四维矩阵),而point是一个Vector3(三维向量)。
使用四维矩阵对三维向量进行转换的时候,会逐一提取出矩阵的position(位置),scale(缩放)和rotation(旋转)分量,分别对三维向量做矩阵乘法。
也就是这么一个操作,使得该point在经过矩阵变换之后,其position已经是一个附加了rotation分量的新的坐标值了,然后就可以直接拿来和box的8个顶点的position做简单的边界比对了。
这里涉及大量的数学知识和ThreeJS底层知识,就不展开讲了,后面找机会单独写一篇关于转换矩阵的。
我们接着看点与框体相对位置判断的第二个问题:如何判断一个点位于立方体的垂直方向(排除体内点)?
首先,我们置换下概念:
垂直方向上的点 = Z轴方向上的点 = 从俯视图看,在XY平面上投射的点 - 框内点
那么,如何判断一个点在一个矩形内,这个问题就进一步转化为:
(AB X AE ) * (CD X CE) >= 0 && (DA X DE ) * (BC X BE) >= 0
这里涉及到的数学知识是向量点乘和叉乘的几何意义,也不展开了,感兴趣的朋友可以自行搜索学习下。
还剩最后一个问题:如何判断一个点位于立方体的周围(排除垂直方向点)?
这个问题我们先放一放,周围点判断主要用来扩展框体的,并不影响本次的边界探测结果,以后再找机会展开讲,这里先跳过了。
到此为止,我们就至少拿到了两类点(框内点,和框体垂直方向的点),接下来就可以开始探测边缘了。
边缘探测
边缘探测的核心逻辑其实也不复杂,就是:
遍历框体内的点,分别将X,Y,Z方向的坐标值存入数组,加权,排序,取第一个满足边缘定义的点,作为该方向极限值。
这里我们可以拆分位两个Step。
Step 1:点位排序
基本思路如下:
选择一个方向,遍历点云,取到该方向上点云的坐标值,放入一个map中,key为坐标值,value为出现次数。同时对该坐标进行排序,并返回有序数组。**
那么问题来了,点云的坐标值多半精确到小数点七八位,如果直接以原值作为key,那么这个map很难命中重复坐标,那map的意义就不大了,难以聚合坐标。
于是这里对原坐标取2个精度后作为key来聚合点云,效果如下:
可以明显看到已经有聚合了。这是源码实现:
Step 2:夹逼探测
拿到了点云坐标的聚合map,和排序数组,那么现在要检测边缘就很简单了,基本思路就是:
从排序数组的两头开始检查,只要该点的聚合度大于DENSE_COUNT(根据需要设置,默认为3),我们就认为这个点是一个相对可信的边缘点。
从这个算法描述来看,这不就是个夹逼算法么,可以一次遍历就拿到两个极值。
到这里,某方向的两个极值(low 和 high)就拿到手了,那么剩下的工作无非就是分别计算XYZ三个方向的极值就好了。
我们来看下效果,真的是“啪”一下,就贴上去了:
上面的案例录制的比较早,有点模糊,再来看个高清带色彩的版本:
这个体验是不是很丝滑?就这效率,拉框速度提升了10倍有吧?(22秒 -> 2秒)
读到这里,不知道大家还记不记得前面,我们刻意跳过了一个环节的介绍,就是“框体周围点位”这一部分,这里简单补充两句吧。
在实际的场景里,有很多物体是靠得很近的,还有很多物体的点云并没有那么整齐,会有一些离散点在物体周围。那么这些点就可能会影响到你的边缘极限值的判断。
因此我在这里引入了两个常量:
附近点判定距离 NEAR_DISTANCE(框体紧凑的场景,NEAR_DISTANCE就小一点,否则就大一点)!
密集点数 DENSE_COUNT(点云稀少的场景,就可以把DENSE_COUNT设置小一点,而点云密集厚重的场景,DENSE_COUNT就适当增加。)
通过在不同的场景下,调整这两个常量的取值,就可以使得边缘探测更加的准确。
遗留问题
其实在3D的世界里,多一个维度之后,很多问题都会变得更加的麻烦起来。上面的方案,在处理大部分场景的时候都能work,但实际上依然有一些小众场景下存在问题,比如:
平时多半都是物体都是围绕Z轴旋转,但如果有上下坡路,物体围绕XY轴旋转,那垂直方向就需要进行矫正。
再比如:
用户移动了镜头方位,在屏幕上拉2D框的时候,就需要对2D框采集到的坐标进行3D投射,拿到真实的世界坐标,才能创建合适的立方体。
当然,这些问题在后面的版本都已经完善修复了,之所以放在遗留问题,是想说明,仅仅依照正文部分的方法去实现的话,还会有这些个遗留的问题需要单独处理。
如果大家感兴趣的话可以留言告诉我,我再决定要不要接着写。
来源:juejin.cn/post/7422338076528181258
已老实!公司的代码再也不敢乱改了!
开篇
大家好,我是聪。想必对于很多初入职场,心中怀着无限激情的兄弟们,对于接手老代码都会有很多愤慨,碰到同事的代码十分丑陋应不应该改!我也是这样,我相信有很多人同样有跟我一样的经历。满打满算实习 + 正式工作,我也敲了两年多代码,我今天来说说我自己的看法吧。
亲身经历
我第一次接手老代码的时候,映入我眼帘的就是侧边栏满页的黄色提示以及代码下面的众多黄色波浪线,以及提交代码时的提示,如下图:
我内心 OS:
1)大干一场,把黄色波浪线全干掉!
2)同事这写的也太不优雅了吧,改成我这样!
3)这代码怎么也没格式化,我来 Ctrl + Alt + L 格式化一波!
已老实,求放过
干掉黄色波浪线,将代码改 ”优雅“ 结局如下:
1)不声不吭动了同事代码,换来同事怒骂,毕竟人家逻辑写好,然后你按你想法来搞,也没有跟人家商量。
2)后续领导找你加需求,你发现原来之前的代码有妙用,你悔不当初,被扣绩效。
3)格式化后,在项目修改记录上面是你的修改,这代码出问题,负责人先来找你。
说说我的看法
代码能跑不要动
前几日我要在老项目中,新增一点小功能,在新增完功能后,我扫了一眼代码,发现有几处逻辑根本不会执行,比如:抛异常后,执行删除操作类似,我也不会去义愤填膺的去干掉这块代码,毕竟我想到一点!项目都跑七八年没出问题了,能跑就别动它。
代码强迫症不要强加于别人
前几日在某金看见了这样一个沸点:
这样的事情其实在小公司经常发生,你觉得它写的不优雅,封装少,可能是别人也有别人的难处,至少不能将自己想法强加于别人,比如领导突然来一个需求,跟你说今天你得完成,然后第二天这个需求,你要这样改、再给我加点新需求上去,你能想到的封装其实只是你冷静下来,而且没有近乎疯狂的迭代需求得到的想法,当你每天都要在原代码上面疯狂按照领导要求修改,可能你会有自己的看法。
新增代码,尽量不影响以前逻辑
新增代码的时候,尽量按照以前的规则逻辑来进行,比如我改的一个老项目,使用的公司自己写的一套 SQL 处理逻辑,我总不能说不行!我用不惯这个!我要用 MyBatis!!!!那真的直接被 T 出门口了。
尊重他人代码风格
每个人的代码风格都有所不同,这个很正常,不同厨师的老师教法不一样,做出的味道还不一样呢,没有最好的代码,只有更适合的代码,刚好我就有这样的例子:
我注入 Spring 依赖喜欢用构造注入、用 Lombook 的注解 @RequiredArgsConstructor 注入,我同事喜欢 @Autowired ,我能说他不准用这个吗,这个是人家的习惯,虽然 Spring 也不推荐使用这个,但改不改这个都不会影响公司收益,反而能少一件事情,促进同事友好关系,哈哈哈哈,我是这样认为的。
处理好同事之间的关系
哈哈哈哈这个真的就是人情事故了,你换位想象一下,如果你写的幸幸苦苦的代码,新来的同事或者实习生,来批评你的代码不规范,要 Diss 你,偷偷改你代码,就算他说的超级对,你心里都十分不好受,会想一万个理由去反驳。
我一般如果需求需要改动同事的代码,我会先虚心的向同事请求,xx哥,我这个需求要改动你这边的代码来配合一下,你来帮我一起看看,你这部分的代码这样改合理吗,或者你自己改下你自己的部分,然后我合并一下~ 谢谢 xx哥。
来源:juejin.cn/post/7383342927508799539
OpenHarmony首次亮相欧洲开源会议
10月22日至24日,由欧洲最大开源组织 Eclipse 基金会主办的Open Community Experience (OCX) 2024开源大会(后文简称“大会”)于德国·美因茨盛大召开。OpenHarmony项目群技术指导委员会(TSC)主席陈海波受邀参加大会主论坛分享,由OpenHarmony 与 Eclipse Oniro 联合筹划的“Think Global, Code Local”分论坛圆满举办,OpenHarmony最新技术与生态进展也成功亮相大会展区。
嘉宾大合影
陈海波主论坛演讲
10月22日下午,OpenHarmony项目群技术指导委员会(TSC)主席,华为Fellow陈海波在大会的主论坛上作了主题为《Empowering a Connected Intelligent World With OpenHarmony and Oniro》的技术报告。在报告中,陈海波强调了智能移动操作系统的重要性以及影响其发展的三大要素(硬件演进、APP模式开发与操作系统技术创新),阐述了万物智联时代操作系统的发展趋势、挑战与机遇,介绍了OpenHarmony的架构设计与关键特性,并分享了OpenHarmony开源项目的最新技术、生态、人才进展与行业实践。此外,他还提到,开放原子开源基金会与欧洲最大的开源组织 Eclipse 基金会合作,基于OpenHarmony底座发布的 Oniro OS 已经成为极具影响力的开源操作系统。
OpenHarmony项目群技术指导委员会(TSC)主席陈海波主论坛演讲
OpenHarmony & Eclipse基金会闭门研讨会
此外,OpenHarmony 项目群技术指导委员会(TSC)主席陈海波、秘书处主任许家喆、副主任刘果,OpenHarmony PMC主席任革林,OpenHarmony项目群工作委员会执行主席助理周顺淦,上海交通大学OpenHarmony技术俱乐部主任夏虞斌,华为德累斯顿研究所所长刘宇涛,华为开源专家王荣泽,华为欧洲开源专家Adrian OSullivan等与 Eclipse 基金会执行董事Mike Milinkovich、董事Bryan Che以及首席会员官Gaël Blondelle等开展了闭门研讨,就OpenHarmony与 Eclipse 基金会的进一步合作以及OpenHarmony技术生态在欧洲的未来发展进行深入交流。
OpenHarmony & Eclipse基金会闭门研讨会
“Think Global,Code Local”分论坛
10月23日下午,OpenHarmony 与 Eclipse Oniro 联合筹划的“Think Global,Code Local”分论坛如期举办。
分论坛嘉宾合影
分论坛现场
Eclipse 基金会首席会员官 Gaël Blondelle 与OpenHarmony项目群工作委员会执行主席助理周顺淦出席分论坛并作开场欢迎致辞。他们表示,全球开源生态的良好发展是技术创新的关键。未来,开放原子开源基金会与 Eclipse 基金会将基于OpenHarmony开展进一步的深度合作,持续推动OpenHarmony开源生态在欧洲的发展。
OpenHarmony项目群工作委员会执行主席助理周顺淦(左)与 Eclipse 基金会首席会员官Gaël Blondelle(右)欢迎致辞
Oniro指导委员会主席Suhail Khan出席分论坛并作《Oniro and OpenHarmony partnership. From code cooperation to a global ecosystem》技术报告。在报告中,Suhail Khan提到,基于OpenHarmony底座的强大能力使 Oniro 项目能够满足多样化的全球产品需求和标准。他介绍了当前 Oniro 项目的最新进展,并展望了未来OpenHarmony和 Oniro 通过进一步的深度合作从而推动全球开源生态的繁荣发展。
Oniro指导委员会主席Suhail Khan技术报告
华为德累斯顿研究所技术专家Hernan Luis Ponce de Leon出席分论坛并作《Introduction to OpenHarmony Concurrent and Collaborative VSync Open Source Project》技术报告。在报告中,Hernan Luis Ponce de Leon详细介绍了OpenHarmony并发与协同TSG孵化的开源项目“VSync”的最新进展。他提到,并行和异构是摩尔定律之后驱动性能的关键,但多核、VMM、NUMA以及异构等复杂现代硬件进一步加剧了软件的复杂性。VSync提供了Vsyncer工具,可在内存一致性较弱CPU上验证并发算法的准确性,并提供一系列使用Vsyncer工具验证过的并发算法。
华为德累斯顿研究所技术专家Hernan Luis Ponce de Leon技术报告
华为华沙研究所技术专家Jaroslaw Marek与华为米兰研究所资深技术专家Francesco Pham出席分论坛并作《An architecture designed to boost innovation,interoperability and opportunities》技术报告。在报告中,Jaroslaw Marek介绍了 Eclipse 基金会 Oniro 开源项目的最新进展,重点包括技术文档、社区交流、GitHub项目以及IP合规性等,并分享了 Oniro 如何基于OpenHarmony建立开源生态以及其对本地市场的适应性;Francesco Pham介绍了欧洲构建的开源移动平台“Oniro Developer Phone”,包括其四个关键的开发阶段、Volla X23内核的技术创新以及原生应用商店的集成,并讨论了该平台对开发者的重要意义以及后续的发展规划。
华为华沙研究所技术专家Jaroslaw Marek技术报告
华为米兰研究所技术专家Francesco Pham技术报告
华为德累斯顿研究所技术专家张越出席分论坛并作《Challenges and difficulties in terminal operating system technology》技术报告。在报告中,张越阐述了万物智联时代终端操作系统技术创新与发展将面临的10个主要挑战和难点,并基于OpenHarmony的技术架构优势给出了相关解决方案。
华为德累斯顿研究所技术专家张越技术报告
Array公司合伙人Alberto Pianon与NOI技术创新中心的开放数据和自由软件技术专家Luca Miotto出席分论坛并作《Collaborative Approaches towards license management》技术报告。在报告中,Alberto Pianon与Luca Miotto介绍了OpenHarmony社区的自动化开源审视工具“OAT (OSS Audit Tool)”,并分享了当前OpenHarmony在识别第三方组件许可证变体类型上可能面临的挑战和困难。此外,他们正在与 Eclipse Oniro 合规工具链项目合作,以进一步改进和完善OpenHarmony的许可合规流程。
NOI技术创新中心的开放数据和自由软件技术专家Luca Miotto技术报告
Array公司合伙人Alberto Pianon技术报告
OpenHarmony PMC主席任革林出席分论坛并作《OpenHarmony Design Philosophy and Latest Progress》技术报告。在报告中,任革林以两个典型的手机、平板、IOT等多设备协同交互的场景,介绍终端操作系统所面临的应用、数据、能力、交互“割裂”问题,并由此引出OpenHarmoy的核心设计理念:“超级终端”,即多物理设备融合成一个逻辑终端。此外,他提到,OpenHarmony已成为发展速度最快的智能终端操作系统根社区,四年内PMC共发布47个版本,其中7个为大版本,社区活跃度持续保持领先。
OpenHarmony PMC主席任革林技术报告
Futurewei开源技术战略总监Mats Lundgren出席论坛并做《A Web Engine For the Future》技术报告。在报告中,Mats Lundgren介绍了Web内核Servo的重要特性,并分享了Servo在OpenHarmony and Oniro项目中的集成进展。他提到,Servo由Rust编写,支持WebGL和WebGPU,适用于桌面、移动和嵌入式应用,能够给用户带来更高效以及更安全的体验。
Futurewei开源技术战略总监Mats Lundgren技术报告
上海交通大学OpenHarmony技术俱乐部主任、软件学院副院长夏虞斌出席论坛并做《Introduction to OpenHarmony Security Related Technologies》技术报告。在报告中,夏虞斌从总体架构设计、实现原理以及实际应用案例等方面详细介绍了基于RISC-V的蓬莱TEE,并介绍了分布式TEE的概念。他还提到,通过将蓬莱整合进OpenHarmony以形成统一的TEE架构,不仅提高了跨平台的兼容性,也简化了可信应用的移植过程。
上海交通大学OpenHarmony技术俱乐部主任、软件学院副院长夏虞斌技术报告
Software Mansion公司软件专家Przemysław Sosna出席论坛并做《Supporting developers, the role of React Native in OpenHarmony and Oniro》技术报告。在报告中,Przemysław Sosna介绍了 Oniro 开源操作系统在移动设备中的应用进展,并阐述了跨平台开发框架“React Native”以及ArkUI的技术架构、实现原理以及最新生态进展。目前,React Native已经逐步扩展到OpenHarmony以及 Onrio 上,为现有应用的移植和开发提供便利与跨平台开发优势。
Software Mansion公司软件专家Przemysław Sosna技术报告
OpenHarmony开源图形驱动SIG、游戏SIG组长,华为终端图形TMG主任黄然出席论坛并做《Introduction to OpenHarmony Graphics Technology》技术报告。在报告中,黄然强调了图形是OpenHarmony实现极致流畅体验的关键,并结合计算机图形发展的过去、现在与未来发展,重点阐述了OpenHarmony图形相关技术架构的演进思路,提出了万物智联时代计算机图形发展趋势的畅想与思考。
OpenHarmony开源图形驱动SIG、游戏SIG组长,华为终端图形TMG主任黄然技术报告
江苏润和软件股份有限公司副总裁刘洋出席论坛并作《Challenges and opportunities for companies with a global business》技术报告。刘洋提到,OpenHarmony-Oniro是一个真正开源的操作系统,支持各类设备的快速互联互通,并优化了性能功耗等系统能力。在中国,OpenHarmony在电力、煤炭和金融等行业得到了广泛应用,随着HarmonyOS 5.0的发布,标志鸿蒙生态在消费电子领域也取得了巨大成功。欧洲一直以来有着深厚技术底蕴和大量兴趣开发者,Oniro 在欧洲商业成功的首要目标是培养足够多的开发者,通过开发者在欧洲建立一个 Oniro 的技术生态。
江苏润和软件股份有限公司副总裁刘洋技术报告
OCX Tracks
Eclipse 基金会Oniro and Cloud Programs的高级经理Juan Rico出席OCX Tracks并做《Cooperation between two OS Foundations to build a global interoperable ecosystem》技术报告。在报告中,Juan Rico介绍了如何进一步推动开放原子开源基金会与 Eclipse 基金会间的合作和交流,促进OpenHarmony和 Oniro 开源项目的技术和生态发展,以打造更具影响力的智能终端操作系统底座,促进全球开源生态的繁荣发展。
Eclipse基金会Oniro and Cloud Programs的高级经理Juan Rico技术报告
Eclipse Oniro 合规工具链项目负责人Alberto Pianon以及OpenHarmony合规SIG负责人高亮出席OCX Tracks并做《Collaborative Approaches to License Compliance: OpenHarmony and Eclipse Oniro Compliance Toolchain》技术报告。在报告中,Alberto Pianon和高亮介绍了OpenHarmony基于OAT(OSS审核工具)的许可证合规验证的解决方案,并分享了当前识别第三方组件许可证变体类型所面临的困难。此外,他们还介绍了目前了目前OpenHarmonyOpenHarmony与 Eclipse Oniro 合规工具链所合作的项目进展,以改进OpenHarmony许可证许可证合规验证流程。
Eclipse Oniro合规工具链项目负责人Alberto Pianon(右)以及OpenHarmony合规SIG负责人高亮(左)技术报告
OpenHarmony展区
此外,OpenHarmony开源项目介绍、社区建设情况、技术与生态的最新进展以及仓颉、Vsync、OpenTrustee、ArkUI-X等孵化技术项目成果等也亮相大会展区,来自SAMSUNG、LG、Bosch、Canonical、S&P Global以及Harman等公司的与会嘉宾在OpenHarmony展区前就相关技术进展和未来的合作机会进行积极交流和思想碰撞。
OpenHarmony展区
OpenHarmony亮相OCX 2024开源大会是OpenHarmony开源技术生态在欧洲拓展的第一站,标志着OpenHarmony在智能终端领域全球开源生态迈出了重要一步。未来,OpenHarmony将与欧洲最大开源组织Eclipse基金会开展更进一步的深度合作,共享技术资源、提升开源项目的可持续性并拓宽开发者的视野,持续推动全球开源生态良好发展。
收起阅读 »接口不能对外暴露怎么办?
在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。
面对这样的情况,我们该如何实现呢?
1. 内外网接口微服务隔离
将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。
该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。
该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。
2. 网关 + redis 实现白名单机制
在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。
该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;
不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;
另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。
3. 方案三 网关 + AOP
相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。
我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。
根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。
该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;
同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。
当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。
具体实操
下面就方案三,进行具体的代码演示。
首先在网关侧,需要对进来的请求header添加外网标识符: from=public
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono < Void > filter ( ServerWebExchange exchange, GatewayFilterChain chain ) {
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate().header('id', '').header('from', 'public').build())
.build()
);
}
@Override
public int getOrder () {
return 0;
}
}
接着,编写内外网访问权限判断的AOP和注解
@Aspect
@Component
@Slf4j
public class OnlyIntranetAccessAspect {
@Pointcut ( '@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnClass () {}
@Pointcut ( '@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnMethed () {
}
@Before ( value = 'onlyIntranetAccessOnMethed() || onlyIntranetAccessOnClass()' )
public void before () {
HttpServletRequest hsr = (( ServletRequestAttributes ) RequestContextHolder.getRequestAttributes()) .getRequest ();
String from = hsr.getHeader ( 'from' );
if ( !StringUtils.isEmpty( from ) && 'public'.equals ( from )) {
log.error ( 'This api is only allowed invoked by intranet source' );
throw new MMException ( ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR);
}
}
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OnlyIntranetAccess {
}
最后,在只能内网访问的接口上加上@OnlyIntranetAccess注解即可
@GetMapping ( '/role/add' )
@OnlyIntranetAccess
public String onlyIntranetAccess() {
return '该接口只允许内部服务调用';
}
4. 网关路径匹配
在DailyMart项目中我采用的是第四种:即在网关中进行路径匹配。
该方案中我们将内网访问的接口全部以前缀/pv开头,然后在网关过滤器中根据路径找到具体校验器,如果是/pv访问的路径则直接提示禁止外部访问。
使用网关路径匹配方案不仅可以应对内网接口的问题,还可以扩展到其他校验机制上。
譬如,有的接口需要通过access_token进行校验,有的接口需要校验api_key 和 api_secret,为了应对这种不同的校验场景,只需要再实现一个校验类即可,由不同的子类实现不同的校验逻辑,扩展非常方便。
最后说一句(求关注!别白嫖!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!
来源:juejin.cn/post/7389092138900717579
2024:写 TypeScript 必须改掉的 10 个坏习惯
大家好,我是CodeQi! 一位热衷于技术分享的码仔。
在过去的几年里,TypeScript 已经逐渐成为了前端开发的首选语言,尤其是那些追求更高代码质量和类型安全的开发者。不过,正如所有编程语言一样,随着时间的推移和技术的进步,我们的编程习惯也应该与时俱进。
👋 你有没有想过,自己在写 TypeScript 时是否养成了一些“坏习惯”?
随着 TypeScript 生态系统的进一步成熟,有些你以前觉得合理的做法,现在可能不太合理。接下来,我将分享10 个常见的 TypeScript 坏习惯,并告诉你如何改进它们,确保你的代码更健壮、性能更高、并且更加易于维护。
1. 不使用 strict 模式
当开发者为了减少“麻烦”而禁用 TypeScript 的 strict
模式时,往往是在给自己埋雷。💣
为什么不好?
strict
模式通过强制进行更严格的类型检查,帮助我们避免潜在的错误。如果你关掉它,TypeScript 就变得更像是 JavaScript,失去了静态类型带来的种种好处。短期内你可能会觉得更自由,但未来的重构和维护将变得更加棘手。
怎么改进?
在 tsconfig.json
中启用 strict
模式,这样你的代码在未来的迭代中会更加稳健:
{
"compilerOptions": {
"strict": true
}
}
2. 依赖 any 类型
any
可能是 TypeScript 中最具“争议”的类型之一,因为它违背了我们使用 TypeScript 的初衷:类型安全。
为什么不好?
any
让 TypeScript 失去意义。它让代码回归到“JavaScript 模式”,绕过了类型检查,最终可能导致各种运行时错误。
怎么改进?
使用 unknown
替代 any
,并在实际使用前对类型进行检查。unknown
更安全,因为它不会自动允许任何操作:
let data: unknown;
if (typeof data === "string") {
console.log(data.toUpperCase());
}
3. 过度使用类型断言
你是否经常用 as
关键字来“消除”编译错误?🙈 这种做法短期内看似有效,但可能会隐藏更多问题。
为什么不好?
类型断言会绕过 TypeScript 的安全机制,告诉编译器“别管了,我知道自己在做什么”。问题是,当你其实并不完全确定时,它会导致难以追踪的运行时错误。
怎么改进?
减少类型断言,使用类型保护函数代替:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
if (isString(data)) {
console.log(data.toUpperCase());
}
4. 忽视联合类型和交叉类型
联合类型 (|
) 和交叉类型 (&
) 是 TypeScript 中极其强大的工具,但它们经常被忽视。🚫
为什么不好?
没有联合和交叉类型,代码容易变得冗长而难以维护。你可能会写大量的冗余代码,而这些类型可以帮你更简洁地表达逻辑。
怎么改进?
使用联合类型来处理不同情况,交叉类型来组合多个类型:
type Admin = { isAdmin: true; privileges: string[] };
type User = { isAdmin: false; email: string };
type Person = Admin | User;
function logUser(person: Person) {
if (person.isAdmin) {
console.log(person.privileges);
} else {
console.log(person.email);
}
}
5. 使用非特定的返回类型
不为函数指定精确的返回类型,可能会让使用者摸不着头脑。🤔
为什么不好?
模糊的返回类型增加了代码的不确定性,调试难度也会增加。你失去了静态类型的优势,最终使代码变得不可靠。
怎么改进?
始终为函数指定明确的返回类型,哪怕它是一个联合类型:
function fetchData(): Promise<{ id: number; name: string }> {
return fetch("/data").then(response => response.json());
}
6. 忽视 null 和 undefined
一些开发者在处理 null
和 undefined
时掉以轻心,结果导致一堆潜在的运行时错误。
为什么不好?
JavaScript 允许变量为 null
或 undefined
,TypeScript 也有相应的工具帮助处理这些值。如果忽视它们,代码可能会在运行时崩溃。
怎么改进?
使用可选链 (?.
) 和空值合并操作符 (??
) 处理 null
和 undefined
:
const name = user?.profile?.name ?? "Guest";
7. 过度使用 Enums
在 TypeScript 中,Enums
有时会被滥用。尽管它们有其应用场景,但并不总是必要。
为什么不好?
Enums
会增加复杂性,尤其是在简单常量足够的情况下。
怎么改进?
考虑用 const
或字面量类型来替代枚举:
type Role = "Admin" | "User" | "Guest";
let userRole: Role = "Admin";
8. 不使用 readonly
如果不使用 readonly
来防止对象或数组的意外修改,代码中的副作用将难以控制。
为什么不好?
可变性会导致对象在不经意间被修改,造成难以调试的问题。
怎么改进?
尽可能使用 readonly
来确保不变性:
const data: readonly number[] = [1, 2, 3];
9. 忽视自定义类型保护
依赖隐式类型检查而非明确的类型保护,可能导致你错过一些重要的类型问题。
为什么不好?
没有自定义类型保护,你可能会在运行时错过一些类型错误,最终导致不可预期的行为。
怎么改进?
编写明确的类型保护函数:
function isUser(user: any): user is User {
return typeof user.email === "string";
}
10. 没有充分利用 unknown 类型
许多开发者默认使用 any
来处理未知类型,其实 unknown
是一个更好的选择。
为什么不好?
any
禁用了类型检查,而这正是使用 TypeScript 的初衷。unknown
则要求你在使用前对类型进行明确的验证。
怎么改进?
用 unknown
代替 any
,并在使用前进行类型缩小:
let input: unknown;
if (typeof input === "string") {
console.log(input.toUpperCase());
}
总结
2024 年,是时候告别这些坏习惯了!通过启用 strict
模式、避免使用 any
、掌握联合和交叉类型等高级特性,你的 TypeScript 代码将变得更强大、更灵活、更具维护性。希望这些建议能够帮助你在 TypeScript 之路上走得更远,写出更加优雅的代码!✨
来源:juejin.cn/post/7426298029286916146
程序员节快乐!– 致所有 1024 码农的幽默与哲思
今天是 10 月 24 日,也就是传说中的 “1024 程序员节”,这一天是属于全世界程序员的“狂欢日”。为什么是 1024 呢?因为 1024 是 2 的 10 次方,是计算机世界里最常见的数字之一——特别是在存储和数据的单位里,如 1KB = 1024 字节。它象征着二进制与数字化生活的核心。
程序员们每天的生活总是充满了奇思妙想、反复调试和对代码的无限热爱。所以,今天我们不聊崩溃的服务器、也不谈工期紧张的项目,来一点轻松幽默,借助我们熟悉的 PHP,用代码带来一些欢乐!
1. 程序员的内心独白
作为一个程序员,最常见的恐惧是什么?是生产环境里的“意外”。让我们看看如何在 PHP 中模拟这种恐惧的场景:
<?php
// 在生产环境执行的一段不该存在的代码
if (getenv('APP_ENV') === 'production') {
echo "啊!为什么这段代码会在生产环境运行?!\n";
} else {
echo "你还在开发环境,放轻松。\n";
}
?>
输出:
啊!为什么这段代码会在生产环境运行?!
程序员的噩梦就是这种了:忘记屏蔽的调试代码在上线后引发了一场“灾难”,手忙脚乱地撤回。还好我们有版本控制和备份。
2. 程序员的平凡一天
程序员的一天通常由无数的 bug
和 debug
组成。我们来看一段有趣的 PHP 代码,模拟一位程序员一天的工作流程:
<?php
$bugs = 10;
$debugs = 0;
$coffee = 1;
while ($bugs > 0) {
$debugs++;
echo "调试第 $debugs 次,修复了1个bug。\n";
$bugs--;
if ($debugs % 3 === 0) {
echo "喝杯咖啡提提神吧!\n";
$coffee++;
}
}
echo "所有bug修复完毕!你喝了 $coffee 杯咖啡。\n";
?>
输出:
调试第 1 次,修复了1个bug。
调试第 2 次,修复了1个bug。
调试第 3 次,修复了1个bug。
喝杯咖啡提提神吧!
调试第 4 次,修复了1个bug。
调试第 5 次,修复了1个bug。
调试第 6 次,修复了1个bug。
喝杯咖啡提提神吧!
调试第 7 次,修复了1个bug。
调试第 8 次,修复了1个bug。
调试第 9 次,修复了1个bug。
喝杯咖啡提提神吧!
调试第 10 次,修复了1个bug。
所有bug修复完毕!你喝了 4 杯咖啡。
这就是程序员的一天,似乎修复一个 bug 的代价往往是多喝一杯咖啡。喝杯咖啡是充满仪式感的事情,只有这样,程序员才能在层出不穷的 bug 中找到自我。
3. 程序员的经典吐槽
编程界有很多经典的笑话,最常见的就是 "It works on my machine",意思是代码在我的电脑上运行没问题,那为什么在其他地方总是崩溃呢?让我们用 PHP 来重现这个场景:
<?php
$environment = "development"; // 在开发环境完美运行
function itWorksOnMyMachine($env) {
if ($env === "production") {
throw new Exception("在生产环境中崩溃了!");
} else {
echo "在开发环境中运行良好!\n";
}
}
try {
itWorksOnMyMachine($environment);
} catch (Exception $e) {
echo $e->getMessage();
}
?>
输出:
在开发环境中运行良好!
然而当我们切换到生产环境时:
$environment = "production"; // 在生产环境突然崩溃
输出:
在生产环境中崩溃了!
这就是所谓的“本机没问题,但上线必然出问题”的尴尬。
4. 程序员的哲学思考
程序员常常思考这样的问题:究竟是我在调试代码,还是代码在调试我?这是每个开发者都要面对的编程哲学问题。我们用 PHP 代码来表达这种深刻的哲学思考:
<?php
function whoIsDebuggingWho($iterations) {
$you = "程序员";
$code = "代码";
for ($i = 0; $i < $iterations; $i++) {
if ($i % 2 == 0) {
echo "$you 在调试 $code\n";
} else {
echo "$code 在调试 $you\n";
}
}
}
whoIsDebuggingWho(5);
?>
输出:
程序员 在调试 代码
代码 在调试 程序员
程序员 在调试 代码
代码 在调试 程序员
程序员 在调试 代码
在调试的过程中,有时候会让你感觉代码有它自己的意志,总是在你修复一个 bug 的时候带来另一个问题,就像代码在调试你一样。
总结
在程序员节这天,致敬所有在键盘前日夜奋斗的开发者们,你们的努力创造了现代数字世界的奇迹。程序员的工作可能常常伴随着压力和挑战,但每一次解决问题的瞬间,都是无价的成就感。1024,不只是一个数字,它是程序员精神的象征:精简、高效、充满逻辑的美感。
祝大家 1024 节快乐,愿每行代码都顺利运行,每次编译都能一键通过!
来源:juejin.cn/post/7429349035893784630
研发团队没有战斗力,怎么解?
研发团队没有战斗力,怎么解?
在现代企业中,研发团队的战斗力是企业竞争力的重要组成部分,尤其是在技术驱动型的公司。
一个高效、有战斗力的研发团队不仅能快速适应市场变化,还能通过技术创新为企业创造更多的价值。那么,如何才能打造一个有战斗力的研发团队?
我们先界定问题,拆解问题,然后再看怎么系统化的去解。
1 界定问题
我们需要明确什么是「有战斗力的研发团队」,并清楚当前团队与理想状态之间的差距。
用我和我们家闺女常说的,当有人和你说一些事情的时候,需要看一下他说的「是一个观点还是一个事实」。「研发团队没有战斗力」,这明显是一个观点。基于这个观点,接下来我们要做的,就是去拆解这个观点背后的事实,并找到支撑这个观点的具体原因。
那事实有哪些呢?
1.1 任务完成效率低
团队的任务完成效率可以通过数据来衡量。如果团队频繁出现项目延期、任务积压,或者在完成某些任务时总是比预期时间拖延很多,这通常会被认为是研发团队没有足够战斗力的重要表现之一。这里的事实包括:
- 项目计划与实际进度的差距有多大?
- 每个任务的平均完成时间是否过长?
- 团队在解决问题时是否常常遇到瓶颈?
这些数据可以通过项目管理工具(如 Jira、Trello 等)来进行追踪和量化。一旦明确了当前的情况,我们就能更好地了解团队效率低下的具体原因。
1.2 沟通不畅
沟通问题是研发团队中非常常见的困扰之一。它可以通过以下事实来体现:
- 团队成员之间是否常常因为沟通不足而产生误解?
- 在跨部门协作中,是否有任务交接不清、信息传递不准确的情况?
- 是否存在因为沟通问题导致的工作重复或返工?
通过团队内部的回顾会议、跨部门的反馈等方式,可以明确沟通问题的具体表现和影响。沟通不畅往往会拖慢整体效率,降低团队的战斗力。
1.3 团队士气低落
士气低落是另一个常见的观点化描述,但它背后有很多具体的事实可以支撑:
- 团队成员是否主动承担任务,还是常常出现推诿现象?
- 团队的离职率是否高于行业平均水平?
- 团队成员是否经常表现出疲惫、倦怠,缺乏对工作的积极性?
如果团队中缺乏成就感、归属感,激励机制不到位,这些都会导致士气低落,进而影响团队的整体战斗力。通过员工满意度调查、绩效考核结果等数据,我们可以准确捕捉到士气低落的事实。
1.4 技术债务积累
「技术债务」经常会被忽视,但它实际上是研发团队战斗力不足的重要原因之一。以下事实可以帮助我们判断团队是否面临技术债务问题:
- 系统是否频繁出现 BUG,导致大量时间用于修复问题而非开发新功能?
- 是否有大量遗留的代码或架构问题,导致团队在进行新功能开发时效率低下?
- 系统的可维护性和可扩展性是否在不断下降?
技术债务的积累不仅会拖慢整个团队的开发进度,还可能让团队陷入“救火”而非创新的状态,这无疑是战斗力下降的一个重要体现。
1.5 质量问题严重
质量问题也是影响研发团队战斗力的一个重要因素,并且算是一种非常关键的事实表现。质量问题不仅影响产品的稳定性和用户体验,还会对团队的效率、士气和创新能力造成负面影响。在「研发团队没有战斗力」这一观点下,质量问题可以归结为以下几个具体事实:
- 有频繁的产品缺陷和返工,可以使用缺陷率、线上故障数、SLA 等指标来衡量
- 项目交付质量不达标,如功能不完整,性能问题,用户反馈差等
- 缺乏严格的代码审查和质量控制流程
1.6 工程化和系统化问题
「工程化和系统化问题」是影响研发团队战斗力的重要因素之一,尤其是在团队规模扩大、项目复杂性增加的情况下。工程化和系统化不足通常会导致团队的开发流程混乱、效率低下、交付质量不稳定、可扩展性差,甚至会影响团队的整体协作能力和长期发展。其主要体现在如下几个方面:
- 缺乏标准化流程
- 自动化程度不足,缺乏自动化测试,手动操作的事项较多,重复劳动多
- 系统化不足,缺乏整体架构设计,模块耦合度高或者扩展性差
1.7 人才梯队问题
人才梯队是指团队中不同层级的人才储备和发展体系。如果团队中缺乏明确的人才梯队,意味着团队内部没有清晰的发展路径,成员的技能水平参差不齐,导致团队的整体战斗力不足。以下是一些具体的事实表现:
- 缺乏明确的晋升机制:团队中没有明确的晋升机制和路径,导致优秀的员工看不到职业发展前景,逐渐失去动力。
- 关键人员依赖严重:团队中的某些核心人员承担了过多的技术关键任务,一旦这些人离职或出问题,整个项目或团队都会陷入停滞。
- 缺乏接班人:当团队中的高层或资深技术人员调岗或离职时,缺乏能够快速接替其工作的接班人,导致项目推进或技术维护出现断档。
这些现象说明团队在人才梯队建设上存在严重不足,导致团队的持续作战能力和抗风险能力较差。
1.8 人才密度问题
人才密度指的是团队中高水平技术人才的比例。如果团队的人才密度不足,即高水平人才较少,团队整体的战斗力自然会大打折扣。以下是一些具体的事实表现:
- 技术水平不均衡:团队中技术能力强的人数较少,大多数成员的技术能力不足以支撑复杂的项目开发,导致高水平的成员承担了大部分工作,而低水平的成员拉低了整体效率。
- 问题解决能力差:团队整体在面对复杂问题时,解决问题的能力不足,往往需要依赖外部资源或高层决策,无法自主高效地解决技术难题。
- 技术创新动力不足:由于缺乏高水平人才的引领,团队内部的技术创新能力较弱,难以提出具有前瞻性的技术方案。
人才密度直接影响到团队的技术创新和问题解决能力,因此提升人才密度是打造高战斗力团队的关键。
2 分解问题
在明确了研发团队战斗力不足的主要表现后,我们需要进一步分解问题,以便逐步分析并找到解决方案。根据 MECE 的原则,可以将战斗力不足的问题分解为下列几个方面:
2.1 效率问题
效率是衡量研发团队战斗力的最直接指标之一。如果团队的任务完成效率低下,项目延期频繁,势必会影响整体战斗力。这一问题可以分为以下几个子问题:
- 流程不清晰:团队的开发流程、测试流程、发布流程是否标准化?是否有明确的职责划分和操作步骤?
- 工具使用不当:项目管理工具、代码管理工具、自动化工具是否充分使用?是否存在大量的手动操作和重复劳动?
- 不合理的资源分配:团队成员的任务分配是否合理?是否存在某些成员工作过载,而其他成员任务量不足的情况?
- 瓶颈无法突破:团队在某些技术领域或开发阶段是否经常遇到瓶颈,导致任务卡住?
2.2 沟通协作问题
沟通不畅往往是导致研发团队效率低下和战斗力不足的主要原因之一。沟通问题可以进一步分解为:
- 跨部门沟通障碍:研发团队和其他部门(如产品、运营、市场等)之间的沟通是否频繁出现误解或信息不对称?
- 内部沟通不畅:团队内部成员之间是否缺乏有效的沟通渠道?是否存在信息流动不畅或不透明的情况?
- 技术与业务脱节:研发团队是否充分理解业务需求?技术方案是否能够及时响应业务的变化?
2.3 士气和激励问题
研发团队的士气低落通常是由激励机制不合理、工作压力过大或缺乏成就感引起的。这个问题可以进一步分解为:
- 激励机制不健全:绩效考核、薪资、奖金等激励机制是否能够有效激励员工?团队中是否存在“吃大锅饭”的问题,导致优秀员工失去动力?
- 成就感缺失:团队成员是否能感受到工作的意义?是否有足够的成就感和归属感?
- 工作倦怠:团队成员是否长期处于高压、加班的状态,导致出现工作倦怠?
2.4 技术债务与质量问题
技术债务和质量问题会严重影响团队的战斗力,因为它们导致团队需要花费大量时间在修复错误和维护上,而不是开发新功能或创新。技术债务和质量问题的细分包括:
- 代码质量差:团队是否有严格的代码评审流程?代码是否有良好的可读性、可维护性?
- 技术债务积累:系统中是否存在大量的历史遗留问题(如未重构的老旧代码、架构问题等),导致维护成本高、开发效率低?
- 缺乏自动化测试:团队是否有足够的自动化测试覆盖?是否依赖大量的手工测试,增加了测试和发布的成本?
2.5 人才梯队建设不足
人才梯队建设不足意味着团队缺乏不同层次的人才储备,导致团队的整体战斗力和可持续发展能力受限。具体问题包括:
- 晋升机制不明确:是否有清晰的晋升机制和职业发展通道?员工是否知道如何通过努力获得晋升或更多的成长机会?
- 接班人缺失:是否有计划培养接班人,确保每个关键岗位都有后备力量?
- 关键依赖严重:团队是否过度依赖某些核心人员,一旦这些人离职或请假,项目进展是否会受到严重影响?
2.6 人才密度不够
人才密度不够会导致团队在面对复杂技术问题时缺乏足够的解决能力,团队的技术创新能力也会因此受到影响。这个问题可以进一步分解为:
- 招不到合适的人:招聘过程是否存在瓶颈,导致无法及时引入高水平的技术人才?
- 人才培养不足:是否有系统的内部培训机制,帮助团队成员提升技术水平?
- 技术水平参差不齐:团队成员的技术能力是否存在较大的差异,导致整体效率不高?
2.7 工程化和系统化不足
工程化和系统化不足会导致团队效率低下、交付质量不稳定,无法应对复杂的项目需求。具体问题包括:
- 开发流程不标准:是否有统一的开发、测试、发布流程?是否存在大量的手动操作?
- 自动化程度不够:系统的开发、测试、部署等环节是否充分利用了自动化工具?是否存在大量重复的手工劳动?
- 架构设计不合理:系统的架构设计是否能够支持业务的扩展和未来的发展需求?是否存在模块耦合度过高、扩展性差等问题?
3 体系化的解决问题
解决研发团队没有战斗力的问题,是一个多维度、跨职能的系统性工程。它涉及到组织文化、组织结构、技术架构、流程设计、工程系统和度量考核等多个方面。每个维度的优化和提升都能够为研发团队带来战斗力的增强,但这些维度并非孤立存在,而是相互关联、彼此支撑的。
我们需要明确的是,研发团队战斗力的提升不仅仅是为了提高「速度」,更是为了提高「质量」和「价值」,即更高效地交付更优质的产品,满足业务需求,并为公司创造长期的价值。
3.1 组织文化和沟通机制构建
组织文化是企业的灵魂,它直接影响员工的行为和思维方式。一个以创新和协作为核心的组织文化能激发员工的创造力,鼓励他们尝试新方法和新技术,并在失败中学习和改进。文化的塑造对研发效能提升而言,是打下「地基」的工作。
如何构建?
- 建立跨部门沟通机制:通过定期的跨部门会议或项目复盘,确保技术、产品、业务等不同职能部门之间的沟通顺畅。可以采用 OKR 或双向沟通机制,让各部门了解彼此的目标和进展,减少信息孤岛。
- 鼓励知识共享:定期组织 技术分享会、内部培训,以及设立 技术博客 或 Wiki,这样可以促进技术积累和知识在团队内的流动。还可以通过内部的 导师制,帮助新员工快速融入团队。
- 认可和激励创新:设立相应的 奖项 或 肯定机制,对提出创新方案或成功实施新技术的员工进行公开表扬和奖励。比如可以设立 季度创新奖,以鼓励员工在日常工作中不断试验和改进。
- 领导层的共识:研发负责人应确保与高层管理者达成一致,使研发效能提升工作得到高层支持。领导层的共识会帮助在资源分配、目标设定、团队管理等层面为研发效能的提升提供保障。
我们可以进行如下的一些具体的操作:
- 定期组织 跨部门的需求讨论会 或 研发复盘会,确保各个部门的需求和反馈能够及时传递。
- 设立 激励计划,对优秀的创新项目和技术方案进行奖励。
- 通过 员工满意度调查 或 一对一访谈,了解员工对现有文化的看法,并持续改进。
3.2 调整组织结构
组织结构决定了信息的流动、资源的分配以及决策的效率。一个灵活的、扁平化的组织结构能够促进创新,加速决策过程,同时减少层级间的沟通障碍。通过合理的组织结构设计,可以让团队在面对复杂问题时具备更强的反应能力。
组织结构的调整需要根据实际的团队情况以及业务情况来做优化,是职能型,还是项目型,还是矩阵型等等,可以有如下的一些参考思路:
- 小型化、自治化的团队:采用 跨职能团队 的形式,促进团队成员之间的紧密合作。每个团队都拥有相对独立的决策权,能够快速响应业务需求。采用 Spotify 模式 或 Scrum 团队 的形式,打破职能部门壁垒,形成更快速决策和执行的团队。
- 灵活的项目管理机制:引入 动态人员管理 和 内部创业机制,让团队能够根据项目的需求灵活调整人员和资源配置。通过设立 内部孵化器,让员工能够在公司内部尝试新的项目和解决方案。
- 减少管理层级:通过扁平化管理,减少中间层级的沟通障碍,形成更直接的反馈机制。管理者应该更多地起到 协调者 和 支持者 的作用,而不是微观管理。
在实际操作过程中,我们可以:
- 设立多个 跨职能团队,每个团队独立负责某个产品或项目的端到端交付。
- 引入 OKR 管理机制,确保各个团队的目标与公司整体战略保持一致,并且团队间可以灵活协作。
- 定期进行 组织结构评估,根据业务需求和人员成长情况灵活调整团队架构。
3.3 评估并调整技术架构
技术架构的合理性直接影响团队的研发效率。如果架构设计不合理,团队的开发成本会持续增加,迭代速度会变慢,系统的稳定性和可扩展性也会下降。通过合理的架构设计,可以让团队更高效地应对变化和扩展需求。
以下为一些评估和调整的思路或原则:
- 模块化、低耦合的架构设计:在架构设计中,遵循 高内聚、低耦合 的原则,确保系统模块之间的依赖性降到最低,便于独立开发和部署。采用 微服务架构 或 服务化架构,将系统拆分为相对独立的服务,确保每个模块可以独立扩展和维护。这虽然是老生常谈,但是很少有组织做得很好。且这里需要根据实际的业务需要和当前架构形态来决策。
- 云原生架构:通过云原生架构,使用 Docker、Kubernetes 等容器化和编排技术,实现系统的一致性和可移植性,支持快速部署和环境隔离。
- 灵活的技术栈:根据业务需求选择合适的技术栈,而不是盲目追求技术潮流。技术选择要与团队的技术能力和业务发展阶段相匹配。
- DevOps 和 CI/CD 实践:通过持续集成和持续交付(CI/CD)来加速产品发布,减少人工操作的错误,提升发布频率和质量。
具体操作过程中,我们可以:
- 进行 架构评审,定期对系统的技术架构进行审查,确保架构能够支持当前和未来的业务发展。
- 引入 DevOps 实践,通过自动化工具(如 Jenkins、GitLab CI 等)实现持续集成和交付。
- 采用 微服务架构 进行系统划分,确保各个服务可以独立开发、测试和部署。
3.4 优化研发流程
研发流程设计是确保研发活动高效进行的关键。良好的流程设计可以减少非必要的工作,清晰定义各个阶段的输入、输出和质量标准。同时,优秀的流程设计能帮助团队在每个环节上减少浪费,提升整体效率。
以下为常用的一些优化思路:
- 引入敏捷开发方法:采用 Scrum 或 Kanban 等敏捷开发方法,确保团队能够快速响应需求变化,并通过短周期迭代逐步交付产品。不能为了敏捷而敏捷,根据当前团队情况来实施。
- 精益开发思想:通过 精益思想(Lean),消除流程中的浪费,减少不增值的工作。例如,减少不必要的会议、文档、审批流程,提升团队专注于高价值任务的时间。
- 自动化流程:通过引入自动化工具,简化开发、测试和发布流程,减少手工操作和人为错误。比如自动化代码检查、自动化测试、自动化部署等。
- 数据驱动的流程优化:通过 数据分析工具(如 Jira、SonarQube 等)监控流程中的瓶颈点和低效环节,并持续优化流程。
实际操作过程中可以通过以下的方式来做一些落地的操作:
- 定期进行 流程审查会议,分析当前流程中的低效环节和瓶颈,提出改进方案。
- 采用 需求交付周期 和 需求吞吐量 等指标,衡量每个迭代的效率,并根据数据优化流程。
- 使用 自动化工具 完成代码检查、测试和部署,减少人工干预。
3.5 优化工程系统
工程系统是研发效能提升的基础设施。包括代码管理、构建、测试、部署等一系列工程实践。通过系统化的工具和方法,可以减少重复性工作,提升研发的效率和稳定性。
工程系统如何优化?
- 统一的开发环境:建立统一的开发环境和工具链,确保团队成员在同一套标准下工作,降低环境差异带来的问题。采用 Docker 等容器化技术,确保本地开发环境与生产环境的一致性。
- 自动化测试平台:通过自动化测试平台(如 Selenium、JUnit、TestNG 等),实现单元测试、集成测试、回归测试的自动化,提高产品质量,减少人工测试的负担。
- 版本控制系统:采用 Git 等版本控制系统,建立合理的分支管理策略(如 GitFlow),确保代码的安全性和可追溯性。
- 监控和日志分析系统:引入 监控工具(如 Prometheus、Grafana)和 日志分析工具(如 ELK Stack),确保系统的运行状况可视化,尽早发现问题并采取措施。
在实际操作过程中我们可以:
- 建立统一的 Docker 镜像仓库,确保开发和生产使用相同的基础环境。
- 使用 持续集成工具(如 Jenkins)进行代码的自动化构建和测试。
- 设立 监控和报警机制,确保系统的健康状况能够被实时监控。
3.6 构建度量考核
度量考核是研发效能提升的反馈机制。它为团队提供了衡量成果和改进的依据,帮助团队识别问题、跟踪进度,并调整优化策略。没有量化的度量,研发效能的提升就缺乏方向和依据。
同时,度量可以让战斗力这个概念可视化出来,更明确什么是有战斗力,什么是没有战斗力。
我们可以用如下的方式落地:
- 建立科学的度量体系:用一套符合团队实际情况的指标体系来衡量效能,覆盖项目进度、产品质量、团队效率等方面。常见的度量指标包括 需求交付周期、缺陷率、代码覆盖率、部署频率 等。
- 定期审视数据:定期对这些指标进行审查,分析趋势和异常,找出影响效能的主要原因,并制定改进措施。
- 将度量结果与激励机制挂钩:通过绩效考核,确保团队成员的贡献能够被量化和认可,并通过奖励机制激励团队不断提升效能。
实际操作:
- 建立 研发效能仪表盘,实时监控团队的效能指标。
- 每月定期召开 效能回顾会议,根据数据分析报告,制定下一步的改进计划。
- 将 研发效能指标 纳入团队的 OKR 或绩效考核体系,确保团队成员的目标与效能提升保持一致。
4 小结
提升研发团队的战斗力是一个体系化、系统化的工程,涉及到组织文化、组织结构、技术架构、流程设计、工程系统和度量考核等多个层面。通过在这些维度上进行持续优化,可以显著增强研发团队的战斗力,提升产品交付的速度、质量和创新能力。
如果要真正的解决研发团队没有战斗力的问题,在上面界定问题、分析问题和解决问题的基础上,还需要有如下的一些操作和逻辑:
- 建立目标和成功判断
- 制定详细的解决方案
- 设定里程碑
- 制定详细的工作计划
- 风险判断和未来改进
只有完整落地详细的工作计划,完成里程碑,一步一个脚印,才能真正的打造出有战斗力的研发团队。
每个企业的实际情况不同,因此在执行时需要根据具体场景进行灵活调整。最终目标是帮助研发团队在高速变化的市场环境中,更高效、更稳定地交付高质量的产品,创造更大的商业价值。
来源:juejin.cn/post/7430058214982926386
听我一句劝,业务代码中,别用多线程。
你好呀,我是歪歪。
前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。
虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。
我只是微微一笑,这不是很正常吗?
业务代码中一般也使不上多线程,或者说,业务代码中不知不觉你以及在使用线程池了,你再 duang 的一下搞一个出来,反而容易出事。
所以提到线程池的时候,我个人的观点是必须把它吃得透透的,但是在业务代码中少用或者不用多线程。
关于这个观点,我给你盘一下。
Demo
首先我们还是花五分钟搭个 Demo 出来。
我手边刚好有一个之前搭的一个关于 Dubbo 的 Demo,消费者、生产者都有,我就直接拿来用了:
这个 Demo 我也是跟着网上的 quick start 搞的:
cn.dubbo.apache.org/zh-cn/overv…
可以说写的非常详细了,你就跟着官网的步骤一步步的搞就行了。
我这个 Demo 稍微不一样的是我在消费者模块里面搞了一个 Http 接口:
在接口里面发起了 RPC 调用,模拟从前端页面发起请求的场景,更加符合我们的开发习惯。
而官方的示例中,是基于了 SpringBoot 的 CommandLineRunner 去发起调用:
只是发起调用的方式不一样而已,其他没啥大区别。
需要说明的是,我只是手边刚好有一个 Dubbo 的 Demo,随手就拿来用了,但是本文想要表达的观点,和你使不使用 Dubbo 作为 RPC 框架,没有什么关系,道理是通用的。
上面这个 Demo 启动起来之后,通过 Http 接口发起一次调用,看到控制台服务提供方和服务消费方都有对应的日志输出,准备工作就算是齐活儿了:
上菜
在上面的 Demo 中,这是消费者的代码:
这是提供者的代码:
整个调用链路非常的清晰:
来,请你告诉我这里面有线程池吗?
没有!
是的,在日常的开发中,我就是写个接口给别人调用嘛,在我的接口里面并没有线程池相关的代码,只有 CRUD 相关的业务代码。
同时,在日常的开发中,我也经常调用别人提供给我的接口,也是一把梭,撸到底,根本就不会用到线程池。
所以,站在我,一个开发人员的角度,这个里面没有线程池。
合理,非常合理。
但是,当我们换个角度,再看看,它也是可以有的。
比如这样:
反应过来没有?
我们发起一个 Http 调用,是由一个 web 容器来处理这个请求的,你甭管它是 Tomcat,还是 Jetty、Netty、Undertow 这些玩意,反正是个 web 容器在处理。
那你说,这个里面有线程池吗?
在方法入口处打个断点,这个 http-nio-8081-exec-1 不就是 Tomcat 容器线程池里面的一个线程吗:
通过 dump 堆栈信息,过滤关键字可以看到这样的线程,在服务启动起来,啥也没干的情况下,一共有 10 个:
朋友,这不就是线程池吗?
虽然不是你写的,但是你确实用了。
我写出来的这个 test 接口,就是会由 web 容器中的一个线程来进行调用。所以,站在 web 容器的角度,这里是有一个线程池的:
同理,在 RPC 框架中,不管是消费方,还是服务提供方,也都存在着线程池。
比如 Dubbo 的线程池,你可以看一下官方的文档:
cn.dubbo.apache.org/zh-cn/overv…
而对于大多数的框架来说,它绝不可能只有一个线程池,为了做资源隔离,它会启用好几个线程池,达到线程池隔离,互不干扰的效果。
比如参与 Dubbo 一次调用的其实不仅一个线程池,至少还有 IO 线程池和业务线程池,它们各司其职:
我们主要关注这个业务线程池。
反正站在 Dubbo 框架的角度,又可以补充一下这个图片了:
那么问题来了,在当前的这个情况下?
当有人反馈:哎呀,这个服务吞吐量怎么上不去啊?
你怎么办?
你会 duang 的一下在业务逻辑里面加一个线程池吗?
大哥,前面有个 web 容器的线程池,后面有个框架的线程池,两头不调整,你在中间加个线程池,加它有啥用啊?
web 容器,拿 Tomcat 来说,人家给你提供了线程池参数调整的相关配置,这么一大坨配置,你得用起来啊:
tomcat.apache.org/tomcat-9.0-…
再比如 Dubbo 框架,都给你明说了,这些参数属于性能调优的范畴,感觉不对劲了,你先动手调调啊:
你把这些参数调优弄好了,绝对比你直接怼个线程池在业务代码中,效果好的多。
甚至,你在业务代码中加入一个线程池之后,反而会被“反噬”。
比如,你 duang 的一下怼个线程池在这里,我们先只看 web 容器和业务代码对应的部分:
由于你的业务代码中有线程池的存在,所以当接受到一个 web 请求之后,立马就把请求转发到了业务线程池中,由线程池中的线程来处理本次请求,从而释放了 web 请求对应的线程,该线程又可以里面去处理其他请求。
这样来看,你的吞吐量确实上去了。
在前端来看,非常的 nice,请求立马得到了响应。
但是,你考虑过下游吗?
你的吞吐量上涨了,下游同一时间处理的请求就变多了。如果下游跟不上处理,顶不住了,直接就是崩给你看怎么办?
而且下游不只是你一个调用方,由于你调用的太猛,导致其他调用方的请求响应不过来,是会引起连锁反应的。
所以,这种场景下,为了异步怼个线程池放着,我觉得还不如用消息队列来实现异步化,顶天了也就是消息堆积嘛,总比服务崩了好,这样更加稳妥。
或者至少和下游勾兑一下,问问我们这边吞吐量上升,你们扛得住不。
有的小伙伴看到这里可能就会产生一个疑问了:歪师傅,你这个讲得怎么和我背的八股文不一样啊?
巧了,你背过的八股文我也背过,现在我们来温习一下我们背过的八股文。
什么时候使用线程池呢?
比如一个请求要经过若干个服务获取数据,且这些数据没有先后依赖,最终需要把这些数据组合起来,一并返回,这样经典的场景:
用户点商品详情,你要等半天才展示给用户,那用户肯定骂骂咧咧的久走了。
这个时候,八股文上是怎么说的:用线程池来把串行的动作改成并行。
这个场景也是增加了服务 A 的吞吐量,但是用线程池就是非常正确的,没有任何毛病。
但是你想想,我们最开始的这个案例,是这个场景吗?
我们最开始的案例是想要在业务逻辑中增加一个线程池,对着一个下游服务就是一顿猛攻,不是所谓的串行改并行,而是用更多的线程,带来更多的串行。
这已经不是一个概念了。
还有一种场景下,使用线程池也是合理的。
比如你有一个定时任务,要从数据库中捞出状态为初始化的数据,然后去调用另外一个服务的接口查询数据的最终状态。
如果你的业务代码是这样的:
//获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
//捕获异常以免一条数据错误导致循环结束
try{
//发起rpc调用
String orderStatus = queryOrderStatus(orderInfo.getOrderId);
//更新订单状态
updateOrderInfo(orderInfo.getOrderId,orderStatus);
} catch (Exception e){
//打印异常
}
}
虽然你框架中使用了线程池,但是你就是在一个 for 循环中不停的去调用下游服务查询数据状态,是一条数据一条数据的进行处理,所以其实同一时间,只是使用了框架的线程池中的一个线程。
为了更加快速的处理完这批数据,这个时候,你就可以怼一个线程池放在 for 循环里面了:
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
//使用线程池
executor.execute(() -> {
//捕获异常以免一条数据错误导致循环结束
try {
//发起rpc调用
String orderStatus = queryOrderStatus(orderInfo.getOrderId);
//更新订单状态
updateOrderInfo(orderInfo.getOrderId, orderStatus);
} catch (Exception e) {
//打印异常
}
});
}
需要注意的是,这个线程池的参数怎么去合理的设置,是需要考虑的事情。
同时这个线程池的定位,就类似于 web 容器线程池的定位。
或者这样对比起来看更加清晰一点:
定时任务触发的时候,在发起远程接口调用之前,没有线程池,所以我们可以启用一个线程池来加快数据的处理。
而 Http 调用或者 RPC 调用,框架中本来就已经有一个线程池了,而且也给你提供了对应的性能调优参数配置,那么首先考虑的应该是把这个线程池充分利用起来。
如果仅仅是因为异步化之后可以提升服务响应速度,没有达到串行改并行的效果,那么我更加建议使用消息队列。
好了,本文的技术部分就到这里啦。
下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。
荒腔走板
不知道你看完文章之后,有没有产生一个小疑问:最开始部分的 Demo 似乎用处并不大?
是的,我最开始构思的行文结构是是基于 Demo 在源码中找到关于线程池的部分,从而引出其实有一些我们“看不见的线程池”的存在的。
原本周六我是有一整天的时间来写这篇文章,甚至周五晚上还特意把 Demo 搞定,自己调试了一番,该打的断点全部打上,并写完 Demo 那部分之后,我才去睡觉的,想得是第二天早上起来直接就能用。
按照惯例周六睡个懒觉的,早上 11 点才起床,自己慢条斯理的做了一顿午饭,吃完饭已经是下午 1 点多了。
本来想着在沙发上躺一会,结果一躺就是一整个下午。期间也想过起来写一会文章,坐在电脑前又飞快的躺回到沙发上,就是觉得这个事情索然无味,当下的那一刻就想躺着,然后无意识的刷手机,原本是拿来写文章中关于源码的部分的时间就这样浪费了。
像极了高中时的我,周末带大量作业回家,准备来个悬梁刺股,弯道超车,结果变成了一睡一天,捏紧刹车。
高中的时候,时间浪费了是真的可惜。
现在,不一样了。
荒腔走板这张图片,就是我躺在沙发上的时候,别人问我在干什么时随手拍的一张。
我并不为躺了一下午没有干正事而感到惭愧,浪费了的时间,才是属于自己的时间。
很久以前我看到别人在做一些浪费时间的事情的时候,我心里可能会嘀咕几句,劝人惜时。
这两年我不会了,允许自己做自己,允许别人做别人。
来源:juejin.cn/post/7297980721590272040
都说PHP性能差,但PHP性能真的差吗?
今天本能是想测试一个PDO持久化,会不会带来会话混乱的问题
先贴一下PHP代码, 代码丑了点,但是坚持能run就行,反正就是做个测试。
<?php
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8';
$user = 'root';
$password = 'root';
// 设置 PDO 选项,启用持久化连接
$options = [
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
];
try {
// 创建持久化连接
$pdo = new PDO($dsn, $user, $password, $options);
$stmt = $pdo->prepare("INSERT INTO test_last_insert_id (uni) VALUES (:uni);");
$uni = uniqid('', true);
$stmt->bindValue(':uni', $uni);
$aff = $stmt->execute(); //
if ($aff === false) {
throw new Exception("insert fail:");
}
$id = $pdo->lastInsertId();
function getExecutedSql($stmt, $params)
{
$sql = $stmt->queryString;
$keys = array();
$values = array();
// 替换命名占位符 :key with ?
$sql = preg_replace('/\:(\w+)/', '?', $sql);
// 绑定的参数可能包括命名占位符,我们需要将它们转换为匿名占位符
foreach ($params as $key => $value) {
$keys[] = '/\?/';
$values[] = is_string($value) ? "'$value'" : $value;
}
// 替换占位符为实际参数
$sql = preg_replace($keys, $values, $sql, 1, $count);
return $sql;
}
$stmt = $pdo->query("SELECT id FROM test_last_insert_id WHERE uni = '{$uni}'", PDO::FETCH_NUM);
$row = $stmt->fetch();
$value = $row[0];
if ($value != $id) {
throw new Exception("id is diff");
}
echo "success" . PHP_EOL;
} catch (PDOException $e) {
header('HTTP/1.1 500 Internal Server Error');
file_put_contents('pdo_perisistent.log', $e->getMessage() . PHP_EOL);
die('Database connection failed: ' . $e->getMessage());
} catch (Exception $e) {
header('HTTP/1.1 500 Internal Server Error');
file_put_contents('pdo_perisistent.log', $e->getMessage() . PHP_EOL);
die('Exception: ' . $e->getMessage());
}
用wrk压测,一开始uniqid因为少了混淆参数还报了500,加了一下参数,用来保证uni值
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52.17ms 7.48ms 103.38ms 80.57%
Req/Sec 0.96k 133.22 1.25k 75.81%
Latency Distribution
50% 51.06ms
75% 54.17ms
90% 59.45ms
99% 80.54ms
5904 requests in 3.10s, 1.20MB read
Requests/sec: 1901.92
Transfer/sec: 397.47KB
1900 ~ 2600 之间的QPS,其实这个数值还是相当满意的,测试会话会不会混乱的问题也算完结了。
但是好奇心突起,之前一直没做过go和php执行sql下的对比,正好做一次对比压测
package main
import (
"database/sql"
"fmt"
"net/http"
"sync/atomic"
"time"
_ "github.com/go-sql-driver/mysql"
"log"
)
var id int64 = time.Now().Unix() * 1000000
func generateUniqueID() int64 {
return atomic.AddInt64(&id, 1)
}
func main() {
dsn := "root:root@tcp(localhost:3306)/test?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer func() { _ = db.Close() }()
//// 设置连接池参数
//db.SetMaxOpenConns(100) // 最大打开连接数
//db.SetMaxIdleConns(10) // 最大空闲连接数
//db.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var err error
uni := generateUniqueID()
// Insert unique ID int0 the database
insertQuery := `INSERT INTO test_last_insert_id (uni) VALUES (?)`
result, err := db.Exec(insertQuery, uni)
if err != nil {
log.Fatalf("Error inserting data: %v", err)
}
lastInsertID, err := result.LastInsertId()
if err != nil {
log.Fatalf("Error getting last insert ID: %v", err)
}
// Verify the last insert ID
selectQuery := `SELECT id FROM test_last_insert_id WHERE uni = ?`
var id int64
err = db.QueryRow(selectQuery, uni).Scan(&id)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}
if id != lastInsertID {
log.Fatalf("ID mismatch: %d != %d", id, lastInsertID)
}
fmt.Println("success")
})
_ = http.ListenAndServe(":8080", nil)
}
truncate表压测结果,这低于预期了吧
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 54.05ms 36.86ms 308.57ms 80.77%
Req/Sec 0.98k 243.01 1.38k 63.33%
Latency Distribution
50% 43.70ms
75% 65.42ms
90% 99.63ms
99% 190.18ms
5873 requests in 3.01s, 430.15KB read
Requests/sec: 1954.08
Transfer/sec: 143.12KB
开个连接池,清表再测,结果半斤八两
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 54.07ms 35.87ms 281.38ms 79.84%
Req/Sec 0.97k 223.41 1.40k 60.00%
Latency Distribution
50% 44.91ms
75% 66.19ms
90% 99.65ms
99% 184.51ms
5818 requests in 3.01s, 426.12KB read
Requests/sec: 1934.39
Transfer/sec: 141.68KB
然后开启不清表的情况下,php和go的交叉压测
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52.51ms 43.28ms 436.00ms 86.91%
Req/Sec 1.08k 284.67 1.65k 65.00%
Latency Distribution
50% 40.22ms
75% 62.10ms
90% 102.52ms
99% 233.98ms
6439 requests in 3.01s, 471.61KB read
Requests/sec: 2141.12
Transfer/sec: 156.82KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 41.41ms 10.44ms 77.04ms 78.07%
Req/Sec 1.21k 300.99 2.41k 73.77%
Latency Distribution
50% 38.91ms
75% 47.62ms
90% 57.38ms
99% 69.84ms
7332 requests in 3.10s, 1.50MB read
Requests/sec: 2363.74
Transfer/sec: 493.98KB
// 这里骤降是我很不理解的不明白是因为什么
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 156.72ms 75.48ms 443.98ms 66.10%
Req/Sec 317.93 84.45 480.00 71.67%
Latency Distribution
50% 155.21ms
75% 206.36ms
90% 254.32ms
99% 336.07ms
1902 requests in 3.01s, 139.31KB read
Requests/sec: 631.86
Transfer/sec: 46.28KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 43.47ms 10.04ms 111.41ms 90.21%
Req/Sec 1.15k 210.61 1.47k 72.58%
Latency Distribution
50% 41.17ms
75% 46.89ms
90% 51.27ms
99% 95.07ms
7122 requests in 3.10s, 1.45MB read
Requests/sec: 2296.19
Transfer/sec: 479.87KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 269.08ms 112.17ms 685.29ms 73.69%
Req/Sec 168.22 125.46 520.00 79.59%
Latency Distribution
50% 286.58ms
75% 335.40ms
90% 372.61ms
99% 555.80ms
1099 requests in 3.02s, 80.49KB read
Requests/sec: 363.74
Transfer/sec: 26.64KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 41.74ms 9.67ms 105.86ms 91.72%
Req/Sec 1.20k 260.04 2.24k 80.33%
Latency Distribution
50% 38.86ms
75% 46.77ms
90% 49.02ms
99% 83.01ms
7283 requests in 3.10s, 1.49MB read
Requests/sec: 2348.07
Transfer/sec: 490.71KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 464.85ms 164.66ms 1.06s 71.97%
Req/Sec 104.18 60.01 237.00 63.16%
Latency Distribution
50% 467.00ms
75% 560.54ms
90% 660.70ms
99% 889.86ms
605 requests in 3.01s, 44.31KB read
Requests/sec: 200.73
Transfer/sec: 14.70KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 50.62ms 9.16ms 85.08ms 75.74%
Req/Sec 0.98k 170.66 1.30k 69.35%
Latency Distribution
50% 47.93ms
75% 57.20ms
90% 61.76ms
99% 79.90ms
6075 requests in 3.10s, 1.24MB read
Requests/sec: 1957.70
Transfer/sec: 409.13KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 568.84ms 160.91ms 1.04s 66.38%
Req/Sec 81.89 57.59 262.00 67.27%
Latency Distribution
50% 578.70ms
75% 685.85ms
90% 766.72ms
99% 889.39ms
458 requests in 3.01s, 33.54KB read
Requests/sec: 151.91
Transfer/sec: 11.13KB
go 的代码随着不断的测试,很明显处理速度在不断的下降,这说实话有点超出我的认知了。
PHP那边却是基本稳定的,go其实一开始我还用gin测试过,发现测试结果有点超出预料,还改了用http库来测试,这结果属实差强人意了。
突然明白之前经常看到别人在争论性能问题的时候,为什么总有人强调PHP性能并不差。
或许PHP因为fpm的关系导致每次加载大量文件导致的响应相对较慢,比如框架laravel 那个QPS只有一两百的家伙,但其实这个问题要解决也是可以解决的,也用常驻内存的方式就好了。再不行还有phalcon
我一直很好奇一直说PHP性能问题的到底是哪些人, 不会是从PHP转到其他语言的吧。
% php -v
PHP 8.3.12 (cli) (built: Sep 24 2024 18:08:04) (NTS)
Copyright (c) The PHP Gr0up
Zend Engine v4.3.12, Copyright (c) Zend Technologies
with Xdebug v3.3.2, Copyright (c) 2002-2024, by Derick Rethans
with Zend OPcache v8.3.12, Copyright (c), by Zend Technologies
% go version
go version go1.23.1 darwin/amd64
这结果,其实不太能接受,甚至都不知道原因出在哪了,有大佬可以指出问题一下吗
加一下时间打印再看看哪里的问题
package main
import (
"database/sql"
"fmt"
"net/http"
"sync/atomic"
"time"
_ "github.com/go-sql-driver/mysql"
"log"
)
var id int64 = time.Now().Unix() * 1000000
func generateUniqueID() int64 {
return atomic.AddInt64(&id, 1)
}
func main() {
dsn := "root:root@tcp(localhost:3306)/test?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer func() { _ = db.Close() }()
// 设置连接池参数
db.SetMaxOpenConns(100) // 最大打开连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
reqStart := time.Now()
var err error
uni := generateUniqueID()
start := time.Now()
// Insert unique ID int0 the database
insertQuery := `INSERT INTO test_last_insert_id (uni) VALUES (?)`
result, err := db.Exec(insertQuery, uni)
fmt.Printf("insert since: %v uni:%d \n", time.Since(start), uni)
if err != nil {
log.Fatalf("Error inserting data: %v", err)
}
lastInsertID, err := result.LastInsertId()
if err != nil {
log.Fatalf("Error getting last insert ID: %v", err)
}
selectStart := time.Now()
// Verify the last insert ID
selectQuery := `SELECT id FROM test_last_insert_id WHERE uni = ?`
var id int64
err = db.QueryRow(selectQuery, uni).Scan(&id)
fmt.Printf("select since:%v uni:%d \n", time.Since(selectStart), uni)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}
if id != lastInsertID {
log.Fatalf("ID mismatch: %d != %d", id, lastInsertID)
}
fmt.Printf("success req since:%v uni:%d \n", time.Since(reqStart), uni)
})
_ = http.ListenAndServe(":8080", nil)
}
截取了后面的一部分输出,这不会是SQL库的问题吧,
success req since:352.310146ms uni:1729393975000652
insert since: 163.316785ms uni:1729393975000688
insert since: 154.983173ms uni:1729393975000691
insert since: 158.094503ms uni:1729393975000689
insert since: 136.831695ms uni:1729393975000697
insert since: 141.857079ms uni:1729393975000696
insert since: 128.115216ms uni:1729393975000702
select since:412.94524ms uni:1729393975000634
success req since:431.383768ms uni:1729393975000634
select since:459.596445ms uni:1729393975000601
success req since:568.576336ms uni:1729393975000601
insert since: 134.39147ms uni:1729393975000700
select since:390.926517ms uni:1729393975000643
success req since:391.622183ms uni:1729393975000643
select since:366.098937ms uni:1729393975000648
success req since:373.490764ms uni:1729393975000648
insert since: 136.318919ms uni:1729393975000699
select since:420.626209ms uni:1729393975000640
success req since:425.243441ms uni:1729393975000640
insert since: 167.181068ms uni:1729393975000690
select since:272.22808ms uni:1729393975000671
单次请求的时候输出结果是符合预期的, 但是并发SQL时会出现执行慢的问题,这就很奇怪了
% curl localhost:8080
insert since: 1.559709ms uni:1729393975000703
select since:21.031284ms uni:1729393975000703
success req since:22.62274ms uni:1729393975000703
经群友提示还和唯一键的区分度有关,两边算法一致有点太难了,Go换了雪法ID之后就正常了。
因为之前 Go这边生成的uni值是递增的导致区分度很低,最终导致并发写入查询效率变低。
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 44.51ms 24.87ms 187.91ms 77.98%
Req/Sec 1.17k 416.31 1.99k 66.67%
Latency Distribution
50% 37.46ms
75% 54.55ms
90% 80.44ms
99% 125.72ms
6960 requests in 3.01s, 509.77KB read
Requests/sec: 2316.02
Transfer/sec: 169.63KB
2024-10-23 更新
今天本来是想验证一下有关,并发插入自增有序的唯一键高延迟的问题,发现整个有问题的只有一行代码。
就是在查询时,类型转换的问题,插入和查询都转换之后,空表的情况下QPS 可以到4000多。即使在已有大数据量(几十万)的情况也有两千多的QPS。
现在又多了一个问题,为什么用雪花ID时不会有这样的问题。雪花ID也是int64类型的,这是为什么呢。
// 旧代码
err = db.QueryRow(selectQuery, uni).Scan(&id)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}
// 新代码 变化只有一个就是把uni 转成字符串之后就没有问题了
var realId int64
err = db.QueryRow(selectQuery, fmt.Sprintf("%d", uni)).Scan(&realId)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}
来源:juejin.cn/post/7427455855941976076
从《逆行人生》聊聊中年程序员的出路
赶在下架前去看了《逆行人生》。
这部电影讲述了高志垒——一个架构师,被裁员后,在找工作两个月颗粒无收的情况下,被逼无奈去跑外卖的故事。
个人感觉还是很值得一看的,是一部很好的现实题材电影,并没有网上说的资本迫害打工人还要打工人努力奋斗的感觉。
有年轻人说,难以共情。70万年薪的人最后要落到为了 15k 的月薪而奔波,他不理解为什么。然而就我亲身经历而言,无路可走的时候,我们的确会做这样的选择。
我们先来看看中年程序员有哪些选择。
中年程序员有哪些出路?
继续打工
打工,无疑是多数人的选择。毕竟上一天班赚一天的钱,这种稳稳的幸福还是大部分人的追求。但打工也不能停滞不前,还是要不断学习、拓展自己的能力,尤其是我们IT行业,技术更新迭代快。如果不学习,很可能 3 到 5 年就被淘汰了。
程序员要晋升、跳槽,主要学习方向以下两种:
- 拓展技术的深度与广度:高级开发、架构师、热门行业的开发如AI等;
- 向外拓展自己的能力:培训机构老师、高校老师;技术管理;
自己单干
继续打工,无疑都会碰到被裁员的风险,你个人的命运始终掌握在老板甚至顶头上司的手里。如果你不甘于此,就要开创性地走其他路了。这对个人的挑战都是极大的。
程序员可以凭借技术能力逐渐向外扩展:
- 独立开发:承接项目或者自研产品
- 创业:成立公司、团队,完成开发工作
彻底转行
也有部分人被彻底伤透了心,完全不再进入这个行业了,转向其他行业了。
- 网络兼职:写手、影视剪辑等;
- 中年三保、铁人三项:保安、保姆、保洁、快递、司机、外卖。这个是被大家调侃最多的;
- 其他行业的打工者:如制造业、外贸等行业;
- 开店或者创业:存上一笔钱开店或者做一间自己喜欢的公司,也是一些人的选择。
我们应该如何选择?
如上所见,程序员能做的选择还是比较多的。我们将这些工作列个表,列一下所需要的能力与所承担的责任,以及最后的风险,以便做选择:
可以看到,每个方向其实都是有风险的,并没有不存在无风险的职业与方向。那是不是我们就可以完全凭借个人喜好来决定呢?并非如此,这些选择对大部分人而言,还是有优劣之分的。
不推荐铁人三项、中年三宝
首先,我个人其实非常不建议程序员转行去做起他行业的,除非迫不得已,尤其是从事体力劳动。
因为这需要消耗大量的体力与时间。中年人无法靠比拼体力取胜,工作时间长,也无法取得工作生活平衡。在电影《逆行人生》中,高志垒虽然赢了第一个单王,但可以看出其靠的更多是运气,行业老大哥或退出竞赛、或家里有事提早离开。
另外就是,AI 技术发展和市场供需变化。不久前武汉的萝卜快跑落地,相信大部分滴滴司机都感受到了被淘汰的可能。而且这类工作市场基本上已经饱和,所以薪酬只会越来越低。
其他的网络兼职、去制造业服务业打工,这些都是门槛低,程序员即使有技术与能力,也不见得有任何优势的,所以也是不推荐的。
而开店或按自己的兴趣来创业,则非常看你个人能力了,同样需要更谨慎的考虑,当然你如果家财万贯,倒是可以任性一把。
更推荐提早规划、提早行动
剩下的职业方向其实都是推荐的,因为多多少少跟我们自身学习的技术是相关的。将我们的能力逐步往外扩,逐渐走出舒适圈,是更合适的一个发展路径。但是需要注意的是,建议尽早立下目标,提前规划,尽快行动的。
如,希望做老师,可以提早在企业内部做讲师、技术讲师,给新人讲解。锻炼好自己的沟通表达能力,多想想如何让新人更好地融入企业、进入工作状态。
又如,你想自己创业,那可以开始就留意你手头上做的产品是如何开发、运营的。公司如何分配人力物力去做的,如何做商业变现的,如何寻找客户的等等这些问题。不仅要站在技术角度、也要站在公司的角度多思考、多学习、多实践。甚至在时机成熟的时候,提出转岗去做产品、技术管理,更早地锻炼自己所需的创业的能力,能让自己日后的路走的更顺。
高志垒为何还是选择送外卖?
回到电影,既然都不建议程序员从事体力劳动,高志垒好好的一个架构师,也是有脑子的,为啥最后还是选择了外卖员呢?
首先,从影片一开始可以看出,高志垒选择了架构师或者技术管理偏技术方向,因其手头上还有一线开发的任务。显然对于 45 岁的他,在打工这条路上几乎已经到顶了。
然而,他并没有做好职业规划,甚至从未考虑过失业的风险。在突然失业时,才发现市场上几乎找不到自己的职位、薪酬,最后简历也是乱投一气了;而中产返贫三件套:高额房贷、全职太太、国际学校,他几乎全都拥有;并且还大笔地投资了 P2P ,因其爆雷导致家庭财产大量损失;再加上其父亲突发重病,住院急需要钱。
所有的状况同时出现,所有的压力压在身上,在两个月投递简历无果时,他听说送外卖能补上房贷月供差额的数目,宛如找到救命稻草一般,毅然加入了外卖行业。
如何避免陷入被动状况?
如何避免我们也陷入高志垒的状况?
除了像上面说的提早积攒自己的能力,提早做规划、更早地行动外,程序员也应提升技能多样性,特别是专业外的技能;同时在职业中后期应寻找到更利于个人发展的公司或项目;还需要拓展人脉,保持与行业内的沟通交流;在最后,保持健康的生活习惯和平衡好工作,让自己的职业寿命尽可能地延长。
而在财务上,做好失业准备、甚至为后续独立开发、创业等积攒资金都是必要的,所以需要采取一些措施,做好家庭财务的规划,如:
- 留出紧急备用金:为应对突发事件,如失业或疾病,应建立足够的紧急基金,一般建议为家庭日常开支的3-6个月。
- 谨慎投资:只投资自己熟悉的产品;了解自身的风险承受能力再投资;同时避免将所有资金投入到单一的高风险产品中,如P2P,应进行资产配置,分散风险。
- 购买保险:为家庭成员购买适当的健康保险,以减轻因病致贫的风险。
- 做好财务预算、规划:每年、每月做好财务预算;同时对于房贷和教育投资等大额支出,应进行详细的财务规划,确保在收入中断时也能应对。
- 增加收入来源:尽可能地增加家庭收入来源,比如配偶就业或开展副业,减少对单一收入的依赖。
总结与思考
在戏里的高志垒无疑是幸运的,家庭和睦,家人都给予最大的支持,愿意一起度过难关。再加上自己开发的小程序“路路通”,同事间互助互利,最后,成功拿到了单王,并帮家里度过经济危机。
然而最后的结局,高志垒并没有“逆袭”人生,而是在“逆行”人生中,调整了自己。最后他卖掉了大房子,搬到了小房子住,老婆依然在工作,孩子也放弃了就读国际学校、老人靠自身意志力完成了康复。
这也是我觉得这部电影还算现实主义之处。并没有理想中的事情发生,就像现实生活中那些受挫的人们一样,最后选择降低生活标准,继续前行。
最后的最后,问一下大家,如果你面临电影结尾彩蛋中的情景,有一个外卖公司的高层老板对你开发的“路路通”小程序感兴趣,你会如何选择?
- 卖掉小程序,拿钱走人
- 加入外卖公司,继续开发
- 不卖,开源
欢迎留下你的答案与思考,一起讨论。
来源:juejin.cn/post/7414732910240972835
15 种超赞的 MyBatis 写法
序言
MyBatis的前身是iBatis,最初是Apache的一个开源项目。随着时间的推移,为了更好地适应Java持久层框架的需求,iBatis在2010年重构并更名为MyBatis。
这一转变标志着MyBatis在功能和性能上的显著提升,同时也意味着它能够更好地服务于日益复杂的企业级应用。
今天,我们就来探讨 15 种超赞的 MyBatis 写法,让你的数据库操作更加高效和灵活。
1. 批量操作优化
批量操作是提高数据库操作效率的重要手段。MyBatis提供了<foreach>
标签,可以有效地进行批量插入、更新或删除操作,从而减少与数据库的交互次数。
批量插入示例:
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO user (username, email, create_time) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.username}, #{item.email}, #{item.createTime})
</foreach>
</insert>
批量更新示例:
<update id="batchUpdate" parameterType="java.util.List">
<foreach collection="list" item="item" separator=";">
UPDATE user
SET username = #{item.username}, email = #{item.email}
WHERE id = #{item.id}
</foreach>
</update>
批量删除示例:
<delete id="batchDelete" parameterType="java.util.List">
DELETE FROM user WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
通过使用<foreach>
标签,我们可以将多个操作合并为一条SQL语句,大大减少了数据库交互次数,提高了操作效率。
2. 动态SQL
动态SQL是MyBatis的强大特性之一,允许我们根据不同的条件动态构建SQL语句。<if>
标签是实现动态SQL的核心。
动态查询示例:
<select id="findUsers" resultType="User">
SELECT * FROM user
WHERE 1=1
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="status != null">
AND status = #{status}
</if>
</select>
在这个例子中,我们根据传入的参数动态添加查询条件。如果某个参数为空,对应的条件就不会被添加到SQL语句中。
3. 多条件分支查询
对于更复杂的查询逻辑,我们可以使用<choose>
、<when>
和<otherwise>
标签来实现多条件分支查询。
多条件分支查询示例:
<select id="findUsersByCondition" resultType="User">
SELECT * FROM user
WHERE 1=1
<choose>
<when test="searchType == 'username'">
AND username LIKE CONCAT('%', #{keyword}, '%')
</when>
<when test="searchType == 'email'">
AND email LIKE CONCAT('%', #{keyword}, '%')
</when>
<otherwise>
AND (username LIKE CONCAT('%', #{keyword}, '%') OR email LIKE CONCAT('%', #{keyword}, '%'))
</otherwise>
</choose>
</select>
这个例子展示了如何根据不同的搜索类型选择不同的查询条件,如果没有指定搜索类型,则默认搜索用户名和邮箱。
4. SQL语句优化
使用<trim>
标签可以帮助我们优化生成的SQL语句,避免出现多余的AND或OR关键字。
SQL语句优化示例:
<select id="findUsers" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="status != null">
AND status = #{status}
</if>
</trim>
</select>
在这个例子中,<trim>
标签会自动去除第一个多余的AND或OR,并在有查询条件时添加WHERE关键字。
5. 自动生成主键
在插入操作中,我们经常需要获取数据库自动生成的主键。MyBatis提供了<selectKey>
标签来实现这一功能。
自动生成主键示例:
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
SELECT 2531020
</selectKey>
INSERT INTO user (username, email, create_time)
VALUES (#{username}, #{email}, #{createTime})
</insert>
在这个例子中,插入操作完成后,会自动执行SELECT 2531020
获取新插入记录的ID,并将其赋值给传入的User对象的id属性。
6. 注解方式使用MyBatis
除了XML配置,MyBatis还支持使用注解来定义SQL操作,这种方式可以使代码更加简洁。
注解方式示例:
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(Long id);
@Insert("INSERT INTO user (username, email, create_time) VALUES (#{username}, #{email}, #{createTime})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);
@Update("UPDATE user SET username = #{username}, email = #{email} WHERE id = #{id}")
int updateUser(User user);
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteUser(Long id);
}
这种方式适合简单的CRUD操作,但对于复杂的SQL语句,仍然建议使用XML配置。
7. 高级映射
MyBatis提供了强大的对象关系映射功能,可以处理复杂的表关系。
一对多映射示例:
<resultMap id="userWithOrdersMap" type="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNumber" column="order_number"/>
<result property="createTime" column="order_create_time"/>
</collection>
</resultMap>
<select id="getUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.id as user_id, u.username, o.id as order_id, o.order_number, o.create_time as order_create_time
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{userId}
</select>
这个例子展示了如何将用户和订单信息映射到一个复杂的对象结构中。
8. MyBatis-Plus集成
MyBatis-Plus是MyBatis的增强工具,它提供了许多便捷的CRUD操作和强大的条件构造器。
MyBatis-Plus使用示例:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
public List<User> findUsersByCondition(String username, String email) {
return this.list(new QueryWrapper<User>()
.like(StringUtils.isNotBlank(username), "username", username)
.eq(StringUtils.isNotBlank(email), "email", email));
}
}
在这个例子中,我们使用MyBatis-Plus提供的条件构造器来动态构建查询条件,无需编写XML。
9. 代码生成器
MyBatis Generator是一个强大的代码生成工具,可以根据数据库表自动生成MyBatis的Mapper接口、实体类和XML映射文件。
MyBatis Generator配置示例:
<!DOCTYPE generatorConfiguration PUBLIC
"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mydb"
userId="root"
password="password">
</jdbcConnection>
<javaModelGenerator targetPackage="com.example.model" targetProject="src/main/java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.example.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<table tableName="user" domainObjectName="User" >
<generatedKey column="id" sqlStatement="MySQL" identity="true" />
</table>
</context>
</generatorConfiguration>
使用这个配置文件,我们可以自动生成与user表相关的所有必要代码。
10. 事务管理
在Spring环境中,我们可以使用@Transactional
注解来管理事务,确保数据的一致性。
事务管理示例:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void createUserWithOrders(User user, List<Order> orders) {
userMapper.insert(user);
for (Order order : orders) {
order.setUserId(user.getId());
orderMapper.insert(order);
}
}
}
在这个例子中,创建用户和订单的操作被包装在一个事务中,如果任何一步失败,整个操作都会回滚。
11. 缓存机制
MyBatis提供了一级缓存和二级缓存,可以有效提高查询性能。
二级缓存配置示例:
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
这个配置启用了LRU淘汰策略的二级缓存,缓存容量为512个对象,每60秒刷新一次。
12. 插件使用
MyBatis插件可以拦截核心方法的调用,实现如分页、性能分析等功能。
分页插件示例 (使用PageHelper):
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public PageInfo<User> getUserList(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
return new PageInfo<>(users);
}
}
这个例子展示了如何使用PageHelper插件实现简单的分页功能。
13. 多数据源配置
在某些场景下,我们需要在同一个应用中操作多个数据库。MyBatis支持配置多个数据源来实现这一需求。
多数据源配置示例:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
@Bean
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
}
这个配置类定义了两个数据源和对应的SqlSessionFactory,可以在不同的Mapper中使用不同的数据源。
14. 读写分离
读写分离是提高数据库性能的常用策略。MyBatis可以通过配置多数据源来实现简单的读写分离。
读写分离配置示例:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER, masterDataSource);
targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
};
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
}
这个例子定义了一个动态数据源,可以根据上下文选择主库或从库。你需要实现一个DataSourceContextHolder
来管理当前线程的数据源类型。
15. SQL分析和优化
MyBatis提供了SQL执行分析功能,可以帮助我们找出性能瓶颈。
SQL分析配置示例:
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<property name="offsetAsPageNum" value="true"/>
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true"/>
</plugin>
<plugin interceptor="org.apache.ibatis.plugin.Interceptor">
<property name="properties">
sqlCollector=com.example.SqlCollector
</property>
</plugin>
</plugins>
在这个配置中,我们不仅加入了分页插件,还加入了一个自定义的SQL收集器,可以用于分析SQL执行情况。
总结
我们详细介绍了15种MyBatis的高级用法和技巧,涵盖了从基本的CRUD操作优化到复杂的多数据源配置和读写分离等高级主题。这些技巧可以帮助开发者更高效地使用MyBatis,构建出性能更好、可维护性更强的应用系统。
来源:juejin.cn/post/7417681630884233268
MapStruct这么用,同事也开始模仿
前言
hi,大家好,我是大鱼七成饱。
前几天同事review我的代码,发现mapstruct有这么多好用的技巧,遇到POJO转换的问题经常过来沟通。考虑到不可能每次都一对一,所以我来梳理五个场景,谁在过来问,直接甩出总结。
环境准备
由于日常使用都是spring,所以后面的示例都是在springboot框架中运行的。关键pom依赖如下:
<properties>
<java.version>1.8</java.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.30</org.projectlombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
场景一:常量转换
这是最简单的一个场景,比如需要设置字符串、整形和长整型的常量,有的又需要日期,或者新建类型。下面举个例子,演示如何转换
//实体类
@Data
public class Source {
private String stringProp;
private Long longProp;
}
@Data
public class Target {
private String stringProperty;
private long longProperty;
private String stringConstant;
private Integer integerConstant;
private Long longWrapperConstant;
private Date dateConstant;
}
- 设置字符串常量
- 设置long常量
- 设置java内置类型默认值,比如date
那么mapper这么设置就可以
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface SourceTargetMapper {
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1l")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001L")
@Mapping(target = "dateConstant", dateFormat = "yyyy-MM-dd", constant = "2023-09-")
Target sourceToTarget(Source s);
}
解释下,constant用来设置常量值,source的值如果没有设置,则会使用defaultValue的值,日期可以按dateFormat解析。
Talk is cheap, show me the code !废话不多说,自动生成的转换类如下:
@Component
public class SourceTargetMapperImpl implements SourceTargetMapper {
public SourceTargetMapperImpl() {
}
public Target sourceToTarget(Source s) {
if (s == null) {
return null;
} else {
Target target = new Target();
if (s.getStringProp() != null) {
target.setStringProperty(s.getStringProp());
} else {
target.setStringProperty("undefined");
}
if (s.getLongProp() != null) {
target.setLongProperty(s.getLongProp());
} else {
target.setLongProperty(-1L);
}
target.setStringConstant("Constant Value");
target.setIntegerConstant(14);
target.setLongWrapperConstant(3001L);
try {
target.setDateConstant((new SimpleDateFormat("dd-MM-yyyy")).parse("09-01-2014"));
return target;
} catch (ParseException var4) {
throw new RuntimeException(var4);
}
}
}
}
是不是一目了然
场景二:转换中调用表达式
比如id不存在使用UUID生成一个,或者使用已有参数新建一个对象作为属性。当然可以用after mapping,qualifiedByName等实现,感觉还是不够优雅,这里介绍个雅的(代码少点的)。
实体类如下:
@Data
public class CustomerDto {
public Long id;
public String customerName;
private String format;
private Date time;
}
@Data
public class Customer {
private String id;
private String name;
private TimeAndFormat timeAndFormat;
}
@Data
public class TimeAndFormat {
private Date time;
private String format;
public TimeAndFormat(Date time, String format) {
this.time = time;
this.format = format;
}
}
Dto转customer,加创建TimeAndFormat作为属性,mapper实现如下:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, imports = UUID.class)
public interface CustomerMapper {
@Mapping(target = "timeAndFormat",
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
@Mapping(target = "id", source = "id", defaultExpression = "java( UUID.randomUUID().toString() )")
Customer toCustomer(CustomerDto s);
}
解释下,id为空则走默认的defaultExpression,通过imports引入,java括起来调用。新建对象直接new TimeAndFormat。有的小伙伴喜欢用qualifiedByName自定义方法,可以对比下,哪个合适用哪个,都能调用转换方法。
生成代码如下:
@Component
public class CustomerMapperImpl implements CustomerMapper {
public CustomerMapperImpl() {
}
public Customer toCustomer(CustomerDto s) {
if (s == null) {
return null;
} else {
Customer customer = new Customer();
if (s.getId() != null) {
customer.setId(String.valueOf(s.getId()));
} else {
customer.setId(UUID.randomUUID().toString());
}
customer.setTimeAndFormat(new TimeAndFormat(s.getTime(), s.getFormat()));
return customer;
}
}
}
场景三:类共用属性,如何复用
比如下面的Bike和车辆类,都有id和creationDate属性,我又不想重复写mapper属性注解
public class Bike {
/**
* 唯一id
*/
private String id;
private Date creationDate;
/**
* 品牌
*/
private String brandName;
}
public class Car {
/**
* 唯一id
*/
private String id;
private Date creationDate;
/**
* 车牌号
*/
private String chepaihao;
}
解决起来很简单,写个共用的注解,使用的时候引入就可以,示例如下:
//通用注解
@Retention(RetentionPolicy.CLASS)
//自动生成当前日期
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
//忽略id
@Mapping(target = "id", ignore = true)
public @interface ToEntity { }
//使用
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface TransportationMapper {
@ToEntity
@Mapping( target = "brandName", source = "brand")
Bike map(BikeDto source);
@ToEntity
@Mapping( target = "chepaihao", source = "plateNo")
Car map(CarDto source);
}
这里Retention修饰ToEntity注解,表示ToEntity注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期,辅助生成mapper实现类。上面定义了creationDate和id的转换规则,新建日期,忽略id。
生成的mapper实现类如下:
@Component
public class TransportationMapperImpl implements TransportationMapper {
public TransportationMapperImpl() {
}
public Bike map(BikeDto source) {
if (source == null) {
return null;
} else {
Bike bike = new Bike();
bike.setBrandName(source.getBrand());
bike.setCreationDate(new Date());
return bike;
}
}
public Car map(CarDto source) {
if (source == null) {
return null;
} else {
Car car = new Car();
car.setChepaihao(source.getPlateNo());
car.setCreationDate(new Date());
return car;
}
}
}
坚持一下,还剩俩场景,剩下的俩更有意思
场景四:lombok和mapstruct冲突了
啥冲突?用了builder注解后,mapstuct转换不出来了。哎,这个问题困扰了我那同事两天时间。
解决方案如下:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
加上lombok-mapstruct-binding就可以了,看下生成的效果:
@Builder
@Data
public class Person {
private String name;
}
@Data
public class PersonDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface PersonMapper {
Person map(PersonDto dto);
}
@Component
public class PersonMapperImpl implements PersonMapper {
public PersonMapperImpl() {
}
public Person map(PersonDto dto) {
if (dto == null) {
return null;
} else {
Person.PersonBuilder person = Person.builder();
person.name(dto.getName());
return person.build();
}
}
}
从上面可以看到,mapstruct匹配到了lombok的builder方法。
场景五:说个难点的,转换的时候,如何注入springBean
有时候转换方法比不是静态的,他可能依赖spring bean,这个如何导入?
这个使用需要使用抽象方法了,上代码:
@Component
public class SimpleService {
public String formatName(String name) {
return "您的名字是:" + name;
}
}
@Data
public class Student {
private String name;
}
@Data
public class StudentDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class StudentMapper {
@Autowired
protected SimpleService simpleService;
@Mapping(target = "name", expression = "java(simpleService.formatName(source.getName()))")
public abstract StudentDto map(StudentDto source);
}
接口是不支持注入的,但是抽象类可以,所以采用抽象类解决,后面expression直接用皆可以了,生成mapperimpl如下:
@Component
public class StudentMapperImpl extends StudentMapper {
public StudentMapperImpl() {
}
public StudentDto map(StudentDto source) {
if (source == null) {
return null;
} else {
StudentDto studentDto = new StudentDto();
studentDto.setName(this.simpleService.formatName(source.getName()));
return studentDto;
}
}
}
思考
以上场景肯定还有其他解决方案,遵循合适的原则就可以。驾驭不了的代码,可能带来更多问题,先简单实现,后续在迭代优化可能适合更多的业务场景。
本文示例代码放在了github,需要的朋友请关注公众号大鱼七成饱,回复关键词MapStruct使用即可获得。
来源:juejin.cn/post/7297222349731627046
如果你想做副业,不妨看看我这3个月的奋斗史
看过我文章的小伙伴应该也都知道,去年我是坚持了每周更新一篇技术文章,去年文章总数算下来也有个50篇左右,然后今年立下的flag是:
- 再学一门语言,比如鸿蒙开发、游戏开发等等。这个flag是做到了,目前为止可以运用kotlin开发Android原生应用。
- 继续在掘金平台输出文章,至少做到每周一更。年初原本想着是每周2-3更,但是目前看来是完全没做到,因为这2个月的个人时间全都扑在了“副业”上。
所以接下来,我会将这3个月的心路历程讲给大家听,如果大家想做副业,可以参考一下我的经历。
过年这段时间的思考
时间回退到去年过年的时候,我没记错的话应该是今年2月份是去年过年的时间点。我司是放了2周的假,那个时候我的工作年限是2年半(可以出道了,哈哈哈)。从大学到目前为止,心里一直有个声音告诉我:
我不可能通过打工来实现理想中的生活状态。原因如下:
1、房贷、车贷、孩子教育、大病、未知的风险,这些因素会导致生活质量非常脆弱。当然这也要因人而异,毕竟过的是否开心只有自己知道。
2、疫情后时代,裁员潮一浪更比一浪强,毕业人数越来越多,岗位越来越少,谋得一份自己满意的工作也趋近于“99%的运气 + 1%的努力”。
3、原来我努力学习技术是为了更好的打工,有人的地方就有江湖,你不能独善其身,因为并不是所有的人都喜欢双赢。
所以,年后在回北京的路上,我决定今年要尝试一下,不要把所有的时间全扑在技术上,可以考虑做一下“副业”。
如何做副业?
我个人比较喜欢看电影、电视剧、动漫、NBA。影视解说这个赛道可能就比较适合我,定了赛道以后,视频发到哪个平台呢?今年短视频平台特别多,抖音、快手、视频号、B站、西瓜、甚至是支付宝里都可以刷视频等等。在综合考虑之后,我选择了微信视频号。原因如下:
- 微信不缺流量,也不缺广告商赞助。
- 视频号绝对是某讯今年发力的重点,因为官方不止一次在公共场合里说明了视频号的重要性,而且春节晚会上也出现了视频号的赞助。
- 视频号上目前竞争不激烈(这句话只在2024年6月前生效),因为用的人还不是很多,所以这个阶段对视频质量的管控还不是很严,毕竟它要吸引用户进来,前期肯定不会管控太严,而且流量也会给的很足。
于是2024年2月18日,我的第一条影视作品在视频号上发布了。
这是我第一次剪辑影视作品,花了3天时间。当时的播放量就是200多,你现在看到710播放量是2月18日 - 6月16日的播放量。那个时候粉丝数量是1,对,没错,就是你们想的那样,那个1就是我自己,哈哈哈。当时剪辑第一条视频的时候,说实话完全是一边学习一边剪辑,而且那个时候我进入了一个误区,就是必须要剪辑的完美,所以前2天一直没有产出,这个时候我慌了,2天一点产出都没有,所以第3天的时候我告诉我自己,先把视频发出去,你又不是只发一条视频,后面的视频慢慢优化,不要想着一口吃个胖子。
前2条视频都是有关三国的视频,那个时候播放量都是200多,但是从第3条视频开始,播放量直接破万,有的能达到10w。为啥会有大的播放差距?后来我复盘了一下,是因为我当时正好踩中热点了,并且竞争不激烈。第三条视频以后,我开始剪辑《南来北往》这部影视剧,当时这部电视剧可以说是非常火,没看过的小伙伴强烈建议你去爱奇艺上观看一遍,真的超级好看。
因为周一到周五上班嘛,所以周六周天我会把下周要发的视频全都剪出来,一天至少按时发一个作品,就这样,差不多2周左右的时间吧,我的有效粉丝突破了100个。
视频号是分等级的,等级跟粉丝有关系,等级越高,视频的基础播放量就越高,能解锁的权益也会越来越多。
有效粉丝数量突破100个,这个阶段是比较难的,原因如下:
- 你要审视自己的作品质量。自己剪出来的东西是否有待提高等等。
- 你要确定细分赛道,影视解说都算是一个大概念,它可以再细分为 “影视解说”、“影视混剪”、“影视情感”。
- 一天分很多个时间段,每个时间段流量不一样,所以你要测出自己的作品在哪个时间段里,播放量比较高。
我原以为影视解说这条道路会这么顺的走下去,结果因为没有版权+播放量太高,被投诉侵权了。不是我吹,如果那个时候你在视频号里搜索“南来北往”,你看到的视频大部分都是我剪出来的。
说实话,我也是第一次见到这种阵仗,吓的我把所有关于“南来北往”的20多部作品连夜下架删除了。
在对比了其他平台后,我得出了如下结论:
- 平台之间是有合作的,这部影视剧在这个平台算侵权,但是在其他平台里就不算侵权。
- 二次创作的质量要高,要不然很容易就会被判违规,所以我已经完全转为纯影视解说。
- 还是要看平台规则,要尊重平台的规则。
影视号在视频号里如何赚钱?
相信这个话题是你们比较喜欢看的,根据我的历程,影视号在视频号里的变现途径有以下几个方向:
- 当你的作品播放量能够稳定突破10w的时候,或者每周的播放总量能够稳定突破50w的时候,会有很多人主动找你合作。
- 视频号里有视频变现任务,你可以主动去接一些变现任务,然后等待任务结束后结算。当然,这个只有当你的有效粉丝数量突破100的时候,你才有资格去接任务。
- 如果你的有效粉丝数量突破1000的时候,你可以挂商品链接,用户从你的链接点进去后,如果发生了交易,你会在中间赚一些分成,这个跟抖音的规则差不多。
- 如果你的有效粉丝突破了5000的时候,你的账号就可以解锁商单功能。就是将自己的报价放出去,如果广告主找你合作,那么他就要遵循你的视频报价。一条1分钟以下的视频报价是多少,一分钟以上的视频报价是多少等等。
当然我说的这几个方向,都必须要遵循平台的规则。所以前2年,在抖音上,80%的探店账号都赚到了钱,就是因为前几年,探店视频在抖音上是趋势,如果前几年你在抖音上发探店视频,能够做到基础的每日一更,你的流量一定不会差,而且你也一定能够接到商单。
最后
又到了该和大家说再见的时候啦,这3个月确实是学到了很多东西,这3个月的奋斗史也绝对不仅仅是视频号,在其他方面上我也有很多很深入的尝试。
以上内容绝对没有任何的引导,只是自己上半年的一个分享,如果你还想看更多的,有关我在其他方面的尝试,欢迎评论区里发言,嘿嘿,我们下期再见,拜拜~~
来源:juejin.cn/post/7380510171640446988
大龄程序员尝试了不一样的兼职体验
- 自我理解
我可能和大部分程序员是一样的,就是一个普普通通的程序员,没有远大理想,没有清晰的职业规划,踏踏实实的做好工作,平时做好技术积累。即使目前已经36了,但不善于交际,依然是一个最底层的程序员,每天依然是写代码。在一家很小的小公司里面干了十多年了,可能公司比较小,危机也没有大公司那么强烈,即使经历了三年疫情,一个小公司活下来也是奇迹。在35岁前,我也并没有什么工作危机感。但是随着过了35岁,感觉可能程序员并不能干一辈子,而且这个感觉越来越强烈(不知道为什么会有)。所以一直就想找找其他的职业方向,列如兼职什么的,先尝试一下,毕竟之前什么也没做过。
- 想法的迸发
自从有了这个想法,我就开始不断的去了解那些是可以做的。例如自媒体,自媒体的选材又很多,知识分享类的,感受分享类的,直播讲课类的等。总之想了很多,但由于自己表达能力不是很好。多次录视频尝试后,还是不行。不得以放弃掉。后来接触送外卖是从一个朋友那里了解的,他也是兼职送,每周末兼职。大概每天能赚个200到300左右。经过朋友的鼓励,我选择尝试一下。
- 开始准备工作
可能送外卖和大家理解的还不一样,不是有个电动车就能送的。得租一个或者买一个能够换电的电动车,能够随时换电,不用担心没电的情况。接单也不是注册app就能接,这个也得有方法的,下面讲。开始准备租车,租车方式很多,大概租车价格都差不多,基本在500到700之间,包含租车和租电。总之租好车和注册号APP,按照流程做好认证,把学习课程做完就可以正式跑单了。
- 跑单开始
开始跑单的时候是由朋友带着跑了一次,怎么接单,接派单,怎么取单,怎么送单。跑了一次后也就明白了整个流程。我就开始自己跑,由于自己刚开始跑,我就选择接一单跑一单(外卖大神都是一次接十多单)。但是跑的过程中也是好多问题,商家找不到了,客户楼栋找错了啊,路线导航错了,送错人了,车到一半没电了等等。好多问题,但是万幸的是到目前还没有超时过。
- 跑单时间和收入
最开始是周末跑的,后来感觉停不下来了,现在每天中午休息的时候跑一个半小时,晚上不跑好好休息,毕竟还要上班,主业还是很重要的,周末继续跑。现在收入大概平时中午能收入二三十左右。周末能到200左右了。截至目前已经连续跑了两周了。
- 跑单后的感受
第一个身体上的感受,感觉之前的身体太弱了,最开始非常累,现在已经习惯点了。感觉自己的精神和专注度比之前要提升很多。虽然累,但是第二天还是很精神的,最起码感觉比之前要有很大的提升。 第二个就是眼界上面,每天都能够接触不同行业的人,看到很多的事情,比之前接触的人要多得多,而且大多数人对送外卖的是非常友好的。第三个就是通过送外卖可以看清很多东西,列如有些外卖真的环境很差,而且点的人还超级多。每次我都想提醒下这些人下次不要点了。有的时候打包外卖的人很快,有的就非常慢(这大概也是摸鱼),通过别人看自己也能理解一些东西。
- 给大家的建议
虽然感觉大家不一定能够点进来看,但是万一你点进来,并且看到了这里。我希望给你的建议就是如果你找到一个兼职的方向,请马上行动起来,并且坚持,如果不行,就换一个,马上行动,再坚持。
也希望大家能够给我一些兼职方面的建议,我的眼界太局限了。可能有些行业我能够做好的,但是我并不知道的。在这里我谢谢大家。
来源:juejin.cn/post/7428785902640316451
Spring 实现 3 种异步流式接口,干掉接口超时烦恼
大家好,我是小富~
如何处理比较耗时的接口?
这题我熟,直接上异步接口,使用 Callable
、WebAsyncTask
和 DeferredResult
、CompletableFuture
等均可实现。
但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地向客户端响应处理结果,这些方法就不够看了。
Spring 框架提供了多种工具支持异步流式接口,如 ResponseBodyEmitter
、SseEmitter
和 StreamingResponseBody
。这些工具的用法简单,接口中直接返回相应的对象或泛型响应实体 ResponseEntity<xxxx>
,如此这些接口就是异步的,且执行耗时操作亦不会阻塞 Servlet
的请求线程,不影响系统的响应能力。
下面将逐一介绍每个工具的使用及其应用场景。
ResponseBodyEmitter
ResponseBodyEmitter
适应适合于需要动态生成内容并逐步发送给客户端的场景,例如:文件上传进度、实时日志等,可以在任务执行过程中逐步向客户端发送更新。
举个例子,经常用GPT你会发现当你提问后,得到的答案并不是一次性响应呈现的,而是逐步动态显示。这样做的好处是,让你感觉它在认真思考,交互体验比直接返回完整答案更为生动和自然。
使用ResponseBodyEmitter
来实现下这个效果,创建 ResponseBodyEmitter 发送器对象,模拟耗时操作逐步调用 send 方法发送消息。
注意:ResponseBodyEmitter 的超时时间,如果设置为
0
或-1
,则表示连接不会超时;如果不设置,到达默认的超时时间后连接会自动断开。其他两种工具也是同样的用法,后边不在赘述了
@GetMapping("/bodyEmitter")
public ResponseBodyEmitter handle() {
// 创建一个ResponseBodyEmitter,-1代表不超时
ResponseBodyEmitter emitter = new ResponseBodyEmitter(-1L);
// 异步执行耗时操作
CompletableFuture.runAsync(() -> {
try {
// 模拟耗时操作
for (int i = 0; i < 10000; i++) {
System.out.println("bodyEmitter " + i);
// 发送数据
emitter.send("bodyEmitter " + i + " @ " + new Date() + "\n");
Thread.sleep(2000);
}
// 完成
emitter.complete();
} catch (Exception e) {
// 发生异常时结束接口
emitter.completeWithError(e);
}
});
return emitter;
}
实现代码非常简单。通过模拟每2秒响应一次结果,请求接口时可以看到页面数据在动态生成。效果与 GPT 回答基本一致。
SseEmitter
SseEmitter
是 ResponseBodyEmitter
的一个子类,它同样能够实现动态内容生成,不过主要将它用在服务器向客户端推送实时数据,如实时消息推送、状态更新等场景。在我之前的一篇文章 我有 7种 实现web实时消息推送的方案 中详细介绍了 Server-Sent Events (SSE)
技术,感兴趣的可以回顾下。
SSE在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream
类型的数据流信息,在有数据变更时从服务器流式传输到客户端。
整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。
客户端JS实现,通过一次 HTTP 请求建立连接后,等待接收消息。此时,服务端为每个连接创建一个 SseEmitter
对象,通过这个通道向客户端发送消息。
<body>
<div id="content" style="text-align: center;">
<h1>SSE 接收服务端事件消息数据</h1>
<div id="message">等待连接...</div>
</div>
<script>
let source = null;
let userId = 7777
function setMessageInnerHTML(message) {
const messageDiv = document.getElementById("message");
const newParagraph = document.createElement("p");
newParagraph.textContent = message;
messageDiv.appendChild(newParagraph);
}
if (window.EventSource) {
// 建立连接
source = new EventSource('http://127.0.0.1:9033/subSseEmitter/'+userId);
setMessageInnerHTML("连接用户=" + userId);
/**
* 连接一旦建立,就会触发open事件
* 另一种写法:source.onopen = function (event) {}
*/
source.addEventListener('open', function (e) {
setMessageInnerHTML("建立连接。。。");
}, false);
/**
* 客户端收到服务器发来的数据
* 另一种写法:source.onmessage = function (event) {}
*/
source.addEventListener('message', function (e) {
setMessageInnerHTML(e.data);
});
} else {
setMessageInnerHTML("你的浏览器不支持SSE");
}
</script>
</body>
在服务端,我们将 SseEmitter
发送器对象进行持久化,以便在消息产生时直接取出对应的 SseEmitter 发送器,并调用 send
方法进行推送。
private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();
@GetMapping("/subSseEmitter/{userId}")
public SseEmitter sseEmitter(@PathVariable String userId) {
log.info("sseEmitter: {}", userId);
SseEmitter emitterTmp = new SseEmitter(-1L);
EMITTER_MAP.put(userId, emitterTmp);
CompletableFuture.runAsync(() -> {
try {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.data("sseEmitter" + userId + " @ " + LocalTime.now())
.id(String.valueOf(userId))
.name("sseEmitter");
emitterTmp.send(event);
} catch (Exception ex) {
emitterTmp.completeWithError(ex);
}
});
return emitterTmp;
}
@GetMapping("/sendSseMsg/{userId}")
public void sseEmitter(@PathVariable String userId, String msg) throws IOException {
SseEmitter sseEmitter = EMITTER_MAP.get(userId);
if (sseEmitter == null) {
return;
}
sseEmitter.send(msg);
}
接下来向 userId=7777
的用户发送消息,127.0.0.1:9033/sendSseMsg/7777?msg=欢迎关注-->程序员小富,该消息可以在页面上实时展示。
而且SSE有一点比较好,客户端与服务端一旦建立连接,即便服务端发生重启,也可以做到自动重连。
StreamingResponseBody
StreamingResponseBody
与其他响应处理方式略有不同,主要用于处理大数据量或持续数据流的传输,支持将数据直接写入OutputStream
。
例如,当我们需要下载一个超大文件时,使用 StreamingResponseBody 可以避免将文件数据一次性加载到内存中,而是持续不断的把文件流发送给客户端,从而解决下载大文件时常见的内存溢出问题。
接口实现直接返回 StreamingResponseBody 对象,将数据写入输出流并刷新,调用一次flush
就会向客户端写入一次数据。
@GetMapping("/streamingResponse")
public ResponseEntity<StreamingResponseBody> handleRbe() {
StreamingResponseBody stream = out -> {
String message = "streamingResponse";
for (int i = 0; i < 1000; i++) {
try {
out.write(((message + i) + "\r\n").getBytes());
out.write("\r\n".getBytes());
//调用一次flush就会像前端写入一次数据
out.flush();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(stream);
}
demo这里输出的是简单的文本流,如果是下载文件那么转换成文件流效果是一样的。
总结
这篇介绍三种实现异步流式接口的工具,算是 Spring 知识点的扫盲。使用起来比较简单,没有什么难点,但它们在实际业务中的应用场景还是很多的,通过这些工具,可以有效提高系统的性能和响应能力。
文中 Demo Github 地址:github.com/chengxy-nds…
来源:juejin.cn/post/7425399689825140786
为什么推荐用Redisson实现分布式锁,看完直呼好好好
开心一刻
一男人站在楼顶准备跳楼,楼下有个劝解员拿个喇叭准备劝解
劝解员:兄弟,别跳
跳楼人:我不想活了
劝解员:你想想你媳妇
跳楼人:媳妇跟人跑了
劝解员:你还有兄弟
跳楼人:就是跟我兄弟跑的
劝解员:你想想你家孩子
跳楼人:孩子是他俩的
劝解员:死吧,妈的,你活着也没啥意义了
写在前面
关于锁,相信大家都不陌生,一般我们用其在多线程环境中控制对共享资源的并发访问;单服务下,用 JDK 中的 synchronized
或 Lock
的实现类可实现对共享资源的并发访问,分布式服务下,JDK 中的锁就显得力不从心了,分布式锁也就应运而生了;分布式锁的实现方式有很多,常见的有如下几种
- 基于 MySQL,利用行级悲观锁(select ... for update)
- 基于 Redis,利用其 (setnx + expire) 或 set
- 基于 Zookeeper,利用其临时目录和事件回调机制
本文不讲这些,网上资料很多,感兴趣的小伙伴自行去查阅;本文的重点是基于 Redis 的 Redisson,从源码的角度来看看为什么推荐用 Redisson 来实现分布式锁;推荐大家先去看看
搞懂了 Redis 的订阅、发布与Lua,Redisson的加锁原理就好理解了
有助于理解后文
分布式锁特点
可以类比 JDK 中的锁
- 互斥
不仅要保证同个服务中不同线程的互斥,还需要保证不同服务间、不同线程的互斥;如何处理互斥,是自旋、还是阻塞 ,还是其他 ?
- 超时
锁超时设置,防止程序异常奔溃而导致锁一直存在,后续同把锁一直加不上
- 续期
程序具体执行的时长无法确定,所以过期时间只能是个估值,那么就不能保证程序在过期时间内百分百能运行完,所以需要进行锁续期,保证业务是在加锁的情况下完成的
- 可重入
可重入锁又名递归锁,是指同一个线程在外层方法已经获得锁,再进入该线程的中层或内层方法会自动获取锁;简单点来说,就是同个线程可以反复获取同一把锁
- 专一释放
通俗点来讲:谁加的锁就只有它能释放这把锁;为什么会出现这种错乱释放的问题了,举个例子就理解了
线程 T1 对资源 lock_zhangsan 加了锁,由于某些原因,业务还未执行完,锁已经过期自动释放了,此时线程 T2 对资源 lock_zhangsan 加锁成功,T2 还在执行业务的过程中,T1 业务执行完后释放资源 lock_zhangsan 的锁,结果把 T2 加的锁给释放了
- 公平与非公平
公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证了队列中的第一个先得到锁
非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时直接去尝试获取锁
JDK 中的 ReentrantLock 就有公平和非公平两种实现,有兴趣的可以去看看它的源码;多数情况下用的是非公平锁,但有些特殊情况下需要用公平锁
你们可能会有这样的疑问
引入一个简单的分布式锁而已,有必要考虑这么多吗?
虽然绝大部分情况下,我们的程序都是在跑正常流程,但不能保证异常情况 100% 跑不到,出于健壮性考虑,异常情况都需要考虑到;下面我们就来看看 Redisson 是如何实现这些特点的
Redisson实现分布式锁
关于 Redisson
,更多详细信息可查看官方文档,它提供了非常丰富的功能,分布式锁 只是其中之一;我们基于 Redisson 3.13.6
,来看看分布式锁的实现
- 先将 Redis 信息配置给 Redisson,创建出 RedissonClient 实例
Redis 的部署方式不同,Redisson 配置模式也会不同,详细信息可查看:Configuration,我们就以最简单的
Single mode
来配置
@Before
public void before() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.110:6379");
redissonClient = Redisson.create(config);
}
- 通过 RedissonClient 实例获取锁
RedissonClient 实例创建出来后,就可以通过它来获取锁
/**
* 多线程
* @throws Exception
*/
@Test
public void multiLock() throws Exception {
RLock testLock = redissonClient.getLock("multi_lock");
int count = 5;
CountDownLatch latch = new CountDownLatch(count);
for (int i=1; i<=count; i++) {
new Thread(() -> {
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 尝试获取锁");
testLock.lock();
System.out.println(String.format("线程 %s 获取到锁, 执行业务中...", Thread.currentThread().getName()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("线程 %s 业务执行完成", Thread.currentThread().getName()));
latch.countDown();
} finally {
testLock.unlock();
System.out.println(String.format("线程 %s 释放锁完成", Thread.currentThread().getName()));
}
}, "t" + i).start();
}
latch.await();
System.out.println("结束");
}
完整示例代码:redisson-demo
用 Redisson 实现分布式锁就是这么简单,但光会使用肯定是不够的,我们还得知道其底层实现原理
知其然,并知其所以然!
那如何知道其原理呢?当然是看其源码实现
客户端创建
客服端的创建过程中,会生成一个 id
作为唯一标识,用以区分分布式下不同节点中的客户端
id 值就是一个 UUID,客户端启动时生成;至于这个 id 有什么用,大家暂且在脑中留下这个疑问,我们接着往下看
锁获取
我们从 lock
开始跟源码
最终会来到有三个参数的 lock 方法
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
// 锁被其他线程占用而获取失败,使用redis的发布订阅功能来等待锁的释放通知,而非自旋监测锁的释放
RFuture<RedissonLockEntry> future = subscribe(threadId);
// 当前线程会阻塞,直到锁被释放时当前线程被唤醒(有超时等待,默认 7.5s,而不会一直等待)
// 持有锁的线程释放锁之后,redis会发布消息,所有等待该锁的线程都会被唤醒,包括当前线程
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
while (true) {
// 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (ttl >= 0) {
try {
// future.getNow().getLatch() 返回的是 Semaphore 对象,其初始许可证为 0,以此来控制线程获取锁的顺序
// 通过 Semaphore 控制当前服务节点竞争锁的线程数量
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
// 退出锁竞争(锁获取成功或者放弃获取锁),则取消锁的释放订阅
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
主要三个点:尝试获取锁
、订阅
、取消订阅
- 尝试获取锁
尝试获取锁主要做了两件事:1、尝试获取锁,2、锁续期;尝试获取锁主要涉及到一段 Lua 代码
结合 搞懂了 Redis 的订阅、发布与Lua,Redisson的加锁原理就好理解了 来看这段 Lua 脚本,还是很好理解的
- 用 exists 判断 key 不存在,则用 hash 结构来存放锁,key = 资源名,field = uuid + : + threadId,value 自增 1;设置锁的过期时间(默认是 lockWatchdogTimeout = 30 * 1000 毫秒),并返回 nil
- 用 hexists 判断 field = uuid + : + threadId 存在,则该 field 的 value 自增 1,并重置过期时间,最后返回 nil
这里相当于实现了锁的重入
- 上面两种情况都不满足,则说明锁被其他线程占用了,直接返回锁的过期时间
给你们提个问题
为什么 field = uuid + : + threadId,而不是 field = threadId
友情提示:从多个服务(也就是多个 Redisson 客户端)来考虑
这个问题想清楚了,那么前面提到的:在 Redisson 客户端创建的过程中生成的 id(一个随机的 uuid 值),它的作用也就清楚了
尝试获取锁成功之后,会启动一个定时任务(即
WatchDog
,亦称看门狗
)实现锁续期,也涉及到一段 Lua 脚本
这段脚本很简单,相信你们都能看懂
默认情况下,锁的过期时间是 30s,锁获取成功之后每隔 10s 进行一次锁续期,重置过期时间成 30s
若锁已经被释放了,则定时任务也会停止,不会再续期
- 订阅
获取锁的过程中,尝试获取锁失败(锁被其他线程锁占有),则会完成对该锁频道的订阅,订阅过程中线程会阻塞;持有锁的线程释放锁时会向锁频道发布消息,订阅了该锁频道的线程会被唤醒,继续去获取锁,
给你们提个问题
如果持有锁的线程意外停止了,未向锁频道发布消息,那订阅了锁频道的线程该如何唤醒
Redisson 其实已经考虑到了,提供了超时机制来处理
默认超时时长 = 3000 + 1500 * 3 = 7500 毫秒
再给你们提个问题
为什么要用 Redis 的发布订阅
如果我们不用 Redis 的发布订阅,我们该如何实现,自旋?自旋有什么缺点? 自旋频率难以掌控,太高会增大 CPU 的负担,太低会不及时(锁都释放半天了才检测到);可以类比
生产者与消费者
来考虑这个问题 - 取消订阅
有订阅,肯定就有取消订阅;当阻塞的线程被唤醒并获取到锁时需要取消对锁频道的订阅,当然,取消获取锁的线程也需要取消对锁频道的订阅
比较好理解,就是取消当前线程对锁频道的订阅
锁释放
我们从 unlock
开始
代码比较简单,我们继续往下跟
主要有两点:释放锁
和 取消续期定时任务
- 释放锁
重点在于一个 Lua 脚本
我们把参数具象化,脚本就好理解了
KEYS[1] = 锁资源,KEYS[2] = 锁频道
ARGV[1] = 锁频道消息类型,ARGV[2] = 过期时间,ARGV[3] = uuid + : + threadId
- 如果当前线程未持有锁,直接返回 nil
- hash 结构的 field 的 value 自减 1,counter = 自减后的 value 值
如果 counter > 0,表示线程重入了,重置锁的过期时间,返回 0
如果 counter <= 0,删除锁,并对锁频道发布锁释放消息(频道订阅者则可收到消息,然后唤醒线程去获取锁),返回 1
- 上面 1、2 都不满足,则直接返回 nil
两个细节:1、重入锁的释放,2、锁彻底释放后的消息发布
- 取消续期定时任务
比较简单,没什么好说的
总结
我们从分布式锁的特点出发,来总结下 Redisson 是如何实现这些特点的
- 互斥
Redisson 采用 hash 结构来存锁资源,通过 Lua 脚本对锁资源进行操作,保证线程之间的互斥;互斥之后,未获取到锁的线程会订阅锁频道,然后进入一定时长的阻塞
- 超时
有超时设置,给 hash 结构的 key 加上过期时间,默认是 30s
- 续期
线程获取到锁之后会开启一个定时任务(watchdog 即 看门狗),每隔一定时间(默认 10s)重置 key 的过期时间
- 可重入
通过 hash 结构解决,key 是锁资源,field(值:uuid + : + threadId) 是持有锁的线程,value 表示重入次数
- 专一释放
通过 hash 结构解决,field 中存放了线程信息,释放的时候就能够知道是不是当前线程加上的锁,是才能够进行锁释放
- 公平与非公平
由你们在评论区补充
- 互斥
来源:juejin.cn/post/7425786548061683727