注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

深度复刻小米AI官网交互动画

web
近日在使用小米AI大模型MIMO时,被其顶部的透视跟随动画深深吸引,移步官网( mimo.xiaomi.com/zh/ ) 效果演示 1. 交互梳理 初始状态底部有浅色水印,且水印奇数行和偶数行有错位 初始状态中间文字为黑色的汉字 鼠标移入后,会在以鼠标为...
继续阅读 »

近日在使用小米AI大模型MIMO时,被其顶部的透视跟随动画深深吸引,移步官网( mimo.xiaomi.com/zh/


效果演示


效果图.gif


1. 交互梳理



  1. 初始状态底部有浅色水印,且水印奇数行和偶数行有错位

  2. 初始状态中间文字为黑色的汉字

  3. 鼠标移入后,会在以鼠标为中心形成一个黑色圆形,黑色圆中有第二种背景水印,且水印依旧奇数行和偶数行有错位

  4. 鼠标移动到中间汉字部分,会有白色英文显示

  5. 鼠标迅速移动时,会根据鼠标移动轨迹有一个拉伸椭圆跟随,然后恢复成圆形的动画效果


现在基于这个交互的拆解,逐步来复刻交互效果


2. 组件结构与DOM设计


2.1 模板结构


采用「静态底层+动态上层」的双层视觉结构,通过CSS绝对定位实现图层叠加,既保证初始状态的视觉完整性,又能让交互效果精准作用于上层,不干扰底层基础展示。两层分工明确,具体如下:


图层类名内容功能
底层.z-1中文标题 "你好,世界!" 和灰色 "HELLO" 文字矩阵静态背景展示
上层.z-2英文标题 "Hello , World!" 和白色 "HELLO" 文字矩阵鼠标交互时的动态效果层

2.2 核心 DOM 结构


<div class="container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousemove="onMouseMove">
<!-- 底层内容 -->
<div class="z-1">
<div class="line" v-for="line in 13">
<span class="line-item" v-for="item in 13">HELLO</span>
</div>
</div>
<h1 class="title-1">你好,世界!</h1>

<!-- 上层交互内容 -->
<div class="z-2" :style="{ 'clip-path': circleClipPath }">
<div class="hidden-div">
<div class="line" v-for="line in 13">
<span class="line-item" v-for="item in 13">HELLO</span>
</div>
</div>
<h1 class="title-2">Hello , World!</h1>
</div>
</div>


关键说明:hidden-div用于包裹上层文字矩阵,配合.z-2的定位规则,确保遮罩效果精准覆盖;两层文字矩阵尺寸一致,保证视觉对齐,增强透视沉浸感。



3. 技术实现


3.1 核心功能模块


3.1.1 轨迹点系统


轨迹点系统是实现平滑鼠标跟随效果的核心,通过维护6个轨迹点的位置信息,创建出具有弹性延迟的跟随动画。


// 轨迹点系统 
const trailSystem = ref({
targetX: 0,
targetY: 0,
trailPoints: Array(6).fill(null).map(() => ({ x: 0, y: 0 })),
animationId: 0,
isInside: false
});



设计思路:6个轨迹点是兼顾流畅度与性能的平衡值——点太少则拖尾效果不明显,点太多则增加计算开销,配合递减阻尼系数,实现“头快尾慢”的自然跟随。



3.1.2 动态 Clip-Path 计算


通过计算鼠标位置和轨迹点的关系,动态生成 clip-path CSS 属性值,实现跟随鼠标的圆形/椭圆形遮罩效果。


// 计算clip-path值
const circleClipPath = computed(() => {
if (!showCircle.value) {
return 'circle(0px at -300px -300px)'; // 完全隐藏状态
}

// 复制轨迹系统数据进行计算
const system = JSON.parse(JSON.stringify(trailSystem.value));

// 更新轨迹点
for (let t = 0; t < 6; t++) {
const prevX = t === 0 ? system.targetX : system.trailPoints[t - 1].x;
const prevY = t === 0 ? system.targetY : system.trailPoints[t - 1].y;
const damping = 0.7 - 0.04 * t; // 阻尼系数,后面的点移动更慢

const deltaX = prevX - system.trailPoints[t].x;
const deltaY = prevY - system.trailPoints[t].y;

// 平滑插值
system.trailPoints[t].x += deltaX * damping;
system.trailPoints[t].y += deltaY * damping;
}

// 获取第一个点(头部)和最后一个点(尾部)
const head = system.trailPoints[0];
const tail = system.trailPoints[5];

const diffX = head.x - tail.x;
const diffY = head.y - tail.y;
const distance = Math.sqrt(diffX * diffX + diffY * diffY);

let clipPathValue = '';

if (distance < 10) { // 如果距离很近,显示圆形
clipPathValue = `circle(200px at ${head.x}px ${head.y}px)`;
} else {
// 创建椭圆形的polygon,连接头尾两点
const angle = Math.atan2(diffY, diffX); // 连接角度
const points = [];

// 从头部开始,画半个椭圆
for (let i = 0; i <= 30; i++) {
const theta = angle - Math.PI / 2 + Math.PI * i / 30;
const x = head.x + 200 * Math.cos(theta);
const y = head.y + 200 * Math.sin(theta);
points.push(`${x}px ${y}px`);
}

// 从尾部开始,画另半个椭圆
for (let i = 0; i <= 30; i++) {
const theta = angle + Math.PI / 2 + Math.PI * i / 30;
const x = tail.x + 200 * Math.cos(theta);
const y = tail.y + 200 * Math.sin(theta);
points.push(`${x}px ${y}px`);
}

clipPathValue = `polygon(${points.join(', ')})`;
}

return clipPathValue;
});


3.1.3 鼠标事件处理


实现了完整的鼠标交互逻辑,包括鼠标进入、离开和移动时的状态管理和动画控制。


事件处理函数功能
mouseenteronMouseEnter激活交互效果,初始化轨迹点
mouseleaveonMouseLeave停用交互效果,重置轨迹点
mousemoveonMouseMove更新目标点位置,驱动动画

4. 技术亮点


4.1 轨迹点系统算法


核心原理:使用6个轨迹点,每个点跟随前一个点移动,并应用不同的阻尼系数,实现平滑的拖尾效果。


技术优势



  • 实现了自然的物理运动效果,比简单的线性跟随更具视觉吸引力

  • 通过阻尼系数的递减,创建出层次感和深度感

  • 算法复杂度低,性能消耗小,适合实时交互场景


4.2 动态 Clip-Path 技术


核心原理:利用CSS clip-path属性的动态特性,结合轨迹点位置计算,实时生成不规则遮罩,替代Canvas/SVG的图形绘制方案,用更轻量化的方式实现复杂视觉效果。


技术优势



  • 无依赖轻量化:无需引入任何图形库,纯CSS+JS即可实现,减少项目依赖体积,降低集成成本

  • 平滑过渡无卡顿:通过数值插值计算,实现圆形与椭圆形遮罩的无缝切换,无帧断裂感,视觉连贯性强

  • 渲染性能优化:配合 will-change: clip-path 提示浏览器,提前分配渲染资源,减少重排重绘,提升动画流畅度


5. 性能优化



  1. 渲染性能



    • 使用 will-change: clip-path 提示浏览器优化渲染

    • 合理使用 Vue 的响应式系统,避免不必要的重计算



  2. 事件处理



    • 仅在鼠标在容器内时更新目标点位置,减少计算量

    • 鼠标离开时停止动画,释放资源



  3. 动画性能



    • 使用 requestAnimationFrame 实现流畅的动画效果

    • 鼠标离开时取消动画帧请求,避免内存泄漏




6. 总结与扩展


本次复刻的小米MiMo透视动画,核心价值在于“用简单技术组合实现高级视觉效果”——无需复杂图形库,仅依托Vue3响应式能力与CSS clip-path属性,就能打造出兼具质感与性能的交互组件。其核心亮点可概括为三点:



  • 交互创新:轨迹点系统与动态clip-path结合,打破传统静态标题的交互边界,带来自然流畅的鼠标跟随体验

  • 视觉精致:双层文字矩阵的分层设计,配合遮罩形变,营造出兼具深度感与品牌性的视觉效果

  • 性能可控:轻量化技术方案+多维度优化策略,在保证视觉效果的同时,兼顾页面性能与可维护性


扩展方向


该组件的实现思路可灵活迁移至其他场景:



  • 弹窗过渡动画:将clip-path遮罩用于弹窗进入/退出效果,实现不规则形状的过渡动画。

  • 滚动动效:结合滚动事件替换鼠标事件,实现页面滚动时的元素透视跟随效果。

  • 移动端适配:增加触摸事件支持,将鼠标交互替换为触摸滑动,适配移动端场景。


完整代码


<template>
<div class="hero-container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousemove="onMouseMove">
<div class="z-1">
<div class="line" v-for="line in 13">
<span class="line-item" v-for="item in 13">HELLO</span>
</div>
</div>
<h1 class="title-1">你好,世界</h1>

<!-- 第二个div,鼠标移入后需要显示的内容,通过clip-path:circle(0px at -300px -300px)达到隐藏效果 -->
<div class="z-2" :style="{ 'clip-path': circleClipPath }">
<div class="hidden-div">
<div class="line" v-for="line in 13">
<span class="line-item" v-for="item in 13">HELLO</span>
</div>
</div>
<h1 class="title-2">HELLO , World</h1>
</div>
</div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

const showCircle = ref(false)
const containerRef = ref(null)

const trailSystem = ref({
targetX: 0,
targetY: 0,
trailPoints: Array(6)
.fill(null)
.map(() => ({ x: 0, y: 0 })),
animationId: 0,
isInside: false,
})

const circleClipPath = computed(() => {
if (!showCircle.value) {
return 'circle(0px at -300px -300px)'
}

// 复制轨迹系统数据进行计算
const system = JSON.parse(JSON.stringify(trailSystem.value))

// 更新轨迹点
for (let t = 0; t < 6; t++) {
const prevX = t === 0 ? system.targetX : system.trailPoints[t - 1].x
const prevY = t === 0 ? system.targetY : system.trailPoints[t - 1].y
const damping = 0.7 - 0.04 * t // 阻尼系数,后面的点移动更慢

const deltaX = prevX - system.trailPoints[t].x
const deltaY = prevY - system.trailPoints[t].y

// 平滑插值
system.trailPoints[t].x += deltaX * damping
system.trailPoints[t].y += deltaY * damping
}

// 获取第一个点(头部)和最后一个点(尾部)
const head = system.trailPoints[0]
const tail = system.trailPoints[5]

const diffX = head.x - tail.x
const diffY = head.y - tail.y
const distance = Math.sqrt(diffX * diffX + diffY * diffY)

let clipPathValue = ''

if (distance < 10) {
// 如果距离很近,显示圆形
clipPathValue = `circle(200px at ${head.x}px ${head.y}px)`
} else {
// 创建椭圆形的polygon,连接头尾两点
const angle = Math.atan2(diffY, diffX) // 连接角度
const points = []

// 从头部开始,画半个椭圆
for (let i = 0; i <= 30; i++) {
const theta = angle - Math.PI / 2 + (Math.PI * i) / 30
const x = head.x + 200 * Math.cos(theta)
const y = head.y + 200 * Math.sin(theta)
points.push(`${x}px ${y}px`)
}

// 从尾部开始,画另半个椭圆
for (let i = 0; i <= 30; i++) {
const theta = angle + Math.PI / 2 + (Math.PI * i) / 30
const x = tail.x + 200 * Math.cos(theta)
const y = tail.y + 200 * Math.sin(theta)
points.push(`${x}px ${y}px`)
}

clipPathValue = `polygon(${points.join(', ')})`
}

return clipPathValue
})

// 动画循环函数
const animate = () => {
if (showCircle.value) {
// 更新轨迹点
for (let t = 0; t < 6; t++) {
const prevX = t === 0 ? trailSystem.value.targetX : trailSystem.value.trailPoints[t - 1].x
const prevY = t === 0 ? trailSystem.value.targetY : trailSystem.value.trailPoints[t - 1].y
const damping = 0.7 - 0.04 * t // 阻尼系数,后面的点移动更慢

const deltaX = prevX - trailSystem.value.trailPoints[t].x
const deltaY = prevY - trailSystem.value.trailPoints[t].y

// 平滑插值
trailSystem.value.trailPoints[t].x += deltaX * damping
trailSystem.value.trailPoints[t].y += deltaY * damping
}

// 请求下一帧
trailSystem.value.animationId = requestAnimationFrame(animate)
}
}

const onMouseEnter = (event) => {
const container = event.currentTarget
const rect = container.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top

showCircle.value = true

// 初始化目标位置和轨迹点
trailSystem.value.targetX = x
trailSystem.value.targetY = y
trailSystem.value.isInside = true

// 初始化所有轨迹点到当前位置
for (let i = 0; i < 6; i++) {
trailSystem.value.trailPoints[i] = { x, y }
}

// 开始动画
if (!trailSystem.value.animationId) {
trailSystem.value.animationId = requestAnimationFrame(animate)
}
}

const onMouseLeave = (event) => {
const container = event.currentTarget
const rect = container.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top

showCircle.value = false
trailSystem.value.isInside = false

// 将目标点移出容器边界,使轨迹点逐渐拉回
let targetX = x
let targetY = y

if (x <= 0) targetX = -400
else if (x >= rect.width) targetX = rect.width + 400

if (y <= 0) targetY = -400
else if (y >= rect.height) targetY = rect.height + 400

trailSystem.value.targetX = targetX
trailSystem.value.targetY = targetY

// 停止动画
if (trailSystem.value.animationId) {
cancelAnimationFrame(trailSystem.value.animationId)
trailSystem.value.animationId = 0
}
}

const onMouseMove = (event) => {
if (showCircle.value) {
const container = event.currentTarget
const rect = container.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top

trailSystem.value.targetX = x
trailSystem.value.targetY = y
}
}
</script>

<style scoped>
.hero-container {
cursor: crosshair;
background: #faf7f5;
border-bottom: 1px solid #000;
justify-content: center;
align-items: center;
width: 100%;
height: 500px;
display: flex;
position: relative;
overflow: hidden;
}

.z-1 {
pointer-events: auto;
-webkit-user-select: none;
user-select: none;
flex-direction: column;
justify-content: flex-start;
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}

.z-1 .line {
display: flex;
align-items: center;
white-space: nowrap;
color: #0000000d;
letter-spacing: 0.3em;
flex-wrap: nowrap;
font-size: 52px;
font-weight: 700;
line-height: 1.6;
display: flex;
}

.z-1 .line-item {
cursor: default;
flex-shrink: 0;
margin-right: 0.6em;
transition:
color 0.3s,
text-shadow 0.3s;
font-family: inherit !important;
}

.z-1 .line:nth-child(odd) {
margin-left: -2em;
background-color: rgb(245, 235, 228);
}

.title-1 {
z-index: 1;
color: #000;
letter-spacing: 0.02em;
text-align: center;
margin: 0;
font-size: 72px;
font-weight: 700;
}

.z-2 {
pointer-events: none;
z-index: 10;
will-change: clip-path;
background: #000;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0;
left: 0;
}

.z-2 .hidden-div {
pointer-events: none;
-webkit-user-select: none;
user-select: none;
flex-direction: column;
justify-content: flex-start;
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}

.z-2 .hidden-div .line {
white-space: nowrap;
color: #ffffff1f;
letter-spacing: 0.3em;
flex-wrap: nowrap;
font-size: 32px;
font-weight: 700;
line-height: 1.6;
display: flex;
}

.z-2 .hidden-div .line:nth-child(odd) {
margin-left: -0.5em;
}

.title-2 {
font-size: 72px;
color: #fff;
letter-spacing: 0.02em;
text-align: center;
white-space: nowrap;
margin: 0;
font-size: 72px;
font-weight: 700;
}
</style>



小米的前端一直很牛,非常有创意,我也通过F12学习源码体会到了新的思路,希望大家也多多关注小米和小米的技术~



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

Tailwind CSS都更新到4.0了,你还在抵触吗?

web
Tailwind CSS的体量Tailwind CSS有多火爆呢?几组数据告诉你?一组数据告诉你 Tailwind CSS 有多受欢迎:github 86.1K的 Star, 足以证明它的受欢迎程度。NPM 周下载量已突破 1000 万, 前端开发者的不二之选...
继续阅读 »

Tailwind CSS的体量

Tailwind CSS有多火爆呢?

几组数据告诉你?

image.png

image.png

一组数据告诉你 Tailwind CSS 有多受欢迎:

  1. github 86.1K的 Star, 足以证明它的受欢迎程度。
  2. NPM 周下载量已突破 1000 万, 前端开发者的不二之选
  3. 被无数大公司采用,如 GitHub、Vercel、Laravel 等。
  4. 被很多框架和打包工具推荐,如Vite,Nuxt,React等

从数据上看,Tailwind CSS 已经成为前端开发的主流选择之一

原子化CSS

什么是原子化CSS

原子化 CSS 是一种 CSS 架构,它提倡使用高度可复用的小类名,每个类名通常只控制单一的样式属性。例如:

<div class="text-red-500 font-bold p-4 text-[14px]">Hello Tailwinddiv>

其中:

  • text-red-500: 代表文字颜色
  • font-bold: 代表加粗
  • p-4: 代表内边距
  • text-[14px]: 代表字体大小为14px

这种方式避免了传统 CSS 中复杂的层叠规则,让样式控制更加直观。

原子化CSS和传统CSS的区别

image.png

说了这么一通,我相信用过的都说“真香”,用过一次后就离不开了。

关键是没用过的呢?是不是心里还在嘀咕。别急,正餐来了!

Tailwind CSS宝藏库

为什么说Tailwind CSS是一个宝藏,因为你担忧和抵触的地方,Tailwind CSS都给你解决了

类名难记

你可能在担忧,我是不是每次用都要查文档呢?那么多css好不容易记住了,现在又让我再学一遍?

答案是:不用。完全和你使用css一样简单。只需要记住几个关键字,智能提示帮你搞定

image.png

VS Code插件 - Tailwind CSS IntelliSense

HTML又长又乱

首先,不可否认,将所有的类名整合到html中,会让你的html变得比较长。但是,当你写的代码又长又乱的时候,你就要停下来想想

  1. 是否违背了创作者的初衷
  2. 架构是否设计不合理

为此,我们简单分析一下,到底是人的问题还是工具的问题。根据以上2点,分析一下你的HTML为什么又长又乱?

  • 因为太长导致太乱

    没有合并之前,你的代码可能是这样的

    class="flex justify-center items-center">
    clsss="bg-blue-500 text-white py-2 px-4 rounded">提交

    合并后是这样的

    .flex-center {
    @apply flex justify-center items-center;
    }
    .btn-submit {
    @apply bg-blue-500 text-white py-2 px-4 rounded;
    }

    class="flex-center">
    clsss="btn-submit">提交

    现在是不是很清晰了呢?

    我敢说,只要你使用@apply合并类名,时刻记着复用样式,你的HMLT至少减少1/3,甚至也可以写出像诗一样的代码

  • 因为没有分组和顺序性导致太乱

    没有顺序和分组的书写,是这样的

    <div class="p-2 font-bold text-[14px] mt-4 color-[#333333] bg-white">Hello world!div>

    想到哪写到哪,会让你的代码一眼望上去比较乱,时间长了,一眼看上去很难维护...

    下面我们就着手解决这2个问题

    1. 类排序

    使用 Prettier 进行类排序(Class sorting with Prettier)

    Tailwind CSS 维护了一个官方 Prettier 插件,它会自动按照我们的 推荐的类顺序 对你的类进行排序。

    使用插件后,代码这样的

    <div class="bg-white color-[#333333] mt-4 p-2 text-[14px] font-bold">Hello world!div>

    现在是不是清晰很多了呢?

    不过还不够

    1. 分组

我们根据样式进行的类别分组,比如颜色,字体,定位,间距等等,每个类别一行,这样你写出的代码会清晰无比

<div class="
bg-white
color-[#333333]
mt-
4 p-2
text-
[14px] font-bold">
Hello world!div>

现在代码是不是清晰的多了

全局类名

不用担心公共类的问题,@apply帮你搞定。

使用 @apply 合并类,前面已经讲过了,就不展开了

样式冲突

也许你还在担心tailwind 的 class 名和我已有的 class 冲突了咋办?我怎么处理兼容问题

别担心,给你的类名加个前缀prefix就搞定了

@import "tailwindcss" prefix(tw);

<div class="tw:flex tw:bg-red-500 tw:hover:bg-red-600"> div>

拥抱Tailwind CSS

Tailwind CSS为什么受到追捧

  1. 再也不用忍受css上下切换的痛苦了
  2. 再也不用花时间去取语义化类名了

    不用纠结container, wrapper, box等被使用的问题后,如何起名的问题了

  3. 为了加权重,不断的加父级类名,甚至!important,永远不知道哪个样式起作用了。冗长的css让项目很难维护!
  4. 简单完成伪类、伪元素、媒体查询等变体的书写

image.png

Tailwind CSS、PrimeFlex、UnoCSS评测

在CSS工具类框架中,除了Tailwind CSS之外,还有其他很多工具类。如PrimeFlex和UnoCSS,它们各有特点,下面我简单的评测一下

  • PrimeFlex: 生态系统较小,多适用于Prime生态,如PrimevVue,PrimeReact。样式和较Tailwind CSS低。只能构建起简单样式框架。最让我吐槽的是,样式竟然用!important。你想替换某个属性,麻烦程度想骂人!

image.png

  • UnoCSS: 未构建良好的生态系统,多用于自定义规则和项目优化

总结

TailwindCSS 已经成为前端开发的趋势之一,随着4.0 版本的发布,它的性能更强大、使用更方便。如果你还在抵触,不妨试试看,它可能会彻底改变你的 CSS 编写方式!


作者:高志小鹏鹏
来源:juejin.cn/post/7480734875723415552

收起阅读 »

为什么没人走后门当程序员?

最近刷 X 乎时看到这样一个耐人寻味的的讨论话题,浏览量超 170w,参与讨论的同学也好多。 问题描述是这样的: “为什么没人走后门当程序员?” 我认真浏览了一圈,心里五味杂陈。 在许多人眼中,程序员是一个高薪的职业。然而,即便程序员们拿着如此令人羡慕的高薪...
继续阅读 »

最近刷 X 乎时看到这样一个耐人寻味的的讨论话题,浏览量超 170w,参与讨论的同学也好多。


问题描述是这样的:


“为什么没人走后门当程序员?”



我认真浏览了一圈,心里五味杂陈。


在许多人眼中,程序员是一个高薪的职业。然而,即便程序员们拿着如此令人羡慕的高薪,尽管互联网行业如此火热,但却几乎很少听说有人说走后门想进去。


其实这事情一点也不难理解,这得先从程序员工作的本质说起。


因为程序员这个职业,从根子上来说压根就不靠后门吃饭。


而且程序员这行,恰恰是最混不了日子的,它要求你持续学习,跟上技术迭代,解决一个个具体而棘手的问题。


编程是一个实实在在的技术活,当你的代码运行不起来,它就是运行不起来,你写的系统有漏洞,它就会在某个深夜悄然崩溃,这种刚性特质就决定了程序员这个岗位无法容忍滥竽充数者。


而程序员的门槛,是技术,是能力,走后门也写不出一行能跑通的代码。


退一步说,哪怕就算你真靠后门挤进了公司,项目一上来,分分钟就会露馅。


那些想走后门的人,大概率是想找一个稳当、轻松、有人脉资源的工作。但反思程序员这行,是这样吗?好……好像哪个也不沾边吧……


所以没人走后门干程序员,不是因为这行没前途,而是因为它太实在、太透明、太难伪装。


这是一份必须用真本事去交换的职业,关系在这里,价值被迅速稀释到近乎为零。


另外大家往往有种误解或者说错觉,总觉得程序员赚得多就是香,而实际却忽略了这个高薪背后所付出的代价,这一切都是来源于高强度脑力劳动和长时间脑力付出所带来的回报。


再者,互联网行业的本质是工程化与扁平化。在这个体系里,你是谁、认识谁、从哪来,其实并不太重要,没人会关注你这个,英雄不问出处。


重要的是,你能不能解决问题,能不能为项目创造价值。


所以,当我们回过头来再看,为什么没人走后门干程序员这个问题,其实本身就蕴含着一种误解。它预设了程序员是一个好差事,一个可以让人躺着赚钱的美差。


但事实上,程序员是一份需要真才实学、持续奋斗、直面挑战的工作。你付出多少努力,掌握多少技能,最终都会在你的代码和收入上得到真实的反馈。


当然,这里还有一点需要反思的是:


该说不说,程序员行业的这种去关系化特质,其实某一角度来说也带来了一些副产品。


比方说,技术至上的工作文化有时会导致个体沟通能力的忽视,对硬技能的过度强调可能让软技能的发展有所滞后,另外代码世界的非黑即白有时候也会让人忽略了现实世界的复杂灰度。


这些其实都是程序员文化中值得反思和平衡的地方。


有一说一,其实很多代码之外的东西对现如今的生存也很重要,因为思维如果不开阔出来的话,路可能就会越走越窄了。


其实很多程序员在年龄大了之后越来越焦虑的一个重要原因就是因为生存技能太过单一了,所以千万不要给自己设限,不要把目光仅仅聚集在自己的一亩三分地上,还是要多培养一些其他方面的一些软实力,会很有帮助。


不知道大家有没有看过《软技能》那两本书,讲的就是代码之外的一些软技能和经验,里面提到了很多有关职场的分析,自我提高的一些路径,个人的持续学习和成长,甚至包括像理财、健身、时间管理、心态调整等等。


有意识地去关注这方面东西的原因在于可以帮助自己把思维给开阔出来,毕竟很多时候有必要跳出来看问题,这时候这些软技能往往就能发挥作用了。


另外,程序员作为一个有个性的创造性群体要专注精进技术这本身没错,但是职场毕竟也是一个充满人情世故的江湖,所以掌握一些通用的职场规则、沟通技巧,甚至是向上管理的艺术,这对于程序员来说也是十分有必要的。


仰望星空,脚踏实地,埋头赶路的同时也不要忘记时常抬头看看周围的环境和机会。


那关于这个问题,你的看法是什么呢,如果有不同的见解,也欢迎一起来分享交流~



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



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

华为擎云发布HarmonyOS 6 MDM能力,赋能政企安全高效数字化转型

2025年11月29日,华为擎云 HarmonyOS 6 MDM(移动终端管理)能力交流会于深圳圆满举行。本次会议汇聚了业界主流EMM应用厂商代表与移动安全领域的行业专家,共同围绕移动终端安全的技术与应用进行交流探讨,分享不同行业信息化建设的成功经验。鸿蒙生态...
继续阅读 »

2025年11月29日,华为擎云 HarmonyOS 6 MDM(移动终端管理)能力交流会于深圳圆满举行。本次会议汇聚了业界主流EMM应用厂商代表与移动安全领域的行业专家,共同围绕移动终端安全的技术与应用进行交流探讨,分享不同行业信息化建设的成功经验。

鸿蒙生态当前已跨越了阶段性里程碑,搭载HarmonyOS 5、HarmonyOS 6的终端设备已突破2700万台,且以每天超过10万台的速度增长,应用市场可搜索应用及元服务数量突破30万,覆盖用户生活和工作的方方面面。作为面向全场景未来的新生态,鸿蒙将持续携手伙伴依托OS的底层创新能力,为用户带来更安全、更高效、更智能的数字生态体验。

政企单位因行业属性特殊、数据敏感度高,在移动办公场景中催生出大量区别于消费端用户的场景化需求,MDM能力由此成为政企数字化转型的重要支撑。作为本次交流会的核心议题,华为终端各领域专家系统阐述了鸿蒙MDM的架构设计理念、核心技术特性,并针对云侧与端侧的具体开发路径提供了详细指导。

鸿蒙MDM能力作为面向政企需求的核心中间件,一端连接用户需求,一端连接OS底层能力,采用生态开放,分层设计的理念,通过开放通信、文件管理、多媒体、UI等多个子系统能力,赋能专业EMM伙伴,结合伙伴对行业场景的深刻洞察与丰富交付经验,共同为政企客户打造覆盖全场景的移动安全解决方案。

在接口特性方面,此次Harmony OS 6上开放了300+的系统API,覆盖设备管理、通信管理、网络配置、应用保活、应用分发、KIOSK展台模式等关键领域。接口的丰富性与精细化,直接提升了鸿蒙设备管理的精准度与安全防护等级,为复杂政企场景提供了更灵活的适配可能。

针对政企客户关注的设备识别与授权安全问题,华为擎云依托HEM(HUAWEI Enterprise Manager)平台构建了全流程自动部署体系,实现企业客户、项目信息、MDM应用与设备SN的多元绑定,让设备使用者、权限分配等信息一目了然。该体系支持企业对鸿蒙设备进行远程快速配置,新设备开箱连网后即可自动完成办公环境配置,涵盖系统参数配置、网络环境配置、管理策略下发、企业应用预装等全环节。

在政企核心需求的应用分发领域,HEM平台整合应用市场AG、MDM核心能力及ISV伙伴资源,构建适配不同应用类型、不同网络环境的分场景应用分发方案,全面满足鸿蒙设备在各类政企办公场景中的应用分发与生命周期管理需求。

同时,HEM平台提供了高效灵活的开箱定制能力。通过云端可视化操作界面,企业可针对设备资源配置、开机向导流程、桌面布局设计、系统参数设定等进行快速定制,助力企业实现敏捷化、多样化、批量化的设备定制交付。

除传统政企COPE(企业配发设备)终端管理模式外,针对消费电子、生物医药等高端制造领域中“人员临时性进入涉密厂区”的特殊场景,HarmonyOS 6基于RBAC(基于角色的访问控制)权限管理理念,从系统底层定义不同管理者角色与权限边界,设计了BDA管理模式。该模式支持BYOD(自带设备)模式的临时管理方案,既保障企业核心信息安全,又兼顾员工个人设备隐私,破解了传统BYOD管理的安全与隐私矛盾。

在移动终端管理领域,华为擎云已拥有十年技术沉淀与实践积累,深度洞察政企客户需求痛点,积累了海量行业项目的交付经验。未来,华为擎云将持续以技术创新为核心驱动力,助力政企客户加速数字化转型,提供更安全、高效、灵活、开放的鸿蒙设备管理解决方案,与生态伙伴在一起,共建共享鸿蒙新世界。

收起阅读 »

裁员为什么先裁技术人员?网友一针见血

最近逛职场社区的时候,刷到一个职场话题,老生常谈了,但是每次参与讨论的同学都好多。 这个问题问得比较扎心: “为什么有些企业的裁员首先从技术人员开始?” 关于这个问题,网上有一个被讨论很多的比喻: “房子都盖起来了,还需要工人么?” 有一说一,这个比喻虽然刺...
继续阅读 »

最近逛职场社区的时候,刷到一个职场话题,老生常谈了,但是每次参与讨论的同学都好多。


这个问题问得比较扎心:


“为什么有些企业的裁员首先从技术人员开始?”



关于这个问题,网上有一个被讨论很多的比喻:


“房子都盖起来了,还需要工人么?”


有一说一,这个比喻虽然刺耳,但却非常形象地揭示了某些企业的用人逻辑,尤其在某些非技术驱动型的公司里


在某些非技术驱动的公司(比如传统企业转型、或者业务模式成型的公司),其实技术部门很多时候是会被视为「成本中心」,而非「利润中心」的,我相信在这类企业待过的技术同学肯定是深有体会。


就像盖大楼一样,公司需要做一个 App,或者搞一个系统,于是高薪招来一帮程序员“垒代码”。


当这个产品上线,业务跑通了,进入了平稳运营期,公司某些大聪明老板总会觉得“房子”已经盖好了。


这时候,一些开发人员在老板眼里就变成了“冗余”的成本。


大家知道,销售部门、业务部门能直接带来现金流,市场部能带来用户,而技术部门的代码是最看不见摸不着的。


一旦没有新的大项目启动,老板会觉得技术人员坐在那里就是在“烧钱”。


那抛开这个“盖楼”的比喻,在这种非技术驱动的公司里,从纯粹的财务角度来看,裁技术岗往往是因为“性价比”太低。


所以这里我们不得不面对的一个现实是:技术人员通常是公司里薪资最高的一群人。


高薪是一把双刃剑呐。


一个初级程序员的月薪可能抵得上两个行政,一个资深架构师的年薪可能抵得上一个小团队的运营费用。当公司面临现金流危机,需要快速削减成本时,裁掉一个高级技术人员省下来的钱,相当于裁掉好几个非技术岗位人员。


除此之外还有一个比较尴尬的事情那就是,在技术团队中,往往存在着一种“金字塔”结构。


随着工龄增长,薪资涨幅很快,但产出效率(在老板眼里)未必能线性增长。


脑补一下这个场景就知道了:



  • 一个 35 岁的高级工程师,月薪 4 万,可能要养家糊口,精力不如 20 多岁的小年轻,加班意愿低。

  • 一个 23 岁的小年轻,月薪 1 万 5,充满激情,能扛能造。


这时候某些大聪明老板的算盘就又打起来了:


裁掉一个 4 万的老员工,招两个 1 万 5 的小年轻,代码量翻倍,团队氛围更活跃,成本还降了,这种“优化”在管理层眼里,简直是“降本增效”的典范。


所以综合上面这种种情形分析,这时候,文章开头的那个问题往往也就会逐渐形成了。


所以事就是这么个事,说再多也没用。


既然环境不能左右,那作为个体,我们又该如何自处呢


这里我不想灌鸡汤,只想务实地聊一聊我所理解的一些对策,希望能对大家有所启发。


同时这也是我给很多后台私信我类似问题小伙伴们的一些共同建议。


1、跳出技术思维,建立业务思维


千万不要只盯着你的 IDE 和那一亩三分地代码,抽空多了解了解业务和流程吧,比如:



  • 项目是靠什么赚钱的?

  • 你的代码在哪个环节为公司省钱或挣钱?

  • 如果你是老板,你会怎么优化现在的系统?


当你能用技术手段去解决业务痛点(比如提升转化率、降低服务器成本)时,你就不再是成本,而是资产。


2、别温水煮青蛙,要保持技能更新


这一点之前咱们这里多次提及,在技术行业,吃“老本”是最危险的。


当今的技术世界变化太快,而作为程序员的我们则恰好处于这一洪流之中,这既是挑战,也是机会。


还是那句话,一定要定期评估一下自己的市场价值:如果明天就离开现在的公司,你的技能和经验是否足以让你在市场上获得同等或更好的位置?


无论在公司工作多久,都要不断更新自己的技能和知识,确保自己始终具有市场竞争力。


3、别让自己的工作经验烂掉,有意识地积累职业资产


这一点我们之前其实也聊过。


除了特定的技术、代码、框架可以作为自己可积累的能力资产之外,其实程序员的职业生涯里也是可以有很多可固化和可积累的有形资产的。


比如你的技术经历、思维、经验、感悟是不是可以写成技术博客文字?你写的代码、工具、框架是不是可以形成开源项目?你的工作笔记和踩坑记录是不是可以整理成技术手册?


千万不要让自己的工作经验烂掉,而是要有意识地将自己的技术资产化,将自己的过往经验、知识、能力转化成在行业里有影响力的硬通货。


4、尽早构建 Plan B,提升抗风险能力


当然这一点虽然说的简单,其实对人的要求是比较高的。前面几点做好了,这一点有时候往往就会水到渠成。


我觉得总体的方向应该是:尽量利用你的技术特长来构建一个可持续的 Plan B。


比方说:开发一个小工具、写写技术专栏、或者运营一个 GitHub 项目、在技术博客或社区中建立个人品牌...等等,这些不仅仅能增加收入,往往还能拓展你的人脉圈。


其实很多程序员在年龄大了之后越来越焦虑的一个重要原因就是因为生存技能太过单一了,所以千万不要给自己设限,埋头赶路的同时也不要忘记时常抬头看看周围的环境和机会。


好了,今天就先聊这么多吧,希望能对大家有所启发,我们下篇见。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



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

我的2025:做项目、跑副业、见人、奔波、搬家、维权、再回上海

2025 年,如果让我用一句话定性,我会说:我在变强,也在重新选择自己的人生结构。这一年我做了很多事,多到我一度不敢回头看。表面上看,我一直在“往前”:写内容、做项目、跑副业、见人、奔波、搬家、维权、再回上海。可只有我自己知道,真正折磨人的不是忙,是那种反复出...
继续阅读 »

2025 年,如果让我用一句话定性,我会说:我在变强,也在重新选择自己的人生结构。

这一年我做了很多事,多到我一度不敢回头看。表面上看,我一直在“往前”:写内容、做项目、跑副业、见人、奔波、搬家、维权、再回上海。可只有我自己知道,真正折磨人的不是忙,是那种反复出现的瞬间——我突然意识到:我不是在冲,我是在被生活推着跑

我确实拿到了一些结果。内容有过爆的时刻,小红书涨了粉,视频剪辑从手忙脚乱到慢慢顺手,有人开始来问我、信我、甚至愿意付费。那段时间我有一种很罕见的笃定:只要我肯学、肯磨,很多事我都能做成。那种“我好像什么都能做”的自信,在这一年里反复把我从低谷里托起来。

但同样是这一年,我也交了一笔不轻的学费。不是钱那么简单,更是对人、对机会、对“看起来很美”的承诺的那种天真。我曾因为信任做了一个很重的决定;也曾在北京的夜里把事情一条条摊开算清楚,最后发现不是值不值的问题,而是我再拖下去,就会把自己耗到没样子。

我不想把这篇复盘写成流水账,也不想写成鸡汤。我只想把这一年最真实的部分摆出来:我怎么一点点变强,怎么被现实教育,怎么止损、怎么维权、怎么把自己从废墟里捡回来。


1. 我开始把表达当成一件正事

三月开始,我把很多注意力放在“说清楚”这件事上。

以前我也输出,但更多像随手记录。2025 年不一样,我开始认真经营表达:每天钻研、每天尝试、每天复盘。公众号有了更明确的正反馈,有几篇文章突然被推起来,评论区开始出现陌生人的共鸣,后台也开始有人来问我问题。那种感觉很奇妙——我写的东西不再只属于我自己,它开始进入别人的生活。

今年使用最多的AI IDE 就是Trae,也参加了第一期的Trae 征文活动,获得了第二名,Trae给我来了很多成长。

今年在Trae 方面的实践:


2.自己维护了一个Trae 生态资源合集

image.png


3.基于Trae 开发的第一个APP

Trae 刚出来Claude模型时,连夜测评它的能力,当时花了5个小时搞出一个App,项目并且还开源了 image.png


  1. 基于Trae 设计的原型稿

tickhaijun.github.io_Podcast_.png

我也开始碰视频。说实话,一开始很狼狈:剪一个一分钟的视频,要花我两三个小时。卡点、配乐、字幕、节奏,哪一样都不像看起来那么简单。我一度怀疑是不是我不适合,但又不甘心。我知道这是一块我之前没尝试过的能力,一旦练出来,就是新的路。

基于Trae还做了原型还原设计稿,没想到视频火了 image.png


图片

这一段给我的礼物,是一种更稳定的自信:很多事看起来复杂,只要拆开、一步步做,就会变得可控。


2. 我把想法做成了作品通过Vibe Coding

五月到八月,我进入了一种“手里有活”的状态。

图片

从懵懂到落地:记录我们第一次成功将大模型“塞”进业务的曲折历程

图片

年初做了自己第一款AI应用

图片图片

那段时间我做了很多作品,也开源了不少东西。说白了,就是把想法从脑子里拎出来,做成一个能跑、能看、能用、能被别人理解的东西。

与此同时,我也给团队做了多次分享,讲我最近在做什么、怎么做、踩了什么坑、怎么绕开。

图片

中间有两次机会我印象很深:一次是来自一家很大的咨询公司,一次是出海方向的远程邀请。它们都挺诱人,但我当时都拒绝了。原因很简单:我知道我还没准备好。能力没到那个厚度、心态没到那个稳定度,我不想靠运气上去,然后靠硬扛撑住。

也有一些小小的惊喜:有人买了我做的东西,虽然数量不算多,但足够让我确认——我做的东西不是自嗨,是真的有人需要。更重要的是,越来越多的网友通过我的内容认识我,联系我,问我问题。

那几个月我最大的收获不是“做了多少”,而是一个更朴素的结论:想法不值钱,做出来才值钱。


3. 有人愿意为我的能力买单

九月到十一月,我的副业开始像一门“正经事”。

咨询变多了。有的是临时问答,有的是更系统的陪跑。我接了三份陪跑,也因此认识了几位很投缘的朋友,都是山西的。我们聊项目、聊选择、聊怎么把事情做成,也聊怎么在现实里不把自己弄丢。

这份关系很珍贵。它不是那种互相吹捧的热闹,而是我能明显感到:对方因为我的建议少走了弯路,事情推进得更顺,而我也因为对方的反馈变得更坚定。那种“我真的帮到了人”的成就感,比数字更实在。

我也在这一段第一次更清晰地看到我的位置:我不是只能埋头做项目的人,我还可以把经验讲清楚,把复杂拆简单,把别人卡住的点指出来。这是一种能力,也是一种责任感。

这一段让我相信:靠自己攒出来的口碑,慢,但稳。


4. 我重新确认了“钱该花在哪”

国庆我和家人自驾出去玩了一趟。

图片

风很大,天很高,羊肉很香。我们在草原上待了一天,我给父母安排了越野卡丁车,让他们在草地上跑一圈;我和姐姐骑了马,笑得像回到小时候。那几天我很放松,甚至有点恍惚——原来我努力这么久,最想换来的并不是某个头衔,而是这种“我能让他们开心”的底气。

我以前对花钱很谨慎,总觉得要攒着、要算计回报。可当我把钱花在家人身上,那种舒坦很直接:不需要证明,不需要解释,花出去就是一种“我扛得住了”的确认。


5. 去北京一趟,我把胆子捡了回来

图片

十月我去北京参加了一个活动,也算第一次为了这类事出远门。2026年,多输出AI,多参加活动。

现场人很多,节奏很快,信息密得让人喘不过气。那天我最大的感受,不是见了什么产品,而是突然明白:机会真的会从我身边走过去,走过去就没了。很多时候不是我不够好,是我不敢站出来,或者我下意识觉得“我还不够格”。

图片

去天津路上,熟悉的感觉

我也去了天津,见了老朋友老李。我们聊了一整天,我帮他搬运整理食品,他带我吃了天津菜,甚至让我体验了一把保时捷 911。最后他把我送到机场。

图片

那一天让我很感慨:这个世界其实很大,也很活,我不能总把自己困在“怕麻烦、怕尴尬、怕出丑”的情绪里。

图片

今年我也买了不少书,也读了不少书。《亲密关系》《认知驱动》《纳瓦尔宝典》……它们没有给我标准答案,但给了我更清醒的视角:我要对自己的情绪负责,对自己的选择负责,对自己的长期负责。


6. 我相信过他,也因此完成了一次祛魅

十一月底,我做了一个很重的决定:离职,去北京试一次。

图片

这件事我并不是冲动。相反,我想了将近一个月。朋友“他”邀请过我三次,前两次我都拒绝了。第三次创始人亲自找我,话说得很漂亮,未来画得很大,而我也确实在那个阶段渴望一次更大的空间。再加上对“他”的信任,我最终点了头。

图片

离开前,我做了一件我很想做的事:把爸爸接到上海。那是他第一次来上海,也是他第一次坐飞机。我去接他的时候,他脸上的喜悦藏不住。我带他逛了很多地方,拍了很多照片。送他去机场那天,我心里很踏实——那种成就感,不来自任何评价,只来自“我能带他看世界”的瞬间。

今年我也给妈妈买了新手机,她之前那部太卡了。再小的事情,落在父母身上都是实在的改变。

然后我去了北京。

现实很快给了我一记闷棍。之前说的和实际差太多太多。我会在很短时间内发现:有些话只是话,有些承诺只是情绪,有些“格局”只是包装。我不想在这里写具体细节,但我可以写结论——这次经历让我完成了一次祛魅:对人、对所谓“机会”、对“看起来很美”的未来。

我也更清楚了一件事:我并不是不能吃苦,我是不愿意把我的尊严和时间押在不靠谱的人和不靠谱的事上。


7. 我救了三只狗,也被这座城市的善意接住

这一年我救了三只狗。

图片

第一只是中华田园犬,在公园遇到的。它很瘦,眼神怯,但又不躲人。

第二只是边牧,在公司附近,它更像是走丢的孩子,聪明又无助。

图片

第三只是阿拉斯加,在豫园附近,体型很大,却一点安全感都没有。

我喜欢狗。遇见它们的时候,我很难装作没看见。我做的事其实也不复杂:拍照、发帖、联系、筛选领养人、把信息对齐清楚,然后送它们去新家。

这件事最打动我的,不是我多善良,而是我发现:大城市真的有很多愿意伸手的人。我发出求助,真的会有人回应。我以为我在救它们,其实在某些时刻,是这些善意在把我从疲惫里接住。


8. 一笔沉没成本:止损、维权、和不再委屈自己

十二月初,北京给了我最硬的一课。

图片

我在北京待了十来天,一直住酒店。对方之前说会报销,但后来什么都没有。入职前一天我找了房子,租房费用、中介费用、再加上各种奔波成本,堆起来是一笔不小的支出。更糟的是:入职第一天我就通过另一位同样处境的人了解到了真实情况;再加上“他”下班后说的一些话,我很快确定——这里不是我该待的地方。

图片

那一刻最难的其实不是离开,而是面对沉没成本。我已经付出那么多,我会本能地想“再忍忍,再等等”。但我很庆幸,那天我没骗自己。我选择止损。

随之而来的就是维权。房子我没入住,合同日期也没开始,但管家很无赖,甚至带着恐吓。那种“我讲理他就耍赖”的感觉很恶心。我一开始也很烦,后来干脆不和她废话,直接走流程,通过 12315 协调,拿回了一部分。理论上可以拿回更多,但要继续耗时间精力,我当时选择到此为止。

这一段时间,让家里也没少操心,哎....

我最想写给自己的不是“钱亏了”,而是一个更重要的结论:以后遇到不公,我不再用委屈换和平。该维权就维权,该翻脸就翻脸。


9. 回到上海:我把自己一点点拉回正轨

图片

十二月中旬我回到了上海。

图片

收拾好家里的工位

那段时间我能量很低。不是累,是一种被现实撞过之后的钝。我会怀疑自己、怀疑判断、怀疑信任,甚至怀疑“是不是我太敏感了”。但生活不会等我缓过来,它只会继续往前。

图片

我做的第一件事是把我自己拉回正常:吃饭、睡觉、见朋友。后来我和老耿去了杭州散心。城市很安静,走在路上我突然发现:风还是一样吹,灯还是一样亮,我不会因为受挫就失去明天。

我慢慢控住场了。把生活拉回正轨了。也把那句最重要的话重新捡回来——我在变强,也在重新选择自己的人生结构。


最后

回头看 2025 年,我最大的变化不是“我做了多少”,而是我对人生结构的要求变高了

以前我会把努力当成答案。现在我更在意:这份努力能不能沉淀,能不能让我拥有更多选择权。以前我遇到烂事会先忍,想着“算了”。但北京那一段之后我更确定:委屈不会换来尊重,只会换来下一次更大的代价。该止损就止损,该维权就维权——哪怕沉没成本已经砸下去,我也要把自己从泥里拎出来。

这一年我也完成了一次祛魅:
对“机会”的祛魅,对“关系”的祛魅,对“画出来的未来”的祛魅。
我开始相信一句话:真正值得的机会,不会只靠嘴说;真正可靠的人,也不会只靠情绪绑架。

如果说 2025 年教会了我什么,我觉得是三件事:

第一,能力不是拿来逞强的,是拿来兜底的。
我在最狼狈的时候,靠自己把局面稳住了。那种“我能扛住”的底气,是真的。

第二,钱花在家人身上,会变成一种很踏实的成就感。
我以前以为成就感来自外界认可,今年我更确定:来自父母的笑、来自家人的安心、来自“我可以照顾他们”。

第三,善意是会流动的。
我帮过人,也被人帮过;我救过狗,也被陌生人的热心治愈过。世界不全是烂人,但我得学会识别,学会筛选,学会保护自己。

2026 年我不想再喊口号了。我只想做三件更具体的事:

  • 把一条能长期跑的主线做出来:让输出、作品和服务真正形成稳定的节奏,而不是靠运气起伏。
  • 给信任立规矩:合作要有边界,承诺要能落地,任何决定都要留后手。
  • 把家放进计划里:不是“有空再说”,而是本来就该排在前面。

2025 年没有把我推到高处,但它把我从幻觉里拽出来了。
我依然会往前走,只是以后我更在乎的不是速度,而是方向;不是热闹,而是结构。

我在变强,也在重新选择自己的人生结构。

就复盘到这吧,用时6个小时,该休息了....

希望2026年一切顺利! 


作者:程序员海军
来源:juejin.cn/post/7595147871939493934
收起阅读 »

Skill 真香!5 分钟帮女友制作一款塔罗牌 APP

最近发现一个 AI 提效神器 ——Skills,用它配合 Cursor 开发,我仅用 5 分钟就帮女友做出了一款塔罗牌 H5 APP!在说如何操作之前,我们先大概了解下 Skills 的原理 一、Skills的核心内涵与技术构成 (一)本质界定 Skills ...
继续阅读 »

最近发现一个 AI 提效神器 ——Skills,用它配合 Cursor 开发,我仅用 5 分钟就帮女友做出了一款塔罗牌 H5 APP!在说如何操作之前,我们先大概了解下 Skills 的原理


一、Skills的核心内涵与技术构成


(一)本质界定


Skills 可以理解为给 AI Agent 定制的「专业技能包」,把特定领域的 SOP、操作逻辑封装成可复用的模块,让 AI 能精准掌握某类专业能力,核心目标是实现领域知识与操作流程的标准化传递,使AI Agent按需获取特定场景专业能力。其本质是包含元数据、指令集、辅助资源的结构化知识单元,通过规范化封装将分散专业经验转化为AI Agent可理解执行的“行业SOP能力包”,让 AI 从‘只会调用工具’变成‘懂专业逻辑的执行者


(二)技术构成要素


完整Skill体系由三大核心模块构成,形成闭环能力传递机制:



  1. 元数据模块:以SKILL.md或meta.json为载体,涵盖技能名称、适用场景等关键信息约 100 个字符(Token),核心功能是实现技能快速识别与匹配,为AI Agent任务初始化阶段的加载决策提供依据。

  2. 指令集模块:以instructions.md为核心载体,包含操作标准流程(SOP)、决策逻辑等专业规范,是领域知识的结构化转化成果,明确AI Agent执行任务的步骤与判断依据。

  3. 辅助资源模块:可选扩展组件,涵盖脚本代码、案例库等资源,为AI Agent提供直接技术支撑,实现知识与工具融合,提升执行效率与结果一致性。


和传统的函数调用、API 集成相比,Skills 的核心优势是:不只是 “告诉 AI 能做什么”,更是 “教会 AI 怎么做”,让 AI 理解专业逻辑而非机械执行


二、Skills与传统Prompt Engineering的技术差异


从技术范式看,Skills与传统Prompt Engineering存在本质区别,核心差异体现在知识传递的效率、灵活性与可扩展性上:



  1. 知识封装:传统为“一次性灌输”,冗余且复用性差;Skills为“模块化封装”,一次创建可跨场景复用,降低冗余成本。

  2. 上下文效率:传统一次性加载所有规则,占用大量令牌且易信息过载;Skills按需加载,提升效率并支持多技能集成。

  3. 任务处理:传统面对复杂任务易逻辑断裂,无法整合外部资源;Skills支持多技能组合调用,实现复杂任务全流程转化。

  4. 知识迭代:传统更新需逐一修改提示词,维护成本高;Skills为独立模块设计,更新成本低且关联任务可同步受益。


上述差异决定Skills更适配复杂专业场景,可破解传统Prompt Engineering规模化、标准化应用的瓶颈。


三、渐进式披露:Skills的核心技术创新


(一)技术原理与实现机制


Skills能在不增加上下文负担的前提下支撑多复杂技能掌握,核心在于“按需加载”的渐进式披露(Progressive Disclosure)设计,将技能加载分为三阶段,实现知识传递与上下文消耗的动态平衡:



  1. 发现阶段(启动初始化):仅加载所有Skills元数据(约100个令牌/个),构建“技能清单”明确能力边界,最小化初始化上下文负担。

  2. 激活阶段(任务匹配时):匹配任务后加载对应技能指令集,获取操作规范,实现精准加载并避免无关知识干扰。

  3. 执行阶段(过程按需加载):动态加载辅助资源,进一步优化上下文利用效率。


(二)技术优势与价值


渐进式披露机制使Skills具备三大核心优势:



  1. 降低令牌消耗:分阶段加载避免资源浪费,支持单次对话集成数十个技能,降低运行成本。

  2. 提升执行准确性:聚焦相关知识组件,减少干扰,强化核心逻辑执行精度。

  3. 增强扩展性:模块化设计支持灵活集成新知识,无需重构系统,适配领域知识快速迭代。


四、Cursor Skills


介绍完 Skills 是什么之后,我将使用的是 Cursor 作为我的开发工具。先说明一下,最开始只有 Claude Code 支持 Skills、Codex 紧随其后,口味自己选。



好消息是,Cursor 的 Skills 机制采用了与 Claude Code 几乎完全一致的 SKILL.md 格式。这意味着,你完全不需要从头编写,可以直接将 Claude Code 的生态资源迁移到 Cursor。



(一)Cursor 设置


因为 Cursor 刚支持不久,并且是 Beta 才能使用,所以要进行下面操作



Agent Skills 仅在 Nightly 更新渠道中可用。

要切换更新渠道,打开 Cursor 设置( Cmd+Shift+J ),选择 Beta,然后将更新渠道设置为 Nightly。更新完成后,你可能需要重新启动 Cursor。 如下图所示




要启用或禁用 Agent Skills:



  1. 打开 Cursor Settings → Rules

  2. 找到 Import Settings 部分

  3. 切换 Agent Skills 开关将其开启或关闭 如下图所示


(二)复制 Claude Skills


然后我们直接去 Anthropic 官方维护的开源仓库 anthropics/skills,里面提供了大量经过验证的 Skill 范例,涵盖了创意设计、开发技术、文档处理等多个领域。


你可以访问 github.com/anthropics/… 查看完整列表。以下是这次用到的 Skills


Frontend Design:这是一个专门用于提升前端设计质量的技能。它包含了一套完整的 UI 设计原则(排版、色彩、布局)


然后我们直接把 Skills 里面的 .claude/skills/frontend-design 到当前项目文件下,如图:



模型和模式如下图



提示词如下,不一定非得用我的。


使用 Skill front-design。我要做一个 H5 ,功能是一个塔罗牌。

你是一名经验丰富的产品设计专家和资深前端专家,擅长UI构图与前端页面还原。现在请你帮我完成这个塔罗牌应用的 UI/UX 原型图设计。请输出一个包含所有设计页面的完整HTML文件,用于展示完整UI界面。

注意:生成代码的时候请一步一步执行,避免单步任务过大,时间执行过长

然后 Cursor 会自动学习 Skills,并输出代码



然后就漫长的等待之后,Cursor 会自动做一个需求技术文档,然后会一步一步的实现出来,这时候可以去喝杯茶,再去上个厕所!


最终输出了 5 个页面



  1. 首页 (Home)

  2. 每日抽牌页 (Daily Draw)

  3. 牌阵占卜页 (Spread Reading)

  4. 塔罗百科页 (Encyclopedia)

  5. 占卜历史页 (History)


最终效果如下,整体效果看起来,完全是一个成熟的前端工程师的水准,甚至还带有过渡动画和背景效。因为掘金无法上传视频,欢迎私信我找我要或者关注我:


image.png


扩展阅读


因为 Cursor 目前仅在 Nightly 版本上才可以使用 Skills。如果担心切换此模式会引发意想不到的情况,可以使用另一种方案


OpenSkills 是一个开源的通用技能加载器。



  • 完全兼容:它原生支持 Anthropic 官方 Skill 格式,可以直接使用 Claude 官方市场或社区开发的技能。

  • 桥梁作用:它通过简单的命令行操作,将这些技能转换为 Cursor、Windsurf 等工具可识别的配置(AGENTS.md),从而让 Cursor 具备与 Claude Code 同等的“思考”与“技能调用”能力。


作者:乘风gg
来源:juejin.cn/post/7593362940071903284
收起阅读 »

秒懂 Headless:为什么现在的软件都要“去头”?

web
简单来说, “Headless”(无头) 在软件开发中指的是:只有逻辑(后端/内核),没有预设界面(前端/GUI) 的软件架构模式。 这里的“Head(头)”比喻的是用户界面(UI/GUI) ,“Body(身体)”比喻的是核心业务逻辑或引擎。 Headless...
继续阅读 »

简单来说, “Headless”(无头) 在软件开发中指的是:只有逻辑(后端/内核),没有预设界面(前端/GUI) 的软件架构模式。


这里的“Head(头)”比喻的是用户界面(UI/GUI) ,“Body(身体)”比喻的是核心业务逻辑或引擎


Headless = 砍掉自带的 UI,只给你提供 API 或核心逻辑,让你自己去画界面。




1. 核心概念图解


想象一下 “传统的软件”(比如 Word):它像一家堂食餐厅。你有厨房(逻辑),也有固定的桌椅板凳和装修风格(UI)。你必须在它提供的环境里吃饭,无法改变装修。


“Headless 软件”:它像一个中央厨房(外卖工厂)。它只负责做饭(逻辑),不提供桌椅(UI)。



  • 你想把菜送到五星级酒店摆盘(Web 端高级定制 UI)?可以。

  • 你想把菜送到路边摊(手机 App)?可以。

  • 你想把菜送到自动售货机(小程序)?也可以。




2. 具体例子


A. 无头浏览器 (Headless Browser)



  • 传统的浏览器(如 Chrome): 你打开它,能看到窗口、地址栏、渲染出来的网页,你能用鼠标点击。

  • 无头浏览器(如 Puppeteer, Playwright):



    • 定义: 它是浏览器内核(Chrome/Webkit),但没有可视化的窗口。它在后台(命令行/服务器)运行。

    • 怎么用? 你写代码控制它:“打开百度 -> 输入关键词 -> 截图”。

    • 有什么用?



      1. 自动化测试: 模拟用户点击,快速跑通几千个测试用例,不需要真的弹出一千个窗口。

      2. 爬虫: 爬取那些需要 JS 渲染的复杂网页。

      3. 生成截图/PDF: 在服务器端把网页渲染成图片或 PDF 报告。






B. 无头编辑器 (Headless Editor)



  • 传统的编辑器(如 CKEditor 旧版, Quill):



    • 你引入它,它就自带一套“加粗、斜体、插入图片”的工具栏,自带一套 CSS 样式。

    • 缺点: 如果设计师说“把工具栏按钮变成圆形的,而且要悬浮在文字上方”,你就要疯狂覆盖它的默认 CSS,非常痛苦。



  • 无头编辑器(如 Tiptap, Plate, Slate.js):



    • 定义: 它只提供文字处理的核心逻辑(比如:选中文本、按下 Ctrl+B 变粗体、撤销重做逻辑)。它不提供任何 UI(没有工具栏,没有按钮)。

    • 怎么用? 你需要自己写一个 <button>,自己写样式,然后调用它的 API editor.toggleBold()

    • 有什么用? 你可以完全自由地定制编辑器的长相。比如 Notion、飞书文档那种高度定制的 UI,必须用无头编辑器开发。






3. 还有哪些常见的 Headless?


除了浏览器和编辑器,现在的开发趋势中还有:


C. 无头组件库 (Headless UI)



  • 例子: Radix UI, Headless UI, React Aria。

  • 解释: 以前我们用 Ant Design 或 Bootstrap,按钮长什么样是库定好的。Headless UI 库只提供交互逻辑(比如下拉菜单怎么打开,键盘怎么选,无障碍怎么读),不提供任何 CSS

  • 好处: 完美配合 Tailwind CSS,长相由你完全控制。


D. 无头 CMS (Headless CMS)



  • 例子: Strapi, Contentful。

  • 解释: 以前用 WordPress,后台管理内容,前台页面也是 WordPress 生成的(耦合)。Headless CMS 只提供后台管理API

  • 好处: 你的一份内容(API)可以同时发给 网站、App、智能手表、甚至冰箱屏幕。




总结:为什么现在流行 Headless?


虽然 Headless 意味着开发者要写更多的代码(因为要自己画 UI),但它解决了现代开发最大的痛点:定制化


维度传统 (Coupled)Headless (无头)
上手难度 (开箱即用) (需要自己写 UI)
自由度 (改样式很难)极高 (随心所欲)
适用场景快速做个标准后台像 Notion/Figma 这种需要极致体验的产品
比喻方便面 (有面有调料包,味道固定)生鲜面条 (只有面,想做炸酱面还是汤面随你)

一句话总结:Headless 就是把“业务逻辑”和“界面表现”彻底分家,让你拥有无限的 UI 定制权。


作者:皮蛋小精灵
来源:juejin.cn/post/7582118218649288730
收起阅读 »

为了让 iframe 支持 keepAlive,我连夜写了个 kframe

web
前几天收到一个bug,说是后台管理系统每次切换标签栏后,xxx内容区自动刷新,操作进度也丢失了,给用户造成很大困扰。作为结丹期修士的我自然不容允许此等存在,开干! 问题分析 该后台管理系统基于 vue3 全家桶开发,多标签页模式,标签页默认 KeepAli...
继续阅读 »

前几天收到一个bug,说是后台管理系统每次切换标签栏后,xxx内容区自动刷新,操作进度也丢失了,给用户造成很大困扰。作为结丹期修士的我自然不容允许此等存在,开干!


28af57256b600c33b123001d5f4c510fdbf9a1df.jpg


问题分析



该后台管理系统基于 vue3 全家桶开发,多标签页模式,标签页默认 KeepAlive,本文以 demo 示例。



切换标签后内容区自动刷新,操作进度丢失?首先想到的是 KeepAlive 问题,但经过排查后才发现,KeepAlive 是正常的,异常的是内嵌于页面的 iframe 内容区,页面每次 onActivated 时,iframe 内容区都会重新加载一次,导致进度丢失。


录制_2025_05_13_19_18_22_59.gif


iframe 并没有被 keep 住,为什么?


通过查阅 Vue 文档得知,KeepAlive缓存的只是 Vue 组件实例,组件实例包含组件状态和 VNode (虚拟 DOM 节点)等。当组件 activated 时,组件 VNode 已经转为真实 DOM 节点插入文档中了,而组件 deactivated 时,已经从文档中移除了组件对应的真实 DOM 节点并缓存组件实例。

image.png


image.png


VNode 是对真实 DOM 节点的映射,包含节点标签名、节点属性等信息。我们打开控制台选中 iframe 元素,右侧那栏就是其对应的 VNode 了。
image.png
从上图可看出,iframe 的内容并不属于节点信息,是个独立的 browsing context(浏览上下文),无法被缓存;iframe 每次渲染(如 DOM 节点插入、移动)都会触发完整的加载过程(相当于打开新窗口)。故组件每次 activated 时,iframe 都会重新加载,创建了新的上下文,之前的操作进度自然是丢失了。


至此,问题原因已找到,接下来看下如何处理。


解决方案


iframe 无法保存于 VNode 中,又不能将 iframe 从文档中移动或移除,那么就想办法在某个地方把 iframe 存起来,比如 body 节点下,然后通过样式控制 iframe 展示与隐藏,顺着思路捋一下整体流程。


image.png


有了上述流程,开始设计下细节。 Iframe 组件是对 iframe 操作流的封装,方便在 vue 项目中使用,内部涉及 iframe 创建、插入、设置样式、移除等操作,为方便操作,将其封装为 Iframe 类;分散的 Iframe 类操作,稍有不当可能造成内存占用过多,故为了统一管理,再设计一个 IframeManage 来统一管理 Iframe。

相关的类关系图如下


classDiagram
class Iframe {
-instance: HTMLIFrameElement
-ops: IframeOptions
+init()
+hide()
+show(rect: IFrameRect)
+resize(rect: IFrameRect)
+destroy()
}

class IFrameManager {
+static frames: Map<string, Iframe>
+static createFrame()
+static showFrame()
+static hideFrame()
+static destroyFrame()
+static resizeFrame()
+static getFrame()
}

class VueComponent {
-frameContainer: Ref
+createFrame()
+destroyFrame()
+showFrame()
+resizeFrame()
-handleLoaded()
-handleError()
}

VueComponent --> IFrameManager : 使用
IFrameManager --> Iframe : 创建/管理
Iframe --> HTMLIFrameElement : 封装

对应的时序图如下


sequenceDiagram
participant VueComponent
participant IFrameManager
participant Iframe
participant DOM

VueComponent->>IFrameManager: createFrame()
IFrameManager->>Iframe: new Iframe(ops)
Iframe->>DOM: createElement('iframe')
Iframe->>DOM: appendChild()
VueComponent->>IFrameManager: resizeFrame()
IFrameManager->>Iframe: resize()
Iframe->>DOM: setElementStyle()
VueComponent->>IFrameManager: destroyFrame()
IFrameManager->>Iframe: destroy()
Iframe->>DOM: remove()

至此思路清晰,开始进入编码


编码实战


首先是 Iframe 类的实现


interface IframeOptions {
uid: string
src: string
name?: string
width?: string
height?: string
className?: string
style?: string
allow?: string
onLoad?: (e: Event) => void
onError?: (e: string | Event) => void
}

type IframeRect = Pick<DOMRect, 'left' | 'top' | 'width' | 'height'> & { zIndex?: number | string }

class Iframe {
instance: HTMLIFrameElement | null = null
constructor(private ops: IframeOptions) {
this.init()
}
init() {
const {
src,
name = `Iframe-${Date.now()}`,
className = '',
style = '',
allow,
onLoad = () => {},
onError = () => {},
} = this.ops

this.instance = document.createElement('iframe')
this.instance.name = name
this.instance.className = className
this.instance.style.cssText = style
this.instance.onload = onLoad
this.instance.onerror = onError
if (allow) this.instance.allow = allow
this.hide()
this.instance.src = src
document.body.appendChild(this.instance)
}
setElementStyle(style: Record<string, string>) {
if (this.instance) {
Object.entries(style).forEach(([key, value]) => {
this.instance!.style.setProperty(key, value)
})
}
}
hide() {
this.setElementStyle({
display: 'none',
position: 'absolute',
left: '0px',
top: '0px',
width: '0px',
height: '0px',
})
}
show(rect: IframeRect) {
this.setElementStyle({
display: 'block',
position: 'absolute',
left: rect.left + 'px',
top: rect.top + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
border: '0',
'z-index': String(rect.zIndex) || 'auto',
})
}
resize(rect: IframeRect) {
this.show(rect)
}
destroy() {
if (this.instance) {
this.instance.onload = null
this.instance.onerror = null
this.instance.remove()
this.instance = null
}
}
}

其次是 IFrameManager 类的实现


export class IFrameManager {
static frames = new Map()
static createFame(ops: IframeOptions, rect: IframeRect) {
const existFrame = this.frames.get(ops.uid)
if (existFrame) {
existFrame.destroy()
}
const frame = new Iframe(ops)
this.frames.set(ops.uid, frame)
frame.show(rect)
return frame
}
static showFrame(uid: string, rect: IframeRect) {
const frame = this.frames.get(uid)
frame?.show(rect)
}
static hideFrame(uid: string) {
const frame = this.frames.get(uid)
frame?.hide()
}
static destroyFrame(uid: string) {
const frame = this.frames.get(uid)
frame?.destroy()
this.frames.delete(uid)
}
static resizeFrame(uid: string, rect: IframeRect) {
const frame = this.frames.get(uid)
frame?.resize(rect)
}
static getFrame(uid: string) {
return this.frames.get(uid)
}
}

最后是 Iframe 组件的实现


<template>
<div ref="frameContainer" class="k-frame">
<span v-if="!src" class="k-frame-tips">
<slot name="placeholder">暂无数据</slot>
</span>
<span v-else-if="isLoading" class="k-frame-tips">
<slot name="loading">加载中... </slot>
</span>
<span v-else-if="isError" class="k-frame-tips"> <slot name="error">加载失败 </slot></span>
</div>

</template>
<script setup lang="ts">
import { onActivated, onBeforeUnmount, onDeactivated, ref, watch } from 'vue'
import { IFrameManager, getIncreaseId } from './core'
import { useResizeObserver, useThrottleFn } from '@vueuse/core'

defineOptions({
name: 'KFrame',
})

const props = withDefaults(
defineProps<{
src: string
zIndex?: string | number
keepAlive?: boolean
}>(),
{
src: '',
keepAlive: true,
},
)

const emits = defineEmits(['loaded', 'error'])

const uid = `kFrame-${getIncreaseId()}`
const frameContainer = ref()
const isLoading = ref(false)
const isError = ref(false)
let readyFlag = false

const getFrameContainerRect = () => {
const { x, y, width, height } = frameContainer.value?.getBoundingClientRect() || {}
return {
left: x || 0,
top: y || 0,
width: width || 0,
height: height || 0,
zIndex: props.zIndex ?? 'auto',
}
}

const createFrame = () => {
isError.value = false
isLoading.value = true

IFrameManager.createFame(
{
uid,
name: uid,
src: props.src,
onLoad: handleLoaded,
onError: handleError,
allow: 'fullscreen;autoplay',
},
getFrameContainerRect(),
)
}
const handleLoaded = (e: Event) => {
isLoading.value = false
emits('loaded', e)
}
const handleError = (e: string | Event) => {
isLoading.value = false
isError.value = true
emits('error', e)
}

const showFrame = () => {
IFrameManager.showFrame(uid, getFrameContainerRect())
}
const hideFrame = () => {
IFrameManager.hideFrame(uid)
}
const resizeFrame = useThrottleFn(() => {
IFrameManager.resizeFrame(uid, getFrameContainerRect())
})

const destroyFrame = () => {
IFrameManager.destroyFrame(uid)
}

const getFrame = () => {
return IFrameManager.getFrame(uid)
}

useResizeObserver(frameContainer, () => {
resizeFrame()
})

onBeforeUnmount(() => {
destroyFrame()
readyFlag = false
})

onDeactivated(() => {
if (props.keepAlive) {
hideFrame()
} else {
destroyFrame()
}
})

onActivated(() => {
if (props.keepAlive) {
showFrame()
return
}
if (readyFlag) {
createFrame()
}
})

watch(
() => [frameContainer.value, props.src],
(el, src) => {
if (el && src) {
createFrame()
readyFlag = true
} else {
destroyFrame()
readyFlag = false
}
},
{
immediate: true,
},
)

defineExpose({
getRef: () => getFrame()?.instance,
})
</script>


<style lang="scss" scoped>
.k-frame {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;

&-tips {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
</style>



看看效果


录制_2025_05_14_14_23_18_474.gif


小结


管理后台多页签切换,iframe 区操作进度丢失,根本原因在于 KeepAlive 缓存机制与iframe 的独立浏览上下文特性存在本质冲突。本文通过物理隔离视觉映射的双重策略,将 iframe 的真实 DOM 节点与Vue 组件实例解耦,实现了 keepAlive 的效果。

当然,该方案在代码实现还有很大优化空间,如 IFrameManager 目前是单例模式、Iframe 池未设计淘汰缓存机制(如 LRU )。嘀嘀嘀...产品催着上线了,没时间优化了,下次一定。

相关代码已上传只 github,欢迎道友们给个 star,在此谢过了


4aef7c55564e925846d4563bd982d158cebf4e55.gif


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

Tailwind 到底是设计师喜欢,还是开发者在硬撑?

web
我们最近刚把一个后台系统从 element-plus 切成了完全自研组件,CSS 层统一用 Tailwind。全员同意设计稿一致性提升了,但代码里怨言开始冒出来。 这篇文章不讲原理,直接上代码对比和团队真实使用反馈,看看是谁在享受,谁在撑着。 1.组件内样式...
继续阅读 »

我们最近刚把一个后台系统从 element-plus 切成了完全自研组件,CSS 层统一用 Tailwind。全员同意设计稿一致性提升了,但代码里怨言开始冒出来。


这篇文章不讲原理,直接上代码对比和团队真实使用反馈,看看是谁在享受,谁在撑着。




1.组件内样式迁移


原先写法(BEM + scoped):


<template>
<div class="card">
<h2 class="card__title">用户概览</h2>
<p class="card__desc">共计 1280 位</p>
</div>
</template>

<style scoped>
.card {
padding: 16px;
background-color: #fff;
border-radius: 8px;
}
.card__title {
font-size: 16px;
font-weight: bold;
}
.card__desc {
color: #999;
font-size: 14px;
}
</style>

Tailwind 重写:


<template>
<div class="p-4 bg-white rounded-lg">
<h2 class="text-base font-bold">用户概览</h2>
<p class="text-sm text-gray-500">共计 1280 位</p>
</div>
</template>

优点:



  • 组件直接可读,不依赖 class 定义

  • 样式即结构,调样式时不用来回翻


缺点:



  • 设计稿变了?全组件搜索 text-sm 改成 text-base

  • 无法抽象:多个地方复用 .text-label 变成复制粘贴




2.复杂交互样式


纯 CSS(原写法)


<template>
<button class="btn">提交</button>
</template>

<style scoped>
.btn {
background-color: #409eff;
color: #fff;
padding: 8px 16px;
border-radius: 4px;
}
.btn:hover {
background-color: #66b1ff;
}
.btn:active {
background-color: #337ecc;
}
</style>

Tailwind 写法


<button
class="bg-blue-500 hover:bg-blue-400 active:bg-blue-700 text-white py-2 px-4 rounded">

提交
</button>

问题来了:



  • ✅ 简单 hover/active 很方便

  • ❌ 多态样式(如 disabled + dark mode + hover 同时组合)就很难读:


<button
class="bg-blue-500 text-white disabled:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-700 hover:bg-blue-600 transition-all">

>
提交
</button>

调试时需要反复阅读 class 字符串,不能直接 Cmd+Click 查看样式来源。




3.统一样式封装,复用方案混乱


原写法:统一样式变量 + class


$border-color: #eee;

.panel {
border: 1px solid $border-color;
border-radius: 8px;
}

Tailwind 使用中经常出现的写法:


<div class="border border-gray-200 rounded-md" />

问题来了:



设计稿调整了主色调或边框粗细,如何批量更新?



BEM 模式下你只需要改一个变量,Tailwind 下必须靠 @apply 或者手动替换所有 .border-gray-200


于是我们项目里又写了一堆“语义类”去封装 Tailwind:


/* 自定义 utilities */
@layer components {
.app-border {
@apply border border-gray-200;
}
.app-card {
@apply p-4 rounded-lg shadow-sm bg-white;
}
}

最后导致的问题是:我们重新“造了个 BEM”,只不过这次是基于 Tailwind 的 apply 写法。




🧪 实测维护成本:100+组件、多人协作时的问题


我们项目有 110 个组件,4 人开发,统一用 Tailwind,协作两个月后出现了这些反馈:



  • 👨‍💻 A 开发:写得很快,能复制设计稿的 class 直接粘贴

  • 🧠 B 维护:改样式全靠人肉找 .text-sm.p-4,没有结构命名层

  • 🤯 C 重构:统一调整圆角半径?所有 .rounded-md 都要搜出来替换


所以我们内部的结论是:



Tailwind 写得爽,维护靠人背。它适合“一次性强视觉还原”,不适合“结构长期型组件库”。





🔧 我们后来的解决方案:Tailwind + token 化抽象


我们仍然使用 Tailwind 作为底层 utilities,但同时强制使用语义类抽象,例如:


@layer components {
.text-label {
@apply text-sm text-gray-500;
}

.btn-primary {
@apply bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded;
}

.card-container {
@apply p-4 bg-white rounded-lg shadow;
}
}

模板中统一使用:


<h2 class="text-label">标题</h2>
<button class="btn-primary">提交</button>
<div class="card-container">内容</div>

这种方式保留了 Tailwind 的构建优势(无 tree-shaking 问题),但代码结构有命名可依,后期批量维护不再靠搜索。




📌 最终思考


Tailwind 是给设计还原速度而生的,不是给可维护性设计的。
设计师爱是因为它像原子操作;
开发者撑是因为它把样式从结构抽象变成了“字串组合游戏”。


如果你的团队更在意开发效率,样式一次性使用,那 Tailwind 非常合适。
如果你的组件系统是要长寿、要维护、要被多人重构的——你最好在 Tailwind 之上再造一层自己的语义层,或者别用。


分享完毕,谢谢大家🙂


📌 你可以继续看我的系列文章



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

项目经理被裁那天,没人替他说话

简单写个自我介绍。 我在团队里算不上的管理层,没有人事权,也不决定谁走谁留,但我参与的项目一旦出问题,最后都会落到我这里。进度卡住、需求反复、线上事故,会议上可以绕一圈再绕一圈,最终还是要有人把代码改完、把锅兜住。我清楚自己的位置,用武之地比较多的一个“多功能...
继续阅读 »

简单写个自我介绍。


我在团队里算不上的管理层,没有人事权,也不决定谁走谁留,但我参与的项目一旦出问题,最后都会落到我这里。进度卡住、需求反复、线上事故,会议上可以绕一圈再绕一圈,最终还是要有人把代码改完、把锅兜住。我清楚自己的位置,用武之地比较多的一个“多功能开发者”而已。还依稀记得,当初总部另外一个项目的入侵测,找的还是我,而不是再招一个人。


所以我很少参与评价人,只谈事,谈事实,谈项目是怎么一步步偏离轨道的。


先直接告诉大家结果吧,他被辞退了。


当初公司决定要不要这个人的时候,无意中跟我提到过。我只讲述了几个项目事故,以及其他同事和他合作时的态度。至于留不留他,我不想决定,也决定不了。


那个项目经理,其实并不“坏”。他不吼人,也不甩脸色,会议纪要写得很勤,群里回复永远是“收到”“我跟一下”。问题出在另一层面:需求变更没有留痕,风险评估永远是“可控”,节点延期总能找到外部理由。


上面看到的是一条被不断抚平的曲线,下面看到的是每天被推翻重来的开发计划。我们不是没提醒过,只是提醒被整理成了更好看的版本,再往上递的时候,已经失去了原本的锋利。当然,这些最后都会被归结为一句话——开发同学多努努力,多扩展下思维,补补这个缺点就好了。


但我认为,有些问题其实在内部一直被反复提起,只是从来没有被真正放到台面上说过。


团队里的其他项目经理,大多都有过开发背景。哪怕代码早就不写了,对功能复杂度、实现成本、技术边界心里都是有尺度的。评估的时候会留余量,也知道什么时候该踩刹车。


只有他完全没有开发经验,对一个需求的理解停留在“看起来不难”的层面。既怕自己显得不专业,又怕在会上被认为拖进度,于是每次评估都偏向最激进的版本,功能报得满,时间压到极限。


开发这边明知道不现实,那又怎么办呢?你能说得过他吗?况且领导也是只看结果,活干得快,公司赚得多,干得慢赚得少。所以开发也只能硬着头皮往前推。


一次延期还能解释成意外,两次三次之后,延期就成了默认选项。项目表面上在跑,实际上每一步都在透支客户的耐心。


他甚至能把一个月的功能,压成 7 个工作日。


结果显而易见。项目连夜上线,第二天直接崩溃:APP、小程序白屏,数据无法保存,ToC 的用户一个都打不开。我们凌晨 4 点发完版本,早上 6 点半问题出现,7 点钟起床开始处理。


我起床的时候就已经料到了。项目有他管控着,您就放一万个心吧,麻烦肯定少不了。


当时写功能的时候,有个同事请了丧假。他来了句逆天发言:“到时候你能把电脑带上吗?有事可以找你。”


我当时真想告诉他,兄弟,全公司不是只有他一个前端,这个项目也不是只有他一个前端。人家就请假 3 天,已经很紧张了,还让人把电脑带着,真特么丧良心。


真正的转折点,是那次 A 项目上线。


我没有提任何人的名字,也没有用情绪化的词,只是把时间线拉直:哪一天确认需求,哪一天推翻,哪一天出 PRD,哪一天出 UI,最终导致了什么结果。那份文档写得很长,不好读,也不“体面”,但它有一个特点——每一个问题,都自然地指向了同一个岗位职责。


我提交的时候,甚至没多想,只觉得这次总算把事情说清楚了。


事后我想过,如果我当初不写那份复盘,不跟领导说这些事,会不会结果不同。答案大概是否定的。项目不会因为沉默变好,问题也不会因为不点名而消失。


那天没人替他说话,并不是因为他人缘差,而是因为在那个位置上,他已经很久没有为任何人、任何结果,真正说过一句“这是我的责任”。


系统从来不需要情绪,它只是在某个时刻,停止了包容。


我后来也明白了一件事:在很多公司里,项目经理这个角色,本质上是一个缓冲层。缓冲需求、缓冲压力、缓冲管理层的焦虑。


但一旦缓冲只剩下过滤,没有承担,系统就会重新校准。


那天被裁的不是一个人,而是一种失效的角色设计。而这件事,迟早会发生在任何一个不再为结果站出来的位置上。


作者:狗头大军之江苏分军
来源:juejin.cn/post/7598174154665623587
收起阅读 »

✨ 前端实现打字机效果的主流插件推荐

web
🎯 总结对比 插件名体积自定义动画丰富推荐场景TypeIt中很强很丰富高级动画、官网Typed.js小一般常用足够个人/博客/主页t-writer.js中很强丰富多样动画ityped极小一般简单极简、加载快 1️⃣ TypeIt(超强大,推荐!) 🎉 特点...
继续阅读 »

🎯 总结对比


插件名体积自定义动画丰富推荐场景
TypeIt很强很丰富高级动画、官网
Typed.js一般常用足够个人/博客/主页
t-writer.js很强丰富多样动画
ityped极小一般简单极简、加载快



1️⃣ TypeIt(超强大,推荐!)



🎉 特点:高自定义、易用、支持暂停、删除、换行等丰富动画



安装:


npm install typeit
# 或直接用 CDN

简单用法:


<div id="typeit"></div>
<script src="https://cdn.jsdelivr.net/npm/typeit@8.8.3/dist/typeit.min.js"></script>
<script>
new TypeIt("#typeit", {
strings: ["Hello, 掘金!", "我是打字机效果~"],
speed: 100,
breakLines: false,
loop: true
}).go();
</script>



2️⃣ Typed.js(最流行的打字机插件)



🚀 轻量、简单、社区大,支持多字符串轮播



安装:


npm install typed.js
# 或 CDN

用法:


<span id="typed"></span>
<script src="https://cdn.jsdelivr.net/npm/typed.js@2.0.12"></script>
<script>
new Typed('#typed', {
strings: ['欢迎来到掘金!', '一起学习前端吧~'],
typeSpeed: 80,
backSpeed: 40,
loop: true
});
</script>



3️⃣ t-writer.js(国人开发,API友好)



🔥 支持多种打字动画,API 设计简洁



安装:


npm install t-writer.js

用法:


<div id="twriter"></div>
<script src="https://cdn.jsdelivr.net/npm/t-writer.js/dist/t-writer.min.js"></script>
<script>
const target = document.getElementById('twriter');
const writer = new TypeWriter(target, {
loop: true,
typeColor: 'blue'
})
writer
.type('你好,掘金!')
.rest(500)
.changeOps({ typeColor: 'orange' })
.type('打字机效果轻松实现~')
.rest(1000)
.clear()
.start()
</script>



4️⃣ ityped(极简小巧)



⚡️ 零依赖,体积小,适合极简需求



安装:


npm install ityped

用法:


<div id="ityped"></div>
<script src="https://unpkg.com/ityped@1.0.3"></script>
<script>
ityped.init(document.querySelector("#ityped"), {
strings: ['Hello 掘金', '前端打字机效果'],
loop: true
})
</script>



🛠️ 补充:用原生 JS 实现简单打字效果


如果你不想引入第三方库,也可以用 setTimeout/async 实现基础打字动画:


<div id="simpleType"></div>
<script>
const text = "Hello, 掘金!这是原生JS打字机效果~";
let i = 0;
function typing() {
if (i < text.length) {
document.getElementById('simpleType').innerHTML += text[i];
i++;
setTimeout(typing, 100);
}
}
typing();
</script>



🌟 结语



  • 需要高级动画,选 TypeIt/t-writer.js

  • 需要轻量简单,选 Typed.js/ityped

  • 只需基础效果,也可以原生 JS 10 行搞定!


作者:前端九哥
来源:juejin.cn/post/7497801626670546984
收起阅读 »

“全栈模式”必然导致“质量雪崩”!和个人水平关系不大

在经济下行的大背景下,越来越多的中小型企业开始放弃“前后端分离”的人员配置,开始采用“全栈式开发”的模式来进行研发费用的节省。 这方法真那么好吗? 作为一名从“全栈开发”自我阉割成“前端开发”的逆行研发,我有很多话想说。 先从一个活生生的真实案例开始吧。 我...
继续阅读 »

在经济下行的大背景下,越来越多的中小型企业开始放弃“前后端分离”的人员配置,开始采用“全栈式开发”的模式来进行研发费用的节省。



这方法真那么好吗?


作为一名从“全栈开发”自我阉割成“前端开发”的逆行研发,我有很多话想说。


先从一个活生生的真实案例开始吧。


我认识一个非常优秀的全栈开发,因为名字最后一个字是阳,所以被大家称为“阳神”。


1. “阳神”的“神狗二相性”


阳神当然是牛逼的。


他不仅精通后端开发,更是对前端了解的非常深。这样来说吧:



当他作为后端开发时,他可以是那群后端同事里库表设计最清晰,代码最规范,效率最高的后端。




当他作为前端开发时,他除了比几位高级别前端稍逊一点外,效率和UI还原性都非常高,还会主动封装组件减少耦合。



但是非常奇怪的事情总是会发生,因为一旦阳神不是全职的“后端”或者“前端”时,一旦让他同时操刀“后端+前端”开发任务,作为一名“全栈”来进行业务推进时,他的表现会让人感到惊讶:



他会写出设计糟糕,不规范,职责混乱的代码。



这个现象我把他戏称为“阳神”的“神狗二相性”,作为单一职责时他是“阳神”,同时兼任多职时,他就有非常大的可能降格为“阳狗”。



为什么呢?这是阳神主观上让自己写更糟糕的代码吗?


不是的兄弟,不是的。


这是系统性的崩塌,几乎不以人的意志为转移。换我去也是一样,换你去也是一样。


2. 分工粗化必然导致技术细节的差异


从前,在软件开发的古老行会里,一个学徒需要花很多年才能出师,专门做一把椅子,或者专门雕一朵花。现在,你被要求从伐木到抛光,从结构力学到表面美学,全部一手包办。


生产力在发展,对人的技能要求也在发展。


因此“分工细化”成为了工业革命之后完全不可逆的趋势。


IT 产业上也是如此。


“软件开发”经过多年被细化出了前端开发后端开发客户端开发大数据开发 等等多种不同的细分职业。


但是现在有人想通过 粗化 职业分功来达到 “提效” 的目的,在我眼中这就是和客观规律对着干。


人的精力是守恒的。当你需要同时关心useEffect的依赖数组会不会导致无限渲染,和kubectl的配置能不能正确拉起Pod时,你的注意力就被稀释了。你不再有那种“针对一个领域,往深里钻,钻到冒油”的奢侈。


当你脑袋里冒出了一个关于前端工程化优化的问题时,身为全栈的你会本能地冒出另一个念头:



在整个全栈体系内,前端工程化优化是多么边角料且无关痛痒的问题啊,我去深入研究和解决它的性价比实在太低了,算了不想了。



如此一来,无论是后端的性能问题还是前端的性能问题都会变得无关紧要。




结果是,只有业务问题是全栈开发要关心的问题。



2. “岗位对立”与“自我妥协”


在日常开发中,前端开发和后端开发之间互相吐槽争论是再正常不过的话题,而且争论的核心非常简单易懂:



前端:这事儿不能在后端做吗?




后端:这事儿前端不能做吗?



可以的,兄弟,最后你会发现都是可以的,代码里大部分的事情无论是在浏览器端完成还是在服务器里完成都是可行的。


但是,总有一个“哪方更适合做”吧?



  • 一个大屏页面的几万几十万条的数据统计,是应该后端做还是前端做?

  • 业务数据到Echarts展示数据的格式转换应该后端做还是前端做?

  • 用户数据权限的过滤应该后端做还是前端做?

  • 一个列表到底要做真分页还是假分页?

  • 列表已经返回了全量实体信息,为什么还要再增加一个详情接口?


这都是日常开发时前端和后端都会去争论思考的问题,身处不同的职位,就会引入不同的立场和思考。



  • 前端需要去思考页面刷新后状态的留存,js单线程下大量数据处理的卡顿,页面dom树爆表的困境。

  • 后端也需要思考并发下服务器资源和内存的分配,可能的死锁问题,以及用户的无状态token如何处理等。


前后端的“争吵”和观点输出是不可避免的。


真理总是越辩越清晰的,后续讨论出的结果多半是最有利于当前现状的。


但如果“前后端”都是同一个人呢?


全栈模式,完美地消灭了这种“有益的摩擦”。当你自己和自己联调时,你不会给自己提挑战灵魂的问题。你不会问:“这个API设计是否RESTful?”因为你赶时间。你也不会纠结:“这个组件的可访问性够好吗?”因为你还得去部署服务器。



这两种思想在你的大脑里打架,最终往往不是最优解胜出,而是最省事的那个方案活了下来。



于是,你的代码里充满了“差不多就行”的妥协。这种妥协,一两个无所谓,当成百上千个“差不多”堆积起来时,质量的基础就酥了。


内部摩擦的消失,使得代码在诞生之初就缺少了一道质量校验的工序。它顺滑地流向生产环境,然后,在某个深夜,轰然引爆。


3. 工程的“不可能三角”


软件开发领域有一个著名的“不可能三角”:



快、好、省,你只能选两样。




全栈模式,在管理者眼中,完美地实现了“省”(一个人干两个人的活)和“快”(省去沟通成本)。那么,被牺牲掉的是谁?


雪崩时,没有一片雪花是无辜的。但更重要的是,当结构性雪崩发生时,问责任何一片雪花,都意义不大。


至于“快、好、省”这三兄弟怎么选?


那主要看老板的认知和他的钱包了。


作者:摸鱼的春哥
来源:juejin.cn/post/7555387521451606068
收起阅读 »

断网、弱网、关页都不怕:前端日志上报怎么做到不丢包

web
系列回顾(性能·错误·埋点三部曲):不做工具人|从 0 到 1 手搓前端监控 SDK 在前三篇文章中,我们搞定了性能、行为和错误的采集。但有掘友在评论区灵魂发问:“数据是抓到了, 发不出去有啥用?进电梯断网了咋办?页面关太快请求被掐了咋办?” 今天这篇,我们就...
继续阅读 »

系列回顾(性能·错误·埋点三部曲):不做工具人|从 0 到 1 手搓前端监控 SDK


在前三篇文章中,我们搞定了性能、行为和错误的采集。但有掘友在评论区灵魂发问:“数据是抓到了,
发不出去有啥用?进电梯断网了咋办?页面关太快请求被掐了咋办?”


今天这篇,我们就来聊聊如何上报数据



  • 用什么方式上报最稳、最省事?

  • 什么时候上报最合适?

  • 遇到断网/弱网/关页怎么兜底?



一、上报方式与策略:如何选出最优解?


我们平时上报数据主要有三种方式:Image(图片请求)sendBeaconXHR/Fetch


1. 三种上报方式详解


1. GIF/Image


这招就是利用图片请求(new Image().src)来传数据。



  • 原理很简单:把要上报的数据拼在 URL 后面(如 https://log.demo.com/gif?id=123),浏览器发起请求,服务器解析参数拿到数据,然后返回一张 1×1 的透明GIF图(体积小、看不见),浏览器收到后触发 onload 回调即完成上报。

  • 特点:天然支持跨域,绝无“预检”请求(因为是简单请求)。

  • 局限:只能发 GET 请求,URL 长度有限(通常 < 2KB),无法携带大数据。


2. sendBeacon



  • 原理navigator.sendBeacon(url, data)。浏览器会将数据放入后台队列,即使页面关闭了,浏览器也会尽力发送

  • 特点:异步非阻塞(不卡主线程),且可靠性极高

  • 局限:数据量有限(约 64KB),且无法自定义复杂的请求头。


3. XHR / Fetch


普通的网络请求。



  • 原理:使用 XMLHttpRequestfetch 发送 POST 请求。

  • 特点容量极大(几兆都没问题),适合发送录屏、长堆栈。

  • 局限:跨域时通常会触发 OPTIONS 预检(成本高),且页面关闭时请求容易被掐断(fetch需配合 keepalive)。


: 所谓的预检,就是浏览器在发送跨域且非简单请求前,先偷偷发个 OPTIONS 问服务器:“大佬,我能发这个请求吗?”。只要你用了自定义 Header 或 application/json 就会触发。这会导致请求量直接翻倍,在弱网下多一次往返就多一分失败的风险。


2. 策略篇:如何组合使用?


怎么选并不是随意决定的,而是为了解决两个核心痛点:



  1. 成本问题(CORS 预检):所谓的预检,就是浏览器在发送跨域且非简单请求前,先偷偷发个 OPTIONS 问服务器:“大佬,我能发这个请求吗?”。



    • 什么时候触发? 只要你用了自定义 Header(如 X-Token)或者 Content-Type: application/json,就会触发。

    • 后果是啥? 请求量直接翻倍,弱网下成功率腰斩。

    • 避坑指南:这也是为什么很多监控SDK通常都故意使用 text/plain 来发送 JSON 数据。虽然数据格式是 JSON,但告诉浏览器“这是纯文本”,就能骗过预检,直接发送!



  2. 存活问题(页面卸载):用户关闭页面时,浏览器通常会直接掐断挂起的异步请求,导致“临终遗言”发不出去。


基于这两个维度,我们将三种方式排个序,也就形成了我们的降级策略


1. 首选方案:sendBeacon(六边形战士)


这是现代浏览器的首选方案



  • 优势:专为监控设计,页面关闭了也能发(浏览器将其放入后台队列)。

  • 特点:容量适中(~64KB),且通常不触发预检,完美平衡了“存活”与“成本”。

  • 适用:绝大多数监控事件。


2. 降级方案:GIF/Image(老牌救星)


sendBeacon 不可用(如 IE)或数据极小的时候用它。



  • 优势天然跨域,绝无预检。利用 new Image().src 发起请求,服务器返回一张 1x1 透明图即可。

  • 特点:兼容性无敌,但数据量受 URL 长度限制(~2KB),且页面关闭时发送成功率低。

  • 适用:PV、点击、心跳等轻量指标。


3. 兜底方案:XHR / Fetch


只有前两招搞不定时(数据量太大)才用它。



  • 优势:容量极大,适合传录屏、大段错误堆栈。

  • 劣势:跨域麻烦(需配 CORS),有预检成本。

  • 注意:使用 Fetch 时务必加 keepalive: true,告诉浏览器“就算页面关了也别杀我”,尽量提升卸载时的成功率。


选型对比表


方案跨域/预检卸载可靠性数据容量核心优势适用场景
sendBeacon支持 / 无预检中 (~64KB)关页也能发,不占主线程首选,大多数监控事件
GIF/Image支持 / 无预检小 (~2KB)兼容性强,无预检降级方案,PV/点击/心跳
XHR/Fetch需 CORS / 有能传大数据错误堆栈、录屏



总结我们的代码套路(降级策略):



  1. 小包(< 2KB,单条事件):优先 sendBeacon;若不支持,再走 Image GET(附 _ts 防缓存)。

  2. 中包(≤ 64KB)sendBeacon 为首选;若不支持,回退到 Fetch/XHRContent-Type: text/plain + keepalive: true

  3. 大包(> 64KB)Fetch/XHR 承载,必要时拆包分批发送。


下面是封装好的 transport上报函数,直接拿去用:


const REPORT_URL = 'https://log.your-domain.com/collect'; 
const MAX_URL_LENGTH = 2048;
const MAX_BEACON_BYTES = 64 * 1024;

function byteLen(s) {
try {
return new TextEncoder().encode(s).length;
} catch (e) {
return s.length;
}
}

/**
* 通用上报函数
* @param {Object|Array} data - 上报数据
* @returns {Promise<void>} - 成功 resolve,失败 reject
*/

function transport(data) {
const isArray = Array.isArray(data);
const json = JSON.stringify(data);

return new Promise((resolve, reject) => {
// 1. 优先尝试 sendBeacon
// 注意:sendBeacon 是同步入队,返回 true 仅代表入队成功,不一定是发送成功
if (navigator.sendBeacon && byteLen(json) <= MAX_BEACON_BYTES) {
const blob = new Blob([json], { type: 'text/plain' });
// 如果入队成功,直接 resolve(乐观策略)
if (navigator.sendBeacon(REPORT_URL, blob)) {
resolve();
return;
}
// 如果入队失败(如队列已满),不 reject,而是继续往下走降级方案
console.warn('[Beacon] 入队失败,尝试降级...');
}

// 2. 单条小数据尝试 Image (GET)
if (!isArray) {
const params = new URLSearchParams(data);
params.append('_ts', String(Date.now()));
const qs = params.toString();
const sep = REPORT_URL.includes('?') ? '&' : '?';

if (REPORT_URL.length + sep.length + qs.length < MAX_URL_LENGTH) {
const img = new Image();
img.onload = () => resolve(); // 成功
img.onerror = () => reject(new Error('Image 上报失败')); // 失败
img.src = REPORT_URL + sep + qs;
return;
}
}

// 3. 兜底方案:Fetch > XHR
if (window.fetch) {
fetch(REPORT_URL, {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: json,
keepalive: true, // 关键:允许页面关闭后继续发送
})
.then((res) => {
if (res.ok) resolve();
else reject(new Error(`Fetch 失败: ${res.status}`));
})
.catch(reject);
} else {
// IE 兼容
const xhr = new XMLHttpRequest();
xhr.open('POST', REPORT_URL, true);
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) resolve();
else reject(new Error(`XHR 失败: ${xhr.status}`));
};
xhr.onerror = () => reject(new Error('XHR 网络错误'));
xhr.send(json);
}
});
}

二、上报时机:不阻塞主线程干扰业务,断网了也不丢数据


1. 调度层:区分优先级,关键时刻不等待


不是所有数据都适合“攒着发”。我们需要根据重要程度将日志分为两类:



  • 即时上报(Immediate):收集到立即上报。



    • 场景:JS 报错阻断了流程、用户点击了“支付”按钮、接口返回 500 等。

    • 原因:这些数据对实时性要求极高,或者关系到监控系统的报警(比如线上白屏了,你得马上知道),不能因为攒着发而耽误了。



  • 批量上报(Batch):攒一波再发。



    • 场景:用户点击、滚动、性能指标、API 成功日志。这类数据量大但实时性要求低

    • 策略“量”与“时”双重触发(竞态关系)。比如:攒够 10 条立马发(防止堆积太多),或者每隔 5 秒发一次(防止数量不够一直不发)。




代码怎么写?其实就是一个简单的双保险调度器


let queue = [];
let timer = null;
const QUEUE_MAX = 10;
const QUEUE_WAIT = 5000;

function flush() {
if (!queue.length) return;

// 1. 把当前队列的数据复制出来
const batch = queue.slice();

// 2. 清空队列与定时器
queue.length = 0;
clearTimeout(timer);
timer = null;

// 3. 利用空闲时间发送(性能优化点)
if ('requestIdleCallback' in window) {
requestIdleCallback(() => transport(batch), { timeout: 2000 });
} else {
// 降级兼容
setTimeout(() => transport(batch), 0);
}
}

function report(log, immediate = false) {
// 1. 紧急情况:绕过队列,直接发
if (immediate) {
transport(log);
return;
}

// 2. 普通情况:进入队列(如 点击、PV)
queue.push({ ...log, ts: Date.now() });

// 3. 检查触发条件(双重保险)
if (queue.length >= QUEUE_MAX) {
flush();
} else if (!timer) {
timer = setTimeout(flush, QUEUE_WAIT);
}
}

// 4. 临终兜底:页面关闭/隐藏时,强制把剩下的都发走
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') flush();
});
window.addEventListener('pagehide', flush);

整体思路:队列暂存 + 多重触发


我们用一个数组(queue)来暂存日志,然后通过 “量够了”、“时间到了”或“页面要关了” 这三个时机来触发发送,确保既不积压也不频繁打扰服务器。


性能优化:闲时优先


发送时,我们首选 requestIdleCallback。告诉浏览器你先忙你的(渲染、响应点击),等你有空了再帮我发监控数据



  • 这样能最大限度减少对业务主线程的阻塞,让用户感觉不到监控的存在。

  • 当然,如果浏览器不支持这个 API,我们再降级用 setTimeout 兜底。


2. 容灾层:断网了,日志怎么办?


如果在电梯里断网了或者弱网环境下,请求发不出去怎么办?日志丢了怎么办。
我们的策略是 “先记在本子上,等有网了再补交作业”



  1. 断网时:把日志存到 localStorage 里(注意设置上限,别把用户浏览器撑爆了,可用IndexedDB优化)。

  2. 连网时:监听 online 事件,把存的日志拿出来,分批发给服务器(别一次性全发过去,容易把后端打挂)。


具体怎么判断有没有网呢?


通常我们用 navigator.onLine 来看。如果返回值是 false ,那肯定是没网,直接存本地。


但坑就坑在,这玩意儿有时候会 “撒谎” —— 比如连上了酒店 WiFi 但没登录,或者宽带欠费了。这时候它虽然显示 true (在线),但其实根本上不了网。


所以咱们得留一手:
哪怕它说“在线”,我们也先试着上报一下。 要是报错了发不出去,别管三七二十一,先把这条日志存本地保底(千万别丢数据),然后再去 Ping 一下看看到底是不是真断网了 ,顺便更新一下网络状态。这样最稳。


1. 网络状态的检测


NetworkManager这个模块专门负责盯着网络,它很聪明,只有在发送日志失败的时候才会去复核网络真伪。


const NetworkManager = {
online: navigator.onLine,

// 初始化:盯着系统的 online/offline 事件
init(onBackOnline) {
window.addEventListener('online', async () => {
// 别高兴太早,先看看是不是真的能上网
const realWait = await this.verify();
if (realWait) {
this.online = true;
onBackOnline(); // 真的回网了,赶紧补传!
}
});
window.addEventListener('offline', () => this.online = false);
},

// “测谎仪”:发个 HEAD 请求看看
async verify() {
try {
// 请求个 favicon 或者 1x1 图片,只要响应了说明网通了
await fetch('/favicon.ico', { method: 'HEAD', cache: 'no-store' });
return true;
} catch {
return false;
}
}
};

2. 核心上报:能发就发,不行就存本地


上报函数现在变得非常有弹性。


export async function reportData(data) {
// 1. 如果明确知道没网,直接存本地 (省一次请求)
if (!NetworkManager.online) {
saveToLocal(data);
return;
}

// 2. 尝试发送
try {
await transport(data);
} catch (err) {
console.error('上报请求失败:', err);

// 3. 不管是因为断网、超时、还是服务器挂了
// 只要没成功,第一件事就是存本地!保证这条日志不丢!
saveToLocal(data);

// 4. 然后再来诊断网络,决定后续策略
// 只有当是网络层面的错误(如 fetch throw Error)才去怀疑网络
// 如果是 500 错误,其实网是通的,不用 forceOffline
if (isNetworkError(err)) {
// 5. Ping 确认
NetworkManager.verify().then(res => NetworkManager.online = res);
}
}
}

/**
* 判断是否为网络层面的错误
*/

function isNetworkError(err) {
// 原生 fetch 的网络错误通常是 TypeError: Failed to fetch
// 如果是使用 Axios,则可以通过 !err.response 来判断
return err instanceof TypeError || (err.request && !err.response);
}

const RETRY_KEY = 'RETRY_LOGS';
const RETRY_MAX_ITEMS = 1000;
function saveToLocal(data) {
const raws = localStorage.getItem(RETRY_KEY);
const logs = raws ? JSON.parse(raws) : [];
logs.push(data);
if (logs.length > RETRY_MAX_ITEMS) {
logs.splice(0, logs.length - RETRY_MAX_ITEMS);
}
localStorage.setItem(RETRY_KEY, JSON.stringify(logs));
}

3. 补传逻辑:别把服务器干崩了


等到网络恢复,本地攒了一堆“欠账”,千万别一股脑儿全发过去(万一本地存了 500 条,一次全发会把服务器打爆的)。


我们要有节奏地补传:


async function flushLogs() {
let logs = JSON.parse(localStorage.getItem('RETRY_LOGS') || '[]');
if (!logs.length) return;

console.log(`[回血] 发现 ${logs.length} 条欠账,开始补传...`);

while (logs.length > 0) {
// 1. 每次只取 5 条,小碎步走
const batch = logs.slice(0, 5);

try {
// 2. 调用上报中心
await transport(batch);

// 3. 只有成功了,才把这 5 条从 logs 里剔除
logs.splice(0, 5);
localStorage.setItem(RETRY_LOGS, JSON.stringify(logs));
} catch (err) {
// 4. 如果失败了(断网或服务器挂了)
// 此时 logs 里面还保留着那 5 条数据,所以不用担心丢失
// 记录一下状态,直接跳出循环,等下次 NetworkManager 唤醒
console.error('补传中途失败,保留剩余欠账');
break;
}

// 2. 歇半秒钟,给正常业务请求让个道
await new Promise(r => setTimeout(r, 500));
}
}

三、总结与实战建议


监控上报这事儿看着不难,其实门道不少。要在数据不丢不打扰用户之间找平衡,咱们得来一套“组合拳”:



  1. 上报方式sendBeacon 为主,Image 为辅,XHR/Fetch 兜底。利用 sendBeacon 的特性解决页面卸载时的丢包问题,利用 Image 解决跨域预检的成本问题。

  2. 上报时机闲时上报 + 批量打包。利用 requestIdleCallback 不占用主线程,通过队列机制减少 HTTP 请求频次。

  3. 断网处理本地缓存 + 网络侦测。断网时将数据持久化到 LocalStorage,待网络恢复后分批补传,确保“一条都不丢”。


最后,给开发者的 3 个避坑小贴士:



  • 不要迷信 navigator.onLine:它只能判断有没有连接到局域网,不能判断是否真的能上网。一定要配合实际的请求探测。

  • 控制补传节奏:网络恢复后,千万别一次性把积压的几百条日志全发出去,这属于“DDoS 攻击”自家服务器。要分批、甚至加随机延迟发送。

  • 隐私与合规:上报数据前,务必对敏感信息(如 Token、用户手机号)进行脱敏处理,这是红线。


如果你有更好的思路,欢迎在评论区交流!


作者:不一样的少年_
来源:juejin.cn/post/7596247009815412762
收起阅读 »

为什么你的 Prompt 越写越长,效果却越来越差?

前言 大语言模型(LLM)在早期阶段主要以对话机器人的形式出现,用户通过自然语言向模型提问,模型返回一段看似智能的文本结果。这一阶段,模型能力的发挥高度依赖用户如何提问,同一个问题,用不同的描述方式,往往会得到质量差异巨大的结果。 在这种背景下,提示词工程作为...
继续阅读 »

前言


大语言模型(LLM)在早期阶段主要以对话机器人的形式出现,用户通过自然语言向模型提问,模型返回一段看似智能的文本结果。这一阶段,模型能力的发挥高度依赖用户如何提问,同一个问题,用不同的描述方式,往往会得到质量差异巨大的结果。


在这种背景下,提示词工程作为一门面向大语言模型的输入设计方法论逐渐成型,本篇文章主要帮助大家快速了解提示词工程的本质以及在书写技巧。


什么是提示词工程?


提示词工程的本质就是在有限上下文窗口内,最大化模型输出的确定性与可用性,减少模型自由发挥的空间。简单来说,就是提供一种提示词书写范式来确保大模型能够精准地按照用户的要求输出高质量的内容。


❌ 差提示词


帮我做一个个人待办清单页面

对于这段提示词,AI 不知道用什么技术、什么风格、要哪些功能、有什么限制。结果就是 AI 自由发挥,生成的代码和项目规范不符合。


✅ 好提示词


## 角色
你是一个擅长 React 和用户体验设计的前端开发者。

## 背景
我需要做一个个人待办清单网页,用来记录每天的待办任务,现在已经完整基本功能的开发。

## 任务
实现任务的"拖拽排序"功能,让用户可以通过拖拽调整任务顺序。

## 要求
- 拖拽时被拖动的任务半透明
- 放置位置有明显的视觉指示线
- 拖拽完成后顺序立即更新

## 约束
- 技术栈为 React + TypeScript + Tailwind CSS
- 不使用第三方拖拽库(如 react-beautiful-dnd)
- 用原生 HTML5 拖拽 API
- 代码要有详细注释,我是拖拽 API 的初学者

## 输出格式
完整的 React 组件代码,包含:
1. 组件文件(TypeScript
2. 关键逻辑的中文注释
3. 简单的使用说明

提示词常见问题


在实际使用中,提示词的质量参差不齐,以下是几类最常见的问题及其本质原因。


信息量过多


我想让 AI 帮我做一个待办清单应用,于是将所有想法一次性列出:


帮我做一个待办清单应用,要有添加任务、删除任务、编辑任务、
标记完成、设置优先级、设置截止日期、分类标签、搜索功能、
数据统计、导出功能,还要有暗黑模式,最好能同步到云端,
支持多设备使用,界面要好看,用 React 写,要有动画效果。

问题本质:无结构、无重点。AI 可能会忽略关键信息,输出与某些要求冲突。


信息量太少


我想让 AI 帮我写个按钮组件,只有一句需求,没有任何背景:


帮我写一个按钮组件

问题本质:缺少上下文。上下文可能是:



  • 项目的技术栈

  • 按钮需要的功能

  • 期望的样式风格


最终 AI 只能给一个通用的结果。


没有目标


我想让 AI 帮我优化代码,但不给优化目标:


帮我优化一下这段代码:
[粘贴了一段代码]

问题本质:只有动作,没有结果。优化指代不明确——是体积、性能还是可读性?最终输出的结果必然不符合预期。


没有约束


我想让 AI 帮我写一个输入框组件,但是没有添加相关约束:


帮我写一个输入框组件。

技术栈:React + TypeScript + SCSS

问题本质:AI 容易引入额外的假设,例如:



  • 使用不兼容的 ui 组件库,例如 antd。

  • 使用复杂的状态管理机制。


导致最终输出不可控,隐性引入错误假设。


提示词模板


了解了常见问题后,我们需要一套结构化的方法来避免它们。这就是提示词模板的价值所在。


提示词的困扰


我们现在知道在使用 AI 时,提供的上下文越清晰,AI 给出的回答就会越符合预期。但是每次写提示词的时候,我们大概率还是会陷入这样的状态:



我要先给 AI 指定一个角色,告诉他背景和任务,还有约束、要求、技术栈……约束要包含什么内容?要求要写什么?技术栈要放到哪里?



这会给 AI 使用者带来很大的认知负担,我们同时要思考"说什么"和"怎么说"。而模板的价值,就是把"怎么说"变成固定格式,让你专注于"说什么"


这与前端的开发框架(React/Vue)很类似:在没有框架之前,开发者既要关注业务,同时还需要关注 DOM 更新及性能问题;随着框架的推出,前端开发者能把更多的精力放到业务功能开发上。


模板目标


提示词模板的目标是减少使用者的思考负担并提高 AI 输出的稳定性。但模板并不是终极目标,因为固定的模板反而会限制灵活性。因此在不同阶段、不同场景,使用者可以对模板进行调整:


阶段做法
初级阶段严格按框架填写,确保不遗漏
熟练阶段根据任务复杂度简化或扩展
高手阶段框架内化成直觉,自然地组织信息

推荐模板


模板结构:


主题必填程度作用
角色必须让 AI 成为某个领域的专家
背景必须让 AI 提前了解任务的背景知识
任务必须告诉 AI 要做什么
要求推荐告诉 AI 任务完成的标准
约束推荐为 AI 划定边界,防止自由发挥
格式可选告诉 AI 最终输出内容的格式
示例可选用实际的例子告诉 AI 要怎么做

模板示例:


## 角色
你是一个擅长 React 和组件开发的的前端开发者。

## 背景
我使用 react 开发了一个基础组件库,里面包含了xx个组件,组件名称如下xxx。

## 任务
帮我为每个组件生成一份 mdr 文件,表示该组件的使用详细说明。

## 要求
- 文档要包含组件的 API、使用示例、xxx。

## 约束
- 使用 md 语法。
- 必须保证 API 的完整,不能漏掉内容。

## 输出格式
Title: 组件名称

description: 组件描述

API:
xxx

Examples:
xxx

## 示例

Title: Alert

description: 警示组件

API:

| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| action | 自定义操作项 | ReactNode | - | 4.9.0 |
| afterClose | 关闭动画结束后触发的回调函数 | () => void | - | |
| banner | 是否用作顶部公告 | boolean | false | |


进阶提示词技巧


Few-shot Prompting


Few-shot 的核心思想就是给 AI 几个例子,让他先按照例子学习,理解任务处理流程及最终的内容输出。这种模式能够更高效的让 AI 理解用户的意图,这和人学习新东西一样,直接看示范比读文档更高效。


任务:为 React 组件生成 TypeScript Props 类型定义

示例1
组件描述:一个显示任务标题的组件,标题必填,可选显示完成状态
输出:
interface TaskTitleProps {
title: string; // 任务标题,必填
isCompleted?: boolean; // 完成状态,可选
}

示例2
组件描述:一个按钮组件,显示文字必填,点击事件必填,可选禁用状态
输出:
interface ButtonProps {
label: string; // 按钮文字,必填
onClick: () => void; // 点击事件,必填
disabled?: boolean; // 禁用状态,可选
}

示例虽然能够更高效的帮助 AI 理解任务,但是过多的示例也会加大 token 的消耗,因此示例不是越多越好,要遵循少而精的原则,通过 2~5 个例子将典型的场景、多样性场景以及边界场景列举出来。


Chain of Thought


Chain of Thought 的核心思想是告诉 AI 让他一步步思考推理,输出推理内容,而不是直接给答案。就像解数学题一样,把解题的每一步都写出来,这样往往能让 AI 输出更准确的答案。


## 角色
你是一个擅长 React 和组件开发的的前端开发者。

## 背景
我使用 react 开发了一个基础组件库,里面包含了xx个组件。

## 任务
帮我给修改的 react 组件补充新的单测。

### 修改点分析步骤
- 分析组件的 props,找出新增/删减/修改的参数,并输出出来。
- 分析组件的内部逻辑,找出新增/删除/修改的逻辑,并输出出来。


这种模式在复杂的场景中会大大提升输出效果,但是也存在一些局限:



  • 输出内容的长度会大大增加,增加 Token 的消耗。

  • 对于某些简单任务,强行使用该模式可能反而会降低效果。

  • 如果推理的过程中某一步出错,可能会导致接下来步骤都会出错,需要配合 Self-Critique 来检查。


Self-Critique


与人类一样,AI 并非总能在首次尝试时就生成最佳输出,Self-Critique 的核心思想是在 AI 生成内容之后,让 AI 再自我检查一遍,发现并修复问题。这个和我们考试答题一样,做完之后再检查一遍往往能发现遗漏的细节或者写错的题。


## 角色
你是一个擅长 React 和组件开发的的前端开发者。

## 背景
我使用 react 开发了一个基础组件库,里面包含了xx个组件。

## 任务
帮我给修改的 react 组件补充新的单测。

## 修改点分析步骤
- 分析组件的 props,找出新增/删减/修改的参数,并输出出来。
- 分析组件的内部逻辑,找出新增/删除/修改的逻辑,并输出出来。

## 要求

- 生成完之后,请严格自查
- 是否覆盖了所有修改点。
- 每个修改点是否覆盖了所有边界情况(空值、空字符串、只有空格)

这种模式下会让 AI 扮演一个审查者的角色重新审下生成的内容,提高内容的准确性,但是也有它的局限:



  • 增加 Token 的消耗,这种模式更推荐在复杂的场景中使用。

  • AI 可能出现自我认可的偏差,认为输出是没问题的,此时需要严格给 AI 设定审查者的角色


总结


本文围绕「提示词工程」展开,从背景、核心目标、常见问题、结构化模板,到 Few-shot、Chain of Thought、Self-Critique 等进阶技巧,系统性地说明了一件事:



提示词工程的本质,不是“如何把话说得更漂亮”,而是如何通过结构化上下文,降低大模型输出的不确定性。



然而,这种提示词优化的思路也带来了新的工程问题:提示词越来越长、结构越来越复杂,最终直接反映为 Token 体积的持续膨胀


因此,在实际工程中,提示词优化并不等同于写得越详细越好,而是需要在信息充分性与 Token 成本之间取得平衡。如何控制上下文规模、避免无效信息堆积、并在复杂任务中持续提供刚刚好的上下文,成为提示词工程之后必须面对的核心问题。


参考资料



作者:西陵
来源:juejin.cn/post/7595808703074074650
收起阅读 »

Linux再添一员猛将,操作完全不输Windows!

提到 Zorin OS 这个操作系统,可能不少喜欢折腾 Linux 系统的小伙伴之前有尝试过。 作为一款以 UI 交互和颜值著称的 Linux 发行版系统,Zorin OS 也曾一度被广大爱好者们称为 Windows 系统的开源替代方案。 Zorin OS ...
继续阅读 »

提到 Zorin OS 这个操作系统,可能不少喜欢折腾 Linux 系统的小伙伴之前有尝试过。


作为一款以 UI 交互和颜值著称的 Linux 发行版系统,Zorin OS 也曾一度被广大爱好者们称为 Windows 系统的开源替代方案



Zorin OS 旨在简单易用,用户无需学习任何新知识即可上手,同时 Zorin OS 作为一款 Linux 发行版系统,专为从 Windows 迁移的用户设计,提供类似 Windows 的图形界面与操作逻辑,并且支持一键切换为 Windows 系统风格



前段时间,Zorin OS 团队在其官博正式宣布,最新的 Zorin OS 18 已经正式突破了 100 万次下载。


并且据官博数据显示,这些下载中有超过 78% 是来自于 Windows 系统的用户,这也再次印证了其可以满足从 Windows 桌面系统迁移到 Linux 发行版的用户需求。



作为一个长期关注 Linux 桌面系统的博主,其实这次 Zorin OS 18 大版本更新刚出来那会我就关注了,不过一直没有抽出时间来写文章、来梳理,所以今天这篇文章正好把这件事情给安排了!


总体来讲,这次的 Zorin OS 18 是以 Ubuntu 24.04 LTS 为基础并由 Linux 6.14 内核提供支持。


并且这次的 Zorin OS 18 是继之前 17 版本以来的一次大版本迭代,带来了诸多新特性和改进。


所以接下来我们也来梳理一下这次 Zorin OS 18 所带来的一些重点更新和变化。


视觉与交互进化


众所周知,Zorin OS 一直以来都以其独特的个性和简约的美学设计风格而著称。


那这次更新后的新外观给人最直观的感受就是圆润和通透。



任务栏这一次采用了全新的悬浮圆角面板设计,不再是死板地贴在屏幕边缘,而是像 macOS 的控制中心一样有一种轻盈的漂浮感。



另外这一次大版本还推出了新主题颜色,新增了黄色和棕色两种主题色,视觉层次更加丰富。


选中元素的色调更加淡雅,背景和侧边栏颜色更深,长时间盯着屏幕写代码或处理文档,眼睛会舒服很多。



另外 Pro 版里还提供了更多可切换的桌面布局。





除此之外,很多经常使用的日常应用也进行了诸多设计调整和改进。


比如文件管理器的侧边栏重新设计了,操作控件更直观,搜索功能支持了全文搜索,找文件效率大增。


日历应用增加了侧边栏,月份和事件视图也一目了然。


相机应用也做了更新,新相机应用界面简洁,支持多摄像头切换,这对于现在动不动就开视频会议的环境非常友好。


Web 应用深度集成


对于用户来说,最大的痛点往往不是系统本身,而是数据迁移和应用生态,那 Zorin OS 18 在这方面下了不少功夫。


首先就是与 Web 应用程序无缝集成。


众所周知,现在很多应用都构建在云端,这些渐进式 Web 应用与原生应用之间的用户体验正逐渐融合。


这次 Zorin OS 18 全新内置的「Web Apps」工具非常强大,它可以将 Web 应用转换为桌面应用,用户的 Web 应用将可以显示在开始菜单中,使用起来与原生应用无异。



「Web Apps」工具可以作为后端与各种热门 Web 浏览器集成,同时也允许用户自定义对应 Web 应用内的体验。


多任务处理:原生窗口平铺


这次 Zorin OS 18 的多任务处理变得好用多了。


Zorin OS 18 引入了一款功能强大的窗口平铺管理器,它能帮助用户更高效地工作,同时上手起来也十分简单。



用户只需要把窗口拖到屏幕顶部,系统就会自动弹出布局选择器。


预设布局支持左右分屏、三栏布局、角落停靠等,同时在智能建议这块,系统也可以根据用户当前所打开的窗口,智能推荐最佳的排列组合。


除此之外它还支持高度自定义,创建用户自己的平铺布局。


这个新特性无论对新手还是资深玩家都非常直观易用,从而定制和提升每个用户的生产力。


迁移神器:Windows 应用支持


用户可以从内置的软件商店发现适用于 Zorin OS 系统的各类应用,这是在 Zorin OS 中安装应用的推荐方式。


其软件商店可让用户开箱即用地从 Zorin OS 与 Ubuntu APT 仓库、Flathub 以及 Snap Store 安装应用。



而如果用户是刚从 Windows 转过来,看到满硬盘的 .exe 安装包肯定会头疼。


Zorin OS 18 的处理方式非常聪明。


系统内置了一个庞大的软件数据库(覆盖超过 170 款软件),当用户双击一个 Windows 安装包(如 setup.exe)时,系统不会直接报错,而是弹出一个友好的对话框。


如果有 Linux 原生版本,它就会引导你安装原生版本应用;如果没有原生版的话,它就会推荐你使用 Web 版,或者利用兼容层运行。



在兼容层优化这一块,Zorin OS 18 深度集成了 Wine,对于一些必须在 Windows 下运行的行业软件或游戏,它提供了一个“Windows 应用支持”层。虽然不能保证 100% 兼容,但对于很多老旧的 .exe 工具,它能让你在不装虚拟机的情况下应急使用。



性能与硬件支持


Zorin OS 18 基于 Ubuntu LTS 版本打造,同时它将获得直到 2029 年的稳定安全更新。



同时官方宣称它甚至可以在十几年前的古董机上流畅运行。最低配置仅需 1GHz 双核 CPU、2GB 内存。



同时从用户安装的实际表现来看,在现代硬件上,它的动画流畅度非常高,即便在老机器上,它运行起来也比 Windows 系统更加轻快。


写在最后


那以上就是关于此次 Zorin OS 18 大版本更新的一些梳理和总结,感兴趣的小伙伴也可以去体验一波。


总的来看,这次的 Zorin OS 18 不仅仅是一个 Linux 发行版,也像极了一个操作系统迁移解决方案。


另外这次 Zorin OS 18 的发布,也使得 Linux 桌面系统的易用性又向前迈进了一步。


文章的最后也期待 Linux 桌面系统在未来能百花齐放,发展得越来越好。


好了,那以上就是今天的内容分享了,希望能对大家有所帮助,我们下篇见。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



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

AI驱动的大前端开发工作流

web
在日常的大前端需求开发中,我们常常需要同时兼顾UI还原和业务逻辑两部分工作。UI方面,就是要尽可能细致地还原设计稿上的每个细节;业务逻辑方面,则往往和需求复杂度以及项目代码规模正相关。今天,我们就来聊聊如何利用AI驱动,提升需求实现过程中的效率和体验。 UI设...
继续阅读 »

在日常的大前端需求开发中,我们常常需要同时兼顾UI还原和业务逻辑两部分工作。UI方面,就是要尽可能细致地还原设计稿上的每个细节;业务逻辑方面,则往往和需求复杂度以及项目代码规模正相关。今天,我们就来聊聊如何利用AI驱动,提升需求实现过程中的效率和体验。


UI设计稿还原


如今市面上已经有不少做得不错的AI设计稿转代码工具,比如v0、bolt.new、codefun等。对于一些个人独立项目来说,这些工具真心牛逼:只需传入设计稿,就可以快速生成模块化、精确的UI代码,而且还能通过对话式的交互来不断调整细节,直到完全符合预期。


但如果把这些工具直接应用于一个成熟项目中,就会暴露一些问题。首先,目前大部分工具主要支持vue和react,而我们的项目往往还涉及flutter、android、iOS等多端开发。其次,成熟项目中往往都有一套独特的代码规范和UI组件库,而这些工具生成的代码往往并不了解这些细节,直接将生成的代码拷贝进项目之后还得二次调整,额外的成本不可忽视。


那么有没有办法既能支持更多编程语言,又能在生成UI代码时就结合项目中已有的规范和组件库,从而减少二次调整成本呢?答案是有的!


目前,figma是市面上使用比较主流的设计稿工具。Builder.io最近发布了一款基于figma设计稿、AI驱动的前端代码生成插件——Visual Copilot。使用它,你只需要把figma切换到Dev Mode,然后选择设计稿中的任意图层,接着在Visual Copilot中直接导出代码。



Visual Copilot生成的代码不仅支持vue、react,还涵盖了其它几乎所有主流的大前端语言和框架,十分全面。你可以实时预览生成的效果,并进行细节调整,直到满意为止。



生成代码后,如何让这些代码完美融入到我们的项目规范中呢? 这里就需要结合Visual Copilot与Cursor的配合来实现。


具体做法是:当Visual Copilot将设计稿图层转化成代码后,它会自动生成一个可远程执行的工作空间,并提供相关命令(图中红框所示)将工作空间的代码集成到我们项目中。


接下来,我们只需在Cursor的Terminal中运行指定指令,就能连接Visual Copilot的远程工作空间,实时获取生成的代码,同时通过交互指令界面明确我们后续的需求。



那么,最终生成的代码如何与项目中的各种规范结合呢? 这就要用到Cursor的AI规则机制——CusorRules。你只需在项目根目录下配置一个.cursorrules文件,声明你的UI代码规范以及封装的组件信息。Cursor在生成代码时就会充分参照这些规则。同时,新版Cursor还支持配置一系列rule文件,你可以通过正则路径来指定规则应用于不同的模块或文件类型,整个过程非常灵活高效(具体可以参与Cursor官方文档)。



业务逻辑开发


在一些中小型规模的项目中,如果想在Cursor中高效地实现业务逻辑,我们需要关注两个关键点:



  • CursorRule的应用

    和上文UI的规范梳理类似,我们还需要对项目各模块的架构规范等信息进行说明,这样可以让Curosr生成的代码能尽量保障和我们项目规范的一致性。

  • 需求拆解:先构建框架,再处理细节

    对于较为复杂的需求,我们可以先把实际需求拆解成多个阶段。首先,构建大致的需求框架,并转换成一系列对Cursor友好的Prompt指令(要求简单、准确)。在Cursor的Compose Agent模式下,通过这些指令生成整体的业务逻辑框架。接下来,再利用Cursor Tab在编辑器区域快速完善那些生成时不够精准的细微逻辑。


在一些中小型项目中,通过以上两个关键点我们通常可以快速完成业务逻辑开发。但在一些大型项目中,会遇到Cursor生成的代码经常不尽如人意的问题。这主要是因为项目越大,整个代码库的复杂性增加,Cursor对项目的理解难度也随之上升,每次修改都可能会有不确定因素,提示词如果不能准确描述需要改动的部分,就容易出错。


此时,我们可以尝试另一种思路:用”软件架构师“与”开发者“角色区分需求规划与执行细节。应该怎么做呢?


我们可以将AI驱动的开发流程分为两个阶段:



  1. 软件架构师角色

    这个角色负责对需求进行高层次的分析和解决方案设计,帮助我们总结、提炼和润色需求中的关键信息,同时生成说明性的提示词。这些提示词会告诉我们:这个需求需要创建哪些文件、修改哪些文件、如何做修改等等。

  2. 开发者角色

    开发者则负责把架构师给出的高层次解决方案转化为具体代码。也就是说,开发者依据架构师生成的详细提示词来生成或修改具体文件,从而实现精确的改动,避免大范围理解失误带来的问题。


这种方式的好处在于,只要“软件架构师”生成的提示词足够精准(即它能详细说明需要修改的文件、具体的改动内容和改动范围),那么“开发者”便能够依照这些提示词精确地进行代码修改,极大降低了因大范围理解产生的错误风险。所以这里的性能瓶颈就在于如何定义”软件架构师“角色以及让其接到需求后能生成精确的提示词。


在实际操作中,我们引入了一个“项目地图”的概念。所谓项目地图,就是为大型项目构建一个完整的文档体系(借助AI辅助生成也完全可行),其中包括了项目架构设计、开发流程、模块划分与用途、文件名和其功能说明等内容。这套文档体系可以独立于一个实际的Cursor项目存在,充当“软件架构师”的角色。也就是说,当遇到bug或新需求时,我们可以通过咨询这个“项目地图”,让AI回答问题并给出相对准确的修改思路。


举个例子,拿知名的fast-api项目来说,我为它生成了一个项目地图,其中包含了核心概念说明、系统设计、项目规范以及各模块和文件的用途说明等内容:



同时,在该项目的 .cursorrule 文件中,我详细规定了“软件架构师”如何根据实际需求生成精确的修改指令:


## 项目背景信息
项目名称:FastAPI
项目类型:Python Web 框架
项目地图:参考 fileNames.md
架构文档:参考 architecture/ 目录
编码规范:参考 guidelines/coding-standards.md

## 需求分析模板
1. 需求描述
[简要描述需要实现的功能或修改]

2. 涉及组件
- 核心组件:[列出受影响的核心组件]
- 依赖组件:[列出相关的依赖组件]
- 测试组件:[列出需要修改的测试]

3. 修改范围
- 主要文件:[列出需要修改的主要文件]
- 次要文件:[列出可能需要修改的次要文件]
- 文档文件:[列出需要更新的文档]

4. 技术要点
- 使用的框架特性:[列出需要使用的 FastAPI 特性]
- 数据验证:[描述数据验证要求]
- 兼容性考虑:[描述向后兼容性要求]

5. 潜在风险
[列出可能的风险点和注意事项]

## 执行指导模板

### 给大模型的执行指导

1. 修改步骤
[详细的步骤说明]

2. 验证点
[列出需要验证的关键点]

4. 测试建议
[提供测试建议和用例]


## 实际案例:添加用户电话号码字段

### 1. 需求分析

需求描述:
在用户模型中添加可选的 phone_number 字段,并在相关 API 端点中支持该字段。

涉及组件:
- 核心组件:用户模型(UserIn, UserOut, UserInDB)
- 依赖组件:无
- 测试组件:用户相关测试

修改范围:
- 主要文件:/docs_
src/extra_models/tutorial001.py
- 次要文件:无
- 文档文件:API 文档可能需要更新

技术要点:
- 使用 Pydantic BaseModel
- 字段类型:Union[str, ]
- 保持向后兼容性

潜在风险:
- 确保不破坏现有的数据验证
- 保持与现有字段风格一致


### 2. 执行指导
给大模型的具体修改指导:

1. 修改步骤:
   a. 在 UserIn 模型中添加 phone_
number 字段
   b. 在 UserOut 模型中添加对应字段
   c. 在 UserInDB 模型中添加对应字段
   d. 确保字段定义与 full_name 保持一致的风格


2. 验证点:
   - 字段是可选的
   - 类型提示正确
   - 与现有代码风格一致
   - 不影响现有功能

3. 测试建议:
   - 测试不提供电话号码的情况
   - 测试提供有效电话号码的情况
   - 测试提供无效数据的情况

## 使用说明

1. **分析阶段**
   - 使用项目背景信息了解上下文
   - 按需求分析模板进行分析
   - 生成详细的执行指导

2. **交付阶段**
   - 将执行指导传递给大模型
   - 确保指导清晰且可执行
   - 包含必要的验证点

3. **验证阶段**
   - 检查实施结果
   - 确认所有要求都已满足
   - 验证代码质量和一致性

4. **输出**
   - 输出执行指导,包括修改步骤、验证点和测试建议,新建analysis目录,在该目录下用一个独立的文件承载,文件名要求符合问题描述诉求<cursor-tools Integration>
# Instructions

基于这些配置,当提交需求时,AI生成的修改思路大致如下,它会精准指出应对哪些文件进行哪些具体改动。


# 需求分析

## 1. 需求描述
在高并发场景下,针对大量用户同时请求数据库导致的性能瓶颈,需要在应用程序中增加缓存机制,降低数据库查询次数,提升整体响应速度和系统稳定性。

## 2. 涉及组件
- 核心组件:数据查询相关业务逻辑(可能涉及CRUD操作的函数或类)
- 依赖组件:可能需要引入第三方缓存库(如 redis、in‐memory 缓存等)
- 测试组件:与数据库查询逻辑相关的测试用例

## 3. 修改范围
- 主要文件:核心业务逻辑所在模块(例如:/app/database.py 或 /app/services/xxx.py)
- 次要文件:如果需要封装缓存工具,则可能新增 /app/utils/cache.py
- 文档文件:如果提供额外的缓存配置,需要更新相关文档

## 4. 技术要点
- FastAPI 配合缓存解决方案(如 in‐memory 缓存、Redis、memcached 等)
- 选择合适的缓存失效策略(LRU、TTL、手动失效等)
- 线程安全、进程安全或分布式缓存方案
- 保证与现有数据校验和返回结构的一致性

## 5. 潜在风险
- 引入缓存后数据一致性问题(可能出现短时间的数据延迟)
- 缓存过期策略不当导致性能问题或脏数据
- 多节点部署时的缓存同步与一致性
- 第三方库的稳定性和可维护性

---

# 执行指导

## 1. 修改步骤
1. 确定缓存方案(例如:Redis 或者内存缓存),并根据业务需求配置失效策略。
2. 在核心数据库查询逻辑中添加缓存查询逻辑:
   - 查询前先在缓存中查找数据;
   - 如果未命中或缓存过期,则查询数据库并将结果写入缓存;
   - 如果命中,则直接返回缓存结果。
3. 为关键业务接口添加缓存逻辑(可封装成一个装饰器,或在查询函数内部实现)。
4. 在配置文件或环境变量中添加缓存相关配置,如主机、端口、缓存大小、过期时间等。
5. 编写或修改测试用例,确保在开启缓存后仍能正确测试业务逻辑。

## 2. 验证点
- 并发请求多时,数据库查询数量显著减少
- 当缓存命中时,响应速度显著提升
- 缓存失效策略(TTL 等)按预期生效
- 高并发情况下是否存在数据不一致或缓存击穿/雪崩问题

## 3. 测试建议
- 正常请求:依次验证在缓存未命中和命中时的响应时间与结果正确性
- 并发请求:使用压力测试工具(locust、JMeter 等)模拟大量请求并观察数据库查询次数与响应时间
- 失效测试:设置短 TTL 并观察缓存自动失效后对系统性能的影响
- 异常测试:故意使缓存服务不可用或网络异常,验证系统能否正常回退到直接查询数据库

随后,我们再将这些详细的提示应用到原项目中,通过AI进一步生成或补全代码,最终大大提高了开发效率和代码准确性。只要前期对项目地图、架构角色和开发规则进行充分准备,我们就能充分借助AI,把原本耗时、易出错的开发流程变得高效且精准。


结语


显而易见,尽管上文主要探讨了大前端领域的AI工作流,但这种思路其实完全可以迁移到其他开发领域。只要我们不断尝试和实践,总结出符合自身业务特点的AI工作模式,就能极大提升我们的工作效率。无论是前端、后端,还是其他技术领域,AI驱动的开发流程都能帮助我们更加精准、高效地解决各类需求和问题。


总之,拥抱AI技术,不断优化工作流程,是我们应对快速变化、不断增长的项目复杂度的关键所在。未来,随着技术的进一步成熟和实践经验的积累,我们必将迎来一个更智能、更高效的开发时代。


作者:北极的树
来源:juejin.cn/post/7474100684374769698
收起阅读 »

前端人必懂的浏览器指纹:不止是技术,更是求职加分项

web
你有没有过这样的经历? 没登录淘宝逛了件卫衣,转头刷抖音、B 站,相似款式的推荐就精准找上门; 或者参与线上投票时,明明没注册账号,却提示 同一用户仅能投一次? 其实这背后藏着一个前端人绕不开的实用技术,浏览器指纹。哪怕你开着无痕模式、频繁切换网络,它依然一样...
继续阅读 »

你有没有过这样的经历?
没登录淘宝逛了件卫衣,转头刷抖音、B 站,相似款式的推荐就精准找上门;
或者参与线上投票时,明明没注册账号,却提示 同一用户仅能投一次?


其实这背后藏着一个前端人绕不开的实用技术,浏览器指纹。哪怕你开着无痕模式、频繁切换网络,它依然一样精准识别你,而这门技术,不仅是日常上网的隐形推手,更是前端求职面试中的高频考点


一、浏览器指纹:到底是怎么认出你的?


核心逻辑很简单:世界上没有完全相同的浏览器环境,就像没有两片一模一样的树叶。


浏览器指纹会收集一系列设备和环境特征,再通过算法组合成唯一的 哈希值,这个哈希值就是你的专属 网络标识。这些特征包括但不限于:



  • 基础信息:浏览器类型及版本 Chrome、Safari 等、操作系统Windows/macOS 等、屏幕分辨率、系统语言;

  • 硬件细节:CPU 核心数、内存大小、显卡型号;

  • 高级特征:Canvas 绘图差异 不同设备绘制同一图形,像素级有细微区别、WebGL 渲染信息、已安装字体列表;

  • 动态信息:IP 地址 虽可变,但结合其他特征仍有识别价值。


举个直观的例子:Canvas 是 HTML5 的绘图功能,我们用一段简单代码就能提取它的指纹可直接在浏览器控制台运行:


function getCanvasFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const text = 'frontend-fingerprint';

ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(0, 0, 100, 60);
ctx.fillStyle = '#069';
ctx.fillText(text, 2, 15);

return canvas.toDataURL();
}

function hashFingerprint(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0;
}
return hash;
}

const dataUrl = getCanvasFingerprint();
const fingerprint = hashFingerprint(dataUrl);
console.log('Canvas指纹结果:', fingerprint);

试着在不同浏览器、甚至不同设备上运行,你会发现每次得到的数值都不一样


这就是浏览器指纹的识别核心。


二、前端人必须掌握的应用场景


浏览器指纹不是 黑科技,而是前端开发、风控、产品设计中高频用到的技术,面试时遇到相关问题,能说清这些场景直接加分:



  1. 广告精准投放:跨平台识别用户兴趣,比如用户在 A 网站浏览电子产品,在 B 网站就能收到相关广告推送,核心是前端与后端的指纹匹配;

  2. 防刷防作弊:投票、抢券、秒杀等场景,通过指纹限制同一用户多次操作,前端需负责特征采集与校验逻辑;

  3. 风控与安全:检测恶意登录、账号盗刷,哪怕黑客换了 IP,浏览器环境特征不变仍能被识别,是前端安全模块的重要知识点;

  4. 区域限制检测:用 VPN 换 IP 后仍能被识别,就是因为浏览器指纹未变,这也是跨境相关产品的常见需求。


对于前端求职者来说,这些场景不仅是面试高频题,更是实际工作中可能遇到的开发需求。如果能在简历中体现对浏览器指纹的理解,或在面试中清晰拆解实现逻辑,很容易让面试官眼前一亮 ,但很多同学要么不懂核心原理,要么不知道怎么把技术点转化为面试优势。


四、最后说句实在的


浏览器指纹是前端领域的 实用 技术,懂它不仅能解决实际开发问题,更能成为求职路上的加分项。但求职不止是懂技术,更要会 表达技术, 简历怎么写才能脱颖而出?面试怎么说才能打动面试官?这些都需要技巧和经验。


如果你正在为前端求职发愁,想让自己的技术优势被看到不妨试试我的「前端简历面试辅导」和「前端求职陪跑」服务。从技术亮点提炼到面试答题技巧,从简历优化到 offer 谈判,我会全程帮你针对性提升,让你在众多求职者中脱颖而出,顺利拿下心仪岗位


作者:海云前端1
来源:juejin.cn/post/7592789801708257321
收起阅读 »

我创建了一个全 AI 员工的一人公司

大家好,我是 Sunday。 现在,用 AI 写代码,已经不算什么新鲜事了。写个组件、补个函数、改个 Bug,几句话交给模型就能搞定。 但是,大家有没有想过一个问题:如果我不只是用一个 AI,而是用一群 AI,会发生什么? 我们现在用 AI,本质上只是把它当成...
继续阅读 »

大家好,我是 Sunday。


现在,用 AI 写代码,已经不算什么新鲜事了。写个组件、补个函数、改个 Bug,几句话交给模型就能搞定。


但是,大家有没有想过一个问题:如果我不只是用一个 AI,而是用一群 AI,会发生什么?


我们现在用 AI,本质上只是把它当成“高级工具”,即:我遇到问题 → 我问 AI → 它给我答案。


那么如果我们把脑洞放大一点呢?


既然我可以调用一个 AI,那是不是也可以 同时调用多个 AI ?既然 AI 可以写代码,那它是不是也可以完成:产品经理、设计师、测试、运维,甚至是 财务、人事 的工作?


换句话说: 我能不能,用一群 AI,组建一家“只有我一个人类员工”的公司?


多个 AI,各自承担不同角色:




  • 产品负责拆需求:告诉产品我的需求,然后产品帮我把需求拆解成可以具体执行的步骤

  • 开发负责编码:把具体的步骤告诉开发,开发负责把整个功能落地

  • 测试负责验收:开发的代码,交给测试进行验收。验收失败则重新打回开发,开发负责修改 BUG

  • 运维负责部署:最终完成的项目,运维负责部署上线,构建整个自动化部署的流程

  • 财务负责算账:每天的 token 支出 和 营收,并给出更省钱的财务方案

  • 人事负责管理:哪个 AI 员工“不合格”,人事负责把它“开掉”,并根据我的需求重新“招聘(生成)”新的 AI 员工


并且,我还希望它们之间可以独立思考,互相沟通,而我只负责:下目标、做决策。


如果这件事真的可行,那意味着什么?意味着一个人,或许可能真的可以撬动一整家公司级别的生产力。


说干就干。


AI 选择


市面上的 AI 模型已经非常多了:Claude、GPT Coder、Gemini、GLM、DeepSeek。。。有的贵,有的便宜,有的擅长推理,有的擅长编码。


最理想的状态其实很明确:



  • 贵的 AI,承担复杂任务:产品设计、系统架构、核心编码

  • 便宜的 AI,承担简单任务:统计、对账、流程、输出财务报表


只要这些 AI 之间可以互相沟通,以上这些都不是问题。


但是,Sunday 在经过一系列的测试之后,发现了一个很残酷的事,就是 不同模型的 AI 之间完全无法沟通。特别是不同厂商的 AI,每一个都被作为了独立的个体。虽然可以通过“协调者”方式强行拼接,但是实现复杂,并且效果也不理想。


额。。。好像这个一人公司的梦想就直接破灭了...


不行,不能那么容易放弃。


所以,在第一阶段,我只能选择一个更“粗暴但稳定”的方案:全部使用 Claude。


因为 Claude 提供了一个非常关键的工具:Claude Code:一个运行在纯终端里的 AI 编码智能体。我们只需要打开终端就可以直接通过语言对话的方式调用 AI 模型。



而接下来,我要做的事情是:用 Claude Code,一步步搭出一家“AI 员工公司”的雏形。


启动多个 AI 终端


一个终端窗口作为一个员工,如果我们想要多个员工那么就只需要启动多个终端就可以。然后我们要给他们赋予角色。一开始,我们可以先从最小可行方案开始:



我们先制定两个角色,他们分别是:



  • 张三:产品经理

  • 李四:程序员


然后,就出现了 大型翻车现场....



在我尝试给 Claude 提示词,让他变成 张三 的时候。Claude 给我的回复是:我是 Claude,我不是张三....


不是,咱说好的玩角色扮演呢...你就这么不配合的吗?


因为在我的设想里,这一步应该是最简单、最“理所当然”的:起个名字、设个背景、分配个角色。然后 AI 就会自动进入对应的工作模式。


结果现实就是这么现实:“我能帮你完成工作,但是我就不承认我是张三,我就是 Claude


这不是能力问题,这赤裸裸的就是个 态度问题 啊! 看来 AI 也不想当牛马啊。。。


没办法,我只能尝试修改下提示词:



好的,产品经理已就位。然后我们可以从一个小的 todolist 的需求开始:



现在我们已经有了一个 todolist 的需求文档了。接下来最好的方案就是 产品经理的 AI 可以直接通知 程序员 AI,完成代码实现。


但是,可惜 不同窗口之间 AI 无法直接通讯。所以,我就必须要承担起这个通讯员的角色。



最终实现的效果如下:



整个功能完善、可用。并且还提供了 主题变化 的功能。。。


反思


可是如果我们仔细去分析上面整个流程,我们可以发现:这个流程远没有我们想象的那么顺畅。甚至有点 多此一举


因为以上这些需求完全可以在一个 Claude code 中完成,没有必要进行这样的划分。甚至可以说:在任务简单时,多 AI 协作不仅没有提升效率,反而增加了沟通成本。


而这样的一种调度+协作的方式,如果任务变得复杂了,恐怕 AI 没有乱呢,我们自己就已经先手忙脚乱了。


这让我开始怀疑:最初设想的这种全 AI 员工的方式,会不会从一开始,就是错的?


但是,当我仔细思考过之后,我发现问题不在于“多角色”这个想法本身,而在于:当前这种“多终端 + 人工调度”的协作方式,本质上是一个非常低效的协作模型。


如果我们换一个角度,从“协作工具”入手,情况可能会完全不同。


这时,我注意到了一个开源项目:vibekanban



它提供了一种非常典型的多人协作看板模式,和我们熟悉的 Teambition、Jira、Trello 几乎一致,包含了:任务可视化、状态流转清晰、每个角色只关注自己的列


teambition 的多人协作看板


vibekanban 的协作看板


通过 vibekanban 这种工具,我们至少可以做到:多任务并行更清晰、角色边界更稳定、协作过程可追踪、可回溯。


但即便如此,我很快又意识到一个更现实的问题:这,依然离“全 AI 员工的一人公司”,依旧差得很远。


全 AI 员工的难点在哪里?


根据 Sunday 的实验,其实我们可以发现,全 AI 员工的最大难点就是 如何高效的完成 AI 之间的自动协作


更具体一点说,核心难点只有一个:如何让多个 AI,在几乎没有人工干预的情况下,自动完成“上下文传递 + 状态对齐 + 任务接力”?


那么这个功能可以实现吗?


答案是 绝对可以


学习过咱们的 商业级 AI 课程 的同学都知道,在 AI 的持续对话中,我们可以通过记录上下文的方式完成跨角色的连续对话


那么同样的道理,如果我们可以记录不同 AI 沟通上下文的 关键内容,然后通过上述的同样逻辑呼叫下一个 AI(让代码通过指令自动传输关键上下文,并调起 AI 服务),那么全 AI 员工的功能就可以实现。只不过在这样的完美的流转过程中,我们还需要做很多的努力才可以。


总结


这一通折腾下来,收获还是很明显的。三个关键:



  1. 单模型,已经足够强

  2. 多模型,真正难的是系统设计

  3. “全 AI 员工”,本质是一个 调度系统 + 状态系统 + 协作协议 的问题


而这,也意味着:下一阶段的探索重点,已经不再是“哪个模型更强”,而是:如何设计一套真正可自动运行的“AI 组织系统”。


作者:程序员Sunday
来源:juejin.cn/post/7597355509747974170
收起阅读 »

32岁程序员猝死背后,我的一些真实感受

上午刷到32岁程序员周末猝死这条消息,其实我并不陌生。 这几年,程序员猝死、倒下、出事的新闻隔一段时间就会出现一次,圈子里的人早就麻木了。刷到的时候,最多叹口气,继续干活,很少真的往心里去。 看到他长期加班的细节时,我突然愣住了,因为太像了。 我也经常这样。 ...
继续阅读 »

image.png


上午刷到32岁程序员周末猝死这条消息,其实我并不陌生。


这几年,程序员猝死、倒下、出事的新闻隔一段时间就会出现一次,圈子里的人早就麻木了。刷到的时候,最多叹口气,继续干活,很少真的往心里去。


看到他长期加班的细节时,我突然愣住了,因为太像了。


我也经常这样。


下午刷到他的妻子和对他的聊天记录,真的感觉很无奈,可惜,他再也回不来了......


3d6157aefc573f83b1eef95b33b3559d.png




我到不是加班,我是下班后干自己的事情,我比较卷。只有下班后的时间是真正属于自己的时间才刚开始。没有人打扰,安静下来,我会干自己的事情、学习、写代码,一不留神就到了凌晨两点。


那一刻,我才真正能进入自己的状态。


学习也好,干活也好,哪怕只是安静地敲键盘,都让我觉得踏实。


于是,凌晨两点成了常态


通过他这件事,我看到我也在里面,看见了自己。




我上一次写代码写到很晚是前几周,老大让我开发一个知识库RAG系统。


给我了我一周的时间,其实对于我是有难度的,因为接触这块不久,一周时间的话肯定弄不好。


后来那天,我连夜和AI 一些协作搞了6个小时左右,搞到了凌晨3点多,初版搞的差不多,那会心率也有点高了, 有点难受,就赶快休息了...


image.png




我们这一代程序员,真的太累了。


这种累,不只是加班,而是一种长期被推着往前,却不敢停下来的状态


房贷在那儿。

家庭在那儿。

未来的不确定性在那儿


我们很清楚,一旦慢下来,就意味着风险。


于是我们学会了忍。

忍困、忍累、忍身体发出的各种提醒。




程序员这个职业有个很危险的地方。


身体开始出问题的时候,能力往往还在线


我们还能写代码,还能解决问题,还能在群里回一句:“好的,我看看”


所以你会误以为自己没事。


可身体不是系统,没有明显的报错提示。

等它真正崩的时候,往往没有给你回滚的机会。


今天刷到这个新闻消息对我来说,更像是一次提醒。我现在体检去,估计都是全红状态,我经常熬夜,现在锻炼的也少了....


我们这一代人,很努力。


努力工作,努力赚钱,努力让生活往前走。


可如果连身体都开始透支,那这条路,真的值得重新想一想。


不是他倒下了。

是我们这一代,真的太累了。


作者:程序员海军
来源:juejin.cn/post/7597701762905309230
收起阅读 »

我天,Java 已沦为老四。。

略想了一下才发现,自己好像有大半年都没有关注过 TIOBE 社区了。 TIOBE 编程社区相信大家都听过,这是一个查看各种编程语言流行程度和趋势的社区,每个月都有榜单更新,每年也会有年度榜单和总结出炉。 昨晚在家整理浏览器收藏夹时,才想起了 TIOBE 社区,...
继续阅读 »

略想了一下才发现,自己好像有大半年都没有关注过 TIOBE 社区了。


TIOBE 编程社区相信大家都听过,这是一个查看各种编程语言流行程度和趋势的社区,每个月都有榜单更新,每年也会有年度榜单和总结出炉。


昨晚在家整理浏览器收藏夹时,才想起了 TIOBE 社区,于是打开看了一眼最近的 TIOBE 编程语言社区指数



没想到,Java 居然已经跌出前三了,并且和第一名 Python 的差距也进一步拉开到了近 18%。



回想起几年前,Java 曾是何等地风光。


各种基于 Java 技术栈所打造的 Web 后端、互联网服务成为了移动互联网时代的中坚力量,同时以 Java 开发为主的后端岗位也是无数求职者们竞相选择的目标。


然而这才过去几年,如今的 Java 似乎也没有了当年那种无与争锋的强劲势头,由此可见 AI 领域的持续进化和繁荣对它的冲击到底有多大。


用数据说话最有说服力。


拉了一下最近这二十多年来 Java 的 TIOBE 社区指数变化趋势看了看,情况似乎不容客观。


可以明显看到的是一个:呈震荡式下降的趋势


Java语言的TIOBE社区指数变化


现如今,Java 日常跌出前三已经成为了常态,并且和常居榜首的 Python 的差距也是越拉越大了。


在目前最新发布的 TIOBE Index 榜单中排名前十的编程语言分别是:



  • Python

  • C++

  • C

  • Java

  • C#

  • JavaScript

  • Visual Basic

  • Go

  • Perl

  • Delphi/Object Pascal



其中 Python 可谓是一骑绝尘,与排名第二的 C++ 甚至拉开了近 17% 的差距,呈现了断崖式领先的格局。


不愧是 AI 领域当仁不让的“宠儿”,这势头其他编程语言简直是望尘莫及!


另外还值得一提的就是 C 语言。


最近这几个月 C 语言的 TIOBE Index Ratings 比率一直在回升,这说明其生命力还是非常繁荣的,这对于一个已经诞生 50 多年的编程语言来说,着实不易。


C 语言于上个世纪 70 年代初诞生于贝尔实验室,由丹尼斯·里奇(Dennis MacAlistair Ritchie)以肯·汤普森(Kenneth Lane Thompson)所设计的 B 语言为基础改进发展而来的。


C语言之父:丹尼斯·里奇


就像之前 TIOBE 社区上所描述的,这可能主要和当下物联网(IoT)技术的发展繁荣,以及和当今发布的大量小型智能设备有关。毕竟 C 语言运行于这些对性能有着苛刻要求的小型设备时,性能依然是最出色的。


说到底,编程语言本身并没有所谓的优劣之分,只有合适的应用场景与项目需求


按照官方的说法,TIOBE 榜单编程语言指数的计算和主流搜索引擎上不同编程语言的搜索命中数是有关的,所以某一程度上来说,可以反映出某个编程语言的热门程度(流行程度、受关注程度)。


而通过观察一个时间跨度范围内的 TIOBE 指数变化,则可以一定程度上看出某个编程语言的发展趋势,这对于学习者来说,可以作为一个参考。


Java:我啥场面没见过



曾经的 Java 可谓是互联网时代不可或缺的存在。早几年的 Java 曲线一直处于高位游走,彼时的 Java 正是构成当下互联网生态繁荣的重要编程语言,无数的 Web 后端、互联网服务,甚至是移动端开发等等都是 Java 的擅长领域。


而如今随着 AI 领域的发展和繁荣,曾经的扛把子如今似乎也感受到了前所未有的压力。


C语言:我厉兵秣马



流水的语言,铁打的 C。


C 语言总是一个经久不衰的经典编程语言,同时也是为数不多总能闯进榜单前三的经典编程语言。


自诞生之日起,C 语言就凭借其灵活性、细粒度和高性能等特性获得了无可替代的位置,就像上文说的,随着如今的万物互联的物联网(IoT)领域的兴起,C 语言地位依然很稳。


C++:我稳中求进



C++ 的确是一门强大的语言,但语言本身的包袱也的确是不小,而且最近这几年的指数趋势稳中求进,加油吧老大哥。


Python:我逆流而上



当别的编程语言都在震荡甚至下跌之时,Python 这几年却强势上扬,这主要和当下的数据科学、机器学习、人工智能等科学领域的繁荣有着很大的关系。


PHP:我现在有点慌



PHP:我不管,我才是世界上最好的编程语言,不接受反驳(手动doge)。




好了,那以上就是今天的内容分享了,感谢大家的阅读,我们下篇见。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



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

AI安全面临灵魂拷问:“意图篡改”怎么防?绿盟科技给你答案!

随着AI Agent规模化落地被按下“加速键”,其安全是否值得信任?意图篡改、调用链投毒、供应链漏洞、合规备案压力等问题,正成为企业AI落地路上的“绊脚石”。应势而生,绿盟科技召开以“清风拂境 · 智御全域”为主题的大模型安全创新成果线上发布会。发布会从分析A...
继续阅读 »

随着AI Agent规模化落地被按下“加速键”,其安全是否值得信任?意图篡改、调用链投毒、供应链漏洞、合规备案压力等问题,正成为企业AI落地路上的“绊脚石”。

应势而生,绿盟科技召开以“清风拂境 · 智御全域”为主题的大模型安全创新成果线上发布会。发布会从分析AI应用需求的变化入手,以体系化方案回应行业最迫切的大模型安全防护诉求,并重磅加码绿盟“清风卫”系列产品智能体安全能力,为各行业客户AI安全落地提供可落地、可验证的最新实战指南。


“意图博弈”威胁新起,AI安全红线在哪里?


绿盟科技高级安全研究员祝荣吉

2025年AI应用经历了从“对话助手”向“智能体”的能力跃迁,高速进步的背后暴露诸多隐患:智能体自主运行时,如何避免行为失控风险?自主智能体具备逻辑主权后,它的安全红线在哪里?随“智”而变,绿盟科技高级安全研究员祝荣吉介绍了AI能力演进与攻防焦点变化趋势。他基于Agent感知、规划、记忆、行动四大核心模块,针对性提出了“感知需净输入、规划需抗干扰、记忆需防污染、行动需控权限”的防御准则。

在攻防焦点的动态演进上,祝荣吉表示AI安全正由“内容检测”向“意图博弈”深度转向2024年聚焦“内容博弈”,重点攻坚对话框安全,解决模型“言多必失”的合规问题;2025年迈入“协议生态”,随MCP工具协议的普及,风险面由对话端延伸至业务系统,核心在于构建调用链的生态信任;2026年的安全重心将直面“意图主权”,严防攻击者通过劫持感知信息实现深层意图篡改与指令劫持。    

基于此演进趋势,会上正式发布AISS年度威胁关注矩阵。该矩阵纵向聚焦基座、数据、模型、应用、身份五大安全支柱,完成了从基础大模型到复杂Agent系统的风险透视。通过系统性梳理威胁的年度动态演进路径,矩阵旨在帮助企业在复杂多变的AI场景中精准识别风险优先级、锁定核心问题,真正实现从“盲目围堵”向“精准治理”的体系化演进与升级。

针对风险评估能力的落地,祝荣吉详细介绍了智能化红队评估的技术路径与方法论。绿盟科技依托动态数据集构建、智能风险判定及智能体业务信息探测等核心能力,通过与前沿攻防对抗方法的组合应用,实现了对MCP工具恶意利用、智能体意图劫持及预期外代码执行等新型风险的检测覆盖,真正将碎片化的红队经验转化为体系化的安全验证能力。


靠“补丁”没用,大模型安全如何实现“主动免疫”?


绿盟科技高级方案经理郝广宾

AI时代的安全,从来不是单点的“补丁式防护”,而是贯穿全流程的体系化工程,是整个AI生态的基石。绿盟科技高级方案经理郝广宾发表《“四道防线”守护大模型系统安全防护》的主题演讲,全面阐释了绿盟大模型系统安全方案,他提出“四道防线”纵深防御体系。该体系构建覆盖“开发、部署、运行”全流程的安全防护能力,以实现大模型从“被动响应”到“主动免疫”的安全升维,满足客户大模型系统安全合规应用与实战防护的双重需求。



【四道防线】实现“主动免疫”的安全升维

大模型系统开发阶段,打造“合规+校验”防线体系。要聚焦语料合规和组件安全,使用语料评估工具或服务,对全部训练数据、外挂知识库数据等进行清洗;优先采购部署经过备案的商业大模型服务,加强模型代码及组件完整性校验和安全测试,构建AI软件物料清单,剖析AI系统所依赖的各类组件,精准识别潜在三方供应链组件风险。

大模型系统部署阶段,构建“评测+加固”和“监测+防护”闭环自进化双道协同防线。让“评测”明确“防护”重点,“防护结果”反哺“模型评测”,打造“越用越聪明”的主动免疫体系。在大模型系统上线前,需围绕内容安全、对抗安全、AI红队、供应链等多维度开展安全评测,保障大模型系统安全上线;在大模型系统部署时,需围绕基础设施、模型、应用、数据等打造纵深防御,部署多级安全认证、多维联防围栏、原生应用防护、数据防泄漏等监测防护能力,打造特殊场景安全代答能力,守护大模型系统应用安全。

大模型系统应用运行阶段,优先加强大模型系统安全管理防线。从“监测预警”“应急处置”“供应链安全保障”“备案、标识双合规”等多维度开展大模型系统日常安全运营工作。


使用智能体接连踩坑,安全“防不住、查不清”?


绿盟科技高级产品经理李斌

基于对智能体安全风险的深度洞察,绿盟科技高级产品经理李斌围绕“资产管理、漏洞管理、运行时检测、MCP安全、数据安全、安全态势、安全审计”七大维度,详细介绍了覆盖智能体全生命周期的安全能力体系。发布会上,绿盟科技“清风卫”AI安全系列产品三大智能体安全组件全新亮相。

智能体资产与风险治理系统:支持对智能体核心组件(模型、工具、MCP、知识库、提示词等)进行细粒度发现与动态清点,构建资产与风险画像;

智能体运行时意图与行为安全防护:基于对智能体职责边界的AI建模,实时监测其与MCP、工具、外部系统的交互行为,实现对越权访问、数据泄露等风险的实时发现与自动阻断;

智能体红队测评与持续验证平台:依托AI红队测评引擎,基于智能体配置与业务场景生成针对性攻击用例,通过单轮与多轮对话模拟,深度挖掘潜在风险。

李斌强调,绿盟清风卫AI安全产品体系具备“平台化集成、场景化适配、自动化运营”三大特点,可灵活对接各类智能体开发平台与既有安全基础设施,为客户提供从开发态到运行态的一体化“监管控”能力。

从AI Copilot到AI Agent,从协作辅助到自主执行,大模型应用形态越深入业务核心,安全的重要性就越凸显。作为网络安全行业排头兵,绿盟科技始终秉承“巨人背后的专家”的使命,未来将持续跟踪AI应用风险与需求的变化,不断优化整体安全防护方案,升级产品和服务,为行业客户破解安全难题,让安全不再是AI创新的“顾虑”,而是驱动业务增长的“底气”!


收起阅读 »

2025 AI原生编程挑战赛收官,5500+战队攻关AIOps工程化闭环

1月14日, 由阿里云主办、云原生应用平台承办的“2025 AI原生编程挑战赛”圆满收官。历经2个多月的角逐,6支队伍从5500多支报名战队中脱颖而出,在云原生环境下跑通AIOps Agent核心技术闭环,成功晋级决赛。最终,来自汽车行业的企业级战队“V-AI...
继续阅读 »

1月14日, 由阿里云主办、云原生应用平台承办的“2025 AI原生编程挑战赛”圆满收官。历经2个多月的角逐,6支队伍从5500多支报名战队中脱颖而出,在云原生环境下跑通AIOps Agent核心技术闭环,成功晋级决赛。最终,来自汽车行业的企业级战队“V-AI”获得总冠军。

AI原生编程挑战赛由发展历程超过10年的“云原生编程挑战赛”升维而来。自2015年创办至今已连续举办十一届,累计吸引全球10余个国家和地区的96,000多支战队参与。

作为国内聚焦AI原生编程与运维场景融合的重磅赛事,本次大赛自启动就展现出“破圈”影响力,参赛选手遍布包括清华大学、中科院等在内的180多所国内外高校及120多家企业。大赛核心命题在于将大模型的推理潜能引入运维实战。选手基于部署在阿里云跨可用区的真实电商服务,通过官方提供的真实多模态可观测数据(Log、Metric、Trace、Entity、Event)构建AI驱动的智能运维Agent,实现对复杂云原生系统中未知故障的自动根因诊断。

为广邀全球开发者共赴“让天下没有难查的故障”的技术实践,大赛组委会提供了通过云监控2.0白屏化操作、通过SPL/SQL语句分析诊断、Workflow/Agent自动化三种解题路径,配以最小可复现步骤、示例查询与产出要求指导,帮助选手借助AI快速、准确、低成本地进行故障根因诊断,收获参赛作品超1000份。

总决赛现场,阿里云智能集团副总裁、基础设施事业部负责人蒋江伟,阿里云智能集团副总裁、市场营销部负责人刘湘雯为冠军战队“V-AI”颁奖。

蒋江伟表示,这次AI原生编程挑战赛见证了AI Agent在处理复杂运维问题上的潜力。选手们在大赛中释放出的创新活力与技术灵感,让我们看到AI与研发、测试与运维全链路的深度融合,正在为构建标准化、可规模化扩展的智能运维新范式夯实根基。

刘湘雯在祝贺获奖战队时指出,从云原生到AI原生,大赛的愿景随着技术的演进不断迭代。希望参赛开发者以本次大赛作为起点,继续勇敢破界,在实战中打磨,让更多创新构想精准落地。

来自华中科技大学计算机学院的“HUST-B507”战队及个人开发者战队“我就看看不参加” 分获亚军和季军,阿里云智能集团资深技术专家司徒放、云原生应用平台负责人周琦为获奖战队颁奖。

阿里云智能云原生应用平台运营负责人王荣刚、产品营销市场负责人陆俊为3支个人开发者战队“scaner”、 “皮卡丘的皮卡”、“那个男孩儿” 颁发优胜奖,鼓励选手在智能运维领域持续探索。

代表冠军战队V-AI分享的车企领域架构师朱迪表示:“工作中的大量IT运维工作,让我们面对提升效率、降低成本的挑战。在这次比赛中我们不仅提升了技术,也加深了对阿里云可观测产品的理解,加速解决实际故障的效率。通过比赛,我们更加相信AI与运维的融合是必然趋势。感谢组委会的支持,期待与阿里云继续携手共进,迎接更加智能的未来。”

多位参赛队伍及选手分享经验时提到,阿里云云监控2.0提供的产品和服务,为参赛提供了稳定的数据底座。其中,UModel作为云监控2.0的核心建模基础,提出基于图模型的统一可观测数据建模范式,不仅解决了传统可观测系统中“数据孤岛”、“语义割裂”、“建模复杂”等痛点,还为AI原生运维(AIOps)、智能根因分析、跨域关联等高级能力提供了结构化、可推理的数据底座,是阿里云为AI时代打造的运维世界本体,让可观测系统从“被动响应”走向“主动认知与优化”。

本次大赛的技术深度也赢得了学术界的关注,其技术逻辑与实验环境已获得中科院等知名高校机构认可,并被正式引入相关科研课题实践,为AIOps产业长期发展储备高质量人才。

阿里云智能资深技术专家、云原生应用平台负责人周琦表示,“AIOps编程挑战赛希望以大模型与AI技术为新起点,帮助开发者开启在Operation Intelligence广阔赛道上的探索,将传统依赖经验的‘老中医式’运维转变为智能化的问题解决能力,实现从被动响应向主动预测的升级。感谢各位参赛选手的创意和创新,和阿里云一同推动AIOps Agent的发展,创造智能运维的未来。”

大赛中沉淀的技术标准与人才生态将持续赋能企业向AI原生演进。阿里云将以云监控2.0为核心智能运维体系,帮助企业在AI时代以更智能、更高效、更低成本的方式构建全栈可观测体系。

收起阅读 »

大小仅 1KB!超级好用!计算无敌!

web
js 原生的数字计算是一个令人头痛的问题,最常见的就是浮点数精度丢失。 // 1. 加减运算 0.1 + 0.2 // 结果:0.30000000000000004(预期 0.3) 0.7 - 0.1 // 结果:0.6000000000000001(预期 0...
继续阅读 »

js 原生的数字计算是一个令人头痛的问题,最常见的就是浮点数精度丢失


// 1. 加减运算
0.1 + 0.2 // 结果:0.30000000000000004(预期 0.3)
0.7 - 0.1 // 结果:0.6000000000000001(预期 0.6)

// 2. 乘法精度偏移
0.1 * 0.2 // 结果:0.020000000000000004(预期 0.02)
3 * 0.3 // 结果:0.8999999999999999(预期 0.9)

// 3. 除法结果异常
0.3 / 0.; // 结果:2.9999999999999996(预期 3)
1.2 / 0.2 // 结果:5.999999999999999(预期 6)

在金额计算的场景中出现这种问题是很危险的,例如「0.1 元 + 0.2 元」本应等于 0.3 元,原生计算却会得出 0.30000000000000004 元,直接导致金额显示错误或支付逻辑异常。


不少人会用toFixed四舍五入,保留 2 位小数来格式化数字,它本质上是 字符串格式化工具,而非精度修复工具,而且还会带来新的精度问题 —— toFixed的四舍五入规则是 “银行家舍入法”,无法解决底层计算的精度误差。


// 问题1. 四舍五入规则不符合预期
1.005.toFixed(2); // 结果:&#34;1.00&#34;(预期 &#34;1.01&#34;)
2.005.toFixed(2); // 结果:&#34;2.00&#34;(同样问题)
1.235.toFixed(2); // 结果:&#34;1.23&#34;(预期 &#34;1.24&#34;)

// 问题2. 无法修复底层计算误差
const sum = 0.1 + 0.2; // 0.30000000000000004
sum.toFixed(2); // 结果:&#34;0.30&#34;(表面正确,但误差仍存在,后续再运算仍然有问题)
sum.toFixed(10); // 结果:&#34;0.3000000000&#34;(仅隐藏误差,未消除)

number-precision 能解决这些问题。


number-precision 的优势在哪?



  • 轻量化,大小仅 1kb

  • API 极简化,只有加减乘除四舍五入

  • 专注精度问题,无额外心智负担

  • 兼容性好,无额外依赖


适用场景



  • 中小型项目、仅需解决基础加减乘除精度问题的场景(如电商、金融类简单计算)

  • 对包体积敏感的前端项目。


如何使用?


pnpm install number-precision

import NP from 'number-precision'

NP.strip(0.09999999999999998); // = 0.1
NP.plus(0.1, 0.2); //加法计算 = 0.3, not 0.30000000000000004
NP.plus(2.3, 2.4); //加法计算 = 4.7, not 4.699999999999999
NP.minus(1.0, 0.9); //减法计算 = 0.1, not 0.09999999999999998
NP.times(3, 0.3); //乘法计算 = 0.9, not 0.8999999999999999
NP.times(0.362, 100); //乘法法计算 = 36.2, not 36.199999999999996
NP.divide(1.21, 1.1); //除法计算 = 1.1, not 1.0999999999999999
NP.round(0.105, 2); //四舍五入,保留2位小数 = 0.11, not 0.1

混合的计算:


import NP from 'number-precision'

// (0.8-0.5)x1000,保留2位小数
NP.round(NP.times(NP.minus(0.8, 0.5), 1000), 2)
// 计算股票收益率
NP.round(NP.times(NP.divide(NP.minus(+price, +cost), +cost), 100),2)

更复杂的计算场景用什么


number-precision有短小精悍的优势在,基本的运算都能拿捏,但那些要求更高的计算场景用什么库呢?


总结了目前社区流行的几款计算库,大家按需取用。


特点场景库体积优势劣势适用场景
toFixed内置方法,仅用于数字格式化,不解决底层精度问题0无需额外引入,使用便捷无法修复计算误差,四舍五入规则非标准非精确场景的临时格式化
number-precision轻量化,提供加减乘除、四舍五入基础功能,无多余1KB体积极小,API 极简,学习成本低不支持超大整数,无复杂数学运算电商价格计算、表单数字校验
big.js专注十进制浮点数运算,API 简洁,默认精度可配置6KB平衡体积与功能,兼容性好功能少于 decimal.js中小型项目精确计算、数据统计
decimal.js功能全面,支持高精度控制、大数字处理、进制转换、三角函数等,可自定义精度配置32KB精度极高,功能覆盖全,灵活性强体积较大,API 较复杂金融核心计算、科学计算
math.js全能型数学库,支持表达式解析、矩阵运算、单位转换等复杂数学能力160KB综合数学能力强,场景覆盖广体积庞大,性能开销高数据可视化、工程计算

附上地址:


number-precisiongithub.com/nefe/number…


big.jsgithub.com/MikeMcl/big…


decimal.jsgithub.com/MikeMcl/dec…


math.jsgithub.com/josdejong/m…



作品推荐


Haotab 新标签页,一个优雅的新标签页


chrome 商店
| edge 商店
| 在线版


静待你的体验❤



作者:学什么前端
来源:juejin.cn/post/7555400502711320576
收起阅读 »

做个大屏既要不留白又要不变形还要没滚动条,我直接怒斥领导,大屏适配就这四种模式

web
在前端开发中,大屏适配一直是个让人头疼的问题。领导总是要求大屏既要不留白,又要不变形,还要没有滚动条。这看似简单的要求,实际却压根不可能。今天,我们就来聊聊大屏适配的四种常见模式,以及如何根据实际需求选择合适的方案。 一、大屏适配的困境 在大屏项目中,适配问题...
继续阅读 »

在前端开发中,大屏适配一直是个让人头疼的问题。领导总是要求大屏既要不留白,又要不变形,还要没有滚动条。这看似简单的要求,实际却压根不可能。今天,我们就来聊聊大屏适配的四种常见模式,以及如何根据实际需求选择合适的方案。


一、大屏适配的困境


在大屏项目中,适配问题几乎是每个开发者都会遇到的挑战。屏幕尺寸的多样性、设计稿与实际屏幕的比例差异,都使得适配变得复杂。而领导的“既要...又要...还要...”的要求,更是让开发者们感到无奈。不过,我们可以通过合理选择适配模式来尽量满足这些需求。


二、四种适配模式


在大屏适配中,常见的适配模式有以下四种:


(以下截图中模拟视口1200px*500px800px*600px,设计稿为1920px*1080px


1. 拉伸填充(fill)


image.png
image.png



  • 特点:内容会被拉伸变形,以完全填充视口框。这种方式可以确保视口内没有空白区域,但可能会导致内容变形。

  • 适用场景:适用于对内容变形不敏感的场景,例如全屏背景图。


2. 保持比例(contain)


image.png
image.png



  • 特点:内容保持原始比例,不会被拉伸变形。如果内容的宽高比与视口不一致,会在视口内出现空白区域(黑边)。这种方式可以确保内容不变形,但可能会留白。

  • 适用场景:适用于需要保持内容原始比例的场景,例如视频或图片展示。


3. 滚动显示(scroll)


image.png
image.png



  • 特点:内容不会被拉伸变形,当内容超出视口时会添加滚动条。这种方式可以确保内容完整显示,但用户需要滚动才能查看全部内容。

  • 适用场景:适用于内容较多且需要完整显示的场景,例如长列表或长文本。


4. 隐藏超出(hidden)


image.png
image.png



  • 特点:内容不会被拉伸变形,当内容超出视口时会隐藏超出部分。这种方式可以避免滚动条的出现,但可能会隐藏部分内容。

  • 适用场景:适用于内容较多但不需要完整显示的场景,例如仪表盘。


三、为什么不能同时满足所有要求?


这四种适配模式各有优缺点,但它们在逻辑上是相互矛盾的。具体来说:



  • 不留白:要求内容完全填充视口,没有任何空白区域。这通常需要拉伸或缩放内容以适应视口的宽高比。

  • 不变形:要求内容保持其原始宽高比,不被拉伸或压缩。这通常会导致内容无法完全填充视口,从而出现空白区域(黑边)。

  • 没滚动条:要求内容完全适应视口,不能超出视口范围。这通常需要隐藏超出部分或限制内容的大小。


这三个要求在逻辑上是相互矛盾的:



  • 如果内容完全填充视口(不留白),则可能会变形。

  • 如果内容保持原始比例(不变形),则可能会出现空白区域(留白)。

  • 如果内容超出视口范围,则需要滚动条或隐藏超出部分。


四、【fitview】插件快速实现大屏适配


fitview 是一个视口自适应的 JavaScript 插件,它支持多种适配模式,能够快速实现大屏自适应效果。


github地址:github.com/pbstar/fitv…

在线预览:pbstar.github.io/fitview


以下是它的基本使用方法:


配置



  • el: 需要自适应的 DOM 元素

  • fit: 自适应模式,字符串,可选值为 fill、contain(默认值)、scroll、hidden

  • resize: 是否监听元素尺寸变化,布尔值,默认值 true


安装引入


npm 安装


npm install fitview

esm 引入


import fitview from "fitview";

cdn 引入


<script src="https://unpkg.com/fitview@[version]/lib/fitview.umd.js"></script>

使用示例


<div id="container">
<div style="width:1920px;height:1080px;"></div>
</div>

const container = document.getElementById("container");
new fitview({
el: container,
});

五、总结


大屏适配是一个复杂的问题,不同的项目有不同的需求。虽然不能同时满足“不留白”“不变形”和“没滚动条”这三个要求,但可以通过合理选择适配模式来尽量满足大部分需求。在实际开发中,我们需要根据项目的具体需求和用户体验来权衡,选择最合适的适配方案。


在选择适配方案时,fitview 这个插件可以提供很大的帮助。它支持多种适配模式,能够快速实现大屏自适应效果。如果你正在寻找一个简单易用的适配工具,fitview 值得一试。你可以通过 npm 安装或直接使用 CDN 引入,快速集成到你的项目中。


希望这篇文章能帮助你更好地理解和选择大屏适配方案。如果你有更多问题或建议,欢迎在评论区留言。


作者:初辰ge
来源:juejin.cn/post/7513059488417497123
收起阅读 »

Vue3 生态再一次加强,网站开发无敌!

web
如果你正在做官网开发,还在辛苦的手动实现那些动画特效,那今天推荐的这个库,至少让你提前4小时开始摸鱼! 以前,面对设计师的那些炫酷动画,实现起来是最耗头发的;产品经理还时不时的说一下,这效果不好看,我要的是五彩斑斓的黑! 还抱着 Element UI + An...
继续阅读 »

如果你正在做官网开发,还在辛苦的手动实现那些动画特效,那今天推荐的这个库,至少让你提前4小时开始摸鱼!


以前,面对设计师的那些炫酷动画,实现起来是最耗头发的;产品经理还时不时的说一下,这效果不好看,我要的是五彩斑斓的黑!


还抱着 Element UI + Animate.css 在那里辛苦调试,苦苦思考好好的效果怎么到了 safari 就变形了呢 ?


现如今,时代变了!


什么是 Inspira UI


Inspira UI 是专门为 Vue3/Nuxt 开发的可复用的动画组件集合。




  • 完全免费和开源

  • 完美支持 vue3/Nuxt3

  • 包括按钮输入框背景卡片设备模拟光标2D/3D效果120+ 个特效组件

  • 样式基于 TailwindCSS

  • 动画使用 motion-vgsap 实现

  • 对移动设备特别优化


来欣赏一下效果:


视频文字


视频文字


图库


图库


3d文字


3d文字


走马灯


走马灯


spline


spline


Inspira UI 的优势


1.兼顾视觉与功能


以**「轻量动效组件库」为定位,核心组件覆盖基础 UI(按钮、输入框等)和模块(3D 交互、动态背景等),所有组件均内置微交互**设计。动效无需额外开发完美适配企业官网、电商页面等需视觉增强的场景,实现 “拿即用” 的开发体验。


Liquid Logo


Liquid Logo


2.基于Tailwind CSS V4


底层基于 Tailwind CSS 构建组件基础样式,确保原子类叠加的灵活性;支持浅色、深色模式一键切换;支持 ypeScript,所有组件与 API 均提供完整类型定义。


浅色模式


浅色模式


3.深度兼容 Vue/Nuxt 生态,性能提升


无论是 Vue 单页应用还是 Nuxt 服务端渲染项目,都能无缝融入现有技术栈,降低开发者的学习与迁移成本。


同时基于 Vue 3.4+ 新增的 defineModel watchEffect 语法重构,减少了至少 30% 的响应式依赖开销;


4.多端性能优化


对于 3D 组件,在支持 WebGPU 的浏览器中,渲染帧率较旧版 WebGL 提升 2-3 倍.


而对于移动端设备、低配置设备会自动调节动效帧率,性能大大提高;同时,对所有组件做了 “懒加载 + 预渲染” 优化,首屏加载速度较旧版提升 35%


如何使用?


Inspira UI 官方文档支持中文,写的也很接地气,通俗易懂 5 分钟就能上手!



  • 安装依赖


    # 安装 tainlwind
pnpm install tailwindcss @tailwindcss/vite

# 安装 tailwindcss 库和实用工具
pnpm install -D clsx tailwind-merge class-variance-authority tw-animate-css

# 安装 VueUse 和其他支持库
pnpm install @vueuse/core motion-v


  • 配置 vite


    import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: \[
    tailwindcss(),
  ],
})


  • 配置主题


可以根据需要自由配置主题色。


    @import tailwindcss;
@import tw-animate-css;
@custom-variant dark (&:is(.dark *));

:root {
  --cardoklch(1 0 0);
  --card-foregroundoklch(0.141 0.005 285.823);
}
.dark {
  --backgroundoklch(0.141 0.005 285.823);
  --foregroundoklch(0.985 0 0);
}
@theme inline {
  --color-backgroundvar(--background);
}
@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }
}
html {
  color-scheme: light dark;
}
html.dark {
  color-scheme: dark;
}
html.light {
  color-scheme: light;
}

最后一步,可以复制源码或者通过 Cli 来安装。



  • 直接使用源码


找到想要的组件,复制粘贴到自己的项目中即可。




  • 通过 Cli 安装


    pnpm dlx shadcn-vue\@latest add "https://registry.inspira-ui.com/gradient-button.json>"

然后,你就有了一个炫酷的按钮。


Gradient Button 效果


Gradient Button 效果


最后


Vue3/Nuxt3开发者再也不用羡慕 React生态的 Aceternity UIMagic UI 了。


Inspira UI 直接填补了 vue3 生态中动效开发这一块的缺陷,可以将这些奇妙的设计应用在企业官网、特效开发中,大大节省开发成本。


让 Vue3 生态再一次得到加强,快去试试这个炫酷的项目把!


附上官网地址:inspira-ui.com/docs/cn



作品推荐


Haotab 新标签页,一个优雅的新标签页


chrome 商店
| edge 商店
| 在线版


静待你的体验❤



作者:学什么前端
来源:juejin.cn/post/7554572856147984424
收起阅读 »

放下你手里的 GIF,这才是前端动画最终的归宿!!

web
一、前端动画的"至暗时刻":每个像素都在燃烧经费 618 前夕,我的 PM 突然发来灵魂拷问:"菜鸡,这个购物车弹性动画,为什么安卓和 iOS 的抖动幅度不一样?还有这个圣诞飘雪特效,为什么 iPhone 13 Pro Max 的耗电量能煎鸡蛋?" 我默默擦掉...
继续阅读 »

一、前端动画的"至暗时刻":每个像素都在燃烧经费


618 前夕,我的 PM 突然发来灵魂拷问:"菜鸡,这个购物车弹性动画,为什么安卓和 iOS 的抖动幅度不一样?还有这个圣诞飘雪特效,为什么 iPhone 13 Pro Max 的耗电量能煎鸡蛋?"


我默默擦掉额头的冷汗,回想起被这些需求支配的恐惧:



  1. GIF地狱



    • 一个3秒的 loading 动画,设计师随手甩来的 GIF 居然有 5MB

    • 在安卓低端机上播放时,仿佛在看 PPT 版的《黑客帝国》



  2. SVG炼狱



    • 设计师用 AE 做的酷炫路径动画,转成 SVG + CSS 后变成了毕加索抽象画

    • 当产品要求动态修改渐变色时,我仿佛听到了 CPU 的惨叫声



  3. 平台鸿沟



    • iOS 工程师用 Core Animation 优雅实现的弹性动画

    • Android 同学用 ValueAnimator 艰难复刻

    • Web 端同事的 CSS transition 在 Safari 上直接摆烂




直到某天,隔壁组的前端突然拍案而起:"用Lottie!这玩意能直接吃AE动画!" —— 那一刻,我仿佛看到了前端动画的文艺复兴曙光。


二、Lottie:动画界的 Rosetta Stone(罗塞塔石碑)


1. 打破巴别塔诅咒的技术本质


image.png


Lottie的魔法可以拆解为三个核心环节:



  • 魔法卷轴(JSON文件)

    设计师在AE中使用 Bodymovin插件 导出的动画配方,包含所有图层、关键帧、路径等元数据,体积通常只有GIF的1/10

  • 咒语解析器(Lottie Runtime)

    各平台的解析引擎(Web/iOS/Android/Flutter等),像精密的手术刀般逐帧解析JSON指令

  • 元素召唤阵(Canvas/SVG/OpenGL)

    根据设备性能自动选择最优渲染方案,低端机用轻量SVG,旗舰机秀Canvas魔法


2. 那些年被 Lottie 拯救的惨案现场


传统方案Lottie解决方案性能对比
序列帧动画矢量路径动画体积减少90%
CSS关键帧复杂贝塞尔曲线运动渲染速度提升300%
GIF动图透明通道+高清显示内存占用降低80%
Lottie的跨平台特性让设计还原度达到99.99%,从此告别"安卓特供版动画"的尴尬

三、Lottie 的文艺复兴之路:从加载动画到元宇宙门票


1. 业务舞台的常青树场景



  • 轻量级演出

    Loading 动画、按钮微交互、表情包(微信的[呲牙]动画仅28KB)

  • 重量级剧场

    新手引导流程、电商促销动效、直播礼物特效(某直播平台的火箭升空动画仅182KB)

  • 沉浸式演出

    游戏化运营活动、元宇宙3D场景过渡(某电商 App 的虚拟试衣间加载动画)


2. 那些让程序员笑醒的代码片段


Web 端 React 全家桶套餐:


import { Player } from '@lottiefiles/react-lottie-player';

<Player
src="/emoji.lottie.json"
style={{ height: '300px' }}
autoplay
loop
onEvent={event =>
{
if (event === 'complete') console.log('老板说这个动画要播10086遍')
}}
/>


Vue3 优雅食用姿势:


<template>
<Lottie :animation-data="rocketJSON" @ready="startLaunch" />
</template>

<script setup>
import { Lottie } from 'lottie-web-vue';
import rocketJSON from './rocket-launch.json';

const startLaunch = (anim) => {
anim.setSpeed(1.5);
anim.play();
}
</script>

微信小程序性能优化版:


<lottie 
animationData="{{lottieData}}"
path="https://static.example.com/animations/coupon.json"
autoPlay="{{true}}"
css="{{'width: 100%; height: 300rpx;'}}"
bind:ready="onAnimReady"
/>

四、打开 Lottie 的正确姿势:从青铜到王者的进阶之路


1. 设计师的防跑偏指南



  • AE图层命名规范:禁止出现"最终版-真的不改了-V12"这类薛定谔命名

  • 合理使用预合成:嵌套层级不要超过俄罗斯套娃的极限

  • 动态属性标记:需要运行时修改的颜色/文字要提前标注


2. 工程师的性能调优包


压缩黑科技三件套:


# 使用 lottie-tools 进行瘦身
npx lottie-tools compress animation.json -o animation.min.json

# 删除无用元数据
npx lottie-tools remove-unused animation.json

# 提取公共资源
npx lottie-tools split animation.json --output-dir ./assets

按需加载策略:


const loadLottie = async () => {
const animation = await import(
/* webpackPrefetch: true */
/* webpackChunkName: "lottie-animation" */
'./animation.json'
);
lottie.loadAnimation({
container: document.getElementById('lottie'),
animationData: animation.default
});
}

五、当Lottie遇到次元壁:那些年我们填过的坑


1. 跨平台兼容性排雷手册


问题现象解决方案原理剖析
iOS 闪退检查 mask 路径是否闭合CoreAnimation 的路径容错较低
安卓颜色失真禁用硬件加速某些 GPU 对渐变支持不完善
微信小程序渲染错位使用 px 单位替代 rpx部分机型 transform-origin 计算 bug

2. 性能优化急救包


// 帧率节流大法
animation.addEventListener('enterFrame', () => {
if(performance.now() - lastTime < 16) return;
lastTime = performance.now();
// 真正执行渲染逻辑
});

// 内存泄漏防护
useEffect(() => {
const anim = lottie.loadAnimation({...});
return () => anim.destroy(); // 比卸载微信还干净
}, []);

六、未来展望:Lottie的元宇宙野望


当我在AR眼镜里看到Lottie渲染的3D购物动画时,突然意识到这个技术正在打开新次元:



  1. Lottie 3D Beta

    支持 AE 的 3D 图层导出,在WebGL中渲染立体动画

  2. 动态数据绑定

    实时修改3D模型的材质参数,实现千人千面的营销动画

  3. 物理引擎集成

    给动画元素添加重力、碰撞等物理特性,让每个像素都遵循真实世界法则


也许不久的将来,我们能用Lottie在元宇宙里复刻《盗梦空间》的折叠城市动画——当然,得先确保产品经理不会要求实时修改地心引力参数。


总结


从被 GIF 支配的恐惧,到用 JSON 驾驭动画的自由,Lottie 让我们离"设计即代码"的理想国又近了一步。下次当设计师又甩来 500MB 的 AE 工程时,你可以优雅地打开 Bodymovin 插件:"亲爱的,这次咱们换个姿势加载。"


作者:__不想说话__
来源:juejin.cn/post/7506418053997428751
收起阅读 »

“改个配置还要发版?”搞个配置后台不好吗

前言 之前我们公司有个项目组搞了个 AI 大模型推荐功能,眼看就要上架了,结果产品突然找过来说: “那个 AI 推荐的模块先别放了,先隐藏吧,怕上架审核出问题。” 为了赶时间,技术那边就临时加了个判断,把入口在前端藏起来,赶紧发了个版本,算是暂时搞定。 结...
继续阅读 »

前言


之前我们公司有个项目组搞了个 AI 大模型推荐功能,眼看就要上架了,结果产品突然找过来说:



“那个 AI 推荐的模块先别放了,先隐藏吧,怕上架审核出问题。”



为了赶时间,技术那边就临时加了个判断,把入口在前端藏起来,赶紧发了个版本,算是暂时搞定。


结果没过几天,又说要发版本。我一问咋了?技术一脸生无可恋:



“产品又说 AI 那个功能现在可以公测了,要放出来了。”



好吧,那就再发一次......


然后没几天,产品又说要隐藏掉,说模型结果不稳定要临时下线。



“先别公测了,发现有问题,先关掉再说。”



听说那天技术已经气到跟产品去厕所单挑了……


我当时的内心 OS 是:这也太反复了吧?到底是想上还是不想上?


这种“上线前隐藏,上线后又展示,再隐藏”的操作,不是一次两次了。


所以我们当时就想了:



既然这些需求只是改个显示开关、调个默认值,为啥不干脆给他们一个“自助按钮”?别每次都让我们改代码发版本,产品自己调着玩不好吗?



于是我们开始在后台做一套简单的业务配置中心,目标就是:



  • 产品、运营可以自己配置功能开关、文案、参数,不用找开发

  • 配置修改能实时生效,不用再发版本

  • 支持输入类型、下拉选项、开关按钮、范围数值,想怎么配就怎么配


这不是为了什么“大中台”,就是想解决那些一天三改、两小时一调的需求,把这些琐碎从开发日常里剥离出去。




这些配置,说改就改,好烦人


其实像上面的这个事情 在我们日常开发中太常见了。


举几个我亲身经历的例子就知道为啥我们非得搞个配置中心:



  • 登录要不要加图形验证码?

    一开始为了用户体验不加,结果突然哪天注册量暴涨,一查是黑产在刷。产品急了:“赶紧加验证码!”

    技术临时改、测试、上线……黑产已经溜了,下次再刷又得重来一遍。为啥不做个开关自己控制?

  • 推荐功能的参数一天一个样

    有一版产品说“默认推荐 5 个兴趣标签”,隔两天又改成 3 个,再过几天又要回 4 个,说“现在运营数据反馈不一样了”。

    我寻思你都能自己看数据了,那你为啥不能自己改参数?

  • 短信通道经常切换

    阿里、腾讯、网易云信……一个月能换仨。原因也很实在,要么是价格问题,要么是运营说“昨天验证码收不到”。

    每次换通道都得技术去代码里改 templateIdsignName,我真想把接口都写成配置项,让你们爱换谁换谁。

  • 活动逻辑说改就改

    运营:“这个弹窗逻辑改一下,注册就弹。”

    上了之后运营又说:“太打扰用户了,还是调成登录 3 天之后再弹。”

    这不就一行逻辑的事,但每次都要发版真心烦。


像这些情况,改的不是业务逻辑,就是个值、个条件、个开关。但只要没抽出来配置,就只能靠技术手动改代码,一点都不优雅,还特别浪费时间。


所以后来我们就想,还是干脆统一搞个配置后台吧,把这些“天天改、随时调”的破事都收进去,让配置能看得见、改得了、控得住,技术这边也能轻松点。


之前我们配置到底怎么管的?说实话,说出来都不太好意思:



  • 有的直接写死在代码里,变量名都不带解释的,谁写的谁才知道啥意思;

  • 稍微规范一点的,会统一搞个 config 文件,但也只是“技术自己看得懂”的那种;

  • 更混乱的是,有的配置写在 yml,有的塞在数据库,还有的干脆“哪里用到就哪里写”,找都找不到;

  • 最离谱的是:业务运营类的配置和技术底层的配置,全堆一起,切短信通道这种运营配置放在 core-service 的 application.yml 里,看得人脑壳疼。


等到要改配置的时候,产品和运营根本不知道去哪改,开发也得翻半天才能定位是哪段逻辑控制的,甚至还会不小心把技术底层配置给动了……这种时候就明白了,没有一套清晰、隔离、可视化的配置系统,迟早乱套。




配置也要讲规矩,不能啥都往后台扔


当然,配置中心也不是说所有配置都往后台一丢就完事了。

我们踩过这个坑。


最早的时候我们也想偷懒,把所有配置(技术的、业务的、运营的)都塞在统一的配置文件里,比如 config.php 或 application.yml 里,统一读就完了,听起来挺美的。


但实际用下来,真的是——乱!成!一!锅!粥!


一方面,业务/运营配的东西,是给人看的,得讲人话。

比如:



  • 推荐位默认展示几个内容?

  • 某个功能在特定版本下是否开启?

  • 发奖的触发条件设置为几天内登录?


这些运营同学自己都能理解,也希望自己能控制,那就该做成后台可配置、能预览、能实时生效的。


但另一方面,有些配置就不能乱动,或者干脆不应该给后台看到:



  • MQ 消费 topic 名

  • 数据库连接池配置

  • 是否启用 debug 模式

  • 线上某些敏感接口的限频值


如果我们把这些放给业务方看,他们可能都不知道是干嘛的,不小心点了一下,系统都能给整崩了……


所以后来我们就统一了一套大致的“配置分级规则”:


有些配置是纯技术底层的,比如 MQ 的 topic 名、接口限流的阈值、日志采样比例、数据库连接池大小……

这些属于“动一下系统都可能出事”的类型,不给任何后台入口,完全由技术团队内部维护,最好写死或写进 yml 里。


然后是那种纯运营向的配置,比如功能开关、首页推荐展示几个卡片、某段文案内容、活动弹窗的显示时间等等。

这些配置逻辑上不会出啥大问题,但会被运营天天来回调整,必须放后台让他们自己搞,不然技术早晚被烦死。


还有一种是产品经常会动的业务规则类配置,比如:



  • 某个功能灰度给哪些版本开放;

  • 某个打点逻辑的间隔时间怎么设;

  • 是否对新用户显示某个引导。


这些其实也不属于“技术配置”,而是产品为了做 A/B 测试、做用户分群、验证效果临时改的,技术只负责“支持能力”,真正的“值”还是应该交给产品自己配


我们就按照这个思路,把技术底层的隐藏掉,只暴露运营类和业务类配置给后台,谁负责谁管理。改错了好歹还能找到人。


这样一来,配置中心的边界清晰了,技术也不用再被无限兜底,系统也能跑得更稳定,团队分工也更顺。




配置中心该长啥样?我们定了几个目标


前面说了那么多需求、痛点、吵架场面(划掉),那我们到底想要一个什么样的配置中心呢?


说白了,我们的目标很简单



能分清模块,分清人,分清配置类型,能改能看能预览,最好技术都不用管。



那我们是怎么拆功能的呢?最初的设计版本是这样的:


1. 配置要能分模块


不能全堆一起。

我们一个项目,最少有这些模块:系统相关、用户系统、运营活动、AI 任务、通知推送、发奖逻辑……每个模块都有自己的配置。


所以我们一开始就支持“按模块分组”,一个模块里挂多个配置项,清清楚楚谁负责啥,谁爱改啥自己管。


2. 每个配置项要有“类型”


这个最开始我们踩过坑。


最初只做了输入框,结果运营输入一堆错格式的东西,改出 bug。后来我们就定了:所有配置项必须类型化,根据使用场景来限定可选值/格式。


目前我们支持的类型大概是这几种:



  • 输入框:最普通的文本输入,比如标题、url、提示语。

  • 范围值:像“10~100”这种,就搞个最小|最大格式,自动校验。

  • 下拉框:适合选模板、选模型、选渠道。

  • 单选框:跟下拉差不多,看场景展示方式不同。

  • 开关:很直观,是否开启、是否显示,一眼看懂。

  • 多选框:比如支持多个渠道、多种规则生效。


每种类型不仅能展示、还能实时预览效果,这样产品在后台改的时候,不会因为“看不懂这个字段到底是啥”而填错。


3. 每个配置项还要有说明、默认值、排序、是否启用这些“附加属性”


比如说明字段是给人看的,告诉我们这个配置是干嘛的。

默认值是防止读取失败时兜底的。

排序值让我们在后台列表里好找,不然一堆配置乱七八糟的。

“是否启用”是加的一个保险,有些配置值可以保留但临时不生效,方便灰度切换或者留作备选。


4. 最重要的:配置改完必须能“立刻生效”


要是改完还得等发版、等服务重启,那这配置中心跟笔记本有啥区别?


所以我们后面加了热更新机制(这部分我后面会细讲),让配置一改,业务侧立刻拿到新值。




配置中心的数据库设计,我们是这么搞的


配置中心的本质,其实就是“配置的结构化存储 + 可控修改 + 有上下文管理”。


所以数据库是核心。我们一共设计了两个主表:模块表 + 配置项表。当然后续可以加变更记录表之类的,这里先讲核心结构。


1. 模块表:配置的分组归属


配置不能全堆一起,所以我们做了个“模块管理”表,每个模块代表一类业务,比如用户系统、AI 推荐、通知设置、发奖逻辑等。


表结构像下面这样:


CREATE TABLE `config_module` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '模块名称',
`sort` int(11) DEFAULT '0' COMMENT '排序值,越小越靠前',
`status` tinyint(4) DEFAULT '1' COMMENT '状态 1启用 0禁用',
`create_time` int(11) NOT NULL,
`update_time` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配置模块分组';

字段说明一下:



  • id 就是主键

  • name 是模块名,比如“用户系统”、“活动发奖”

  • sort 控制在前端显示顺序,方便找

  • status 是不是启用这个模块的配置

  • 时间字段保留是为了后续查操作记录


2.配置项表:一条配置的所有核心信息


模块分好了之后,接下来就是每个模块下面的具体配置项。我们所有的配置内容,都存在这张 config 表里。


我们当时在设计的时候,就围绕几个问题来定字段的:



  • 这个配置是给谁看的?(产品、运营、开发)

  • 他们需要怎么填?(输入框?下拉?多选?)

  • 填的时候怎么确保不出错?(要不要加参数说明?校验?默认值?)

  • 配置项能不能启用/禁用?排序顺序怎么控制?

  • 有没有必要展示说明/备注?


最终我们定下了下面这个表结构:


CREATE TABLE `config` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '所属模块ID',
`name` varchar(100) NOT NULL COMMENT '配置项名称(展示用)',
`key` varchar(100) NOT NULL COMMENT '配置项key(英文唯一标识)',
`value` text COMMENT '配置项当前值',
`input_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '输入类型:1输入框 2范围 3下拉 4单选 5开关 6多选',
`param` text COMMENT '参数说明:选项或范围,如 "A-1|B-2|C-3"',
`desc` varchar(255) DEFAULT '' COMMENT '配置项说明/备注',
`sort` int(11) DEFAULT '0' COMMENT '排序值(越小越前)',
`status` tinyint(4) DEFAULT '1' COMMENT '状态:1启用 0禁用',
`create_time` int(11) NOT NULL,
`update_time` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置项表';

字段说明



  • id:自增主键,没啥说的。

  • pid:配置项属于哪个模块(模块表里的 id)。

  • name:配置项的中文名,比如“是否展示弹窗”。

  • key:这个是英文标识,用来在代码里读取,比如 home.pop.enabled

  • value:配置的当前值,比如 1(开启)或 {"a":1,"b":2}

  • input_type:决定这个配置项在后台怎么展示:



    • 1 = 输入框

    • 2 = 范围(比如“10|100”)

    • 3 = 下拉框

    • 4 = 单选按钮

    • 5 = 开关

    • 6 = 多选框



  • param:这个字段就很灵活了,用来定义下拉/单选/多选的选项,或者范围的上下限,比如:



    • "中文-zh|英文-en"

    • "10|100"



  • desc:配置项的说明,告诉用户这个配置干嘛的,防止乱填。

  • sort:排序值,配置项多了之后按顺序展示比较清楚。

  • status:这个配置当前是否启用,方便临时关闭某个配置而不删除它。

  • create_time / update_time:时间戳,用来做修改记录、日志追踪之类的。

  • UNIQUE KEY (key):保证每个配置 key 唯一,不会撞名。


总体来说,这张表的核心就是:一个配置项,长什么样、值是多少、长得像啥、能不能动,都在这张表里写清楚了。




模块设计:把配置管得清清楚楚,落到后台页面里


前面我们提到,配置中心一定要支持“按模块分组”,不然配置一多,找起来比翻快递单还费劲。


这里就简单说下我们是怎么把“模块”这个概念,真正落到后台界面和数据库结构里的。


我们每个模块其实就是一个大分类,比如:



  • 系统配置

  • 用户配置

  • AI 相关配置

  • 活动发奖配置

  • 通知推送相关……


每个模块下可以挂多个配置项,类似“一个文件夹里放一类东西”。


我们后台页面上的模块管理界面,大概就长这样:


image.png


这里我们可以设计的支持排序、状态控制、修改、删除这些操作。

点击“新增模块”就会弹出这个表单:


image.png


看起来非常简洁对不对。上面的模块表结构对应的核心字段就三个:



  • 模块名称:展示用,方便识别

  • 排序值:用来控制前端列表显示顺序

  • 是否启用:可以临时禁用整个模块下的配置项


后台页面支持模块的增删改查,基础功能已足够覆盖大多数业务配置需求。


当然,如果业务后续需要更复杂的结构(比如“系统配置 → 登录模块 → 登录相关配置”这种),我们也可以扩展支持二级模块或模块分组。当前只是保留了基础能力,足够轻量、上手快。


配置项怎么设计的?


有了模块分组之后,接下来的重头戏就是:配置项的核心玩法。


每个配置项,本质上就是一个“可调参数”。比如这些熟悉的问题:



  • 用户登录要不要启用谷歌验证?

  • 运营活动的推荐数值范围是多少?

  • 发奖逻辑该切哪个短信商?


以前这些配置,不是写死在代码里,就是散落在 yml、env 文件里,甚至不同环境各一份,改一次还得发版,改完还得祈祷别出问题。


这次我们干脆做成可视化配置项,页面上就能:


新增、修改、启用/禁用

设置不同类型的输入方式

即填即预览,所见即所得


我们支持的配置类型包括:



  • 输入框:适合输入纯文本,比如一个 URL、token、默认值等。

  • 范围:用 最小值|最大值 格式,比如推荐人数限制就写成 10|100

  • 下拉选择:多个固定选项,用 | 分隔,比如 中文-zh|英文-en

  • 单选按钮:和下拉差不多,但前端展示为横向圆点,更直观。

  • 开关:布尔值场景,1 是开启,0 是关闭。

  • 多选框:允许选多个选项,比如某功能适用于多个角色、多个平台。


每种类型在新增配置时都有专属提示,比如范围要填最小|最大,下拉要写选项清单,填完之后还能看到实时预览,确保填的值就是我们想要的。




接下来我们就一个个举例,一边介绍场景,一边实际新增配置项来看效果。


类型一:输入框


适用场景:

输入框是最通用、最基础的配置类型,适合填写纯文本、数字、链接、key 等,不需要做复杂校验,直接存直接用。


常见场景举例:



  • default_jump_url:默认跳转链接,比如用户扫码登录后跳去哪个页面。

  • login_timeout:用户登录状态超时时间,单位秒。

  • system_notice:系统公告文案。

  • token_prefix:JWT 或其它 token 的前缀标识。


我们现在来新增一个配置项



  • 所属模块:系统配置

  • 配置名称:登录超时(秒)

  • 配置 Key:login_timeout

  • 输入类型:输入框

  • 默认值:600

  • 参数说明:留空(输入框不需要)

  • 描述:用户登录后多少秒内无操作将自动退出

  • 排序值:0

  • 是否启用:是


image.png


类型二:范围(最小值 | 最大值)


适用场景:

范围类型适合那种“值不能随便填,必须在某个区间内”的配置,比如:



  • 推荐系统中:每天最多推荐多少次?

  • 活动配置中:用户每次最多能抽几次奖?

  • 发奖逻辑中:奖励金额必须在一个上下限之间。


用配置来写这种规则,业务方只要改数字就行,不用再去翻代码或改逻辑,非常方便。


示例配置:推荐数值范围


假设我们现在要配置一个推荐值范围:



  • 所属模块:系统配置

  • 配置名称:每日推荐数量范围

  • 配置 Key:recommend_count_range

  • 输入类型:范围

  • 默认值:50

  • 参数说明:10|100 (表示最小值是 10,最大值是 100)

  • 描述:控制推荐系统每天给用户推荐的最小/最大条数

  • 排序值:0

  • 是否启用:是


我们在页面中选择「输入类型:范围」之后,系统会提示填写参数格式为:


最小值|最大值,例如:10|100

image.png


类型三:下拉选择(select)


适用场景:

如果某个配置值只能从一组选项中选一个,比如:



  • 默认语言:中文 / 英文 / 日文

  • 消息推送渠道:极光 / 个推 / 小米推送

  • 推荐策略:粗放型 / 精细化 / AB 测试组


这类配置,业务经常调整,但必须选“规定范围内”的值,用下拉最合适。


示例配置:默认语言设置


我们现在来配置一个「默认语言」的选项:



  • 所属模块:系统配置

  • 配置名称:默认语言

  • 配置 Key:default_language

  • 输入类型:下拉选择

  • 默认值:zh

  • 参数说明:中文-zh|英文-en|日文-jp

  • 描述:系统默认语言,决定用户首次进入时的显示语言

  • 排序值:0

  • 是否启用:是


注意参数说明的格式:

每个选项写成“名称-值”,多个选项用 | 隔开,比如:


中文-zh|英文-en|日文-jp

我们可以随意扩展选项,只要格式统一就行。


image.png


类型四:单选按钮(radio)


适用场景:

单选按钮适合那种选项数量不多、用户希望“一眼看清楚当前选的是啥”的配置,比如:



  • 登录方式:密码 / 验证码 / 三方授权

  • 首页布局:列表 / 瀑布流

  • 推送等级:重要 / 普通 / 弱提示


相比下拉,单选按钮更直接,不用点一下再展开,适合管理后台中高频使用的布尔或枚举项


示例配置:登录方式选择


假设我们想设置一个登录方式的配置:



  • 所属模块:系统配置

  • 配置名称:登录方式

  • 配置 Key:login_method

  • 输入类型:单选按钮

  • 默认值:pwd

  • 参数说明:密码登录-pwd|验证码登录-code|三方授权-oauth

  • 描述:控制用户使用哪种方式登录

  • 排序值:0

  • 是否启用:是


参数说明格式:

和下拉一样,用 名称-值 的格式写选项,多个用 | 分隔:


密码登录-pwd|验证码登录-code|三方授权-oauth

image.png


类型五:开关(switch)


适用场景:

布尔型逻辑的最爱!


只要我们有 “开关类” 配置,比如:



  • 是否开启 AI 推荐功能

  • 是否启用登录验证码

  • 是否允许用户取消订单

  • 是否开启调试日志打印


这些“启用 / 禁用”型的业务控制,都可以直接用开关来配置,后台切换一次立即生效,不用发版,非常方便。


示例配置:启用登录验证码


这次我们来添加一个“是否启用图片验证码”的配置:



  • 所属模块:系统配置

  • 配置名称:启用图片验证码

  • 配置 Key:login_captcha_enabled

  • 输入类型:开关

  • 默认值:1(1 表示启用,0 表示关闭)

  • 参数说明:留空(开关类型不需要)

  • 描述:是否对用户登录行为开启图形验证码验证

  • 排序值:0

  • 是否启用:是


image.png


这类配置用处非常多,一些 灰度开关、紧急兜底、临时下线功能 都可以通过这个来做,非常适合给非技术人员使用。


类型六:多选框(checkbox)


适用场景:

当我们希望用户可以勾选多个选项时,单选就不够用了,比如:



  • 消息推送支持的渠道:短信 / App / 微信 / 邮件

  • 用户允许绑定的第三方平台:微信 / QQ / 微博

  • 内容推荐的标签:热门 / 最新 / AI / 精选


多选框让这些“可以组合”的配置变得灵活,谁要开就勾谁,要多选就多选,不受限制。


示例配置:允许的推送渠道


我们来配置一个「支持的消息推送渠道」:



  • 所属模块:系统配置

  • 配置名称:推送渠道

  • 配置 Key:push_channels

  • 输入类型:多选框

  • 默认值:app,wechat(多个值用英文逗号隔开)

  • 参数说明:短信-sms|App-app|微信-wechat|邮件-mail

  • 描述:平台支持的推送方式,可多选

  • 排序值:0

  • 是否启用:是


image.png


参数说明格式 & 默认值说明



  • 参数格式:展示文本-值| 分隔


    短信-sms|App-app|微信-wechat|邮件-mail


  • 默认值:用英文逗号 , 隔开多个值,必须是参数里定义过的值


    app,wechat



配置项列表展示效果(后台页面)


配置添加完以后,在后台配置中心的列表中展示是这样:


image.png


每一项都根据类型展示了不同的 UI 组件,页面清晰、可读、可点、可编辑,操作起来一目了然。


而我们的数据库记录示例(config 表)存储为这样的:


image.png


每条记录都绑定了模块 ID(这里都挂在“系统配置”模块下),并且通过 input_type 字段区分了类型,param 字段为配置项的结构补充说明(下拉/单选/多选专用),desc 字段用于给配置者提示用途。


至于页面上的 UI 展示逻辑、预览区域怎么动态渲染、后端接口怎么接收和保存这些配置,我这边就不展开一一举例了。


说到底,这套配置中心的重点不是“多高级的交互”,而是“足够简单、稳定、好用”,让我们能快速落地配置项、快速修改参数,而不是天天写死在代码里改个值还要发版。


给产品和运营用的“安全编辑页”


配置项都建好了,页面也能预览,那产品和运营想调参数的时候,是直接去编辑配置项吗?


当然不能。


你想啊,运营只是想把推荐数从 100 调成 50,结果点到 key 了,把 recommend_range 改成 recommend_rang,那后端一拿不到值,整个推荐系统直接罢工了。


所以我们专门做了一套“参数调整页”,就像上图这样的界面,产品和运营只需要点点选项、输个值、开个关,完全不用接触 key 和底层结构,修改也更安全。


这个页面其实是对配置项的“业务层封装”——模块和配置项的创建,还是需要研发来做的。因为只有开发才能知道每个 key 该怎么在代码里接,哪些是支持实时生效的,哪些改了之后要重启服务,业务逻辑怎么走,这些都不是运营自己能处理的。


换句话说:



  • 配置项创建时,研发定义 key + 类型 + 默认值 + 参数说明。

  • 产品和运营后续修改时,只改值,不动结构,不容易出错


那我们既然添加了配置项之后,下一步就是把它展示出来,让运营和产品能方便地修改配置、实时查看效果。


我们设计了一个专门的页面,用来承载这些配置项的「操作界面」。页面整体是按模块分 tab 展开的,每个模块下展示自己对应的配置项,表单类型跟配置项定义时保持一致,比如输入框、开关、下拉、多选等一应俱全。


如下图所示,就是我们系统初版配置模块下的实际页面效果:


image.png


当然如果某个模块配置太多我们也可以切换为纵向展示:


image.png


从上图我们可以看到:



  • 每个配置项都有自己的「说明文案」,方便使用者理解配置含义;

  • 类型化配置项有明确的 UI 控件,比如「每日推荐数量范围」就是滑动条;

  • 实时编辑,保存即生效(根据配置项定义的类型和读取方式);

  • 页面左上角还能切换模块,快速定位。


这个页面是专门为非技术人员设计的,不需要他们懂 key 是什么,也不用关心类型怎么定义,他们只管调值就行了,一切都变得可控又安全。


有了这个配置页之后,产品和运营基本上就能脱离研发,自主修改参数了。接下来我们来聊聊配置值在后端是怎么被接入的。


配置中心只是“存”,真正怎么“用”还得看后端


配置中心做得再强大,最终目的还是要服务业务逻辑。


页面上填的那些 key 和 value 并不是为了好看,它们必须在后端代码里“用起来”,才算真正落地。


那后端是怎么接这些配置的呢?其实就两件事:



  1. 读取配置值

  2. 根据 key 做对应逻辑处理


比如我们在后台新增了一个配置项:


key: enable_google_auth
value: 1

表示用户登录时是否开启 Google 验证。那后端代码里就可能是这样写的:


func ShouldUseGoogleAuth() bool {
val := configService.Get("enable_google_auth")
return val == "1"
}

而且每次新建一个配置项,一定要先让开发把读取逻辑写好,配置才能真正生效。不然页面配得再漂亮,后端代码不接,等于白改。


因此,我们建议配置项的新增一定要有“二次审核”机制——业务逻辑没走通之前,别急着让配置项上线。


配置读取:要读得快,还要改得稳


我们虽然只是做了一个小小的配置中心,但依旧严格遵守几个“配置铁律”:


原则含义
读快写稳配置是读多写少,必须优先保障读取性能
缓存兜底数据库抗不了高并发,缓存必须做主力
更新可控配置改动要支持热更新,不能等发版
不信网络避免每次都走 RPC / HTTP,配置必须能“本地感知”

所以我们最后的设计是:



所有配置值都先读 Redis,Redis 没有再查数据库,查出来的值再写回 Redis。



我们可以简单封装了一个方法来统一读取配置:


func (s *ConfigService) Get(key string) string {
val, err := redisClient.Get(ctx, "config:"+key).Result()
if err == redis.Nil {
val = db.GetConfigFromDB(key)
redisClient.Set(ctx, "config:"+key, val, time.Hour) // 缓存 1 小时
}
return val
}

这样做的好处:



  • 性能好:Redis 读取速度快,尤其适合配置这种读多写少的场景;

  • 调用方便:业务方不需要知道配置在哪,直接调 configService.Get()

  • 支持热更新:后台一改配置,Redis 一更新,后端逻辑立刻用上新的值。


配置改错了怎么办?我们加了「刷新机制」来兜底


想象一下,有个产品小哥在后台把短信通道从 A 改成了 B,然后 Redis 秒同步,结果是 B 接口根本没打通……



“线上短信全挂了”+“谁改的都不知道”+“运营拉着技术去机房单挑”


这事我们也不是没经历过。



为了防止“手滑即事故”,我们可以引入了 配置刷新机制


也就是说,后台页面改配置只是“提交更新”,真正生效得靠一个“刷新动作”


我们可以设计一种配置刷新方案:保存 ≠ 生效,需要“手动刷新”才能同步


比如我们之前初版采用的模式,更保险一些:



  1. 后台页面改配置,只写入数据库,不同步 Redis;

  2. 系统标记这个配置为“待刷新”;

  3. 产品或运营点“刷新”按钮,才真正写入 Redis;

  4. 同步操作记录 & 通知消息,方便追踪。


优点是:



  • 不怕误操作,有一步确认机会;

  • 可接入审批流程;

  • 所有操作都有记录,排查问题不含糊。


当然我们也可以设计一个“刷新 API”:


POST /api/config/refresh?key=xxx
POST /api/config/refresh_all

支持单个或批量刷新,用于后台管理或开发联调。


安全兜底机制


除了“刷新机制”,我们还需要做几件事:


功能说明
操作记录谁改了什么,啥时候改的,值变了多少
修改通知配置一改,系统自动发钉钉提醒相关同事
关键配置加锁比如短信、支付相关配置默认加锁,解锁需审批
限制字段编辑页面上只能改 value,不允许改 key,防止配置失效

总的来说呢,配置中心不是“你点保存我就给你改”,而是一个受控的配置发布系统


读得快、改得稳、改完能溯源、有通知有审计——这才是一个靠谱的配置中心。


最后的碎碎念


说实话,这套配置中心,说难也不难,说重要吧,也不是业务核心。


但对我们来说,真的很刚需。纯粹就是日常工作中被“配置这点事儿”折磨太久了。 尤其是对业务开发流程不规范的公司来说。


以前在小公司,改个配置就得发版,发版就有几率中奖,一不小心就全服出事。久了大家都怕动,连产品都不敢随便提需求,说白了就是被流程和风险绑住手脚。


后来我们才想明白,像“推荐数量改一下”“开关先关一阵看看效果”这种,完全没必要动代码、改逻辑、走上线流程。给他们一个地方自己调就好了嘛。


所以这套配置中心,说不上啥高级架构,也不是啥大厂必备,但就是解决了我们日常那些“看起来不重要但天天遇到”的小问题。


业务推进更顺了,产品改需求也不再靠嘴说,技术也不用动不动上线连夜发包。这不比啥都强?



更多架构实战、工程化经验和踩坑复盘,我会在公众号 「洛卡卡了」 持续更新。

如果内容对你有帮助,欢迎关注我,我们一起每天学一点,一起进步。



作者:洛卡卡了
来源:juejin.cn/post/7534632857504989238
收起阅读 »

Vue3 防重复点击指令 - clickOnce

web
Vue3 防重复点击指令 - clickOnce 一、问题背景 在实际的 Web 应用开发中,我们经常会遇到以下问题: 用户快速多次点击提交按钮:导致重复提交表单,产生多条相同数据 异步请求未完成时再次点击:可能导致数据不一致或服务器压力增大 用户体验不佳:...
继续阅读 »

Vue3 防重复点击指令 - clickOnce


一、问题背景


在实际的 Web 应用开发中,我们经常会遇到以下问题:



  1. 用户快速多次点击提交按钮:导致重复提交表单,产生多条相同数据

  2. 异步请求未完成时再次点击:可能导致数据不一致或服务器压力增大

  3. 用户体验不佳:没有明确的加载状态反馈,用户不知道操作是否正在进行


这些问题在以下场景中尤为常见:



  • 表单提交(注册、登录、创建订单等)

  • 数据保存操作

  • 文件上传

  • 支付操作

  • API 调用


二、解决方案


clickOnce 指令通过以下机制解决上述问题:


1. 节流机制


使用 @vueuse/coreuseThrottleFn,在 1.5 秒内只允许执行一次点击操作。


2. 按钮禁用


点击后立即禁用按钮,防止用户再次点击。


3. 视觉反馈


自动添加 Element Plus 的 Loading 图标,让用户明确知道操作正在进行中。


4. 智能恢复



  • 如果绑定的函数返回 Promise(异步操作),则在 Promise 完成后自动恢复按钮状态

  • 如果是同步操作,则立即恢复


三、核心特性


自动防重复点击:1.5秒节流时间

自动 Loading 状态:无需手动管理 loading 变量

支持异步操作:自动检测 Promise 并在完成后恢复

优雅的清理机制:组件卸载时自动清理事件监听

类型安全:完整的 TypeScript 支持


四、技术实现


关键技术点



  1. Vue 3 自定义指令:使用 Directive 类型定义

  2. VueUse 节流useThrottleFn 提供稳定的节流功能

  3. 动态组件渲染:使用 createVNoderender 动态创建 Loading 图标

  4. Promise 检测:自动识别异步操作并在完成后恢复状态


工作流程


用户点击按钮

节流检查(1.5秒内只执行一次)

禁用按钮 + 添加 Loading 图标

执行绑定的函数

检测返回值是否为 Promise

Promise 完成后(或同步函数执行完)

移除 Loading + 恢复按钮状态

五、使用方法


1. 注册指令


// main.ts
import clickOnce from '@/directives/clickOnce'

app.directive('click-once', clickOnce)

2. 在组件中使用


<template>
<!-- 异步操作示例 -->
<el-button
type="primary"
v-click-once="handleSubmit">
提交表单
</el-button>

<!-- 带参数的异步操作 -->
<el-button
type="success"
v-click-once="() => handleSave(formData)">
保存数据
</el-button>
</template>

<script setup lang="ts">
const handleSubmit = async () => {
// 模拟 API 调用
await api.submitForm(formData)
ElMessage.success('提交成功')
}

const handleSave = async (data: any) => {
await api.saveData(data)
ElMessage.success('保存成功')
}
</script>

六、优势对比


传统方式


<template>
<el-button
type="primary"
:loading="loading"
:disabled="loading"
@click="handleSubmit">
提交
</el-button>
</template>

<script setup lang="ts">
const loading = ref(false)

const handleSubmit = async () => {
if (loading.value) return

loading.value = true
try {
await api.submit()
} finally {
loading.value = false
}
}
</script>

问题



  • 需要手动管理 loading 状态

  • 每个按钮都要写重复代码

  • 容易遗漏 finally 清理逻辑


使用 clickOnce 指令


<template>
<el-button
type="primary"
v-click-once="handleSubmit">
提交
</el-button>
</template>

<script setup lang="ts">
const handleSubmit = async () => {
await api.submit()
}
</script>

优势



  • 代码简洁,无需管理状态

  • 自动处理 loading 和禁用

  • 统一的用户体验


七、注意事项



  1. 仅用于异步操作:该指令主要为异步操作设计,同步操作会立即恢复

  2. 绑定函数必须返回 Promise:对于异步操作,确保函数返回 Promise

  3. 节流时间固定:当前节流时间为 1.5 秒,可根据需求调整 THROTTLE_TIME 常量

  4. 依赖 Element Plus:使用了 Element Plus 的 Loading 图标和样式


八、适用场景


适合使用



  • 表单提交按钮

  • 数据保存按钮

  • 文件上传按钮

  • API 调用按钮

  • 支付确认按钮


不适合使用



  • 普通导航按钮

  • 切换/开关按钮

  • 需要快速连续点击的场景(如计数器)


九、指令源码


import type { Directive } from 'vue'
import { createVNode, render } from 'vue'
import { useThrottleFn } from '@vueuse/core'
import { Loading } from '@element-plus/icons-vue'

const THROTTLE_TIME = 1500

const clickOnce: Directive<HTMLButtonElement, () => Promise<unknown> | void> = {
mounted(el, binding) {
const handleClick = useThrottleFn(
() => {
// 如果元素已禁用,直接返回(双重保险)
if (el.disabled) return

// 禁用按钮
el.disabled = true
// 添加 loading 状态
el.classList.add('is-loading')

// 创建 loading 图标容器
const loadingIconContainer = document.createElement('i')
loadingIconContainer.className = 'el-icon is-loading'

// 使用 Vue 的 createVNode 和 render 来渲染 Loading 组件
const vnode = createVNode(Loading)
render(vnode, loadingIconContainer)

// 将 loading 图标插入到按钮开头
el.insertBefore(loadingIconContainer, el.firstChild)

// 将 loading 图标存储到元素上,以便后续移除
;(el as any)._loadingIcon = loadingIconContainer
;(el as any)._loadingVNode = vnode

// 执行绑定的函数(应返回 Promise 或普通函数)
const result = binding.value?.()

const removeLoading = () => {
el.disabled = false
// 移除 loading 状态
el.classList.remove('is-loading')
const icon = (el as any)._loadingIcon
if (icon && icon.parentNode === el) {
// 卸载 Vue 组件
render(null, icon)
el.removeChild(icon)
delete (el as any)._loadingIcon
delete (el as any)._loadingVNode
}
}

// 如果返回的是 Promise,则在完成时恢复;否则立即恢复
if (result instanceof Promise) {
result.finally(removeLoading)
} else {
// 非异步操作,立即恢复(或根据需求决定是否恢复)
// 通常建议只用于异步操作,所以这里也可以不处理,或给出警告
removeLoading()
}
},
THROTTLE_TIME,
)

// 将 throttled 函数存储到元素上,以便在 unmount 时移除
;(el as any)._throttledClick = handleClick
el.addEventListener('click', handleClick)
},

beforeUnmount(el) {
const handleClick = (el as any)._throttledClick
if (handleClick) {
el.removeEventListener('click', handleClick)
// 取消可能还在等待的 throttle
handleClick.cancel?.()
delete (el as any)._throttledClick
}
},
}

export default clickOnce


十、总结


clickOnce 指令通过封装防重复点击逻辑,提供了一个开箱即用的解决方案,让开发者可以专注于业务逻辑,而不用担心重复点击的问题。它结合了节流、状态管理和视觉反馈,为用户提供了更好的交互体验。


作者:码途进化论
来源:juejin.cn/post/7589839767816355878
收起阅读 »

2026年的IT圈,看看谁在“裸泳”,谁在“吃肉”

Hello,兄弟们,我是V哥! 最近不少粉丝私信问我:“V哥,现在这行情卷得跟麻花似的,35岁危机就在眼前,你说咱们搞IT的,到了2026年还有出路吗?这技术迭代快得像坐火箭,我到底该往哪边押注?” V哥我就一句话:焦虑个屁!机会全是给有准备的人留着的。 你们...
继续阅读 »

Hello,兄弟们,我是V哥!


最近不少粉丝私信问我:“V哥,现在这行情卷得跟麻花似的,35岁危机就在眼前,你说咱们搞IT的,到了2026年还有出路吗?这技术迭代快得像坐火箭,我到底该往哪边押注?”


V哥我就一句话:焦虑个屁!机会全是给有准备的人留着的。


你们现在看是“寒冬”,V哥我看是“洗牌”。等到2026年,IT行业的格局早就翻天覆地了。那些只会写重复代码的“代码搬运工”确实该慌,但懂趋势、会借力的兄弟,那会儿绝对是香饽饽。


今天,V哥我就掏心窝子地聊聊,2026年咱们这行的几大“风口”。特别是最后两块大肉,听进去了,你下半年的年终奖就稳了。




一、 AI智能体开发:2026年的“新物种”


兄弟们,先把“ChatGPT”这种对话机器人放一边。V哥告诉你,2026年是AI智能体爆发的一年。


啥叫智能体?现在的AI像个博学的书呆子,你问它答。而智能体,那是带着“脑子”和“手脚”的打工人。它不仅能理解你的意图,还能自己拆解任务、自己去调用工具、自己反思纠错,最后把活儿干完了给你交差。



  • 现在是: 你写代码,AI帮你补全一行。

  • 2026年是: 你说“帮我做个电商后台”,智能体自己写代码、自己测、自己部署、甚至自己写文档。


V哥的研判:
到了2026年,不会开发智能体的程序员,就像2010年不会用智能手机的人一样落伍。你不需要自己去造一个大模型(那是大厂的事儿),你需要做的是做中间的“Controller”(控制器)。怎么用LangChain(或者那时候更牛的框架)把大模型串起来?怎么给智能体挂载API接口?怎么设计它的“记忆”和“规划”能力?


这块儿目前还是蓝海,谁能率先把“数字员工”搞定,谁就是那个省下百万人力成本的老板眼里的红人。





二、 鸿蒙开发:国产操作系统的“成年礼”


这块儿,V哥必须得敲黑板!这可能是未来几年里,中国普通程序员最大的红利期


别总盯着Android和iOS卷了,那是存量市场,杀得头破血流。你看华为现在的动作,HarmonyOS NEXT(纯血鸿蒙) 已经切断了对安卓代码的依赖。这意味什么?意味着这不仅仅是换个皮肤,这是一套全新的、独立的生态!


V哥的预言:
到了2026年,鸿蒙不再是手机的配角,而是全场景(手机、车机、家电、工控)的霸主



  • 技术栈: 赶紧把ArkTS(Ark TypeScript)学熟了,ArkUI这套声明式开发范式非常顺手。

  • 机会在哪? 现在市面上大量的APP都需要重构鸿蒙原生版。这中间有一个巨大的缺口!前两年进去的那批人,现在都成技术总监了。2026年,随着万物互联真正落地,鸿蒙开发者的薪资会比同级别的安卓开发高出至少30%。


V哥我一直说,技术要跟着国运走。鸿蒙这条路,不仅是写代码,更是在参与基础设施建设。这碗饭,香!





三、 后端开发:告别“CRUD”,拥抱“编排”


兄弟们,别再笑话写Java/Go的后端枯燥了。虽然简单的增删改查(CRUD)真的会被AI干掉,但后端的逻辑核心地位永远不会动摇


2026年的后端,不再是单纯的写接口,而是做“AI时代的管家”


以前你的服务是给前端APP用的,2026年,你的服务大部分是给上面的“AI智能体”用的。智能体需要调用你的数据库、调用你的业务逻辑。你的接口设计得更规范、更原子化、响应更快。


V哥建议:
Go语言和Rust会在后端越来越火(因为性能好、并发强)。而且,后端得懂点云原生,容器化、Service Mesh(服务网格)这些都是标配。你得学会怎么把一个庞大的系统拆得碎碎的,还能用AI把它们管得服服帖帖。





四、 前端开发:从“画页面”到“造体验”


前端死了吗?V哥告诉你,前端才刚刚开始“性感”起来。


写HTML/CSS这种活儿,2026年估计UI设计师直接说一句话,AI就生成了。那前端干嘛?前端负责“交互的灵魂”


随着WebGPU的普及,浏览器里能跑3D大作、能跑复杂的物理引擎。鸿蒙的ArkUI也是跨端的前端技术。未来的前端,更多是图形学、人机交互和3D可视化。你打开一个网页,不再是看图文,而是进入一个虚拟空间,这背后全是前端工程师的功力。


V哥一句话: 放下jQuery,搞深Three.js,搞透React/Vue原理,往图形学和全栈方向发展。





五、 嵌入式开发:软硬件结合的“硬核浪漫”


以前搞嵌入式感觉是“修收音机的”,2026年搞嵌入式那是“造智能机器人”的。


因为上面说的鸿蒙AI,最后都要落脚到硬件上。智能眼镜、智能家电、自动驾驶,哪个离得开嵌入式?


重点来了: 嵌入式未来会和AI深度融合,叫TinyML(微型机器学习)。在芯片上跑小型的AI模型,让摄像头能识别人脸,让传感器能听懂声音。如果你既懂C语言底层,又懂一点AI算法部署,你是各大硬件厂抢着要的“国宝”。





六、 大数据开发:从“存数据”到“喂AI”


大数据没凉,只是换了个活法。


前几年大家搞Hadoop、Spark,是为了存日志、做报表。2026年,搞大数据主要是为了给AI当“饲养员”


AI需要高质量的数据清洗、向量化处理。这就涉及到向量数据库、数据湖、实时计算流。怎么把企业的几十亿条数据,变成AI能看懂的“知识”,这是大数据工程师的新活儿。不懂AI的数据工程师,未来路会越走越窄。





七、 AI运维与 AI测试:机器管机器


最后说说这两个容易被忽视的领域。



  • AI运维: 以前服务器报警了,运维兄弟半夜爬起来看日志。2026年,AI运维系统会自动定位故障、自动修复、自动扩容。运维工程师不需要敲那么多命令了,而是负责训练这个“运维AI”,制定策略。这叫SRE(站点可靠性工程)的进化版

  • AI测试: 测试不仅是找Bug,更是“攻防演练”。用AI去生成几万条变态测试用例去轰炸你的系统,甚至用AI去对抗AI生成的代码。只有AI才能测出AI写的Bug。




V哥总结一下


兄弟们,2026年其实并不远。


V哥我看了一圈,未来的趋势就两个字:融合



  • 鸿蒙是万物互联的底座,必须要抓;

  • AI智能体是提升效率的神器,必须要懂;

  • 其他所有的后端、前端、嵌入式、数据,都要围绕着这两者去进化。


别再纠结Java还是Python,Go还是Rust了。语言只是工具,解决问题的思路才是王道。从今天起,试着用AI去帮你干活,试着去了解一下鸿蒙的ArkTS,试着把你的工作流程“智能化”。


等到了2026年,当别人还在为裁员瑟瑟发抖时,V哥希望看到你已经站在风口上,笑傲江湖!


我是V哥,带你不仅看懂技术,更看懂未来。


作者:威哥爱编程
来源:juejin.cn/post/7593139476839874566
收起阅读 »

用 npm 做免费图床,这操作绝了!

web
最近发现了一个骚操作 —— 用 npm 当图床,完全免费,还带全球 CDN 加速。分享一下具体实现过程。为啥要用 npm 做图床?先说说背景,我经常在各大平台写文章,需要上传图片。但:免费图床不稳定,容易挂自建图床成本高其他平台限制多然后想到 npm,这不就是...
继续阅读 »

最近发现了一个骚操作 —— 用 npm 当图床,完全免费,还带全球 CDN 加速。分享一下具体实现过程。

为啥要用 npm 做图床?

先说说背景,我经常在各大平台写文章,需要上传图片。但:

  • 免费图床不稳定,容易挂
  • 自建图床成本高
  • 其他平台限制多

然后想到 npm,这不就是现成的 CDN 吗?全球访问速度还快。

怎么实现的?

1. 基本原理

npm 包本质上就是一堆文件,我们可以把图片放进去。发布后,npm 的 CDN 会自动分发这些文件。

访问方式:

# unpkg
https://unpkg.com/包名@版本号/图片路径

# jsdelivr
https://cdn.jsdelivr.net/npm/包名@版本号/图片路径

# PS
https://unpkg.com/cosmium@latest/images/other/npm-pic.png

2. 自动化发布npm包

每次提交图片后都需要手动发布到 npm那不是很烦, 别急github Actions可以帮我们自动发包, 可以直接fork 我的项目:github.com/Cosmiumx/co…

name: Publish to npm

on:
push:
branches:
- master

jobs:
....

3. 配置步骤

  1. Fork 本项目
    • 将本项目 Fork 到你的 GitHub 账号下。
  2. 修改包名
    • 编辑 package.json,将包名改为你自己的:
{
"name": "your-package-name",
"version": "0.0.1",
...
}

注意:包名必须是 npm 上未被占用的名称。

  1. 创建 npm token

    • 访问 npmjs.com,
    • 进入 Access Tokens 页面
    • 点击 Generate New Token → 选择 Bypass 2FA 类型 (npm最新规则token最长只能设置90天)
    • 记住这个 token,只显示一次
  2. 配置 GitHub Secrets

    • 在你 Fork 的仓库中:
    • 仓库 Settings → Secrets and variables → Actions
    • 添加 NPM_TOKEN,值为刚才的 token
  3. 上传图片

    • 把图片放到 images 目录
    • 提交代码,工作流自动发布

4. 访问方式

发布后,图片可通过以下 CDN 访问:

# unpkg
https://unpkg.com/cosmium@latest/images/your-image.png

# jsdelivr
https://cdn.jsdelivr.net/npm/cosmium@latest/images/your-image.png

实际体验

优点:

  • 完全免费,npm 不收费
  • 全球 CDN,访问速度快
  • 自动化流程,上传图片后自动发布
  • 版本管理清晰

注意事项:

  • ⚠️ npm 包一旦发布无法删除,版本号会永久保留
  • ⚠️ 不要上传敏感信息,npm 包是完全公开的
  • ⚠️ 遵守 npm 使用条款,不要滥用 CDN 服务
  • ⚠️ 图片版权,确保你有权使用并分发上传的图片

总结

这个方案算是找到了一个不错的图床替代方案,特别适合经常写技术文章的同学。虽然有点折腾,但效果不错。

有兴趣的可以 fork 我的项目:github.com/Cosmiumx/co…

配置好之后,以后上传图片就只是 git push 的事情了,还是很方便的。


如果这个方法对你有帮助,别忘了点赞支持一下~


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

浏览器中如何摆脱浏览器下12px的限制

web
目前Chrome浏览器依然没有放开12px的限制,但Chrome仍然是使用人数最多的浏览器。 在笔者开发某个项目时突发奇想:如果实际需要11px的字体大小怎么办?这在Chrome中是实现不了的。关于字体,一开始想到的就是rem等非px单位。但是rem只是为了响...
继续阅读 »

目前Chrome浏览器依然没有放开12px的限制,但Chrome仍然是使用人数最多的浏览器。


在笔者开发某个项目时突发奇想:如果实际需要11px的字体大小怎么办?这在Chrome中是实现不了的。关于字体,一开始想到的就是rem等非px单位。但是rem只是为了响应式适配,并不能突破这一限制。



em、rem等单位只是为了不同分辨率下展示效果提出的换算单位,常见的库px2rem也只是利用了js将px转为rem。包括微信小程序提出的rpx单位也是一样!



这条路走不通,就只剩下一个方法:改变视觉大小而非实际大小


理论基础


css中有一个属性:transform: scale();



  • 值的绝对值>1,就是放大,比如2,就是放大2倍

  • 值的绝对值 0<值<1,就是缩小,比如0.5,就是原来的0.5倍;

  • 值的正负,负值表示图形翻转。


默认情况下,scale(x, y):以x/y轴进行缩放;如果y没有值,默认y==x
也可以分开写:scaleX() scaleY() scaleZ(),分开写的时候,可以对Z轴进行缩放


第二种写法:transform: scale3d(x, y, z)该写法是上面的方法的复合写法,结果和上面的一样。


但使用这个属性要注意一点:scale 缩放的时候是以“缩放元素所在空间的中心点”为基准的。
所以如果用在改变元素视觉大小的场景下,一般还需要利用另一个元素来“恢复位置”:


transform-origin: top left;

语法上说,transform-origin 拥有三个属性值:


transform-origin: x-axis y-axis z-axis;

默认为:


transform-origin:50% 50% 0;

属性值可以是百分比、em、px等具体的值,也可以是top、right、bottom、left和center这样的关键词。作用就是更改一个元素变形的原点。


实际应用


<div class="mmcce__info-r">
<!-- 一些html结构 -->
<div v-show="xxx" class="mmcce-valid-mj-period" :class="{'mmcce-mh': showStr}">
<div class="mmcce-valid-period-child">xxx</div><!-- 父级结构,点击显示下面内容 -->
<div class="mmcce-valid-pro" ref="mmcceW">
<!-- 下面内容在后面有讲解 -->
<div class="mmcce-text"
v-for="(item, index) in couponInfo.thresholdStr"
:key="index"
:index="index"
:style="{height: mTextH[index] + 'px'}"
>
{{item}}</div>
</div>
</div>
</div>

.mmcce-valid-mj-period {
max-height: 15px;
transition: all .2s ease;

&.mmcce-mh {
max-height: 200px;
}

.mmcce-valid-pro {
display: flex;
flex-direction: column;
padding-bottom: 12px;

.mmcce-text {
width: 200%; // !
font-size: 22px;
height: 15px;
line-height: 30px;
color: #737373;
letter-spacing: 0;
transform : scale(.5);
transform-origin: top left;
}
}
}

.mmcce-valid-period-child {
position: relative;
width : 200%;
white-space: nowrap;
font-size : 22px;
color : #979797;
line-height: 30px;

transform : scale(.5);
transform-origin: top left;

//xxx
}

屏幕截图 2025-12-17 194818.png


可以明确说明的是,这样的 hack 需要明确规定缩放元素的height!!!


上面代码中为什么.mmcce-valid-mj-period类中要用max-height ?为什么对展开元素中的文字类.mmcce-text中使用height
我将类.mmcce-text中的height去掉后,看下效果:


屏幕截图 2025-12-17 194840.png


(使用min-height是一样的效果)


OK,可以看到,占高没有按我们想的“被缩放”。影响到了下面的元素位置。



本质上是“视觉大小改变了但实际(占位)大小无变化”。
这时候,宽高实际也被缩放了的。这一点通过代码中width:200%也可以看出来。或者你设置了overflow:hidden;也可以有相应的效果!



这一点需要注意,一般来说,给被缩放元素显式设置一个大于等于其font-sizeheight值即可。


缩放带来的其它问题


可能在很多人使用的场景中是不会考虑到这个问题的:被缩放元素限制高度以后如果元素换行那么会出现文字重叠的现象。


屏幕截图 2025-12-17 194858.png


为此,我采用了在mounted生命周期中获取父元素宽度,然后动态计算是否需要换行以及换行的行数,最后用动态style重新渲染每一条数据的height值。
这里有三点需要注意:



  1. 这里用的是一种取巧的方法:用每个文字的视觉font-size值*字符串长度。因为笔者遇到的场景不会出现问题所以可以这么用。在不确定场景中更推荐用canvas或dom实际计算每个字符的宽度再做判断(需要知道文字、字母和数字的宽度是不一样的);

  2. 需要注意一些特殊机型的展示,比如三星的galaxy fold,这玩意是个折叠屏,它的计算会和一般的屏幕计算的不一致;

  3. 在vue生命周期中,mounted可以操作dom,你可以通过this.$el获取元素。但要注意:在这个时期被获取的元素不能用v-if(即:必须存在于虚拟tree中)。这也是上面代码中笔者使用v-showopacity的原因。



关于第三点,还涉及到加载顺序的问题。比如刚进入页面时要展示弹窗,弹窗是一个组件。那你在index.vue中是获取不到这个组件的。但是你可以将比如header也拆分出来,然后在header组件的mounted中去调用弹窗组件暴露出的方法。



mounted(){
let thresholdStr = this.info.dropDownTextList;
let minW = false;
if(this.$el.querySelector('.mmcce-valid-pro').clientWidth < 140) { // 以iPhone5位准,再小于其中元素宽度的的机型就要做特殊处理了
minW = true
}
let mmcw = this.$el.querySelector('.mmcce-valid-pro').getBoundingClientRect().width;

let mmch = [];

for(let i=0;i<thresholdStr.length;i++) {
// 11是指缩放后文字的font-size值,这是一种取巧的方式
if(11*(thresholdStr[i].length) > mmcw) {
if(minW) {
mmch[i] = Math.floor((11*thresholdStr[i].length) / mmcw) * 15;
}else {
mmch[i] = Math.floor((11*(thresholdStr[i].length) + 40) / mmcw) * 15;
}
}else {
mmch[i] = 15;
}
}

this.mTextH = mmch;
},


笔者前段时间弄了一个微信公众号:前端Code新谈。里面暂时有webrtc、前端面试和用户体验系列文章,最近暂时搁置了webrtc,新开了一个系列“three.js”,欢迎关注!希望能够帮到大家,也希望能互相交流!一起学习共同进步



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

微服务正在悄然消亡:这是一件美好的事

最近在做的事情正好需要系统地研究微服务与单体架构的取舍与演进。读到这篇文章《Microservices Are Quietly Dying — And It’s Beautiful》,许多观点直击痛点、非常启发,于是我顺手把它翻译出来,分享给大家,也希望能给同...
继续阅读 »

最近在做的事情正好需要系统地研究微服务与单体架构的取舍与演进。读到这篇文章《Microservices Are Quietly Dying — And It’s Beautiful》,许多观点直击痛点、非常启发,于是我顺手把它翻译出来,分享给大家,也希望能给同样在复杂性与效率之间权衡的团队一些参考。


微服务正在悄然消亡:这是一件美好的事


为了把我们的创业产品扩展到数百万用户,我们搭建了 47 个微服务。


用户从未达到一百万,但我们达到了每月 23,000 美元的 AWS 账单、长达 14 小时的故障,以及一个再也无法高效交付新功能的团队。


那一刻我才意识到:我们并没有在构建产品,而是在搭建一座分布式的自恋纪念碑。


image.png


我们都信过的谎言


五年前,微服务几乎是教条。Netflix 用它,Uber 用它。每一场技术大会、每一篇 Medium 文章、每一位资深架构师都在高喊同一句话:单体不具备可扩展性,微服务才是答案。


于是我们照做了。我们把 Rails 单体拆成一个个服务:用户服务、认证服务、支付服务、通知服务、分析服务、邮件服务;然后是子服务,再然后是调用服务的服务,层层套叠。


到第六个月,我们已经在 12 个 GitHub 仓库里维护 47 个服务。我们的部署流水线像一张地铁图,架构图需要 4K 显示器才能看清。


当“最佳实践”变成“最差实践”


我们不断告诫自己:一切都在运转。我们有 Kubernetes,有服务网格,有用 Jaeger 的分布式追踪,有 ELK 的日志——我们很“现代”。


但那些光鲜的微服务文章从不提的一点是:分布式的隐性税


每一个新功能都变成跨团队的协商。想给用户资料加一个字段?那意味着要改五个服务、提三个 PR、协调两周,并进行一次像劫案电影一样精心编排的数据库迁移。


我们的预发布环境成本甚至高于生产环境,因为想测试任何东西,都需要把一切都跑起来。47 个服务在 Docker Compose 里同时启动,内存被疯狂吞噬。


那个彻夜崩溃的夜晚


凌晨 2:47,Slack 被消息炸翻。


生产环境宕了。不是某一个服务——是所有服务。支付服务连不上用户服务,通知服务不断超时,API 网关对每个请求都返回 503。


我打开分布式追踪面板:一万五千个 span,全线飘红。瀑布图像抽象艺术。我花了 40 分钟才定位出故障起点。


结果呢?一位初级开发在认证服务上发布了一个配置变更,只是一个环境变量。它让令牌校验多了 2 秒延迟,这个延迟在 11 个下游服务间层层传递,超时叠加、断路器触发、重试逻辑制造请求风暴,整个系统在自身重量下轰然倒塌。


我们搭了一座纸牌屋,却称之为“容错架构”。


我们花了六个小时才修复。并不是因为 bug 复杂——它只是一个配置的单行改动,而是因为排查分布式系统就像破获一桩谋杀案:每个目击者说着不同的语言,而且有一半在撒谎。


那个被忽略的低语


一周后,在复盘会上,我们的 CTO 说了句让所有人不自在的话:


“要不我们……回去?”


回到单体。回到一个仓库。回到简单。


会议室一片沉默。你能感到认知失调。我们是工程师,我们很“高级”。单体是给传统公司和训练营毕业生用的,不是给一家正打造未来的 A 轮初创公司用的。


但随后有人把指标展开:平均恢复时间 4.2 小时;部署频率每周 2.3 次(从单体时代的每周 12 次一路下滑);云成本增长速度比营收快 40%。


数字不会说谎。是架构在拖垮我们。


美丽的回归


我们用了三个月做整合。47 个服务归并成一个模块划分清晰的 Rails 应用;Kubernetes 变成负载均衡后面的三台 EC2;12 个仓库的工作流收敛成一个边界明确的仓库。


结果简直让人尴尬。


部署时间从 25 分钟降到 90 秒;AWS 账单从 23,000 美元降到 3,800 美元;P95 延迟提升了 60%,因为我们消除了 80% 的网络调用。更重要的是——我们又开始按时交付功能了。


开发者不再说“我需要和三个团队协调”,而是开始说“午饭前给你”。


我们的“分布式系统”变回了结构良好的应用。边界上下文变成 Rails 引擎,服务调用变成方法调用,Kafka 变成后台任务,“编排层”……就是 Rails 控制器。


它更快,它更省,它更好。


我们真正学到的是什么


这是真相:我们为此付出两年时间和 40 万美元才领悟——


微服务不是一种纯粹的架构模式,而是一种组织模式。Netflix 需要它,因为他们有 200 个团队。你没有。Uber 需要它,因为他们一天发布 4,000 次。你没有。


复杂性之所以诱人,是因为它看起来像进步。 拥有 47 个服务、Kubernetes、服务网格和分布式追踪,看起来很“专业”;而一个单体加一套 Postgres,看起来很“业余”。


但复杂性是一种税。它以认知负担、运营开销、开发者幸福感和交付速度为代价。


而大多数初创公司根本付不起这笔税。


我们花了两年时间为并不存在的规模做优化,同时牺牲了能让我们真正达到规模的简单性。


你不需要 50 个微服务,你需要的是自律


软件架构的“肮脏秘密”是:好的设计在任何规模都奏效。


一个结构良好的单体,拥有清晰的模块、明确的边界上下文和合理的关注点分离,比一团由希望和 YAML 勉强粘合在一起的微服务乱麻走得更远。


微服务并不是因为“糟糕”而式微,而是因为我们出于错误的理由使用了它。我们选择了分布式的复杂性而不是本地的自律,选择了运营的负担而不是价值的交付。


那些悄悄回归单体的公司并非承认失败,而是在承认更难的事实:我们一直在解决错误的问题。


所以我想问一个问题:你构建微服务,是在逃避什么?


如果答案是“一个凌乱的代码库”,那我有个坏消息——分布式系统不会修好坏代码,它只会让问题更难被发现。


作者:程序猿DD
来源:juejin.cn/post/7563860666349649970
收起阅读 »

🚀从 autofit 到 vfit:Vue 开发者该选哪个大屏适配工具?

web
在数据可视化和大屏开发中,"适配"永远是绕不开的话题。不同分辨率下如何保持元素比例、位置精准,往往让开发者头疼不已。 autofit.js 作为老牌适配工具,早已在许多项目中证明了价值;而新晋的 vfit 则专为 Vue 3 量身打造。今天我们就来深入对比这两...
继续阅读 »

82124a861a16a62aeb0124414ab4f8ff7aabbfbf5eadf7-FP4HV4.png
在数据可视化和大屏开发中,"适配"永远是绕不开的话题。不同分辨率下如何保持元素比例、位置精准,往往让开发者头疼不已。
autofit.js 作为老牌适配工具,早已在许多项目中证明了价值;而新晋的 vfit 则专为 Vue 3 量身打造。今天我们就来深入对比这两款工具,看看谁更适合你的场景。


一、核心定位:通用方案 vs Vue 专属


首先得明确两者的定位差异:



  • autofit.js:无框架依赖的通用缩放工具,通过计算容器与设计稿的比例,对整个页面进行缩放处理,核心逻辑是 transform: scale(ratio) 的全局应用。

  • vfit.js:专为 Vue 3 设计的轻量方案,不仅提供全局缩放,更通过组件化思想解决精细定位问题,是"缩放+定位"的一体化方案。


二、核心能力对比


1. 缩放逻辑:全局统一 vs 灵活可控


autofit.js 的缩放逻辑相对直接:



  • 计算容器宽高与设计稿的比例(取宽/高比例的最小值或按配置选择)

  • 对目标容器应用整体缩放,实现"一缩全缩"


vfit.js 则提供了更灵活的缩放策略:


// vfit 初始化配置
createFitScale({
  target: '#app',        // 监听缩放的容器
  designHeight: 1080,    // 设计稿高度
  designWidth: 1920,     // 设计稿宽度
  scaleMode: 'auto'      // 缩放模式:auto/height/width
})


  •  auto 模式会自动对比容器宽高比与设计稿比例,智能选择按宽或按高缩放

  • 支持在组件内通过 useFitScale() 获取当前缩放值,实现局部自定义缩放


2. 定位能力:粗犷适配 vs 精细控制


这是两者最核心的差异。


autofit.js 由于是全局缩放,元素定位依赖原始 CSS 布局,在复杂场景下容易出现:



  • 固定像素定位的元素在缩放后偏离预期位置

  • 相对定位元素在不同分辨率下比例失调


vfit.js 则通过 FitContainer 组件解决了这个痛点,支持两种定位单位:


<!-- 百分比定位:位置不受缩放影响,适合居中场景 -->
<FitContainer :top="50" :left="50" unit="%">
  <div class="card" style="transform: translate(-50%, -50%)">居中内容</div>
</FitContainer>

<!-- 像素定位:位置随缩放自动计算,适合固定布局 -->
<FitContainer :top="90" :left="90" unit="px">
  <div class="box">固定位置元素</div>
</FitContainer>


  •  unit="%":位置基于容器百分比,适合居中、靠边等相对位置

  •  unit="px":位置会自动乘以当前缩放值,保证设计稿像素与实际显示一致


更贴心的是,vfit.js 还支持通过 right/bottom 定位,并自动处理不同原点的缩放计算(比如右上角、右下角)。


3. 框架融合:独立工具 vs Vue 生态


autofit.js 作为独立库,需要手动在 Vue 项目中处理初始化时机(通常在 onMounted 中),且无法直接与 Vue 的响应式系统结合。


vfit.js 则完全融入 Vue 3 生态:



  • 通过 app.use() 安装,自动处理初始化时机

  • 缩放值通过 Ref 实现响应式,组件内可实时获取

  • FitContainer 组件支持 props 动态更新,适配动态布局场景


三、适用场景分析


场景更推荐原因
Vue 3 项目开发vfit.js组件化开发更自然,响应式集成更顺畅
非 Vue 项目(React/原生)autofit.js无框架依赖,通用性更强
简单大屏(整体缩放即可)两者均可autofit 配置更简单,vfit 稍重
复杂布局(多元素精细定位)vfit.js两种定位单位+组件化,解决位置偏移问题
需局部自定义缩放vfit.jsuseFitScale() 可灵活控制局部元素

四、迁移成本与上手难度



  • autofit.js:API 简单,几行代码即可初始化,学习成本低,适合快速接入简单场景。

  • vfit.js:需要理解组件化定位思想,初期有一定学习成本,但对于复杂场景,后期维护成本更低。


如果你从 autofit.js 迁移到 vfit.js,只需:



  1. 替换初始化方式(app.use(createFitScale(...))

  2. 将需要定位的元素用 FitContainer 包裹

  3. 根据需求调整 top/left 与单位


总结:没有最好,只有最合适


autofit.js 胜在通用性和简单直接,适合非 Vue 项目或简单的全局缩放场景;而 vfit.js 则在 Vue 3 生态中展现了更强的针对性,通过组件化和精细定位,解决了复杂大屏的适配痛点。


如果你是 Vue 开发者,且正在为元素定位偏移烦恼,不妨试试 vfit——它可能正是你寻找的"Vue 大屏适配最优解"。


官网地址:web-vfit.netlify.app,可以直接在线体验效果~


github:github.com/v-plugin/vf…


作者:一颗烂土豆
来源:juejin.cn/post/7577970969395445801
收起阅读 »

一张 8K 海报差点把首屏拖垮

web
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一...
继续阅读 »

你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。

老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。




解决方案:三条技术路线,你全踩了一遍


1. 最偷懒:原生 loading="lazy"


一行代码就能跑,浏览器帮你搞定。


<img
src="https://cdn.xxx.com/poster1.jpg"
loading="lazy"
decoding="async"
width="800" height="450"
/>


🔍 关键决策点



  • loading="lazy" 2020 年后现代浏览器全覆盖,IE 全军覆没。

  • 必须写死 width/height,否则 CLS 会抖成 PPT。


适用场景:内部系统、用户浏览器可控,且图片域名已开启 Accept-Ranges: bytes(支持分段加载)。




2. 最稳妥:scroll 节流 + getBoundingClientRect


老项目里还有 5% 的 IE11 用户,我们只能回到石器时代。


// utils/lazyLoad.js
const lazyImgs = [...document.querySelectorAll('[data-src]')];
let ticking = false;

const loadIfNeeded = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
lazyImgs.forEach((img, idx) => {
const { top } = img.getBoundingClientRect();
if (top < window.innerHeight + 200) { // 提前 200px 预加载
img.src = img.dataset.src;
lazyImgs.splice(idx, 1); // 🔍 及时清理,防止重复计算
}
});
ticking = false;
});
};

window.addEventListener('scroll', loadIfNeeded, { passive: true });

🔍 关键决策点



  • requestAnimationFrame 把 30 ms 的节流降到 16 ms,肉眼不再掉帧。

  • 预加载阈值 200 px,实测 4G 网络滑动不白屏。


缺点:滚动密集时 CPU 占用仍高,列表越长越卡。




3. 最优雅:IntersectionObserver 精准观测


新项目直接上 Vue3 + TypeScript,我们用 IntersectionObserver 做统一调度。


// composables/useLazyLoad.ts
export const useLazyLoad = (selector = '.lazy') => {
onMounted(() => {
const imgs = document.querySelectorAll<HTMLImageElement>(selector);
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
const img = e.target as HTMLImageElement;
img.src = img.dataset.src!;
img.classList.add('fade-in'); // 🔍 加过渡动画
io.unobserve(img); // 观测完即销毁
}
});
},
{ rootMargin: '100px', threshold: 0.01 } // 🔍 提前 100px 触发
);
imgs.forEach((img) => io.observe(img));
});
};


  1. 浏览器合成线程把「目标元素与视口交叉状态」异步推送到主线程。

  2. 主线程回调里只做一件事:把 data-src 搬到 src,然后 unobserve

  3. 整个滚动期间,零事件监听,CPU 占用 < 1%。




原理剖析:从「事件驱动」到「观测驱动」


维度scroll + 节流IntersectionObserver
触发时机高频事件(~30 ms)浏览器内部合成帧后回调
计算量每帧遍历 N 个元素仅通知交叉元素
线程占用主线程合成线程 → 主线程
兼容性IE9+Edge79+(可 polyfill)
代码体积0.5 KB0.3 KB(含 polyfill 2 KB)

一句话总结:把「我每隔 16 ms 问一次」变成「浏览器你告诉我啥时候到」。




应用扩展:把懒加载做成通用指令


在 Vue3 项目里,我们干脆封装成 v-lazy 指令,任何元素都能用。


// directives/lazy.ts
const lazyDirective = {
mounted(el: HTMLImageElement, binding) {
const io = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value; // 🔍 binding.value 就是 data-src
io.disconnect();
}
},
{ rootMargin: '50px 0px' }
);
io.observe(el);
},
};

app.directive('lazy', lazyDirective);

模板里直接写:


<img v-lazy="item.url" :alt="item.title" />



举一反三:三个变体场景思路



  1. 无限滚动列表

    IntersectionObserver 绑在「加载更多」占位节点上,触底即请求下一页,再把新节点继续 observe,形成递归观测链。

  2. 广告曝光统计

    广告位 50% 像素可见且持续 1 s 才算一次曝光。设置 threshold: 0.5 并在回调里用 setTimeout 延迟 1 s 上报,离开视口时 clearTimeout

  3. 背景图懒加载

    背景图没有 src,可以把真实地址塞在 style="--bg: url(...)",交叉时把 background-image 设成 var(--bg),同样零回流。




小结



  • 浏览器新特性能救命的,就别再卷节流函数了。

  • 写死尺寸、加过渡、及时 unobserve,是懒加载不翻车的三件套。

  • 把观测器做成指令/组合式函数,后续业务直接零成本接入。


现在你的「企业风采」首屏降到 1.2 s,老板滑得开心,运营继续传 8K 图,世界和平。


作者:前端微白
来源:juejin.cn/post/7530854092869615635
收起阅读 »

🤦‍♂️ 产品又来了:"能不能把Table的滚动条放到页面底部?

web
😅 又是熟悉的对话 产品:"小王,这个表格用户体验不好啊,用户要滚动到底部才能看到横向滚动条,能不能把滚动条固定在页面底部?" 我:"emmm... 这个... 技术上可以实现,但是..." 产品:"那就这么定了!明天上线!" 我:"😭" 相信很多前端同学都遇...
继续阅读 »

😅 又是熟悉的对话


产品:"小王,这个表格用户体验不好啊,用户要滚动到底部才能看到横向滚动条,能不能把滚动条固定在页面底部?"


:"emmm... 这个... 技术上可以实现,但是..."


产品:"那就这么定了!明天上线!"


:"😭"


相信很多前端同学都遇到过类似的场景。面对超宽的 el-table,用户确实需要先滚动到表格底部才能进行左右滚动,体验确实不够友好。


💡干!


于是一个通用的水平滚动条组件 vue-horizontal-scrollbar 诞生了!


🌟 快速体验


想看看实际效果?访问 在线演示


🚀 最终的解决方案


<template>
<el-table style="width: 100%">
</el-table>

<HorizontalScrollbar
:target-selector="getSelector('.el-table__body-wrapper .el-scrollbar .el-scrollbar__wrap')"
:content-selector="getSelector('.el-table__body-wrapper .el-scrollbar .el-scrollbar__view')"
/>

</template>

<script setup>
import { ref } from 'vue'
import { VueHorizontalScrollbar } from 'vue-horizontal-scrollbar'
import "vue-horizontal-scrollbar/dist/style.css"

function getSelector(selector: string) {
const elements = document.querySelectorAll<HTMLElement>(selector) // 兼容展开行
if (elements.length) {
return elements[elements.length - 1]
}
else {
console.warn(`Selector "${selector}" did not match any elements.`)
return null
}
}
// 💡 tips: 如果是有侧边菜单的管理系统需要动态修改vue-horizontal-scrollbar-container的left
</script>

✨ 这样做的好处


🎯 用户体验升级



  • 滚动条始终可见,无需滚动页面

  • 位置固定,操作便捷

  • 支持键盘和鼠标滚轮操作


🛠️ 开发体验友好



  • 一行代码解决问题

  • 不破坏原有组件结构

  • 支持任意 DOM 元素


🎨 高度可定制


<template>
<div>
<!-- Your scrollable content -->
<div id="scroll-container" style="overflow-x: auto; width: 100%;">
<div id="scroll-content" style="width: 2000px; height: 200px;">
<!-- Wide content here -->
<p>This content is wider than the container...</p>
</div>
</div>

<!-- Horizontal Scrollbar -->
<VueHorizontalScrollbar
target-selector="#scroll-container"
content-selector="#scroll-content"
:auto-show="true"
@scroll="onScroll"
/>

</div>
</template>

<script setup>
import { VueHorizontalScrollbar } from 'vue-horizontal-scrollbar'
import "vue-horizontal-scrollbar/dist/style.css"
function onScroll(info) {
console.log('Scroll info:', info)
// { scrollLeft: 100, maxScroll: 1000, scrollPercent: 10 }
}
</script>

✨ Features



  • 🎯 Vue 3 & TypeScript - Full TypeScript support with Vue 3 Composition API

  • 🎨 Customizable - Flexible styling and configuration options

  • ♿ Accessible - ARIA labels and keyboard navigation support

  • 📱 Touch Friendly - Mobile-friendly touch gestures

  • 🚀 Performance - Optimized with throttling and efficient updates

  • 🎪 Flexible - Works with any scrollable content

  • 🎛️ Event Rich - Comprehensive event system for interactions

  • 📦 Lightweight - Minimal dependencies


📖 API Reference


Props


PropTypeDefaultDescription
targetSelectorstring | FunctionRequired. CSS selector or function returning the scroll container element
contentSelectorstring | FunctionRequired. CSS selector or function returning the content element
autoShowbooleantrueAuto show/hide scrollbar based on content width
minScrollDistancenumber50Minimum scroll distance to show scrollbar (when autoShow is true)
heightnumber16Scrollbar height in pixels
enableKeyboardbooleantrueEnable keyboard navigation (Arrow keys, Home, End)
scrollStepnumber50Scroll step for keyboard navigation
minThumbWidthnumber30Minimum thumb width in pixels
throttleDelaynumber16Throttle delay for scroll events in milliseconds
zIndexnumber9999Z-index for the scrollbar
disabledbooleanfalseDisable the scrollbar
ariaLabelstring'Horizontal scrollbar'ARIA label for accessibility
teleportTostring'body'Teleport to target element



🎪 更多有趣的玩法


除了解决表格滚动问题,这个组件还能用在:



  • 商品展示:电商网站的商品横向滚动

  • 图片画廊:摄影作品展示

  • 时间轴:项目进度展示

  • 标签导航:当标签太多时的横向滚动


📦 立即使用


bash


npm install vue-horizontal-scrollbar

🎉 结语


从此以后,再也不怕产品提这种"奇葩"需求了!


产品:"这个滚动条能不能再加个渐变效果?"


:"没问题!改个 CSS 就行!"


产品:"能不能支持触摸滑动?"


:"早就支持了!"




项目地址GitHub

NPM 包vue-horizontal-scrollbar


如果这个组件帮到了你,记得给项目点个 ⭐ 哦!让我们一起让前端开发变得更轻松!🎉


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

一个大龄程序员的地铁日记(第5期),几乎都关于读书

最近一个月地铁上,输出内容几乎都围绕着某个主题,于是本篇内容当中都有小标题。 1、读什么书可以让人变得平静、淡定? 以我的经验——过去5年,读了一些书,整个人变得内敛沉稳安静很多——来看,似乎变化与某一本书的关系不大,而仅仅只关于持续阅读这件事,只要持续阅读,...
继续阅读 »

最近一个月地铁上,输出内容几乎都围绕着某个主题,于是本篇内容当中都有小标题。


1、读什么书可以让人变得平静、淡定?


以我的经验——过去5年,读了一些书,整个人变得内敛沉稳安静很多——来看,似乎变化与某一本书的关系不大,而仅仅只关于持续阅读这件事,只要持续阅读,过个三五年,整个人便会平静、淡定许多。


我这结论,大概是很主观的,我一时说不太清变化如何发生,但我将尽量将发生在我身上的变化说清楚。


首先,我以为仅仅读一本书,是不能做到改变的,不管是认知还是行为,都不行。


以《学习之道》和《人性的弱点》为例。


《学习之道》,是一位世界冠军两次夺得冠军的个人传记,作者在书中分享了他的学习之道——一切都基于重复练习,练习过程中需要辅以热爱。读完本书前后几个月,我是真有按照作者的分享去行事的,篮球从微小动作调整,台球也一点一滴进步,但这些微小调整只持续两三个月便被抛之脑后。


《人性的弱点》,是教导与人为善的一本书。阅读当时的我,天天脸带笑意,认真倾听,耐烦讨论。但这些善良的特点,在几个月后也慢慢消退。


即从书中收获的认知影响自己一段时间行为后,并不能持续。


然后,是“几乎所有的书都是有关联的”。这些慢慢退化的认知,在后来的读书当中,又被强化与巩固。


天才假象》也在说重复练习与热爱,《乔布斯传》甚至将热爱化作极致,《废邮存底》说读书写作如是。总之,这“基于热爱的重复练习是成功/熟练的必要条件”认知,慢慢在我内心变得巩固。


类似的认知,如沉稳、如安静、如三思而后行、如换位思考……都在读书当中慢慢变得巩固。被巩固的认知,会不自觉体现在行为当中,于是,便自然而然的平静且淡定了。


这过程无关某一本书,只在于持续阅读。


2、看过的人物传记当中,你最推荐的是哪一本?


我目前看过的人物传记,有这样几本:《曾国藩传》《人生随时可以重来》《从文自传》《一个瑜伽行者的自传》《乔布斯传》《漫漫回家路》《为奴十二年》《多余的话》《人生由我》《我的前半生》以及《我曾走在崩溃的边缘》。


这些书当中,我最喜欢的,是沈从文先生的《从文自传》,喜欢的缘由很简单,一是沈先生的文字简单真实且真诚,二是从这传记当中所感受到的安静与顽强。


对的,沈先生生命中的那些变化,在他的文字当中显得安静自然;顽强,则来自于安静背后的变化,那些变化(比如真正成为一个靠文字养活自己的作家),是需要很强生命力很强执行力的。


这些书当中,我最想要推荐的,是溥仪先生的《我的前半生》。


诚然,末代皇帝做过许多于中国很不好的错事,但他终于在人生后半段意识到自己错误并反省并给予后人以反省。


我自己读《我的前半生》过程,恰似以第一人称视角看正发生的历史。皇帝到底每天在想什么?皇帝失势后会怎样?


原来皇帝也慌张荒唐,皇帝也会焦虑到睡不着觉。


我想自己,看到了一位很真实的皇帝。


到全书最末尾,溥仪先生有体验到一种人生真谛,即“吃得下饭,睡得着觉”是人生最完美状态。看完全书,我是有感受到自己多出一种“任其自然”态度的:“既然连皇帝都追求吃饱穿暖睡好,为何我自己不珍惜正拥有的人生状态呢?”


《我的前半生》已经看了许久,好些即时体验记不太住,但当下这一刻,它是我看过人物传记当中最被推荐的那本。


3、今年看了哪些书?


今年读完的书,大概(不确定的意思,是其中有两本书大概是前些年读完但没记录的)是25本,它们分作这样几个类别:


心理学有《行为主义》《无条件养育》《十分钟冥想》《幸福的勇气》《甘于平凡的勇气》《爱的艺术》和《亲密关系》。心理学书籍,依然帮助我更好的认识自己。


历史有《秦谜》《五代十国全史》《细说宋朝》。这些书,我依然对照着《国史大纲》来看,我以为现在的自己,虽然依然记不住五代十国有哪些人、宋朝如何变化,但心中对这两个朝代是多出许多印象的,五代十国最黑暗,而宋积贫积弱。


传记以及记录自己或他人生活事的书有《闭经记》《我曾走在崩溃的边缘》《瓦尔登湖》《初老的女人》《沈从文评传》《我的老师沈从文》。


我依然喜欢看人物传记,我以为读人物传记这选择真不错,它们总给予我一种借鉴:我经历过的,前人也经历过。


关于沈从文先生的书,我新加入书架的还有《执拗的拓荒者》以及《沈从文全传》。


小说有《额尔古纳河右岸》《活着》《多情剑客无情剑》《长安的荔枝》。这几本小说,都短短的;《额尔古纳河右岸》和《活着》,我都哭着读完。


其它类别暂时只有一本,只是《人体简史》,这是一本很值得推荐的关于人体的科普书。


我正在阅读,或许在不久将来会读完的书,有《我看见的世界》《苏东坡新传》《李自成》《安娜·卡列尼娜》《金钱心理学》以及《高效能人士的七个习惯》。


4、电子书有哪些优点?


对我来说,现在电子书已经完完全全代替了纸质书。我买纸质书,更多只为“收藏”,我想要的,只是在某个比较闲散时间,找一找翻书的感觉。


对我来说,电子书相较纸质书有以下优点:


首先,也是最重要的那一点,电子书很方便。现在我已经在《微信读书》上连续阅读三年多(之前在《京东读书》连续阅读一年多),我的碎片时间——地铁上、卫生间、等电梯——都能拿出手机看书,使用手机读书,那许多的碎片时间都慢慢攒成帮助我进步的阶梯。


其次,基于碎片化阅读,电子书更容易帮助养成读书习惯。不管纸质书或者Kindle,身上多带一个设备,看书的门槛总是高了许多。


然后,是电子书相较纸质书便宜许多。我在微读的第一年,买一年付费会员只花168元,这会员可以阅读所有在微读上架的出版书。后来,微读开通了50块钱挑战赛(花50块钱,每天阅读持续一年,累积时长达到300小时),我便只需要50块钱,便能读成千上万本我想读的书了。


然后,是笔记与重新翻阅的方便。微读上面读书,可以在给予自己感触内容下方划线或者写上自己的即时感悟,这些感悟被收集在一起,可以搜索,可以直接定位。当书中内容记不太清需要回顾时,直接搜索笔记,直接搜索全书即可。


总之,电子书于我,是方便许多的。


5、一篇日记


现在时间是晚上的9点05分,我已经在回家地铁上前进两站。三站过后我要转车,我打算再去拼一拼下一趟列车的及时——在它关门之前——上车,我于是站在距离转站路线最近那个口子,只待列车到站。


我的右手边,大概是两个小学生(或者是初中生)正坐地上,他们的对话内容,我有听到两三句:


“那个别人刚走了的座位还是热乎的,如果坐了就会长痔疮。”


“我下一站就到红旗河沟了。反正没作业……反正没作业。”


对于今天的准时上车,我是很有些骄傲的。我似乎已经好久好久,没有9点准时下班。


(我跑过换乘路线,下一趟列车还有一分钟,今天的换乘计划成功了。)


今天的工作,又是和提示词作斗争的一天。


今天的新闻,大概只有一条:我的同事说,他的一位朋友从外地回重庆,Offer年包已经过了60。啊哈?所以重庆还是开得起工资的?


嗐!又有人在地铁上开很大声的外放,他应该正在玩某一种打枪游戏。


说起地铁外放,我想重庆和北京(上周去了北京出差)是真有一些差距的,我在北京一周坐地铁次数不多(大概五六七次),从没听到过手机外放,而重庆?每一趟地铁,我都得和外放侠做一点小小斗争:那些不能专心的时间,我总告诉自己“没关系的,不听不听”。


最近两天的晚饭,都是地铁口的一碗洋芋,7块钱。吃洋芋很快,于是我可以在晚饭时间向家里打几个电话。身体健康,就该是最高优的事情。


还有其它的应该记录的内容么?好像没有的。


不对,还有一条。


最近的读书时间,全部给了小说,一本是《安娜·卡列尼娜》,一本是《李自成》。这两本小说,都好长好长。


6、读书会忘记应该怎么办呢?


基于我现在的读书认知(过去五年日均阅读一小时多一点,读书类型多种多样,累积读完大概120本书)来看,我以为记住书中内容这件事,仿佛是可以不需要刻意追求的。


那些我们想要记住的,总能够印象不熄。


以《学习之道》里面的“Zone概念”为例,投篮不管怎么投都有,台球不管怎么打都进,全没有技巧,只凭借着一种感觉,这种感觉是可以有方法做到频繁进入的。


如何进入,带着热爱,练习不止即可。


还有些内容,是读过便忘记的,但这忘记的内容,可以通过一种简单的方法被记住。这简单的方法是不停地读,读相关联的书,看相关联的内容。


这内容当中的一个示例,是算法“快慢指针”,最初我并不知道如何探测链表是否循环,第一次刷题后也很快忘记。直到在《剑指Offer》《我的第一本算法书》《程序员面试金典》中多次强化,它被刻在脑子里。


我想自己,读过书当中可能超过99%,是都被我忘记的。这些被忘记的内容,如何能够被更好的记住,我知道的方法来自于《津巴多普通心理学》名叫“精细复述”,即将自己学到的知识,用自己的话认真地重新组织一遍,这件事做完,便能将知识记得更久些。


这样记住的内容,有“将愤怒或者悲伤写成文字,能够缓解自己的情绪”“记忆能力可以不随年龄变化,只需要一些组装技巧”……


最后,现在的我真的以为“读书想要记住”这件事是可以不强求的,只要不停看,外加一些思考,就好的。


7、推荐几本能够快速读完的小书吧?


看到这个问题,我脑子里冒出来两三本小书,它们都是我读完认为不错不厚甚至很薄的三本小书:《宝贵的人生建议》《受戒》以及《活出生命的意义》。


《宝贵的人生建议》,是一位老者(作者名字以及生平我并不能记住,记住的只是作者是一位六十多岁的老人)写给他子女以及孙辈的人生建议。每一条建议,都不长。


我记住的建议有好些,比如老生常谈的“种一棵树的最好时机是十年前,其次是现在”,比如“写不出文章时,假装以讲故事形式说给朋友听,当写完后删去朋友名字,便是一篇不错草稿”。(此处只是自己的复述,并非书中原文。)


我以为《宝贵的人生建议》是一本好书的缘由在于作者在末尾所说“书中建议恰似帽子,可能并不适用每一个人,选择自己喜欢的那顶就好”,即作者并不强迫读者接受他的认知。


不强迫,便是长者风范。


《受戒》是汪曾祺先生的短篇小说,我大概看完过不下五遍,全书很短,或许不到一小时便能读完。每一遍阅读,我都感受到一种简单的美好:明子和英子的爱情,唯美、自然,充满活力。


《活出生命的意义》,是一本短短传记,我从书中收获的认知有两个:



  1. 拥有希望是一个人活下去的最基本条件;

  2. 不管怎样的绝望场景,我们都拥有最后的自由,选择以何种态度面对这绝望的自由。


以上三本书,如果快一点,或许一两天内可以全部读完。但回味无穷。


8、怎样做到坚持阅读?


大概靠一点惯性,一点贪念,以及对书中内容的许多期待。


惯性,来源于过去五年的作息,以及用手机的习惯。


过去五年以来,我有两年半上班通勤是坐地铁的。地铁上的时间,都被我用来看书,这时间的看书,毫无心理负担。


上班路上:“我正要去公司干活呢,现在通勤路上肯定不会有人找。”


下班路上:“今天的事情已经都有了交代,看点轻松的书打发下时间吧。”


于是看书,专注且有趣。


当看书次数变多,很自然便会打开手机上的《微信读书》,于是再看两分钟。


贪念,也可以说成是对认知的渴求。五年以前,我想着靠自己的生活日记换取流量,只写两周便发现没了东西可写,于是找书来看。


我的看书,是为写作言之有物,是为更新(我给自己定下目标,不管是怎样内容,每周必须完成一篇更新)有内容可写,是为内容能够被人阅读换成收入。


于是看书,与写作相辅相成,世俗又带目的。


期待,是对书中内容最纯粹的期待,我仅仅想知道,书中那人物、事物、理论的未来是怎样的。


《崇祯传》里的崇祯怎么就亡国了?《安娜·卡列尼娜》里的安娜和弗隆斯基有未来么?《我曾走在崩溃的边缘》中俞敏洪老师是怎样不崩溃的?《也许你该找个人聊聊》当中心理咨询师每天都做些什么?


总之,那些无尽的期盼与好奇牵着我,看书不停。


9、糖醋排骨怎么做呢?


最近刚刚试过再做一次糖醋排骨,不脆,但依然有点好吃。制作步骤如下,每一步骤后面括号中是我当下存在的疑惑:


选排骨。(这一步很重要,上一次做时排骨都骨头中间肉包周边,成品好看许多。这一次买的打六折的排骨,成品则很不规则。)


排骨洗净焯水再用热水洗一洗。(这个步骤我从网上学来的,其实没掌握原理,也不确定不焯水味道是否不一样?焯水出锅后用冷水洗是否真的会柴?甚至焯水之后有必要洗么?)


锅中放油,小火放糖,把糖炒出糖色。(这个步骤我想自己油放多了些而糖放少了点,于是糖色不黑,排骨不甜。当然,没有饭店里面的甜,却似乎更符合自己的口味。)


下排骨,不停地翻翻翻翻,直到排骨有些焦黄。


下入没过排骨再深一指的热水,加入调味的盐、醋、酱油,焖。(什么时候放盐依然是一个问题,我看到一条高赞视频说提前放盐排骨会柴,但真正制作时,会忘记这注意事项。又由于没有天天做,于是不能验证。)


水快干,再翻翻翻,收汁。尝一下,不够甜再加一点糖。出锅,撒一点葱粒、红椒粒,增香增色。


最终成品,不嫩,有一点柴,有嚼劲,颜色不深,但依然好吃的。


下次做时的优化:少一点油多一点糖。


10、最近正阅读的书有哪些?


我最近正阅读的书,是这几本:


《李自成》和《崇祯传》,《安娜·卡列尼娜》,《高效能人士的七个习惯》,以及《亲密关系》。


目前《李自成》刚刚看到四分之一,我对于书中作者对崇祯的评价——刚愎自用、犹疑猜忌——不太相信,于是先读《崇祯传》。


《崇祯传》也正读到四分之一处,崇祯灭了魏忠贤,已经杀掉袁崇焕,现在不信任大臣,正启用宦官到各处监督。


现在崇祯给予我的印象,是勤劳皇帝做了一个错误决定(杀袁崇焕),以及身边帮手很不够。


《安娜·卡列尼娜》正听到六分之五处,基迪和列文正在莫斯科等待他们的第一个小孩出生;安娜和弗隆斯基在乡下生活,弗隆斯基感受到安娜限制着他,安娜刚刚给丈夫卡列宁写信申请离婚。


到现在,我以为安娜和弗隆斯基之间,似乎也并非是真正的爱情,在最初的激情、温情过后,当真正回到生活当中,他们的关系也是并不稳定的。或许安娜并不是出轨状态,此种情况会好一些?


《高效能人士的七个习惯》已经看到最后一个习惯——不断更新——了。虽然我对作者在书中的那些佐证并不很信服,但随着阅读时长增加,我以为这七个习惯是真有效的,它们是智慧,来自于生活、历史中积攒的经验。


《亲密关系》,我之前已经听完一遍有声书,现在刚刚读到第二章。我想要的,是在未来将这本书中内容理解的更多些。


对的,《亲密关系》看两遍的原因是,它是我超级推荐的一本书,我想自己理解更多些后再给出有理有据的推荐理由。


对的,书读第二遍,速度慢许多许多。


作者:我要改名叫嘟嘟
来源:juejin.cn/post/7587245616754343987
收起阅读 »

uni-app使用瓦片实现离线地图的两种方案

web
最近接到一个安卓App的活儿,虽然功能上不算复杂,但因为原本没怎么做过安卓端,所以也是"摸着石头过河"。简单写一下踩过的坑和淌的水吧~ uni-app实现离线地图主要用 leafletjs 实现,但是因为在安卓端运行,存在渲染问题,所以还要用上 renderj...
继续阅读 »

最近接到一个安卓App的活儿,虽然功能上不算复杂,但因为原本没怎么做过安卓端,所以也是"摸着石头过河"。简单写一下踩过的坑和淌的水吧~


uni-app实现离线地图主要用 leafletjs 实现,但是因为在安卓端运行,存在渲染问题,所以还要用上 renderjs


实现方案一:web-view


因为uni-app引入第三方可以采用传统的 NPM 安装的方式,也可以采用引入打包完的js文件的方式。


这里采用 leafletjs 打包完的文件,将 leafletjs 放入 static 文件夹内。


在网上下载了公开的瓦片地图图片,以 {z}/{x}/{y} 的目录结构放入 tiles 文件夹中,将 tiles 放入 static 文件夹内。


在static文件夹下新建一个 offline-map.html 文件


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>离线地图</title>
<link rel="stylesheet" href="./leaflet/leaflet.css" />
<style>
html,
body {
margin: 0;
padding: 0;
}

#map {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="./leaflet/leaflet.js"></script>
<script>
const baseUrl = './tiles/{z}/{x}/{y}.jpg';
const map = L.map('map').setView([23.56, 113.23], 15);

L.tileLayer(baseUrl, {
minZoom: 15,
maxZoom: 18,
tms: true,
attribution: 'Offline Tiles',
errorTileUrl: ''
}).addTo(map);
</script>
</body>
</html>

找到 pages/index/index.vue 文件,采用 web-view 引用的方式引入上述 html 文件。


// pages/index/index.vue
<template>
<view class="content">
<web-view src="/static/offline-map.html"></web-view>
</view>
</template>

<style>
.content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>

实现方案二:renderjs


仍然将 leafletjs瓦片图片文件夹tiles 放入 static 文件夹中。


// pages/index/index.vue
<view class="content">
<view id="map" class="map-container"></view>
</view>

<script module="leaflet" lang="renderjs">
import '@/static/leaflet/leaflet.css';
import * as L from "@/static/leaflet/leaflet.js";

export default {
mounted() {
this.initMap();
},
methods: {
initMap() {
const baseUrl = 'static/tiles/{z}/{x}/{y}.jpg'
map = L.map('map').setView([23.56, 113.23], 15);

L.tileLayer(baseUrl, {
minZoom: 15,
maxZoom: 18,
tms: true,
attribution: 'Offline Tiles',
errorTileUrl: ''
}).addTo(map);
},
}
}
</script>

这里需要注意的是一定要在 renderjs 中实现上述代码,如果在常规 script 中实现,在 H5端 没有任何问题,但是运行到真机上会白屏。(这个问题我反复试了好几次都不行,结果还是上传到 Trae 上解决了这个问题)。


导致这种情况的原因是在常规 sctipt 中的代码,在真机上是运行在 逻辑层 的代码,无法干扰到 视图层 的结构,这一点和Web是不同的。


而 renderjs 是运行在 视图层 的js,具备操作 DOM 的能力。


其次是引用 static 文件的路径,import static 中的文件可以使用 @/static 的方式,但是在代码中引用 static 文件需要采用 static/ 的形式。


总结


最后我做完以后让 Trae 给了一下评价,Trae 表示不建议采用这种方式实现离线地图,首先瓦片地图文件一般非常大,我用的仅仅是其中的一小部分,也超过了 60MB,打包出来的 App 包太大了。


其次无论是 web-view 还是 renderjs 本质上是一样的。在app-vue环境下,视图层由webview渲染,而renderjs就是运行在视图层的。


所以无论是渲染效率还是开发上基本没差。


作者:李剑一
来源:juejin.cn/post/7592531796044185615
收起阅读 »

巧用辅助线,轻松实现类拼多多的 Tab 吸顶效果

web
前言:吸顶交互的挑战在移动端开发中,Tab 吸顶是一种非常常见的交互效果:页面滚动时,位于内容区域的 Tab 栏会“吸附”在顶部导航栏下方,方便用户随时切换。比如拼多多百亿补贴 H5 的效果如下:要实现这个效果、并处理其他关联吸顶的效果,开发者通常需要精确处理...
继续阅读 »

前言:吸顶交互的挑战

在移动端开发中,Tab 吸顶是一种非常常见的交互效果:页面滚动时,位于内容区域的 Tab 栏会“吸附”在顶部导航栏下方,方便用户随时切换。比如拼多多百亿补贴 H5 的效果如下:

pdd.gif

要实现这个效果、并处理其他关联吸顶的效果,开发者通常需要精确处理两个问题:

  1. 状态判断:如何准确判断 Tab 栏是否应进入或退出吸顶状态?
  2. 临界值计算:页面滚动到哪个位置时,才是触发吸顶的精确临界点?

传统的方案往往依赖于监听页面的 scroll 事件,在回调中频繁计算元素位置,不仅逻辑复杂、容易出错,还可能引发性能问题。那么,有没有一种更简单、更优雅的方式呢?

本文将介绍一种巧妙的思路,仅用一条辅助线,就能轻松解决上述两个问题,极大简化实现逻辑。

我是印刻君,一位前端程序员,关注我,了解更多有温度的轻知识,有深度的硬内容。

核心思路:一条辅助线

我们的核心方法是:在 Tab 组件的父容器内,放置一条辅助线。这条线的高度可以忽略(例如 1px),定位在 Tab 上方,与 Tab 的距离正好等于顶部导航栏的高度(navbarHeight)。

这条看似简单的辅助线,为我们提供了两个至关重要的信息:

  1. 判断吸顶状态:当页面滚动,导致这条辅助线完全离开视窗顶部时,恰好就是 Tab 栏需要吸顶的时刻。我们可以使用 IntersectionObserver API 来监听其可见性变化,从而轻松更新吸顶状态。

  1. 获取吸顶临界值:在页面初始布局完成后,该辅助线距离页面顶部的偏移量(offsetTop),就等于触发 Tab 吸顶时页面的滚动距离(scrollTop)。我们无需计算,直接获取即可。

原理与实现

1. 判断吸顶状态

IntersectionObserver 是一个现代浏览器 API,可以异步观察目标元素与其祖先或顶级视窗的交叉状态,而无需在主线程上执行高频计算。

在我们的方案中,我们将辅助线作为观察目标。当它向上滚动并与视窗顶部完全分离(isIntersecting 变为 false)时,就意味着 Tab 栏的顶部即将触碰到导航栏的底部。此时,我们只需更新一个状态(例如 isSticky = true),即可触发 Tab 吸顶。这种方式性能优异且逻辑清晰。

2. 获取吸顶临界值

为什么辅助线的 offsetTop 就是吸顶时的滚动距离呢?让我们通过简单的几何关系来证明。

  • 吸顶临界点:如图所示,当 Tab 栏的顶部需要滚动到导航栏(navbar)的底部时,页面滚动的距离 pageScrollTop 应为:

    pageScrollTop = tabOffsetTop - navbarHeight

  • 辅助线的位置:根据我们的设计,辅助线位于 Tab 上方 navbarHeight 的位置。因此,它距离页面顶部的距离 lineOffsetTop 为:

    lineOffsetTop = tabOffsetTop - navbarHeight

结合以上两个等式,可以清晰地得出:

pageScrollTop = lineOffsetTop

这证明了我们可以在页面加载后,直接通过读取辅助线的 offsetTop 属性,预先获得精确的吸顶滚动临界值。

3. 代码示例:React Hooks 实现

下面是一个基于 React Hooks 的简单实现,展示了如何将上述原理付诸实践。

import React, { useState, useEffect, useRef } from 'react';

const StickyTabs = ({ navbarHeight }) => {
const [isSticky, setIsSticky] = useState(false);
const [stickyScrollTop, setStickyScrollTop] = useState(0);

// Ref 指向我们的辅助线
const helperLineRef = useRef(null);

useEffect(() => {
const helperLineEl = helperLineRef.current;
if (!helperLineEl) {
return;
}

// 1. 获取吸顶临界值:页面加载后,直接读取 offsetTop
setStickyScrollTop(helperLineEl.offsetTop);

// 2. 监听辅助线可见性,判断吸顶状态
const observer = new IntersectionObserver(
([entry]) => {
// 当辅助线与视窗不再交叉时,意味着 Tab 需要吸顶
setIsSticky(!entry.isIntersecting);
},
// root: null 表示观察与视窗的交叉
// threshold: 0 表示元素刚进入或刚离开视窗时触发
{ root: null, threshold: 0 }
);

observer.observe(helperLineEl);

return () => observer.disconnect();
}, [navbarHeight]);

return (
<div>
{/* ... 其他页面内容 ... */}
<div style={{ position: 'relative' }}>
{/* 辅助线:绝对定位到 Tab 上方 navbarHeight 的位置 */}
<div
ref={helperLineRef}
style={{ position: 'absolute', top: -`${navbarHeight}px`, height: '1px' }}
/>


{/* Tab 组件 */}
<div
style={{
position: isSticky ? 'fixed' : 'static',
top: isSticky ? `${navbarHeight}px` : 'auto',
width: '100%',
zIndex: 10,
// ... 其他样式
}}
>

{/* Tabs... */}
div>
div>
{/* ... 列表等内容 ... */}
div>
);
};

在这个例子中:

  • helperLineRef 指向我们的辅助线。
  • useEffect 在组件挂载后执行:
    • 通过 helperLineRef.current.offsetTop 一次性获取并存储吸顶临界值 stickyScrollTop
    • 创建 IntersectionObserver 监听辅助线,当它离开视窗时,将 isSticky 设为 true,反之则为 false
  • Tab 组件的 position 样式根据 isSticky 状态动态切换,从而实现吸顶和取消吸顶的效果。

总结

通过引入一条简单的辅助线,我们将一个动态、复杂的滚动计算问题,巧妙地转化为了一个静态、简单的布局问题。

这种方法的优势显而易见:

  1. 逻辑清晰:用 IntersectionObserver 判断状态,用 offsetTop 获取临界值,职责分明,代码易于理解和维护。
  2. 性能更优:避免了高频的 scroll 事件监听和其中复杂的计算,将性能开销降到最低。
  3. 实现简单:无需引入复杂的第三方库,仅依靠浏览器原生 API 即可优雅地实现功能。

我是印刻君,一位前端程序员,关注我,了解更多有温度的轻知识,有深度的硬内容。


作者:印刻君
来源:juejin.cn/post/7572539461479546923
收起阅读 »

为什么有些人边框不用border属性

web
1) border 会改变布局(占据空间) border 会参与盒模型,增加元素尺寸。 例如,一个宽度 200px 的元素加上 border: 1px solid #000,实际宽度会变成: 200 + 1px(left) + 1px(right) = 202...
继续阅读 »

1) border 会改变布局(占据空间)


border 会参与盒模型,增加元素尺寸。


例如,一个宽度 200px 的元素加上 border: 1px solid #000,实际宽度会变成:


200 + 1px(left) + 1px(right) = 202px

如果不想影响布局,就很麻烦。


使用 box-shadow: 0 0 0 1px #000不会改变大小,看起来像 border,但不占空间。




2) border 在高 DPI 设备上容易出现“模糊/不齐”


特别是 0.5px border(发丝线),在某些浏览器上有锯齿、断线。


transform: scale(0.5) 或伪元素能做更稳定的发丝线。




3) border 圆角 + 发丝线 常出现不规则效果


border + border-radius 在不同浏览器的渲染不一致,容易出现不均匀、颜色不一致的问题。


outline / box-shadow 圆角更稳定。




4) border 不适合做阴影/多层边框


如果你需要两层边框:


双层边框用 border 很难做

而用:


box-shadow: 0 0 0 1px #333, 0 0 0 2px #999;

非常简单。




5) border 和背景裁剪一起用时容易出 bug


比如 background-clipoverflow: hidden 配合 border 会出现背景被挤压、不应该被裁剪却裁剪等问题。




6) hover/active 等状态切换时会“跳动”


因为 border 会改变元素大小。


例子:


.btn { border: 0; }
.btn:hover { border: 1px solid #000; }

鼠标移上去会抖动,因为尺寸变大了。


box-shadow 的话就不会跳。





25/11/25更新,来自评论区大佬补充



除了动态外有时候 overflow 也会导致原本刚刚好的布局不会删除滚动条,由于有了 border 1px 导致刚好出现滚动条但其实根本滚不了。


总结


边框可以分别使用border、outline、box-shadow三种方式去实现,其中outline、box-shadow不会像border一样占据空间。而box-shadow可以用来解决两个元素相邻时边框变宽的问题。不使用border并不是因为它不好,而是因为outline和box-shadow的兼容性和灵活性相对border会更好一点。


作者:爆浆麻花
来源:juejin.cn/post/7575065042158633010
收起阅读 »

如果产品经理突然要你做一个像抖音一样流畅的H5

web
从前端到爆点!抖音级 H5 如何炼成? 在万物互联的时代,H5 页面已成为产品推广的利器。当产品经理丢给你一个“像抖音一样流畅的 H5”任务时,是挑战还是机遇?别慌,今天就带你走进抖音 H5 的前端魔法世界。 一、先看清本质:抖音 H5 为何丝滑? 抖音 H5...
继续阅读 »

从前端到爆点!抖音级 H5 如何炼成?


在万物互联的时代,H5 页面已成为产品推广的利器。当产品经理丢给你一个“像抖音一样流畅的 H5”任务时,是挑战还是机遇?别慌,今天就带你走进抖音 H5 的前端魔法世界。


一、先看清本质:抖音 H5 为何丝滑?


抖音 H5 之所以让人欲罢不能,核心在于两点:极低的卡顿率和极致的交互反馈。前者靠性能优化,后者靠精心设计的交互逻辑。比如,你刷视频时的流畅下拉、点赞时的爱心飞舞,背后都藏着前端开发的“小心机”。


二、性能优化:让页面飞起来


(一)懒加载与预加载协同作战


懒加载是 H5 性能优化的经典招式,只在用户即将看到某个元素时才加载它。但光靠懒加载还不够,聪明的抖音 H5 还会预加载下一个可能进入视野的元素。以下是一个基于 IntersectionObserver 的懒加载示例:


document.addEventListener('DOMContentLoaded', () => {
const lazyImages = [].slice.call(document.querySelectorAll('img.lazy'));
if ('IntersectionObserver' in window) {
let lazyImageObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazy Images.forEach((lazyImage) => {
lazyImageObserver.observe(lazyImage);
});
}
});

(二)图片压缩技术大显神威


图片是 H5 的“体重”大户。抖音 H5 常用 WebP 格式,它在保证画质的同时,能将图片体积压缩到 JPEG 的一半。你可以用以下代码轻松实现图片格式转换:


function compressImage(inputImage, quality) {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = inputImage.naturalWidth;
canvas.height = inputImage.naturalHeight;
ctx.drawImage(inputImage, 0, 0, canvas.width, canvas.height);
const compressedImage = new Image();
compressedImage.src = canvas.toDataURL('image/webp', quality);
compressedImage.onload = () => {
resolve(compressedImage);
};
});
}

三、交互设计:让用户欲罢不能


(一)微动画营造沉浸感


在点赞、评论等关键操作上,抖音 H5 会加入精巧的微动画。比如点赞时的爱心从手指位置飞出,这其实是一个 CSS 动画加 JavaScript 事件监听的组合拳。以下是一个简易版的点赞动画代码:


@keyframes flyHeart {
0% {
transform: scale(0) translateY(0);
opacity: 0;
}
50% {
transform: scale(1.5) translateY(-10px);
opacity: 1;
}
100% {
transform: scale(1) translateY(-20px);
opacity: 0;
}
}
.heart {
position: fixed;
width: 30px;
height: 30px;
background-image: url('../assets/heart.png');
background-size: contain;
background-repeat: no-repeat;
animation: flyHeart 1s ease-out;
}

document.querySelector('.like-btn').addEventListener('click', function(e) {
const heart = document.createElement('div');
heart.className = 'heart';
heart.style.left = e.clientX + 'px';
heart.style.top = e.clientY + 'px';
document.body.appendChild(heart);
setTimeout(() => {
heart.remove();
}, 1000);
});

(二)触摸事件优化


在移动设备上,触摸事件的响应速度直接影响用户体验。抖音 H5 通过精准控制触摸事件的捕获和冒泡阶段,减少了延迟。以下是一个优化触摸事件的示例:


const touchStartHandler = (e) => {
e.preventDefault(); // 防止页面滚动干扰
// 处理触摸开始逻辑
};

const touchMoveHandler = (e) => {
// 处理触摸移动逻辑
};

const touchEndHandler = (e) => {
// 处理触摸结束逻辑
};

const element = document.querySelector('.scrollable-container');
element.addEventListener('touchstart', touchStartHandler, { passive: false });
element.addEventListener('touchmove', touchMoveHandler, { passive: false });
element.addEventListener('touchend', touchEndHandler);

四、音频处理:让声音为 H5 增色


抖音 H5 的音频体验也很讲究。它会根据用户的操作实时调整音量,甚至在不同视频切换时平滑过渡音频。以下是一个简单的声音控制示例:


const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const audioElement = document.querySelector('audio');
const audioSource = audioContext.createMediaElementSource(audioElement);
const gainNode = audioContext.createGain();
audioSource.connect(gainNode);
gainNode.connect(audioContext.destination);

// 调节音量
function setVolume(level) {
gainNode.gain.value = level;
}

// 音频淡入效果
function fadeInAudio() {
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + 1);
}

// 音频淡出效果
function fadeOutAudio() {
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1);
}

五、跨浏览器兼容:让 H5 无处不在


抖音 H5 能在各种浏览器上保持一致的体验,这离不开前端开发者的兼容性优化。常用的手段包括使用 Autoprefixer 自动生成浏览器前缀、为老浏览器提供 Polyfill 等。以下是一个为 CSS 动画添加前缀的示例:


const autoprefixer = require('autoprefixer');
const postcss = require('postcss');

const css = '.example { animation: slidein 2s; } @keyframes slidein { from { transform: translateX(0); } to { transform: translateX(100px); } }';

postcss([autoprefixer]).process(css).then(result => {
console.log(result.css);
/*
输出:
.example {
animation: slidein 2s;
}
@keyframes slidein {
from {
-webkit-transform: translateX(0);
transform: translateX(0);
}
to {
-webkit-transform: translateX(100px);
transform: translateX(100px);
}
}
*/

});

打造一个像抖音一样的流畅 H5,需要前端开发者在性能优化、交互设计、音频处理和跨浏览器兼容等方面全方位发力。希望这些技术点能为你的 H5 开发之旅提供助力,让你的产品在激烈的市场竞争中脱颖而出!


作者:前端的日常
来源:juejin.cn/post/7522090635908251686
收起阅读 »

写 CSS 用 px?这 3 个单位能让页面自动适配屏幕

web
在网页开发中,CSS 单位是控制元素尺寸、间距和排版的基础。长期以来,px(像素)因其直观、精确而被广泛使用。然而,随着设备屏幕尺寸和用户需求的多样化,单纯依赖 px 已难以满足现代 Web 对可访问性、灵活性和响应式能力的要求。什么是 p...
继续阅读 »

在网页开发中,CSS 单位是控制元素尺寸、间距和排版的基础。

长期以来,px(像素)因其直观、精确而被广泛使用。

然而,随着设备屏幕尺寸和用户需求的多样化,单纯依赖 px 已难以满足现代 Web 对可访问性灵活性响应式能力的要求。

什么是 px?

px 是 CSS 中的绝对长度单位,代表像素(pixel)。

在标准密度屏幕上,1px 通常对应一个物理像素点。

开发者使用 px 可以精确控制元素的大小,例如:

.container {
width: 320px;
font-size: 16px;
padding: 12px;
}

这种写法简单直接,在固定尺寸的设计稿还原中非常高效。但问题也正源于它的绝对性。

px 存在哪些问题?

1. 缺乏响应能力

px 的值是固定的,不会随屏幕宽度、容器大小或用户设置而变化。

在一个 320px 宽的手机上显示良好的按钮,在 4K 显示器上可能显得微不足道,反之亦然。

2. 不利于可访问性

许多用户(尤其是视力障碍者)会调整浏览器的默认字体大小。

但使用 px 定义的字体不会随之缩放,导致内容难以阅读。

相比之下,使用相对单位(如 rem)能尊重用户的偏好设置。


更好的选择

为解决上述问题,CSS 提供了一系列更智能、更灵活的单位和功能。以下是几种核心方案:

1. 相对单位:rem 与 em

  • rem(root em):相对于根元素()的字体大小。默认情况下,1rem = 16px,但可通过设置 html { font-size: 18px } 改变基准。
  • em:相对于当前元素或其父元素的字体大小,常用于局部缩放。

示例:

html {
font-size: 16px; /* 基准 */
}

.title {
font-size: 1.5rem; /* 24px */
margin-bottom: 1em; /* 相对于自身字体大小 */
}

优势:支持用户自定义缩放,便于构建比例一致的排版系统。

2. 视口单位:vw、vh、vmin、vmax

这些单位基于浏览器视口尺寸:

  • 1vw = 视口宽度的 1%
  • 1vh = 视口高度的 1%
  • vmin 取宽高中较小者,vmax 取较大者

用途:适合全屏布局、动态高度标题等场景。

示例:

.hero {
height: 80vh; /* 占视口高度的 80% */
font-size: 5vw; /* 字体随屏幕宽度缩放 */
}

注意:在移动端,vh 可能受浏览器地址栏影响,需谨慎使用。

3. clamp() 函数:实现流体响应

clamp() 是 CSS 的一个重要进步,允许你在一个属性中同时指定最小值、理想值和最大值:

font-size: clamp(16px, 4vw, 32px);

含义:

  • 在小屏幕上,字体不小于 16px;
  • 在中等屏幕,按 4vw 动态计算;
  • 在大屏幕上,不超过 32px。

这行代码即可替代多个 @media 查询,实现平滑、连续的响应效果。

更推荐结合相对单位使用:

font-size: clamp(1rem, 2.5vw, 2rem);

这样既保留了可访问性,又具备响应能力。

4. 容器查询(Container Queries)

过去,响应式布局只能基于整个视口(通过 @media)。

但组件常常需要根据自身容器的大小来调整样式——这就是容器查询要解决的问题。

使用步骤:

  1. 为容器声明 container-type
.card-wrapper {
container-type: inline-size; /* 基于内联轴(通常是宽度) */
}
  1. 使用 @container 编写查询规则:
@container (min-width: 300px) {
.card-title {
font-size: 1.25rem;
}
}

@container (min-width: 500px) {
.card-title {
font-size: 1.75rem;
}
}

现在,只要 .card-wrapper 的宽度变化,内部元素就能自动响应,无需关心页面整体布局。这对构建可复用的 UI 组件库至关重要。

容器查询已在主流浏览器(Chrome 105+、Firefox 116+、Safari 16+)中得到支持。


建议

  • 避免在字体大小、容器宽度、内边距等关键布局属性中使用纯 px
  • 优先使用 rem 作为全局尺寸基准,em 用于局部比例。
  • 对需要随屏幕缩放的元素,使用 clamp() + vw/rem 组合。
  • 构建组件时,考虑启用容器查询,使其真正“自适应”。
  • 保留 px 仅用于不需要缩放的场景,如边框(border: 1px solid)、固定图标尺寸等。

本文首发于公众号:程序员大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期内容

写前端久了,我用 Node.js 给自己造了几个省力小工具

我也是写了很久 TypeScript,才意识到这些写法不对

ThreadLocal 在实际项目中的 6 大用法,原来可以这么简单

重构了20个SpringBoot项目后,总结出这套稳定高效的架构设计


作者:程序员大华
来源:juejin.cn/post/7593292445300899859
收起阅读 »

CSS终于支持渐变色的过渡了🎉

web
背景 在做项目时,总会遇到UI给出渐变色的卡片或者按钮,但在做高亮的时候,由于没有过渡,显得尤为生硬。 过去的解决方案 在过去,我们如果要实现渐变色的过渡,通常会使用如下几种方法: 添加遮罩层,通过改变遮罩层的透明度做出淡入淡出的效果,实现过渡。 通过bac...
继续阅读 »

背景


在做项目时,总会遇到UI给出渐变色的卡片或者按钮,但在做高亮的时候,由于没有过渡,显得尤为生硬。


过去的解决方案


在过去,我们如果要实现渐变色的过渡,通常会使用如下几种方法:



  1. 添加遮罩层,通过改变遮罩层的透明度做出淡入淡出的效果,实现过渡。

  2. 通过background-size/position使得渐变色移动,实现渐变色移动的效果。

  3. 通过filter: hue-rotate滤镜实现色相旋转,实现过渡。


但这几种方式都有各自的局限性:



  1. 遮罩层的方式看似平滑,但不是真正的过渡,差点意思。

  2. background-size/position的方式需要计算好background-sizebackground-position,否则会出现渐变不完整的情况。并且只是实现了渐变的移动,而不是过渡。

  3. filter: hue-rotate也需要计算好旋转角度,实现复杂度高,过渡的也不自然。



@property新规则


@property规则可以定义一个自定义属性,并且可以指定该属性的语法、是否继承、初始值等。


@property --color {
syntax: '<color>';
inherits: false;
initial-value: #000000;
}

我们只需要把这个自定义属性--color应用到linear-gradient中,在特定的时候改变它的值,非常轻松就可以实现渐变色的过渡了。

我们再看看@property规则中这些属性的含义。


Syntax语法描述符


Syntax用于描述自定义属性的数据类型,必填项,常见值包括:



  • <number> 数字(如0,1,2.5)

  • <percentage> 百分比(如0%,50%,100%)

  • <length> 长度单位(如px,em,rem)

  • <color> 颜色值

  • <angle> 角度值(如deg,rad)

  • <time> 时间值(如s,ms)

  • <image> 图片

  • <*> 任意类型


Inherits继承描述符


Inherits用于描述自定义属性是否从父元素继承值,必填项:



  • true 从父元素继承值

  • false 不继承,每个元素独立


Initial-value初始值描述符


Initial-value用于描述自定义属性的初始值,在Syntax为通用时为可选。


兼容性


@property目前仍是实验性规则,但主流浏览器较新版本都已支持。
b70bdd98-15d5-4aa3-a3c4-b4d08a7aba9c.png


总结与展望


@property规则的出现,标志着CSS在动态样式控制方面迈出了重要一步。它不仅解决了渐变色过渡的技术难题,更为未来的CSS动画和交互设计开辟了新的可能性。
随着浏览器支持的不断完善,我们可以期待:



  • 更丰富的动画效果

  • 更简洁的代码实现

  • 更好的性能表现


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

浅谈 import.meta.env 和 process.env 的区别

web
这是一个前端构建环境里非常核心、也非常容易混淆的问题。下面我们从来源、使用场景、编译时机、安全性四个维度来谈谈 import.meta.env 和 process.env 的区别。一句话结论process.env&nbs...
继续阅读 »

这是一个前端构建环境里非常核心、也非常容易混淆的问题。下面我们从来源、使用场景、编译时机、安全性四个维度来谈谈 import.meta.env 和 process.env 的区别。


一句话结论

process.env 是 Node.js 的环境变量接口 import.meta.env 是 Vite(ESM)在构建期注入的前端环境变量


一、process.env 是什么?

1️⃣ 本质

  • 来自 Node.js
  • 运行时读取 服务器 / 构建机的系统环境变量
  • 本身 浏览器里不存在
console.log(process.env.NODE_ENV);

2️⃣ 使用场景

  • Node 服务
  • 构建工具(Webpack / Vite / Rollup)
  • SSR(Node 端)

3️⃣ 前端能不能用?

👉 不能直接用

浏览器里没有 process

// 浏览器原生环境 ❌
Uncaught ReferenceError: process is not defined

4️⃣ 为什么 Webpack 项目里能用?

因为 Webpack 帮你“编译期替换”了

process.env.NODE_ENV
// ⬇️ 构建时被替换成
"production"

本质是 字符串替换,不是运行时读取。


二、import.meta.env 是什么?

1️⃣ 本质

  • Vite 提供
  • 基于 ES Module 的 import.meta
  • 构建期 + 运行期可用(但值是构建期确定的)
console.log(import.meta.env.MODE);

2️⃣ 特点

  • 浏览器里 原生支持
  • 不依赖 Node 的 process
  • 更符合现代 ESM 规范

三、两者核心区别对比(重点)

维度process.envimport.meta.env
来源Node.jsVite
标准Node APIESM 标准扩展
浏览器可用❌(需编译替换)
注入时机构建期构建期
是否运行时读取
推荐前端使用

⚠️ 两者都不是“前端运行时读取服务器环境变量”


四、Vite 中为什么不用 process.env

1️⃣ 因为 Vite 不再默认注入 process

// Vite 项目中 ❌
process.env.API_URL

会直接报错。

2️⃣ 官方设计选择

  • 避免 Node 全局污染
  • 更贴近浏览器真实环境
  • 更利于 Tree Shaking

五、Vite 环境变量的正确用法(非常重要)

1️⃣ 必须以 VITE_ 开头

# .env
VITE_API_URL=https://api.example.com
console.log(import.meta.env.VITE_API_URL);

❌ 否则 不会注入到前端


2️⃣ 内置变量

import.meta.env.MODE        // development / production
import.meta.env.DEV // true / false
import.meta.env.PROD // true / false
import.meta.env.BASE_URL

六、安全性

⚠️ 重要警告

import.meta.env 里的变量 ≠ 私密

它们会:

  • 被 打进 JS Bundle
  • 可在 DevTools 直接看到

❌ 不要这样做

VITE_SECRET_KEY=xxxx

✅ 正确做法

  • 前端:只放“公开配置”(API 域名、开关)
  • 私密变量:只放在 Node / 服务端

七、SSR / 全栈项目里怎么区分?

在 Vite + SSR(如 Nuxt / 自建 SSR):

Node 端

process.env.DB_PASSWORD

浏览器端

import.meta.env.VITE_API_URL

两套环境变量是刻意分开的

  1. 为什么必须分成两套?(设计原因)

1️⃣ 执行环境不同(这是根因)

位置运行在哪能访问什么
SSR ServerNode.jsprocess.env
Client Bundle浏览器import.meta.env

浏览器里 永远不可能安全地访问服务器环境变量


2️⃣ SSR ≠ 浏览器

很多人误解:

“SSR 是不是浏览器代码先在 Node 跑一遍?”

❌ 不完全对

SSR 实际是:

Node.js 先跑一份 → 生成 HTML
浏览器再跑一份 → hydrate

这两次执行:

  • 环境不同
  • 变量来源不同
  • 安全级别不同

  1. 在 Vite + SSR 中,变量的“真实流向”

1️⃣ Node 端(SSR Server)

// server.ts / entry-server.ts
const dbPassword = process.env.DB_PASSWORD;

✔️ 真实运行时读取

✔️ 不会进 bundle

✔️ 只存在于服务器内存


2️⃣ Client 端(浏览器)

// entry-client.ts / React/Vue 组件
const apiUrl = import.meta.env.VITE_API_URL;

✔️ 构建期注入

✔️ 会打进 JS

✔️ 用户可见


3️⃣ 中间那条“禁止通道”

// ❌ 绝对禁止
process.env.DB_PASSWORD → 浏览器

SSR 不会、也不允许,自动帮你“透传”环境变量


  1. SSR 中最容易踩的 3 个坑(重点)


❌ 坑 1:在“共享代码”里直接用 process.env

// utils/config.ts(被 server + client 共用)
export const API = process.env.API_URL; // ❌

问题:

  • Server OK
  • Client 直接炸(或被错误替换)

✅ 正确方式:

export const API = import.meta.env.VITE_API_URL;

或者:

export const API =typeof window === 'undefined'
? process.env.INTERNAL_API
: import.meta.env.VITE_API_URL;

❌ 坑 2:误以为 SSR 可以“顺手用数据库变量”

// Vue/React 组件里
console.log(process.env.DB_PASSWORD); // ❌

哪怕你在 SSR 模式下,这段代码:

  • 最终仍会跑在浏览器
  • 会被打包
  • 是严重安全漏洞

❌ 坑 3:把“环境变量”当成“运行时配置”

// ❌ 想通过部署切换 API
import.meta.env.VITE_API_URL

🚨 这是 构建期值

build 时确定
CDN 缓存
→ 所有用户共享

想运行期切换?只能:

  • 接口返回配置
  • HTML 注入 window.CONFIG
  • 拉 JSON 配置文件

  1. SSR 项目里“正确的分层模型”(工程视角)

┌──────────────────────────┐
│ 浏览器 Client
import.meta.env.VITE_* │ ← 公开配置
└───────────▲──────────────┘

HTTP / HTML

┌───────────┴──────────────┐
Node SSR Server
│ process.env.* │ ← 私密配置
└───────────▲──────────────┘

内部访问

┌───────────┴──────────────┐
DB / Redis / OSS
└──────────────────────────┘

这是一条 单向、安全的数据流


  1. Nuxt / 自建 SSR 的对应关系

类型用途
runtimeConfigServer-only
runtimeConfig.publicClient 可见
process.env仅 server

👉 Nuxt 本质也是在帮你维护这条边界


八、常见误区总结

❌ 误区 1

import.meta.env 是运行时读取

❌ ,仍是构建期注入


❌ 误区 2

可以用它动态切换环境

❌ 不行,想动态只能:

  • 接口返回配置
  • 或运行时请求 JSON

❌ 误区 3

Vite 里还能继续用 process.env

❌ 除非你手动 polyfill(不推荐)


九、总结

  • 前端(Vite)只认 import.meta.env.VITE_*
  • 服务端(Node)只认 process.env
  • 永远不要把秘密放进前端 env


作者:在西安牧羊的牛油果
来源:juejin.cn/post/7592062873829916722
收起阅读 »

Maven 4 终于快来了,新特性很香!

大家好,我是 Guide!在 Java 生态中,Maven 绝对是大家每天都要打交道的“老朋友”。 InterviewGuide 这个开源 AI 项目中,我使用了 Gradle。不过,根据大家的反馈来看还是更愿意使用 Maven 一些。 目前(2026 年 ...
继续阅读 »

大家好,我是 Guide!在 Java 生态中,Maven 绝对是大家每天都要打交道的“老朋友”。


InterviewGuide 这个开源 AI 项目中,我使用了 Gradle。不过,根据大家的反馈来看还是更愿意使用 Maven 一些。



目前(2026 年 1 月)Maven 4.0 仍处于 Release Candidate 阶段,最新版本为 4.0.0-rc-5(2025 年 11 月 08 日发布),尚未正式 GA(General Availability)。



虽然目前 Maven 4 还处于 Release Candidate(RC)阶段,但它展现出来的特性足以让我们这些长期被 Maven 3 “历史债”折磨的开发者感到兴奋。


一句话总结:Maven 4 要求最低 Java 17 运行环境,通过分离构建与消费模型、树形生命周期等黑科技,彻底告别了臃肿且难以维护的 POM。


下面简单介绍一下 Maven 4 的最重要新特性(基于官方文档和发布记录):


Build POM 与 Consumer POM 的分离


这是 Maven 4 解决的最大痛点。在 Maven 3 时代,你发布的 pom.xml 既要管“怎么构建”,又要管“别人怎么依赖”,导致发布的元数据极其臃肿,甚至带有大量的 profile 和本地路径。


Maven 4 解决方案



  • Build POM:这就是你本地编辑的 pom.xml(模型升级至 4.1.0)。它包含所有的构建细节,比如插件配置、私有 profile 等。

  • Consumer POM:当你执行 deploy 时,Maven 4 会自动生成一个“纯净版”的 pom.xml(固定为 4.0.0 模型)。它去掉了所有插件、build 逻辑和 parent 继承关系,仅保留 GAV 坐标和核心依赖。



默认关闭 ,需显式开启:


mvn deploy -Dmaven.consumer.pom.flatten=true

或在项目根 .mvn/maven-user.properties 中永久配置:


maven.consumer.pom.flatten=true

这样的话,发布的 artifact 更干净,依赖解析更快,生态(Gradle、sbt、IDE、Sonatype 等)兼容性更好,无需再依赖 flatten-maven-plugin 等 hack 方案。



POM 模型升级到 4.1.0 + 多项简化语法


Maven 4 引入了全新的命名空间(**maven.apache.org/POM/4.1.0**…


1. 自动发现子项目



  • 新标签 <subprojects> :正式取代了容易产生术语混淆的 <modules>(标记为 deprecated)。

  • 隐式发现 :如果父项目 packaging=pom 且没有声明子项目,Maven 4 会自动扫描包含 pom.xml 的直接子目录。再也不用手动一行行写子模块名了!


2. 坐标推断(Inference)


<parent> 中,如果你按默认路径放置项目,可以省略 versiongroupId 甚至整个坐标。Maven 会自动从相对路径推断父 POM 坐标。


3. CI 友好变量原生支持


${revision}${sha1} 等变量现在是原生一等公民,不需要再写 hack 插件就能直接在命令行定义版本。


构建性能:从线性生命周期到树形并发


Maven 3 的生命周期是线性的,这意味着如果你的项目很大,构建过程就像“老牛拉破车”。


1. 树形生命周期与钩子


Maven 4 将生命周期升级为树形结构,并引入了 before:xxxafter:xxx 阶段。你可以更精准地在每个阶段前后绑定插件。


默认还是 Maven 3 时代的线性行为(向后兼容)。


要真正用上树形 + 更细粒度并发,必须显式加参数 -b concurrent(或 --builder concurrent)。


2. 并发构建器 (-b concurrent)


传统的并发构建往往受限于父子依赖。Maven 4 的并发构建器只要依赖模块进入 “Ready” 状态就会立即开跑,不再傻等父模块完成所有阶段。


开发者体验优化


1. 构建恢复 (-r / --resume)


大型项目构建到 90% 挂了?在 Maven 4 里直接 -r 即可从失败处继续,自动跳过已成功的模块。这简直是多模块项目的“救命稻草”。


2. 延迟发布 (deployAtEnd 默认开启)


为了防止出现“半成品”发布(一部分模块发了,另一部分报错没发),Maven 4 默认会在所有模块全部构建成功后才进行最后的统一发布。



3. 官方迁移助手 (mvnup)


担心升级出问题?官方直接给了 mvnup 工具,自动扫描并建议如何将你的 3.x 项目迁移到 4.1.0 模型。



现在该升级吗?



  • 生产环境:由于目前还在 RC 阶段,且最低要求 Java 17,建议观望,等正式 GA 之后再小范围灰度。

  • 新项目/个人实验:强烈建议开启 POM 4.1.0 进行尝试。特别是 Build/Consumer POM 的分离,能让你的项目元数据管理水平提升一个档次。

  • 大厂多模块项目:如果你深陷“Maven 构建慢、POM 维护难”的泥潭,Maven 4 的并发构建和自动子项目发现正是你需要的解药。


面对 Maven 二十年来最大的变动,你最期待哪个功能?或者你已经转向了 Gradle?欢迎在评论区留言,我们一起“对齐”一下!


相关地址:



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

一些我推荐的前端代码写法

web
使用解构赋值简化变量声明 const obj = { a:1, b:2, c:3, d:4, e:5, } // 不好的写法 const a = obj.a; const b = obj.b; const c = ob...
继续阅读 »

使用解构赋值简化变量声明


const obj = {
a:1,
b:2,
c:3,
d:4,
e:5,
}

// 不好的写法
const a = obj.a;
const b = obj.b;
const c = obj.c;
const d = obj.d;
const e = obj.e;

// 我推荐的
const {a: newA = '',b,c,d,e} = obj || {};


  • 要注意解构的对象不能为undefinednull。否则会报错。所以可以给个空对象作为默认值

  • 解构的 key 如果不存在,可以给个默认值,避免后续逻辑出错


合并数据


const a = [1,2];
const b = [3,4];
const obj1 = {
a:1,
}
const obj2 = {
b:1,
}

// 一般的写法
const c = a.concat(b);
const obj = Object.assign({}, obj1, obj2);

// 我推荐的写法
const c = [...arr1, ...arr2];
const obj = { ...obj1, ...obj2 };

Object.assign 和 Array.concat 其实也可以,只不过拓展运算符的优势如下:



  • 更简洁,阅读性更好

  • 会创建新的对象/数组,不会污染原数据(避免副作用)

  • 支持深层次嵌套结构的合并

  • 类型安全,编译时检查


条件判断


条件判断的话有几种情况,第一种是常见的多个条件判断


// 不好的写法
if(
type == 1 ||
type == 2 ||
type == 3 ||
type == 4 ||
){
//...
}


// 我推荐的
const typeArr = [1,2,3,4]
if (typeArr.includes(type)) {
//...
}

这样写代码会更简洁。如果其他地方也有相同的条件判断逻辑,当需要同时修改时,只需要修改 typeArr 即可。


第二种是三目运算符的条件判断,三目运算符我个人认为如果是简单的判断可以写,但是稍微复杂或着未来会改动的判断,最好不要使用三目运算符。容易三目运算符无限嵌套


let c = 1, d = 2, e = 3

// 不好的写法
const obj = {
a: 1,
b: (c === 1 || d === 1) ? 'bb' : d === 2 ? 'vv' : e === 3 ? '66' : null
}


// 我推荐的写法1
const obj = {
a: 1,
}
if (c === 1 || d === 1) {
obj.b = 'bb'
} else if (d === 2) {
obj.b = 'vv'
} else if (e === 3) {
obj.b = '66'
} else {
obj.b = null
}

// 我推荐的写法2
const valueMap = [
{ condition: (c, d, e) => c === 1 || d === 1, value: 'bb' },
{ condition: (c, d, e) => d === 2, value: 'vv' },
{ condition: (c, d, e) => e === 3, value: '66' }
];
function getValueByMap(c, d, e) {
const match = valueMap.find(item => item.condition(c, d, e));
return match ? match.value : null;
}
getValueByMap(c, d, e


// 我推荐的写法3
const conditionConfig = {
rules: [
{ name: 'rule1', check: (c, d, e) => c === 1 || d === 1, result: 'bb' },
{ name: 'rule2', check: (c, d, e) => d === 2, result: 'vv' },
{ name: 'rule3', check: (c, d, e) => e === 3, result: '66' }
],
defaultValue: null
};

function evaluateConditions(c, d, e, config) {
for (const rule of config.rules) {
if (rule.check(c, d, e)) {
return rule.result;
}
}
return config.defaultValue;
}
evaluateConditions(c, d, e, conditionConfig)

写法1、写法2、写法3都可以,具体可以看团队代码规范。


一般来说,写法1适用于比较简单的条件判断,比如请求参数时,可能会不同的情况添加额外的参数


写法2适用于条件比较多的情况


写法3使用于条件判断经常改的情况,这种情况可以使用配置化的方式封装条件判断。(ps:甚至在后续迭代时,如果产品跟你battle,你可以拿代码怼回去。兜底留痕)


纯函数


最好一个函数只做一件事,可以组合可以拆分


// 不好的写法
function createObj(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}

// 我推荐的写法
function createFile(name) {
fs.create(name);
}

function createTempFile(name) {
createFile(`./temp/${name}`)
}

不好的写法不满足纯函数的概念,相同的输入有了不同的输出


再举一个例子:


//不好的写法
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}


//我推荐的写法
function emailClients(clients) {
clients
.filter(isClientRecord)
.forEach(email)
}
function isClientRecord(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive()
}

这样写逻辑更清晰,易读。



  • 巧用filter函数,把filter的回调单开一个函数进行条件处理,返回符合条件的数据

  • 符合条件的数据再巧用forEach,执行email函数


函数参数个数不要超过2个


就我个人而言,当函数的参数个数超过2个时,我会以对象的形式作为参数传入


// 不好的写法
function create(p1, p2, p3, p4) {
// ...
}
create(1,'2',true,[])


// 我推荐的写法
const config = {
p1: 1,
p2: '2',
p3: true,
p4: []
}
function create(config) {

}
create(config)

这样写在调用函数时,代码更简洁,可读性更好。


获取对象属性值


// 不好的写法
const name = obj && obj.name;

// 我推荐的写法
const name = obj?.name;

可选链让语法更简洁


箭头函数简化


// 传统函数定义
function add(a, b) {
return a + b;
}

// 箭头函数简化
const add = (a, b) => a + b;

需要注意的时,如果函数体涉及到了 this,则需要注意箭头函数 this 的指向问题


简化函数参数


  // 不好的写法
function greet(name) {
const finalName = name || 'Guest';
console.log(`Hello, ${finalName}!`);
}

// 我推荐的写法
function greet({ name = 'Guest' }) {
console.log(`Hello, ${name}!`);
}

过滤操作


前端一般会涉及到过滤操作,比如精准过滤


const a = [1,2,3,4,5];

// 不好的写法
const result = a.filter(
item => {
return item === 3
}
)

// 我推荐的写法
const result = a.find(item => item === 3)

find相较于 filter 来说,有结果时不会继续遍历数组,性能更好


非空条件判断


有些时候,我们要判断值是否是 null、undefined 时,可以通过 ?? 判断


// 一般的写法
if (a !== null && a !== undefined) {
const b = 'BBBB'
}

// 我推荐的写法
const b = a ?? 'BBBB'

??运算符是当左侧是 null 或者 undefined 时,会取右侧的值


注释


在适当的地方写上注释,方便后续迭代


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

从一个程序员的角度告诉你:“12306”有多牛逼?

每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票! 12306 抢票,极限并发带来的思考 虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会。 尤其是春节期间,大家不仅使用 12306,还会考虑“智行”和其...
继续阅读 »

每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票!


12306 抢票,极限并发带来的思考


虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会。


尤其是春节期间,大家不仅使用 12306,还会考虑“智行”和其他的抢票软件,全国上下几亿人在这段时间都在抢票。


“12306 服务”承受着这个世界上任何秒杀系统都无法超越的 QPS,上百万的并发再正常不过了!


笔者专门研究了一下“12306”的服务端架构,学习到了其系统设计上很多亮点,在这里和大家分享一下并模拟一个例子:如何在 100 万人同时抢 1 万张火车票时,系统提供正常、稳定的服务。


Github代码地址:


https://github.com/GuoZhaoran/spikeSystem

大型高并发系统架构


高并发的系统架构都会采用分布式集群部署,服务上层有着层层负载均衡,并提供各种容灾手段(双火机房、节点容错、服务器灾备等)保证系统的高可用,流量也会根据不同的负载能力和配置策略均衡到不同的服务器上。


下边是一个简单的示意图:


图片




负载均衡简介


上图中描述了用户请求到服务器经历了三层的负载均衡,下边分别简单介绍一下这三种负载均衡。


①OSPF(开放式最短链路优先)是一个内部网关协议(Interior Gateway Protocol,简称 IGP)


OSPF 通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,OSPF 会自动计算路由接口上的 Cost 值,但也可以通过手工指定该接口的 Cost 值,手工指定的优先于自动计算的值。


OSPF 计算的 Cost,同样是和接口带宽成反比,带宽越高,Cost 值越小。到达目标相同 Cost 值的路径,可以执行负载均衡,最多 6 条链路同时执行负载均衡。


②LVS (Linux Virtual Server)


它是一种集群(Cluster)技术,采用 IP 负载均衡技术和基于内容请求分发技术。


调度器具有很好的吞吐率,将请求均衡地转移到不同的服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能的、高可用的虚拟服务器。


③Nginx


想必大家都很熟悉了,是一款非常高性能的 HTTP 代理/反向代理服务器,服务开发中也经常使用它来做负载均衡。


Nginx 实现负载均衡的方式主要有三种:



  • 轮询

  • 加权轮询

  • IP Hash 轮询


下面我们就针对 Nginx 的加权轮询做专门的配置和测试。





Nginx 加权轮询的演示


Nginx 实现负载均衡通过 Upstream 模块实现,其中加权轮询的配置是可以给相关的服务加上一个权重值,配置的时候可能根据服务器的性能、负载能力设置相应的负载。


下面是一个加权轮询负载的配置,我将在本地的监听 3001-3004 端口,分别配置 1,2,3,4 的权重:


#配置负载均衡
    upstream load_rule {
       server 127.0.0.1:3001 weight=1;
       server 127.0.0.1:3002 weight=2;
       server 127.0.0.1:3003 weight=3;
       server 127.0.0.1:3004 weight=4;
    }
    ...
    server {
    listen       80;
    server_name  load_balance.com http://www.load_balance.com;
    location / {
       proxy_pass http://load_rule;
    }
}

我在本地 /etc/hosts 目录下配置了 http://www.load_balance.com 的虚拟域名地址。


接下来使用 Go 语言开启四个 HTTP 端口监听服务,下面是监听在 3001 端口的 Go 程序,其他几个只需要修改端口即可:


package main

import (
    "net/http"
    "os"
    "strings"
)

func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3001"nil)
}

//处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
    failedMsg :=  "handle in port:"
    writeLog(failedMsg, "./stat.log")
}

//写入日志
func writeLog(msg string, logPath string) {
    fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    defer fd.Close()
    content := strings.Join([]string{msg, "\r\n"}, "3001")
    buf := []byte(content)
    fd.Write(buf)
}

我将请求的端口日志信息写到了 ./stat.log 文件当中,然后使用 AB 压测工具做压测:


ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket

统计日志中的结果,3001-3004 端口分别得到了 100、200、300、400 的请求量。

这和我在 Nginx 中配置的权重占比很好的吻合在了一起,并且负载后的流量非常的均匀、随机。


具体的实现大家可以参考 Nginx 的 Upsteam 模块实现源码,这里推荐一篇文章《Nginx 中 Upstream 机制的负载均衡》:


https://www.kancloud.cn/digest/understandingnginx/202607

秒杀抢购系统选型


回到我们最初提到的问题中来:火车票秒杀系统如何在高并发情况下提供正常、稳定的服务呢?

从上面的介绍我们知道用户秒杀流量通过层层的负载均衡,均匀到了不同的服务器上,即使如此,集群中的单机所承受的 QPS 也是非常高的。如何将单机性能优化到极致呢?

要解决这个问题,我们就要想明白一件事: 通常订票系统要处理生成订单、减扣库存、用户支付这三个基本的阶段。


我们系统要做的事情是要保证火车票订单不超卖、不少卖,每张售卖的车票都必须支付才有效,还要保证系统承受极高的并发。


这三个阶段的先后顺序该怎么分配才更加合理呢?我们来分析一下:




下单减库存


图片当用户并发请求到达服务端时,首先创建订单,然后扣除库存,等待用户支付。

这种顺序是我们一般人首先会想到的解决方案,这种情况下也能保证订单不会超卖,因为创建订单之后就会减库存,这是一个原子操作。

但是这样也会产生一些问题:



  • 在极限并发情况下,任何一个内存操作的细节都至关影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是可想而知的。

  • 如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制 IP 和用户的购买订单数量,这也不算是一个好方法。




支付减库存


图片


如果等待用户支付了订单在减库存,第一感觉就是不会少卖。但是这是并发架构的大忌,因为在极限并发情况下,用户可能会创建很多订单。


当库存减为零的时候很多用户发现抢到的订单支付不了了,这也就是所谓的“超卖”。也不能避免并发操作数据库磁盘 IO。




预扣库存


图片


从上边两种方案的考虑,我们可以得出结论:只要创建订单,就要频繁操作数据库 IO。

那么有没有一种不需要直接操作数据库 IO 的方案呢,这就是预扣库存。先扣除了库存,保证不超卖,然后异步生成用户订单,这样响应给用户的速度就会快很多;那么怎么保证不少卖呢?用户拿到了订单,不支付怎么办?

我们都知道现在订单都有有效期,比如说用户五分钟内不支付,订单就失效了,订单一旦失效,就会加入新的库存,这也是现在很多网上零售企业保证商品不少卖采用的方案。

订单的生成是异步的,一般都会放到 MQ、Kafka 这样的即时消费队列中处理,订单量比较少的情况下,生成订单非常快,用户几乎不用排队。


扣库存的艺术


从上面的分析可知,显然预扣库存的方案最合理。我们进一步分析扣库存的细节,这里还有很大的优化空间,库存存在哪里?怎样保证高并发下,正确的扣库存,还能快速的响应用户请求?


在单机低并发情况下,我们实现扣库存通常是这样的:


图片


为了保证扣库存和生成订单的原子性,需要采用事务处理,然后取库存判断、减库存,最后提交事务,整个流程有很多 IO,对数据库的操作又是阻塞的。


这种方式根本不适合高并发的秒杀系统。接下来我们对单机扣库存的方案做优化:本地扣库存。


我们把一定的库存量分配到本地机器,直接在内存中减库存,然后按照之前的逻辑异步创建订单。


改进过之后的单机系统是这样的:


图片


这样就避免了对数据库频繁的 IO 操作,只在内存中做运算,极大的提高了单机抗并发的能力。

但是百万的用户请求量单机是无论如何也抗不住的,虽然 Nginx 处理网络请求使用 Epoll 模型,c10k 的问题在业界早已得到了解决。

但是 Linux 系统下,一切资源皆文件,网络请求也是这样,大量的文件描述符会使操作系统瞬间失去响应。

上面我们提到了 Nginx 的加权均衡策略,我们不妨假设将 100W 的用户请求量平均均衡到 100 台服务器上,这样单机所承受的并发量就小了很多。


然后我们每台机器本地库存 100 张火车票,100 台服务器上的总库存还是 1 万,这样保证了库存订单不超卖,下面是我们描述的集群架构:


图片问题接踵而至,在高并发情况下,现在我们还无法保证系统的高可用,假如这 100 台服务器上有两三台机器因为扛不住并发的流量或者其他的原因宕机了。那么这些服务器上的订单就卖不出去了,这就造成了订单的少卖。

要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存。

有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“Buffer 库存”用来防止机器中有机器宕机的情况。


我们结合下面架构图具体分析一下:


图片


我们采用 Redis 存储统一库存,因为 Redis 的性能非常高,号称单机 QPS 能抗 10W 的并发。

在本地减库存以后,如果本地有订单,我们再去请求 Redis 远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖。

当机器中有机器宕机时,因为每个机器上有预留的 Buffer 余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖。

Buffer 余票设置多少合适呢,理论上 Buffer 设置的越多,系统容忍宕机的机器数量就越多,但是 Buffer 设置的太大也会对 Redis 造成一定的影响。

虽然 Redis 内存数据库抗并发能力非常高,请求依然会走一次网络 IO,其实抢票过程中对 Redis 的请求次数是本地库存和 Buffer 库存的总量。


因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑。


这在一定程度上也避免了巨大的网络请求量把 Redis 压跨,所以 Buffer 值设置多少,需要架构师对系统的负载能力做认真的考量。


代码演示


Go 语言原生为并发设计,我采用 Go 语言给大家演示一下单机抢票的具体流程。




初始化工作


Go 包中的 Init 函数先于 Main 函数执行,在这个阶段主要做一些准备性工作。

我们系统需要做的准备工作有:初始化本地库存、初始化远程 Redis 存储统一库存的 Hash 键值、初始化 Redis 连接池。


另外还需要初始化一个大小为 1 的 Int 类型 Chan,目的是实现分布式锁的功能。


也可以直接使用读写锁或者使用 Redis 等其他的方式避免资源竞争,但使用 Channel 更加高效,这就是 Go 语言的哲学:不要通过共享内存来通信,而要通过通信来共享内存。


Redis 库使用的是 Redigo,下面是代码实现:


...
//localSpike包结构体定义
package localSpike

type LocalSpike struct {
    LocalInStock     int64
    LocalSalesVolume int64
}
...
//remoteSpike对hash结构的定义和redis连接池
package remoteSpike
//远程订单存储健值
type RemoteSpikeKeys struct {
    SpikeOrderHashKey string    //redis中秒杀订单hash结构key
    TotalInventoryKey string    //hash结构中总订单库存key
    QuantityOfOrderKey string   //hash结构中已有订单数量key
}

//初始化redis连接池
func NewPool() *redis.Pool {
    return &redis.Pool{
        MaxIdle:   10000,
        MaxActive: 12000// max number of connections
        Dial: func() (redis.Conn, error) {
            c, err := redis.Dial("tcp"":6379")
            if err != nil {
                panic(err.Error())
            }
            return c, err
        },
    }
}
...
func init() {
    localSpike = localSpike2.LocalSpike{
        LocalInStock:     150,
        LocalSalesVolume: 0,
    }
    remoteSpike = remoteSpike2.RemoteSpikeKeys{
        SpikeOrderHashKey:  "ticket_hash_key",
        TotalInventoryKey:  "ticket_total_nums",
        QuantityOfOrderKey: "ticket_sold_nums",
    }
    redisPool = remoteSpike2.NewPool()
    done = make(chanint, 1)
    done <- 1
}



本地扣库存和统一扣库存


本地扣库存逻辑非常简单,用户请求过来,添加销量,然后对比销量是否大于本地库存,返回 Bool 值:


package localSpike
//本地扣库存,返回bool值
func (spike *LocalSpike) LocalDeductionStock() bool{
    spike.LocalSalesVolume = spike.LocalSalesVolume + 1
    return spike.LocalSalesVolume < spike.LocalInStock
}

注意这里对共享数据 LocalSalesVolume 的操作是要使用锁来实现的,但是因为本地扣库存和统一扣库存是一个原子性操作,所以在最上层使用 Channel 来实现,这块后边会讲。


统一扣库存操作 Redis,因为 Redis 是单线程的,而我们要实现从中取数据,写数据并计算一些列步骤,我们要配合 Lua 脚本打包命令,保证操作的原子性:


package remoteSpike
......
const LuaScript = `
        local ticket_key = KEYS[1]
        local ticket_total_key = ARGV[1]
        local ticket_sold_key = ARGV[2]
        local ticket_total_nums = tonumber(redis.call('HGET', ticket_key, ticket_total_key))
        local ticket_sold_nums = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))
        -- 查看是否还有余票,增加订单数量,返回结果值
       if(ticket_total_nums >= ticket_sold_nums) then
            return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
        end
        return0
`
//远端统一扣库存
func (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {
    lua := redis.NewScript(1, LuaScript)
    result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))
    if err != nil {
        returnfalse
    }
    return result != 0
}

我们使用 Hash 结构存储总库存和总销量的信息,用户请求过来时,判断总销量是否大于库存,然后返回相关的 Bool 值。


在启动服务之前,我们需要初始化 Redis 的初始库存信息:


hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0



响应用户信息


我们开启一个 HTTP 服务,监听在一个端口上:


package main
...
func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3005"nil)
}

上面我们做完了所有的初始化工作,接下来 handleReq 的逻辑非常清晰,判断是否抢票成功,返回给用户信息就可以了。


package main
//处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
    redisConn := redisPool.Get()
    LogMsg := ""
    <-done
    //全局读写锁
    if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
        util.RespJson(w, 1,  "抢票成功"nil)
        LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
    } else {
        util.RespJson(w, -1"已售罄"nil)
        LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
    }
    done <- 1

    //将抢票状态写入到log中
    writeLog(LogMsg, "./stat.log")
}

func writeLog(msg string, logPath string) {
    fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    defer fd.Close()
    content := strings.Join([]string{msg, "\r\n"}, "")
    buf := []byte(content)
    fd.Write(buf)
}

前边提到我们扣库存时要考虑竞态条件,我们这里是使用 Channel 避免并发的读写,保证了请求的高效顺序执行。我们将接口的返回信息写入到了 ./stat.log 文件方便做压测统计。




单机服务压测


开启服务,我们使用 AB 压测工具进行测试:


ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket

下面是我本地低配 Mac 的压测信息:


This is ApacheBench, Version 2.3 <$revision: 1826891="">
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:
Server Hostname:        127.0.0.1
Server Port:            3005

Document Path:          /buy/ticket
Document Length:        29 bytes

Concurrency Level:      100
Time taken for tests:   2.339 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      1370000 bytes
HTML transferred:       290000 bytes
Requests per second:    4275.96 [#/sec] (mean)
Time per request:       23.387 [ms] (mean)
Time per request:       0.234 [ms] (mean, across all concurrent requests)
Transfer rate:          572.08 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    8  14.7      6     223
Processing:     2   15  17.6     11     232
Waiting:        1   11  13.5      8     225
Total:          7   23  22.8     18     239

Percentage of the requests served within a certain time (ms)
  50%     18
  66%     24
  75%     26
  80%     28
  90%     33
  95%     39
  98%     45
  99%     54
 100%    239 (longest request)

根据指标显示,我单机每秒就能处理 4000+ 的请求,正常服务器都是多核配置,处理 1W+ 的请求根本没有问题。


而且查看日志发现整个服务过程中,请求都很正常,流量均匀,Redis 也很正常:


//stat.log
...
result:1,localSales:145
result:1,localSales:146
result:1,localSales:147
result:1,localSales:148
result:1,localSales:149
result:1,localSales:150
result:0,localSales:151
result:0,localSales:152
result:0,localSales:153
result:0,localSales:154
result:0,localSales:156
...

总结回顾


总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略


完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。


我们实现了高并发抢票的核心逻辑,可以说系统设计的非常的巧妙,巧妙的避开了对 DB 数据库 IO 的操作。

对 Redis 网络 IO 的高并发请求,几乎所有的计算都是在内存中完成的,而且有效的保证了不超卖、不少卖,还能够容忍部分机器的宕机。

我觉得其中有两点特别值得学习总结:

①负载均衡,分而治之


通过负载均衡,将不同的流量划分到不同的机器上,每台机器处理好自己的请求,将自己的性能发挥到极致。


这样系统的整体也就能承受极高的并发了,就像工作的一个团队,每个人都将自己的价值发挥到了极致,团队成长自然是很大的。


②合理的使用并发和异步

自 Epoll 网络架构模型解决了 c10k 问题以来,异步越来越被服务端开发人员所接受,能够用异步来做的工作,就用异步来做,在功能拆解上能达到意想不到的效果。


这点在 Nginx、Node.JS、Redis 上都能体现,他们处理网络请求使用的 Epoll 模型,用实践告诉了我们单线程依然可以发挥强大的威力。

服务器已经进入了多核时代,Go 语言这种天生为并发而生的语言,完美的发挥了服务器多核优势,很多可以并发处理的任务都可以使用并发来解决,比如 Go 处理 HTTP 请求时每个请求都会在一个 Goroutine 中执行。


总之,怎样合理的压榨 CPU,让其发挥出应有的价值,是我们一直需要探索学习的方向。


作者:皮皮林551
来源:juejin.cn/post/7541770924800163875
收起阅读 »

如何优雅地实现每 5 秒轮询请求?

web
在做实时监控系统时,比如服务器状态面板、订单处理中心或物联网设备看板,每隔 5 秒自动拉取最新数据是再常见不过的需求了。 但你有没有遇到过这些问题? 页面切到后台还在疯狂发请求,浪费资源 上一次请求还没回来,下一次又发了,接口雪崩 用户切换标签页回来,发现数...
继续阅读 »

在做实时监控系统时,比如服务器状态面板、订单处理中心或物联网设备看板,每隔 5 秒自动拉取最新数据是再常见不过的需求了。


但你有没有遇到过这些问题?



  • 页面切到后台还在疯狂发请求,浪费资源

  • 上一次请求还没回来,下一次又发了,接口雪崩

  • 用户切换标签页回来,发现数据“卡”在旧状态

  • 页面销毁了定时器还在跑,内存泄漏


今天我就以一个运维监控平台的真实场景为例,带你从“能用”做到“好用”。




一、问题场景:设备在线状态轮询


假设我们要做一个 IDC 机房设备监控页,需求如下:



  • 每 5 秒查询一次所有服务器的在线状态

  • 接口 /api/servers/status 响应较慢(平均 1.2s)

  • 用户可能切换到其他标签页处理邮件

  • 页面关闭时必须停止轮询


如果直接写个 setInterval,很容易踩坑。我们一步步来优化。




二、第一版:基础轮询(能跑,但有隐患)


import { ref, onMounted, onUnmounted } from 'vue'

const servers = ref([])
let timer = null

onMounted(() => {
const poll = () => {
fetch('/api/servers/status')
.then(res => res.json())
.then(data => {
servers.value = data
})
}

poll() // 首次立即执行
timer = setInterval(poll, 5000) // 每5秒轮询
})

onUnmounted(() => {
clearInterval(timer) // 🔍 清理定时器
})

✅ 实现了基本功能

❌ 但存在三个致命问题:



  1. 接口未完成就发起下一次请求 → 可能雪崩

  2. 页面不可见时仍在轮询 → 浪费带宽和电量

  3. 异常未处理 → 网络错误可能导致后续不再轮询




三、第二版:可控轮询 + 可见性优化


我们改用“请求完成后再延迟 5 秒”的策略,避免并发:


import { ref, onMounted, onUnmounted } from 'vue'

const servers = ref([])
let abortController = null // 用于取消请求

const poll = async () => {
try {
// 支持取消上一次请求
abortController?.abort()
abortController = new AbortController()

const res = await fetch('/api/servers/status', {
signal: abortController.signal
})

if (!res.ok) throw new Error('Network error')

const data = await res.json()
servers.value = data
} catch (err) {
if (err.name !== 'AbortError') {
console.warn('轮询失败,将重试...', err)
}
} finally {
// 🔍 请求结束后再等5秒发起下一次
setTimeout(poll, 5000)
}
}

onMounted(() => {
poll() // 启动轮询
})

onUnmounted(() => {
abortController?.abort()
})

🔍 关键点解析:



  • finallysetTimeout 实现“串行轮询”,避免并发

  • AbortController 可在组件卸载时主动取消进行中的请求

  • 错误被捕获后仍继续轮询,保证稳定性




四、第三版:智能节流 —— 页面可见性控制


现在解决“页面不可见时是否轮询”的问题。我们引入 visibilitychange 事件:


let isVisible = true

const handleVisibilityChange = () => {
isVisible = !document.hidden
console.log('页面可见性:', isVisible ? '可见' : '隐藏')
}

onMounted(() => {
// 监听页面可见性
document.addEventListener('visibilitychange', handleVisibilityChange)

const poll = async () => {
try {
abortController?.abort()
abortController = new AbortController()

const res = await fetch('/api/servers/status', {
signal: abortController.signal
})

const data = await res.json()
servers.value = data
} catch (err) {
if (err.name !== 'AbortError') {
console.warn('轮询失败:', err)
}
} finally {
// 🔍 只有页面可见时才继续轮询
if (isVisible) {
setTimeout(poll, 5000)
} else {
// 页面隐藏,等待恢复后再请求
document.addEventListener('visibilitychange', function waitVisible() {
if (!document.hidden) {
document.removeEventListener('visibilitychange', waitVisible)
setTimeout(poll, 1000) // 恢复后1秒再查
}
}, { once: true })
}
}
}

poll()
})

🔍 这里做了两层控制:



  1. 页面隐藏时,不再自动发起下一轮请求

  2. 页面重新可见时,延迟 1 秒触发一次查询,避免瞬间唤醒过多资源




五、封装成可复用的轮询 Hook


把这套逻辑抽象成通用 usePolling Hook:


// composables/usePolling.js
import { ref } from 'vue'

export function usePolling(fetchFn, interval = 5000) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
let abortController = null
let isVisible = true

const poll = async () => {
if (loading.value) return // 防止重复执行

loading.value = true
error.value = null

try {
abortController?.abort()
abortController = new AbortController()

const result = await fetchFn(abortController.signal)
data.value = result
} catch (err) {
if (err.name !== 'AbortError') {
error.value = err
console.warn('Polling error:', err)
}
} finally {
loading.value = false
// 🔍 根据可见性决定是否继续
if (isVisible) {
setTimeout(poll, interval)
}
}
}

const start = () => {
// 移除旧监听避免重复
document.removeEventListener('visibilitychange', handleVisibility)
document.addEventListener('visibilitychange', handleVisibility)
poll()
}

const stop = () => {
abortController?.abort()
document.removeEventListener('visibilitychange', handleVisibility)
}

const handleVisibility = () => {
isVisible = !document.hidden
if (isVisible) {
setTimeout(poll, 1000)
}
}

return { data, loading, error, start, stop }
}

使用方式极其简洁:


<script setup>
import { usePolling } from '@/composables/usePolling'

const fetchStatus = async (signal) => {
const res = await fetch('/api/servers/status', { signal })
return res.json()
}

const { data, loading } = usePolling(fetchStatus, 5000)

// 自动在 onMounted 启动
</script>

<template>
<div v-if="loading">加载中...</div>
<ul v-else>
<li v-for="server in data" :key="server.id">
{{ server.name }} - {{ server.status }}
</li>
</ul>
</template>



六、对比主流轮询方案


方案实现方式优点缺点适用场景
setInterval固定间隔触发简单直观不考虑响应时间,易并发快速原型
串行 setTimeout请求完再延时避免并发,稳定周期不严格多数业务场景 ✅
WebSocket服务端推送实时性最高成本高,兼容性差股票行情、聊天
Server-Sent Events单向流式推送轻量级实时不支持 IE日志流、通知
智能轮询(本方案)可见性+串行控制节能、稳定、用户体验好略复杂生产环境推荐 ✅



七、举一反三:三个变体场景实现思路



  1. 动态轮询频率

    如网络异常时降频至 30s 一次,正常后恢复 5s。可在 finally 中根据 error.value 动态调整 setTimeout 时间。

  2. 多接口协同轮询

    多个 API 轮询但希望错峰发送。可用 Promise.all 组合请求,在 finally 统一控制下一轮时机,避免瞬间并发。

  3. 离线重连机制

    当检测到网络断开(fetch 超时),改为指数退避重试(1s → 2s → 4s → 8s),恢复后再切回 5s 正常轮询。




小结


实现“每 5 秒轮询”看似简单,但要做到稳定、节能、用户体验好,需要考虑:



  • ✅ 使用 串行 setTimeout 替代 setInterval,避免请求堆积

  • ✅ 利用 AbortController 主动取消无用请求

  • ✅ 结合 页面可见性 API 节省资源

  • ✅ 封装为 可复用 Hook,提升工程化水平


记住一句话:好的轮询,是“聪明地少做事”,而不是“拼命做事情”


下次当你接到“每隔 X 秒刷新”的需求时,别急着写 setInterval,先问问自己:用户真的需要这么频繁吗?能不能用 WebSocket?页面看不见的时候还要刷吗?


作者:前端微白
来源:juejin.cn/post/7530948113120624675
收起阅读 »

妙啊!Js的对象属性居然还能这么写

web
Hi,我是石小石~ 静态属性获取的缺陷 前段时间在做项目国际化时,遇到一个比较隐蔽的问题: 我们在定义枚举常量时,直接调用了 i18n 的翻译方法: export const OverdueStatus: any = { ABOUT_TO_OVERDUE...
继续阅读 »

Hi,我是石小石~




静态属性获取的缺陷


前段时间在做项目国际化时,遇到一个比较隐蔽的问题:


我们在定义枚举常量时,直接调用了 i18n 的翻译方法:


export const OverdueStatus: any = {
ABOUT_TO_OVERDUE: {
value: 'ABOUT_TO_OVERDUE',
name: i18n.global.t('common.about_to_overdue'),
color: '#ad0000',
bgColor: '#ffe1e1'
},
}

结果发现翻译始终不生效。排查后才发现原因很简单 —— OverdueStatus 对象的初始化早于 i18n 实例的生成,因此取到的翻译结果是空的。


虽然最后我通过封装自定义 Vue 插件的方式彻底解决了问题,但排查过程中其实还有一个可选思路。


当时我想到的最直接办法是:让 name 在被访问时再去执行 i18n.global.t,而不是在对象定义时就执行。比如把 OverdueStatus 定义为函数:


export const OverdueStatus = () => ({
ABOUT_TO_OVERDUE: {
value: 'ABOUT_TO_OVERDUE',
name: i18n.global.t('common.about_to_overdue'),
color: '#ad0000',
bgColor: '#ffe1e1'
},
})

这样在调用时:


OverdueStatus().ABOUT_TO_OVERDUE.name

就能确保翻译逻辑在 i18n 实例创建完成之后再执行,从而避免初始化顺序的问题。不过,这种方式也有明显的缺点:所有类似的枚举都要改成函数,调用时也得多加一层执行,整体代码会变得不够简洁。


如何优雅地实现“动态获取属性”?


上面提到的“把枚举改成函数返回”虽然能解决问题,但在实际业务中显得有些笨拙。有没有更优雅的方式,让属性本身就支持 动态计算 呢?


其实,JavaScript 本身就为我们提供了解决方案 —— getter


举个例子,我们可以把枚举对象改写成这样:


export const OverdueStatus: any = {
ABOUT_TO_OVERDUE: {
value: 'ABOUT_TO_OVERDUE',
get name() {
return i18n.global.t('common.about_to_overdue')
},
color: '#ad0000',
bgColor: '#ffe1e1'
},
}

这样一来,在访问 name 属性时,才会真正执行 i18n.global.t,确保翻译逻辑在 i18n 实例创建完成后才生效,完美解决问题。


访问器属性的原理


在 JavaScript 规范里,get 定义的属性叫 访问器属性,区别于普通的 数据属性 (Data Property) 。简单来说getter 其实就是对象属性的一种特殊定义方式。


当我们写:


const obj = {
get foo() {
return "bar"
}
}

等价于用 Object.defineProperty


const obj = {}
Object.defineProperty(obj, "foo", {
get: function() {
return "bar"
}
})

所以访问 obj.foo 时,其实是触发了这个 get 函数,而不是读取一个固定的值。


类比Vue的computed


在 Vue 里,我们经常写 computed 计算属性,其实就是 getter 的思想。


import { computed, ref } from "vue"

const firstName = ref("Tom")
const lastName = ref("Hanks")

const fullName = computed(() => `${firstName.value} ${lastName.value}`)

computed 内部其实就是包装了一个 getter 函数。


注意点



  • getter 不能跟属性值同时存在:


const obj = {
get name() { return "石小石" },
name: "石小石Orz" // 会报错
}


  • getter 是只读的,如果你想支持赋值,需要配合 setter


const obj = {
_age: 18,
get age() { return this._age },
set age(val) { this._age = val }
}

obj.age = 20
console.log(obj.age) // 20

其他实用场景


延迟计算


有些值计算比较复杂,但只有在真正使用时才去算,可以提升性能


const user = {
firstName: "石",
lastName: "小石",
get fullName() {
// 类比一个计算,实现开发中,一个很复杂的计算才使用此方法
console.log("计算了一次 fullName")
return `${this.firstName} ${this.lastName}`
}
}

console.log(user.fullName) // "石小石"

这种写法让 API 看起来更自然,不需要调用函数 user.getFullName(),而是 user.fullName


数据封装与保护


有些属性可能并不是一个固定字段,而是基于内部状态计算出来的:


const cart = {
items: [100, 200, 300],
get total() {
return this.items.reduce((sum, price) => sum + price, 0)
}
}

console.log(cart.total) // 600

这样 cart.total 永远是最新的,不用担心手动维护,你也不用写一个函数专门去更新这个值。


作者:石小石Orz
来源:juejin.cn/post/7543300730116325403
收起阅读 »