普通视图

发现新文章,点击刷新页面。
今天 — 2025年5月20日技术

“就跑一下”的代码,不该为它单独开一台服务器!直接上亚马逊云 Lambda!

2025年5月20日 09:04

很多人第一次用云服务,脑子里装的是“架个服务、跑个 API、调个模型”,于是打开 EC2,跟着教程买了一台虚拟机,从此踏上了无休止的运维之路。

但是请冷静想一想,你真的需要 7x24 的服务吗?

我们平时要跑的很多任务,说白了就是“写完跑一下”:

  • 写个 webhook,接收 GitHub 的 Push 回调
  • 每天凌晨处理 S3 里新上传的文件,入库、通知一下
  • 写个 AI agent,帮你分析报表,执行一次就够

这种需求压根没必要常驻服务、维护端口、配置环境,更不值得你为它开台 EC2 或部署容器集群。真的,是 不值当

而 Amazon Lambda,正是为这种“碎片化、轻量级、按需触发”的任务设计的。


🧠 为什么 Lambda 正好兜住这类需求?

因为它做了一件事:把“运维责任”彻底抽走了

你不再需要:

  • 考虑服务该放在哪儿跑(Lambda 自带执行环境)
  • 管理运行时状态(调用完自动释放资源)
  • 挂接口、配安全组、启监听(触发器接上就好)

而且它默认就和 亚马逊云科技 生态联动紧密: S3、EventBridge、API Gateway、SNS、Bedrock……通通支持作为触发来源。你只要写好函数逻辑,剩下的“怎么调用”都有人兜底。

说得再直白点:Lambda 就是 函数即服务(Function as a Service)的标杆实现。你写逻辑,它帮你跑。


🧾 成本呢?0元也能跑

你没听错。Amazon Lambda 提供的是永久性的免费额度:

  • 每月 100 万次请求
  • 每月 40 万 GB·s 运行时长

不是试用期,不是隐藏条款,是每个账户都自带的“Always Free”额度5-1。

你写个 webhook,平时每小时触发一次,一年都用不完这额度。

你跑个日报生成服务,每天执行一次脚本,也能在免费范围内轻松搞定。

你甚至可以连模型调用逻辑一并托管,用 Lambda 处理 prompt,再打通 Bedrock 或 SageMaker。


⚙️ 一个真实例子:我只想接个表格,处理一下,然后走人

这是我几周前在项目中写的一个 Lambda 逻辑:

  • 用户上传了一个 Excel 到 S3
  • EventBridge 监听到上传事件,触发 Lambda
  • Lambda 调用 Pandas 分析表格、提取内容、写入数据库
  • 分析结果推送到企业微信机器人通知

目录:

project/
├── lambda_function.py        # 核心逻辑
├── requirements.txt          # Pandas 依赖
└── deploy.zip                # 打包上传

依赖库:

pandas
openpyxl
boto3
requests

py脚本:

import json
import boto3
import pandas as pd
import requests
import os
from io import BytesIO

dynamodb = boto3.resource('dynamodb')
s3 = boto3.client('s3')

# 环境变量提前设置
TABLE_NAME = os.environ.get("TABLE_NAME")
WECHAT_WEBHOOK = os.environ.get("WECHAT_WEBHOOK")

def lambda_handler(event, context):
    # 1. 从 S3 事件中提取 Bucket 和 Key
    record = event['Records'][0]['s3']
    bucket = record['bucket']['name']
    key = record['object']['key']
    
    print(f"Processing file: s3://{bucket}/{key}")
    
    # 2. 下载 Excel 文件
    response = s3.get_object(Bucket=bucket, Key=key)
    content = response['Body'].read()
    df = pd.read_excel(BytesIO(content), engine='openpyxl')
    
    print(f"Loaded DataFrame with shape: {df.shape}")
    
    # 3. 简单分析示例(比如统计每个“部门”的数量)
    summary = df['部门'].value_counts().to_dict()
    print(f"Summary: {summary}")
    
    # 4. 写入 DynamoDB(每个部门写一行)
    table = dynamodb.Table(TABLE_NAME)
    for department, count in summary.items():
        table.put_item(Item={
            'Department': department,
            'Count': count,
            'UploadedFile': key
        })
    
    # 5. 推送到企业微信机器人
    content_lines = [f"{dept}: {cnt}人" for dept, cnt in summary.items()]
    msg = {
        "msgtype": "text",
        "text": {
            "content": f"📊 新文件分析完成:{key}\n\n" + "\n".join(content_lines)
        }
    }
    r = requests.post(WECHAT_WEBHOOK, json=msg)
    print(f"WeChat notification sent: {r.status_code}")
    
    return {
        'statusCode': 200,
        'body': json.dumps('Excel 分析完成,通知已发送')
    }

打包上传:

pip install -r requirements.txt -t .
zip -r deploy.zip .
# 上传到 AWS Lambda 控制台

这整个流程的服务端,没有服务器,没有 Docker,没有定时器,也没有守护进程。

只有 Lambda 一行一行执行着我写的函数,执行完就自动退出,什么都不留。 它不是“常驻应用”,它更像一个“轻触即发”的服务弹簧。


🤖 AI Agent 的最佳容器?

我现在越来越多地把一些“函数型 AI agent”也挂在 Lambda 上:

  • 它们只是根据 prompt 推理、做个判断、输出一段话
  • 每次触发一次,跑完就销毁,不需要上下文
  • 数据流接 S3、DynamoDB、API Gateway 都方便

甚至连安全问题都考虑进去了。比如你可以配合 Amazon Bedrock 的 Guardrails 做内容审查,或者设置角色权限只允许读取某个特定资源。

AI 工程不等于部署大模型服务,有时候更像是 orchestrate 几个“用完即走”的 AI 小模块。Lambda 就非常适合当这个 glue layer。


📦 小结:不是所有任务都配得上“服务器”

Lambda 给我们的启发不只是“便宜”,而是一个理念上的转变:

有些任务,不该为了“能执行”就搞一个服务,而是该被“托管执行”。

我们开发者大多数时候不是在跑系统,而是在跑函数。Lambda 把这个过程变得极致简单:

  • 没有服务,只写逻辑
  • 没有常驻,调用即走
  • 没有费用,跑着还免费

哪怕你不做 AI、也不想用 亚马逊云科技 生态,Lambda 都是一个值得一试的工具。它适合跑任何“用完即走”的逻辑,像是现代编程世界的“if this then that”,只不过你自己能决定逻辑、能接模型、能连数据。


🧪 综上,我的建议很简单:下一次你想“跑个脚本看看”的时候,别再打开本地终端了。试试 Lambda,你会喜欢上那种“无负担”的开发感。

如果你已经在用了,也欢迎回来告诉我你怎么用它的;如果你在部署 AI 应用,也可以聊聊用 Lambda 的痛点,我这边也有踩过一些坑。

简单盘点下前端日志采集的几种方式

2025年5月20日 09:00

作者:前端开发爱好者

前端日志采集,说简单也简单,说复杂也复杂,取决于业务想要什么粒度的数据,以及开发者能接受多少侵入性、延迟和兼容性问题。今天我们就来盘一盘常见的几种前端上报方式,以及各自的优劣势和适用场景。

1. + GIF / Pixel 上报

原理:   通过在页面上动态创建一个Image对象,把要上报的数据编码到请求 URL 的 query 参数中,然后加载一个 1x1 的透明 GIF 图片(当然不需要真的返回一张图,后端 204 也行),重点是通过src请求资源的同时,让服务端记录.gif后的数据。

代码示例:

const img = new Image();
img.src = `https://logserver.com/collect?event=click&userId=123`;

优点:

  • 兼容性超好,从古早 IE 到现代浏览器都能用。
  • 不受 CORS 限制,因为图片加载天然跨域
  • 简单,几乎不会影响页面性能。

缺点:

  • 请求量小,受 URL 长度限制(一般 2KB 左右)。
  • 只能 GET,不能 POST。
  • 无法拿到发送成功或失败的准确回调。

适用场景:

  • 简单 PV/UV 打点,曝光上报。
  • 无需保证必达,只要发出请求就行。

2. fetch / XMLHttpRequest 上报

原理:   使用fetch或者XMLHttpRequest直接发 HTTP 请求,数据格式可以是 JSON 或表单数据,GET/POST 都行。

代码示例:

js
 体验AI代码助手
 代码解读
复制代码
// fetch版
fetch('https://logserver.com/collect', {
  method'POST',
  headers: { 'Content-Type''application/json' },
  body: JSON.stringify({ event'click', userId: 123 })
});

优点:

  • 可以 POST,支持大体积数据。
  • 可以拿到请求成功或失败的反馈。
  • 灵活,能带复杂头部,比如认证信息。

缺点:

  • 受 CORS 限制,需要服务器支持。
  • 发送过程中可能阻塞主线程,尤其是同步 XHR。
  • 如果页面关闭得太快,可能请求还没发出去(不过可以用keepalive选项优化)。

适用场景:

  • 错误日志、性能埋点。
  • 需要可靠上报、需要认证或者复杂参数时。

3. navigator.sendBeacon

原理:   专门为这种场景设计的 API,可以在页面卸载(比如跳转、关闭)时,异步且可靠地把数据发送到服务器,不阻塞页面卸载流程。

代码示例:

navigator.sendBeacon('https://logserver.com/collect', JSON.stringify({ event'unload', userId: 123 }));

优点:

  • 适合页面关闭前发送数据,不容易丢包。
  • 不阻塞 unload 流程,不卡界面。
  • 支持 POST,发送简单。

缺点:

  • 兼容性略差(但现在主流浏览器基本都支持了)。
  • 不支持自定义请求头。
  • 只支持 Content-Type 是application/x-www-form-urlencodedtext/plain的请求体。

适用场景:

  • 页面 unload 时的上报,比如用户行为日志、退出日志。

4. WebSocket 上报

原理:   建立长连接,把日志实时推送到服务器。

优点:

  • 实时性超强。
  • 理论上吞吐量高,连接一旦建立数据传输非常轻量。

缺点:

  • 建连、保活有成本,移动端或弱网环境下容易掉线。
  • 服务端也要有能力管理大量持久连接。
  • 不适合小流量、轻量应用。

适用场景:

  • 高实时要求的埋点,比如游戏、IM、股票类应用。

总结

方式 优势 劣势 常用场景
+ GIF 简单兼容 只能 GET,小数据量 曝光打点
fetch/XHR 灵活可靠 受 CORS 限制 错误日志、性能上报
sendBeacon 页面关闭也能发 兼容性略差,简单数据 离开页面上报
WebSocket 实时性强 成本高 游戏、IM 实时上报

实际开发里,我们会根据业务场景,多种方式结合使用:普通打点用fetch,页面 unload 用sendBeacon,曝光用Image兜底,再加一些重试机制,做到不丢、不卡、可靠。

CSS平移函数完全指南与三大应用

作者 云浪
2025年5月20日 08:50

平移函数(Translate functions)的作用是在二维或三维空间中,对元素进行平移操作,改变元素的位置。

目前平移函数有 translateX()translateY()translateZ()translate()translate3d()

translateX()

translateX() 用于水平移动一个元素。

例子如下:

<div>Static</div>
<div class="moved">Moved</div>
<div>Static</div>
div {
  width: 60px;
  height: 60px;
  background-color: skyblue;
}

.moved {
  transform: translateX(10px); /* Equal to translate(10px) */
  background-color: pink;
}

pic1.png

更多可见 translateX()

translateY()

垂直移动一个元素。

例子:

<div>Static</div>
<div class="moved">Moved</div>
<div>Static</div>
div {
  width: 60px;
  height: 60px;
  background-color: skyblue;
}

.moved {
  transform: translateY(10px);
  background-color: pink;
}

pic2.png

更多可见 translateY()

translateZ()

沿 z 轴平移一个元素。

translateZ(tz) 等同于 translate3d(0, 0, tz)

tz 为正值,则使元素朝向观察者移动;负值,则朝远离观察者方向移动

在这个示例中,创建了两个盒子。第一个盒子在页面上正常定位,完全未进行平移。第二个盒子则通过应用透视(perspective)来创建 3D 空间,然后向用户方向移动。

html:

<div>Static</div>
<div class="moved">Moved</div>

css:

div {
  position: relative;
  width: 60px;
  height: 60px;
  left: 100px;
  background-color: skyblue;
}

.moved {
  transform: perspective(500px) translateZ(200px);
  background-color: pink;
}

在类 moved 中,使用 perspective() 函数将观察者相对于 z=0 所在的平面(本质上是屏幕表面)进行定位。值为 500px 意味着用户位于 z=0 处图像的前方 500 像素位置。

然后,translateZ() 函数将元素从屏幕 “向外” 移动 200 像素,朝向用户方向。这会产生以下效果:在 2D 显示器上观看时,元素显得更大;在使用 VR 头显或其他 3D 显示设备时,元素则显得更近。

需要注意的是,如果 perspective() 的值小于 translateZ() 的值(例如 transform: perspective(200px) translateZ(300px);),变换后的元素将不可见,因为它超出了用户视口的范围。透视值与 translateZ() 值的差值越小,用户离元素就越近,平移后的元素看起来就越大。

使用变换函数的组合时,变换函数的顺序很重要,通常来说,需要将 perspective() 放在 translateZ() 前面。 如果类 moved 中将 translateZ() 放在 perspective() 前面,元素就不具备放大效果。

此例子的结果:

pic3.png

更多可见 translateZ()

translate()

在二维平面上,可同时对一个元素进行水平或垂直移动。

translate(10px)translateX(10px)translate(10px, 0) 作用一样。

更多可见 translate()

translate3d()

在三维空间中平移一个元素。

这种变换由三维向量 [tx, ty, tz] 构成,该向量的坐标确定了元素在各个方向上的移动距离。

translate3d(tx, ty, tz)

tx 为 x 轴方向的移动距离;ty 为 y 轴方向的移动距离;tz 为 z 轴方向的移动距离。可以是长度值或百分比。

如下面的例子,html 为

<div>Static</div>
<div class="moved">Moved</div>
<div>Static</div>

css 为

div {
  width: 60px;
  height: 60px;
  background-color: skyblue;
}

.moved {
  /* Equivalent to perspective(500px) translateX(10px) */
  transform: perspective(500px) translate3d(10px, 0, 0px);
  background-color: pink;
}

最终结果为

pic4.png

更多可见 translate3d()

平移函数的应用

避免布局抖动

使用平移函数移动元素,元素不会脱离文档流,可以保证页面布局稳定,元素移动后,不挤占或释放原位置空间,周围元素的布局完全不受影响。。

如这个例子,实现悬浮按钮,按钮向下移动 5px

<button>按钮</button>
<div>内容</div>
button:hover {
  margin-top: 5px; /* 下方元素下移 */
}

👆 此例子使用 margin 实现,由于 margin 调整元素外间距后,会挤占其他元素的空间,从而导致按钮下面的元素也向下移动:

gif1.gif

<button>按钮</button>
<div>内容</div>
button:hover {
  position: absolute;
  top: 5px;
}

👆 此例子使用绝对定位,将按钮元素向下移动 5px ,由于绝对定位会使元素脱离文档流,使元素的占位消失,所以按钮下面的元素会占据原按钮的位置,造成页面布局的破坏:

gif2.gif

<button>按钮</button>
<div>内容</div>
button:hover {
  transform: translateY(5px);
}

👆这个例子使用 translateY 使元素向下移动 5px ,由于平移函数不会脱离文档流,不改变元素在文档流中的原始位置,仅改变视觉渲染位置。因此按钮下面的内容不会跟着向下移动,保证页面布局的稳定:

gif3.gif

在开发类似于 tooltips(提示框)的组件时,可借助平移函数进行定位,避免底层内容被推开,保持页面结构稳定。

性能优化

translate() 属于 CSS 变换(transform),其渲染由 GPU 独立处理,不会触发浏览器的回流

若通过修改 margintop/left(定位)移动元素,改变了元素的几何位置,会导致浏览器重新计算布局(回流),性能开销较大(尤其在复杂页面中)。

平移函数仅改变视觉位置,不影响布局树,因此性能更优,适合高频动画(如轮播图滑动、列表项平移)。

元素居中

如果使用绝对定位 + 负 margin 实现元素居中,需要提前知道子元素的宽高和手动计算并硬编码负 margin 值

<div class="container">
  <div class="centered-box">我是固定尺寸的居中元素!</div>
</div>
.container {
  position: relative;  /* 关键:定位基准 */
  width: 80vw;   
  height: 300px;
  border: 2px dashed #e5e7eb;
  margin: 50px auto;
  background: #f8f9fa;
}
/* 子元素:需要居中的目标元素(需已知宽高) */
.centered-box {
  position: absolute;  /* 绝对定位,基于父容器 */
  left: 50%;           /* X轴:左上角移至父容器水平中心 */
  top: 50%;            /* Y轴:左上角移至父容器垂直中心 */
  
  /* 关键:负margin 调整 */
  width: 200px;        /* 子元素固定宽度 */
  height: 100px;       /* 子元素固定高度 */
  margin-left: -100px; /* 向左移动自身宽度的一半(200px/2) */
  margin-top: -50px;   /* 向上移动自身高度的一半(100px/2) */

  background: #3b82f6;
  color: white;
  border-radius: 8px;
  display: flex;
  align-items: center; 
  justify-content: center;
  font-size: 18px;
}

效果如下:

pic5.png

如果使用平移函数,则不需要提前知道自身的尺寸,代码更加简洁:

<div class="container">
  <div class="centered-box">
    我是动态居中的元素!<br>
    无论内容多少,都会始终居中。<br>
    试试调整我的文字长度或父容器尺寸~
  </div>
</div>
.container {
  position: relative;  /* 关键:定位基准 */
  width: 80vw;        
  height: 400px;      
  border: 2px dashed #e5e7eb; 
  margin: 50px auto;  
  background: #f8f9fa;
}
/* 子元素:需要居中的目标元素 */
.centered-box {
  position: absolute;  /* 绝对定位,基于父容器 */
  left: 50%;           /* X轴:左上角移至父容器水平中心 */
  top: 50%;            /* Y轴:左上角移至父容器垂直中心 */
  transform: translate(-50%, -50%);  /* 关键:基于自身尺寸偏移 */

  /* 子元素样式(动态可变,无需固定宽高) */
  padding: 24px 32px;  /* 内边距 */
  background: #3b82f6; /* 背景色 */
  color: white;        /* 文字颜色 */
  border-radius: 8px;  /* 圆角 */
  font-size: 18px;     /* 文字大小 */
  max-width: 80%;      /* 最大宽度不超过父容器的80% */
}

效果如下:

pic6.png

总结

平移函数可以方便的移动元素的位置,并且可以触发硬件加速,减少浏览器的回流,性能更优。由于平移函数不会脱离文档流,布局更加稳定,同时实现元素居中也方便,代码简洁。

pic7.png

这个Web新API让任何内容都能画中画!

2025年5月20日 08:44

作者:沉浸式趣谈

“画中画”(Picture-in-Picture, PiP)说白了,就是让你能一边看视频,一边干别的,互不耽误。就像给你的桌面贴了个能播放视频的“便利贴”,你可以随便拖动它,调整大小,它还总在最前面,贼方便。

图片

现在主流的搞法:老朋友 requestPictureInPicture()

其实,想让 <video> 元素实现画中画,现在已经有挺成熟的方法了,那就是直接在视频元素上调用 requestPictureInPicture() 这个 API。

用起来也挺简单,基本上就是:

    1. 先搞个 <video> 标签,放上你的视频。
    1. 找个时机(比如用户点个按钮),用 JavaScript 拿到这个 video 元素,然后调用 video.requestPictureInPicture()

搞定!

给个简单的代码片段:

<video id="myVideo" src="your_video.mp4" controls width="300"></video>
<button id="pipButton">开启画中画</button>

<script>
    const video = document.getElementById('myVideo');
    const pipButton = document.getElementById('pipButton');

    pipButton.addEventListener('click'async () => {
        // 先检查浏览器支不支持,是个好习惯
        if (document.pictureInPictureEnabled) {
            try {
                // 如果视频没在画中画模式,就请求进入
                if (document.pictureInPictureElement !== video) {
                    await video.requestPictureInPicture();
                } else {
                    // 如果已经在画中画了,就退出
                    awaitdocument.exitPictureInPicture();
                }
            } catch (error) {
                console.error('操作画中画失败:', error);
            }
        } else {
            console.log('你的浏览器不支持画中画功能。');
        }
    });

    // 还可以监听进入和退出的事件,搞点事情
    video.addEventListener('enterpictureinpicture'() => {
        console.log('进入画中画啦!');
        pipButton.textContent = '退出画中画';
    });

    video.addEventListener('leavepictureinpicture'() => {
        console.log('退出画中画了。');
        pipButton.textContent = '开启画中画';
    });
</script>

大部分现代浏览器(Chrome, Edge, Firefox, Safari 这些)对这个 API 支持得都还不错(当然,细节上可能有点小差异,用的时候最好还是查查 MDN 或者 Can I Use)。

那 documentPictureInPicture.requestWindow 是个啥?

window.documentPictureInPicture.requestWindow 更像是个“升级版”或者说“野心更大”的亲戚。

requestPictureInPicture() 这个老朋友,它的目标很明确,就是把  <video> 元素 扔进画中画窗口。

而 documentPictureInPicture.requestWindow() 这个新来的呢,它的目标是 把任意的 HTML 内容(理论上是这样的,比如一个 <div>,里面可以包含视频、按钮、文字等等)放进那个悬浮的小窗口里!

小结一下

  • • 目前最常用、最稳妥的实现方式是针对 <video> 元素的 requestPictureInPicture() API。兼容性相对较好,用起来也直接。
  • • 那个新出的 documentPictureInPicture.requestWindow API 呢,目标更宏大,想让任意 HTML 都能 PiP。

对这个新技术感兴趣的朋友,可以去翻翻官方文档(下面附了链接),了解下最新进展。

不过动手实践的话,还是先从老朋友 requestPictureInPicture() 开始吧,至少不会被兼容性搞得头秃,哈哈。

继 Vite 之后,ESLint 也接入了 AI!

2025年5月20日 08:41

作者:前端开发爱好者

在之前的文章中有分享过  🔗Vite 是首个接入 AI 能力的构建工具,而现在 ESLint 也紧随其后,使用 AI 来管理代码规范!

作为一名前端开发,咱们平时写代码的时候,代码规范这事儿一直挺让人头疼的吧。

不过现在好消息来了!ESLint 这工具居然也开始用 AI 了,以后咱们可以让 AI 帮着管代码规范了!

ESLint 为啥要用 AI ?

ESLint 大家都熟悉吧,就是用来检查 JavaScript 代码问题的工具。

图片

现在它支持 Model Context Protocol(MCP)了,简单来说,就是能让 AI 模型跟它配合起来。

这样 AI 就能在咱们常用的开发工具,比如 IDE 里直接用 ESLint,帮咱们更智能地检查代码分析问题

特别是用 GitHub Copilot 这类 AI 编程助手的小伙伴,这功能太实用了!

以后写代码的时候,AI 就能在旁边帮着盯着代码规范,及时提醒哪儿写得不对,减少出错的可能。

怎么在 VS Code 和 Cursor 里用上 ESLint 的 AI 功能?

在 VS Code 里设置

先得安装个 Copilot Chat 扩展。然后跟着这几步走:

建个配置文件

在项目根目录下新建个文件,叫 .vscode/mcp.json,内容写成这样:

{
    "servers": {
        "ESLint": {
            "type": "stdio",
            "command": "npx",
            "args": ["eslint", "--mcp"]
        }
    }
}

这配置就是告诉 VS Code 怎么启动 ESLint 的 AI 功能。

全局启用(可选)

如果想在所有项目里都能用这个功能,就选 “用户设置”  ,把上面的配置加到 settings.json 里。

这样不管在哪个项目里,AI 都能帮着管代码规范。

在 Cursor 里设置

步骤也简单:

建配置文件

在项目目录里建个 .cursor/mcp.json,内容是:

{
    "mcpServers": {
        "eslint": {
            "command": "npx",
            "args": ["eslint", "--mcp"],
            "env": {}
        }
    }
}

全局配置(可选)

要是想在所有 Cursor 项目里都用上,就在主目录建个 ~/.cursor/mcp.json 文件,内容跟上面一样。

检查工具是否可用

配置好了以后,去 Cursor 的 MCP 设置页看看“可用工具”里有没有 ESLint MCP 服务器。

要是有了,就说明设置成功了,AI 就能帮你检查代码规范了。

用 AI 管代码规范有什么好处?

用上 ESLint 的 AI 功能,好处可不少:

  • 提高开发效率:AI 能快速发现代码里的问题,咱们不用花太多时间自己去找,写代码速度自然就上去了。
  • 提升代码质量:AI 检查代码比人更细致,能确保代码符合规范,减少潜在问题。
  • 团队协作更顺畅:一个团队里大家代码风格不一致很让人头疼。AI 帮着管代码规范,大家写出来的代码风格更统一,协作起来也更轻松。

ESLint 接入 AI 真的是开发流程里的一大进步。

以后写代码,有 AI 帮着管规范,咱们可以更专注于实现功能。

还没试过的小伙伴赶紧在自己的项目里试试吧!相信用过之后,你会越来越依赖这个强大的功能,让 AI 成为你开发过程中的得力助手!

  • ESLint MCP 官方文档https://eslint.org/docs/latest/use/mcp

7 款让人“上头”的开源小游戏

作者 HelloGitHub
2025年5月20日 08:30

好久没发开源游戏集合了,接下来我们就来盘点一下「js13kGames」比赛 2024、2023 年的获奖小游戏。

也许有些朋友是第一次听说「js13kGames」——这是一个每年举办的网页游戏编程竞赛,参赛者需要在一个月内开发一款大小不超过 13KB 的网页游戏。规则如下:

  1. 文件大小限制:提交的游戏压缩包(zip 格式),包含所有代码和资源,不得超过 13KB。
  2. 禁止外部依赖:不允许使用任何外部库、服务或资源,全部内容都要打包进 zip 文件。
  3. 入口文件:压缩包内必须包含一个 index.html 文件,解压后直接打开就能玩。
  4. 源码分享:鼓励参赛者在 GitHub 上开源游戏代码。

地址:js13kgames.com

每年的 Js13k 比赛都能看到许多有趣、好玩的创意小游戏,下面就让我们一起「玩一玩」过去两年里那些令人惊艳的游戏吧!

一、令人害怕的数字 13

2024 年的主题是「Triskaidekaphobia」,意为“对数字 13 的恐惧或回避”。让我们一起来看看,围绕这个概念的 13KB 游戏长什么样吧!

1.1 13th Floor(第十三层)

这是一款潜行恐怖(Stealth Horror)游戏,玩家需要时刻保持警觉,巧妙利用阴影进行躲藏。游戏开始,你带着一把钥匙出现在第 13 层,需要找到对应房间,获取下一把钥匙,逐步解锁新房间,直到最终抵达终点房间——1313。

在探索过程中,你可以使用手电筒(按 F 键)照亮前路,搜寻物品。但当“它”出现时,务必小心:关闭手电,避免暴露自己,或迅速奔跑寻找藏身之处。建议戴着耳机玩,沉浸感更强,不过要做好被吓到的准备哦!

操作说明:

  • 移动:WASD
  • 视角/转向:鼠标
  • 互动:E 键或鼠标左键
  • 手电筒:F 键

试玩:play.js13kgames.com/13th-floor/

源码:github.com/js13kGames/…

1.2 Coup Ahoo(阿胡起义)

这是一款操作简单、轻松上手的小游戏,全程仅需鼠标或触屏(支持移动端)。玩家将扮演一位发动叛变的船长,逐一挑战并击败 13 名手下。在冒险途中,还会遇到商人、船匠、想加入你的船员,助你一举登顶海上霸主之位。

游戏中的骰子“货物”至关重要——所有骰子的点数总和既代表你的总 HP(生命值),也决定了你在战斗中能造成的伤害。然而,务必小心避开“13”这个不祥的数字,否则可能招致不利后果。

试玩:play.js13kgames.com/coup-ahoo/

源码:github.com/js13kGames/…

1.3 Ghosted(幽灵)

这是一款类似“推箱子”的解谜游戏,共有 12 个关卡,通关后还会解锁一个隐藏关卡(第 13 关)。玩家将扮演一个外星人,目标是收集地图上的所有金币。有趣的是,只有在幽灵状态下才能收集金币,但在幽灵状态下无法推动石块,也不能原路返回。

操作说明:

  • 移动:方向键 ↑ ↓ ← →
  • 暂停:ESC 或回车
  • 重新开始:R 键
  • 撤回:Z 键或 Delete 键

试玩:play.js13kgames.com/ghosted/

源码:github.com/js13kGames/…

二、十三世纪

2023 年的主题是「13th Century」(十三世纪),满满的怀旧骑士风扑面而来,让我们看看有哪些有趣的小游戏吧!

2.1 Path to Glory(光荣之路)

这是一款制作精良的格斗游戏,拥有令人印象深刻的操作手感与战斗体验。你将扮演一名孤独的士兵,迎战一波又一波的敌人,直至最终 Boss 登场。

游戏细节丰富:角色进入水中会被减速,风、雨、摇曳的草丛与闪电等环境元素为战场增添了沉浸感。击败敌人时的慢动作特写,更是令人热血沸腾。视野范围的控制机制也极为实用,尤其适合 2D 动作砍杀类 roguelike 游戏,令人回味无穷。

试玩:play.js13kgames.com/path-to-glo…

源码:github.com/js13kGames/…

2.2 Casual Crusade(休闲十字军)

这是一款轻松有趣的休闲小游戏。你需要用手里的地块牌,一步步铺出一条通向所有土地的道路,顺手拿点战利品,强化自己的牌组。偶尔还能获得强力技能,让你的冒险之路更加精彩!

试玩:play.js13kgames.com/casual-crus…

源码:github.com/js13kGames/…

2.3 Knight Dreams(骑士之梦)

这是一款无尽的跑酷 2D 动作类游戏,特别是直升机头盔非常有趣。玩法简单,你只需要做两件事收集宝石、一直向前。

操作说明:

  • 移动:← → 或 AD
  • 跳跃/飞行:↑ 或 W
  • 空格:长枪攻击
  • 暂停:回车

试玩:play.js13kgames.com/knight-drea…

源码:github.com/js13kGames/…

2.4 Tiny Yurts(小小蒙古包)

这是一款受《Mini Motorways》启发的休闲策略游戏,将路径规划与资源管理巧妙结合。你只需用触摸或鼠标点击拖拽,为你的蒙古包(yurts)和各类农场铺设连接道路,让动物们幸福生活。轻松上手乐趣无穷,快来挑战吧!

游戏机制与小技巧:

  • 游戏支持随时暂停,利用暂停时间优化路线。
  • 起始农场自带的路径可以删除,自由调整布局。
  • 水面无法铺路,连接水中渔场需通过浮桥。
  • 路径距离影响效率,合理布局能更好地满足农场需求。
  • 定居者迷路时需重新铺路,帮助他们回家后才能继续工作。
  • 对角线路可节省路段,但移动时间会增加。
  • 不同动物对农场的需求不同,难度也会逐步提升。

试玩:play.js13kgames.com/tiny-yurts/

源码:github.com/js13kGames/…

三、最后

篇幅有限,今天的 7 款开源小游戏就先“开箱”到这里!在 13KB 的极限挑战下,它们的创意和完成度着实让人印象深刻。

希望这次的分享能让你找到心仪的小游戏,度过一段快乐时光。如果你也对这些“方寸之间显乾坤”的作品背后的技术实现感兴趣,别忘了它们都是开源的哦!好奇!仅 13kB 大小的游戏,源码长啥样?

哪款游戏最让你拍案叫绝?有没有哪一款让你一玩就停不下来?又或者,在这些游戏中发现了哪些让你眼前一亮的设计或技术?欢迎在评论区留言分享,一起交流这些 13KB 的奇迹!

☀️在cesium中使用动图的几种方式

作者 贾斯克
2025年5月20日 08:15

前言

最近在做cesium的时候遇到一个需求, 需要在地图上面展示动图。

经过查阅资料,大致实现方式如下:

  1. 通过第三方库将动图分解成每一帧
  2. 通过CallbackProperty方法,实现轮播每帧图片

网络的动图格式很多,在日常中主要使用的还是gif格式和apng格式。这两种,所以下面也主要介绍怎么在cesium中加载这两种动图点位。

gif图片

这边用libgif.js这个第三方库来帮助我们解析gif文件

引入libgif

libgif地址:libgif.js

这个库目前没有找到好用的库,只要用第三方引入的方式进行引入。

image-20250519210306663

将这个文件下载下来,在index.html中引入

image-20250519210353885

这里我准备了一张gif动图进行演示

sum.gif

image-20250519211609564

实现过程

根据官网的使用例子,我们需要一个img标签,并且new一个SuperGif

// 加载gif动图
const loadGifImg = () => {
  const map = getMap() // 获取地图对象
  let gifImg = document.createElement('img')
  gifImg.setAttribute('rel:animated_src', '/img/sum.gif')
  gifImg.setAttribute('rel:auto_play', '1')
  const imgDiv = document.createElement('div')
  imgDiv.appendChild(gifImg)
  const superGif = new SuperGif({ gif: gifImg })
}

上面的代码中,我创建了一个div和一个img并且设置了两个属性

其中rel:animated_src是必须的,SuperGif内部会读取这个属性

rel:auto_play则是是否自动播放,不加也没关系 最后new了一个superGif

 superGif.load(function () {
    map.entities.add({
      position: Cesium.Cartesian3.fromDegrees(120.9677706, 30.7985748),
      billboard: {
        image: new Cesium.CallbackProperty(() => {
          return superGif.get_canvas().toDataURL()
        }, false),
        scale: 0.25
      }
    })
  })

superGif有一个load事件表示加载完毕,像上面我加载完毕之后创建了一个点位实体,并且imgnew了一个CallbackProperty对象。

image-20250519212943954

完整代码

完整代码如下:

// 加载gif动图
const loadGifImg = () => {
  const map = getMap()
  let gifImg = document.createElement('img')
  gifImg.setAttribute('rel:animated_src', '/img/sum.gif')
  gifImg.setAttribute('rel:auto_play', '1')
  const imgDiv = document.createElement('div')
  imgDiv.appendChild(gifImg)
  const superGif = new SuperGif({ gif: gifImg })
  superGif.load(function () {
    map.entities.add({
      position: Cesium.Cartesian3.fromDegrees(120.9677706, 30.7985748),
      billboard: {
        image: new Cesium.CallbackProperty(() => {
          return superGif.get_canvas().toDataURL()
        }, false),
        scale: 0.25
      }
    })
  })
}

效果如下:

1.gif

apng图片

gif一样,apng也需要一个第三方库来进行解析

apng-js 是一个用于解析和渲染 APNG(动画 PNG)文件的 JavaScript 库。它提供了丰富的 API,可以让开发者完全控制 APNG 动画的播放、暂停、复位等操作。

这个库直接用npm下载就好了

安装依赖

// pnpm
pnpm add apng-js
// npm 
npm install apng-js

代码实现

  • 将图片文件转化为ArrayBuffer属性对象
 let blob = await fetch('/img/wind.png').then(res => res.blob())
  const reader = new FileReader()
  let canvas = document.createElement('canvas')
  let ctx = canvas.getContext('2d')
  reader.readAsArrayBuffer(blob)
  reader.onload = async () => {
    console.log('👉 ~ reader.onload= ~ reader.result:', reader.result)
 }

image-20250519214930428

  • 使用apng- js进行解析
import parseAPNG from 'apng-js'
 let apng = reader.result
let player = await apng.getPlayer(ctx)
player.play()
  • 创建图片点位
map.entities.add({
  position: Cesium.Cartesian3.fromDegrees(120.9677706, 30.7985748),
  billboard: {
    image: new Cesium.CallbackProperty(() => {
      return player.currentFrame.imageElement
    }, false)
  }
})

完整代码

// 加载apng动图
const loadApngImg = async () => {
  const map = getMap()
  let blob = await fetch('/img/wind.png').then(res => res.blob())
  const reader = new FileReader()
  let canvas = document.createElement('canvas')
  let ctx = canvas.getContext('2d')
  reader.readAsArrayBuffer(blob)
  reader.onload = async () => {
    let apng = parseAPNG(reader.result)
    let player = await apng.getPlayer(ctx)
    player.play()
    map.entities.add({
      position: Cesium.Cartesian3.fromDegrees(120.9677706, 30.7985748),
      billboard: {
        image: new Cesium.CallbackProperty(() => {
          return player.currentFrame.imageElement
        }, false)
      }
    })
  }
}

效果如下:

2

这边的apng图片,是我在网上别人的例子里面找到的。如果在用的过程中,有发现各种报错,可以检查一下图片的格式是不是不对

结尾

cesium中并没有办法直接使用动图,如果想要用动图或者视频,思路基本都是一样的,通过解析动图,得到每一帧图片,再通过更新图片的方式来实现动态。

520表白神器

2025年5月20日 07:55

大家好,我是晓凡

520来了,给大家做了一个浪漫的网页表白工具,帮助你向心爱的人表达爱意。

image-20250519215821562

需要源码的小伙伴直接跳转到文章末尾获取。

功能介绍

  • 浪漫启动页:显示相识天数和小时数,营造浪漫氛围

  • 动态情书系统:打字机效果展示情书内容,支持自定义内容

  • 趣味互动游戏

    • 爱心捕捉:点击飘动的爱心获取分数
    • 记忆拼图:上传照片创建拼图游戏
  • 时光相册墙:上传和展示珍贵照片,支持本地存储

  • 终极表白仪式:生成爱情证书,烟花特效,礼物二维码展示

项目配置

基本配置

编辑 js/config.js 文件,可以自定义以下内容:

const CONFIG = {
    // 初始相识日期(格式:年-月-日)
    firstMeetDate: '2023-05-20',
    
    // 收件人姓名
    recipientName: '未来的您',
    
    // 情书内容 - 可自定义多条消息
    loveLetters: [
        '从遇见你的那一刻起,我的世界就开始变得不一样。',
        // 可添加更多内容...
    ],
    
    // 爱情证书信息
    certificate: {
        lover1: '我的名字',
        lover2: '对方名字'
    },
    
    // 礼物二维码链接 - 可以是任何图片URL
    giftQRCodeUrl: '你的二维码图片URL',
    
    // 服务器端口
    serverPort: 3008
};

自定义音乐

assets/music/ 目录下放置MP3音乐文件,并在 config.js 中更新音乐列表:

musicList: [
    {
        title: '歌曲名称',
        artist: '歌手名',
        src: 'assets/music/你的音乐文件.mp3'
    }
]

自定义图片

  • 爱心图片:替换 assets/images/heart.png
  • 证书背景:替换 assets/images/certificate-bg.jpg
  • 玫瑰图片:替换 assets/images/rose.png

部署方法

本地部署

  1. 环境准备

    • 安装 Node.js(建议v14.0.0或更高版本)
  2. 启动服务器

    方法一:使用Node.js直接启动

    node server.js
    

    方法二:使用NPM启动

    npm start
    

    方法三:Windows用户可双击运行 start.bat 文件

  3. 访问网站

线上部署

  1. 使用云服务器

    • 将整个项目上传到云服务器

    • 安装Node.js环境

    • 使用PM2等工具保持服务运行:

      npm install -g pm2
      pm2 start server.js --name "love520"
      
  2. 使用Vercel/Netlify等静态网站托管

    • 注册VercelNetlify账号
    • 连接你的GitHub仓库或直接上传项目
    • 按照平台指引完成部署
  3. 使用GitHub Pages

    • 创建GitHub仓库并上传项目
    • 在仓库设置中启用GitHub Pages
    • 注意:需要修改服务器逻辑,改为纯静态网站

注意事项

  • 照片和数据保存在浏览器本地存储中,清除浏览器数据会导致数据丢失
  • 音乐自动播放可能受到浏览器策略限制,需要用户交互后才能播放
  • 为获得最佳体验,建议使用Chrome、Edge或Firefox最新版本浏览器

源码地址:pan.quark.cn/s/5a0dfc6ef… 提取码:23Hc

我是晓凡,再小的帆也能远航~

希望本篇文章能帮助到您~

我们下期再见 ヾ(•ω•`)o (●'◡'●)

[Python3/Java/C++/Go/TypeScript] 一题一解:差分数组(清晰题解)

作者 lcbin
2025年5月20日 06:27

方法一:差分数组

我们可以使用差分数组来解决这个问题。

定义一个长度为 $n + 1$ 的数组 $d$,初始值全部为 $0$。对于每个查询 $[l, r]$,我们将 $d[l]$ 加 $1$,将 $d[r + 1]$ 减 $1$。

然后我们遍历数组 $d$ 在 $[0, n - 1]$ 范围内的每个元素,累加前缀和 $s$,如果 $\textit{nums}[i] > s$,说明 $\textit{nums}$ 不能转换为零数组,返回 $\textit{false}$。

遍历结束后,返回 $\textit{true}$。

###python

class Solution:
    def isZeroArray(self, nums: List[int], queries: List[List[int]]) -> bool:
        d = [0] * (len(nums) + 1)
        for l, r in queries:
            d[l] += 1
            d[r + 1] -= 1
        s = 0
        for x, y in zip(nums, d):
            s += y
            if x > s:
                return False
        return True

###java

class Solution {
    public boolean isZeroArray(int[] nums, int[][] queries) {
        int n = nums.length;
        int[] d = new int[n + 1];
        for (var q : queries) {
            int l = q[0], r = q[1];
            ++d[l];
            --d[r + 1];
        }
        for (int i = 0, s = 0; i < n; ++i) {
            s += d[i];
            if (nums[i] > s) {
                return false;
            }
        }
        return true;
    }
}

###cpp

class Solution {
public:
    bool isZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
        int n = nums.size();
        int d[n + 1];
        memset(d, 0, sizeof(d));
        for (const auto& q : queries) {
            int l = q[0], r = q[1];
            ++d[l];
            --d[r + 1];
        }
        for (int i = 0, s = 0; i < n; ++i) {
            s += d[i];
            if (nums[i] > s) {
                return false;
            }
        }
        return true;
    }
};

###go

func isZeroArray(nums []int, queries [][]int) bool {
d := make([]int, len(nums)+1)
for _, q := range queries {
l, r := q[0], q[1]
d[l]++
d[r+1]--
}
s := 0
for i, x := range nums {
s += d[i]
if x > s {
return false
}
}
return true
}

###ts

function isZeroArray(nums: number[], queries: number[][]): boolean {
    const n = nums.length;
    const d: number[] = Array(n + 1).fill(0);
    for (const [l, r] of queries) {
        ++d[l];
        --d[r + 1];
    }
    for (let i = 0, s = 0; i < n; ++i) {
        s += d[i];
        if (nums[i] > s) {
            return false;
        }
    }
    return true;
}

时间复杂度 $O(n + m)$,空间复杂度 $O(n)$。其中 $n$ 和 $m$ 分别为数组 $\textit{nums}$ 和 $\textit{queries}$ 的长度。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

从零实现模块级代码影响面分析方案|得物技术

作者 得物技术
2025年5月19日 12:00
在过往交易域稳定性建设中,我们完成了多项关键工作,包括后台应用拆分、历史债务重构、权限配置管控和核心H5页面定期巡检任务等。此外,我们还整合了前端监控平台的各类异常数据分析与告警能力

每日一题-零数组变换 I🟡

2025年5月20日 00:00

给定一个长度为 n 的整数数组 nums 和一个二维数组 queries,其中 queries[i] = [li, ri]

对于每个查询 queries[i]

  • 在 nums 的下标范围 [li, ri] 内选择一个下标 子集
  • 将选中的每个下标对应的元素值减 1。

零数组 是指所有元素都等于 0 的数组。

如果在按顺序处理所有查询后,可以将 nums 转换为 零数组 ,则返回 true,否则返回 false

 

示例 1:

输入: nums = [1,0,1], queries = [[0,2]]

输出: true

解释:

  • 对于 i = 0:
    • 选择下标子集 [0, 2] 并将这些下标处的值减 1。
    • 数组将变为 [0, 0, 0],这是一个零数组。

示例 2:

输入: nums = [4,3,2,1], queries = [[1,3],[0,2]]

输出: false

解释:

  • 对于 i = 0: 
    • 选择下标子集 [1, 2, 3] 并将这些下标处的值减 1。
    • 数组将变为 [4, 2, 1, 0]
  • 对于 i = 1:
    • 选择下标子集 [0, 1, 2] 并将这些下标处的值减 1。
    • 数组将变为 [3, 1, 0, 0],这不是一个零数组。

 

提示:

  • 1 <= nums.length <= 105
  • 0 <= nums[i] <= 105
  • 1 <= queries.length <= 105
  • queries[i].length == 2
  • 0 <= li <= ri < nums.length

【模板】差分数组(Python/Java/C++/Go)

作者 endlesscheng
2024年11月17日 16:18

题意可以转换成:

  • 把 $[l_i,r_i]$ 中的元素都减一,最终数组中的所有元素是否都 $\le 0$?

如果所有元素都 $\le 0$,那么我们可以撤销一部分元素的减一,使其调整为 $0$,从而满足原始题意的要求。

这可以用差分数组计算,原理讲解(推荐和【图解】从一维差分到二维差分 一起看)。

本题视频讲解,欢迎点赞关注~

###py

class Solution:
    def isZeroArray(self, nums: List[int], queries: List[List[int]]) -> bool:
        diff = [0] * (len(nums) + 1)
        for l, r in queries:
            # 区间 [l,r] 中的数都加一
            diff[l] += 1
            diff[r + 1] -= 1

        for x, sum_d in zip(nums, accumulate(diff)):
            # 此时 sum_d 表示 x=nums[i] 要减掉多少
            if x > sum_d:  # x 无法变成 0
                return False
        return True

###java

class Solution {
    public boolean isZeroArray(int[] nums, int[][] queries) {
        int n = nums.length;
        int[] diff = new int[n + 1];
        for (int[] q : queries) {
            // 区间 [l,r] 中的数都加一
            diff[q[0]]++;
            diff[q[1] + 1]--;
        }

        int sumD = 0;
        for (int i = 0; i < n; i++) {
            sumD += diff[i];
            // 此时 sumD 表示 nums[i] 要减掉多少
            if (nums[i] > sumD) { // nums[i] 无法变成 0
                return false;
            }
        }
        return true;
    }
}

###cpp

class Solution {
public:
    bool isZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
        int n = nums.size();
        vector<int> diff(n + 1);
        for (auto& q : queries) {
            // 区间 [l,r] 中的数都加一
            diff[q[0]]++;
            diff[q[1] + 1]--;
        }

        int sum_d = 0;
        for (int i = 0; i < n; i++) {
            sum_d += diff[i];
            // 此时 sum_d 表示 nums[i] 要减掉多少
            if (nums[i] > sum_d) { // nums[i] 无法变成 0
                return false;
            }
        }
        return true;
    }
};

###go

func isZeroArray(nums []int, queries [][]int) bool {
diff := make([]int, len(nums)+1)
for _, q := range queries {
// 区间 [l,r] 中的数都加一
diff[q[0]]++
diff[q[1]+1]--
}

sumD := 0
for i, x := range nums {
sumD += diff[i]
// 此时 sumD 表示 x=nums[i] 要减掉多少
if x > sumD { // x 无法变成 0
return false
}
}
return true
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n+q)$,其中 $n$ 是 $\textit{nums}$ 的长度,$q$ 是 $\textit{queries}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

更多相似题目,见下面数据结构题单中的「§2.1 一维差分」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

差分

作者 tsreaper
2024年11月17日 12:13

解法:差分

我们首先把题目的包装拆开。如果有 $x$ 个区间覆盖了某个元素,则那个元素最多可以被减去 $x$ 次。因此题目等价于:问每个元素 nums[i] 是否被至少 nums[i] 个询问区间覆盖。

这就是非常经典的差分问题。用差分维护每个元素被几个区间覆盖即可。复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###cpp

class Solution {
public:
    bool isZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
        int n = nums.size();
        // 差分维护每个元素被几个区间覆盖
        vector<int> d(n + 1);
        for (auto &qry : queries) {
            d[qry[0]]++;
            d[qry[1] + 1]--;
        }
        // 枚举每个元素,求区间覆盖数
        for (int i = 0, now = 0; i < n; i++) {
            now += d[i];
            if (now < nums[i]) return false;
        }
        return true;
    }
};

vue3中的form表单层级嵌套问题

2025年5月19日 23:52

先上代码

parent.vue

<script setup>
import { ref, reactive } from "vue";
import TaskList from "./ChildForm.vue";

const formRef = ref();
const formData = reactive({
  projectName: "",
  manager: "",
  tasks: [],
});

const rules = reactive({
  projectName: [
    { required: true, message: "项目名称不能为空", trigger: "blur" },
    { min: 3, max: 50, message: "长度在3到50个字符", trigger: "blur" },
  ],
  manager: [{ required: true, message: "负责人不能为空", trigger: "change" }],
});

const validateTasks = (rule, value, callback) => {
  if (formData.tasks.length === 0) {
    callback(new Error("至少需要添加一个任务"));
  } else {
    callback();
  }
};

const submit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      console.log("提交数据:", formData);
    }
  });
};
</script>

<template>
  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
    <el-form-item label="项目名称" prop="projectName">
      <el-input v-model="formData.projectName" />
    </el-form-item>

    <el-form-item label="负责人" prop="manager">
      <el-select v-model="formData.manager">
        <el-option label="张三" value="zhangsan" />
        <el-option label="李四" value="lisi" />
      </el-select>
    </el-form-item>

    <el-form-item prop="tasks" :rules="[{ validator: validateTasks }]">
      <TaskList v-model="formData.tasks" />
    </el-form-item>

    <el-button type="primary" @click="submit">提交</el-button>
  </el-form>
</template>

child.vue

<script setup>
import { ref, computed, reactive } from "vue";

const props = defineProps({
  modelValue: {
    type: Array,
    default: () => [],
  },
});

const emit = defineEmits(["update:modelValue"]);

const taskRules = reactive({
  name: [{ required: true, message: "任务名称不能为空", trigger: "blur" }],
  priority: [{ required: true, message: "请选择优先级", trigger: "change" }],
});

const tasks = computed({
  get: () => props.modelValue,
  set: (value) => emit("update:modelValue", value),
});

const addTask = () => {
  tasks.value.push({ name: "", priority: "medium" });
};

const removeTask = (index) => {
  tasks.value.splice(index, 1);
};
</script>

<template>
  <div class="task-list">
    <el-button @click="addTask">添加任务</el-button>

    <el-form
      v-for="(task, index) in tasks"
      :key="index"
      :model="task"
      :rules="taskRules"
      class="task-form"
    >
      <el-form-item prop="name">
        <el-input v-model="task.name" placeholder="任务名称" />
      </el-form-item>

      <el-form-item prop="priority">
        <el-select v-model="task.priority">
          <el-option label="高" value="high" />
          <el-option label="中" value="medium" />
          <el-option label="低" value="low" />
        </el-select>
      </el-form-item>

      <el-button @click="removeTask(index)">删除</el-button>
    </el-form>
  </div>
</template>

<style scoped>
.task-form {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
}
</style>

效果如下:

image.png

一、组件结构设计原理

  1. 数据流向设计父组件通过v-model实现数据双向绑定:
// ProjectForm.vue
const formData = reactive({
  tasks: [] // 自动同步到子组件
})

// TaskList.vue
const tasks = computed({
  get: () => props.modelValue,
  set: (v) => emit('update:modelValue', v)
})

2. 验证责任划分

  • 父组件:验证任务列表非空
  • 子组件:验证单个任务字段

二、分步实现流程

步骤1:父组件基础验证

// ProjectForm.vue
const validateTasks = (_, __, callback) => {
  formData.tasks.length === 0 
    ? callback(new Error('至少需要1个任务')) 
    : callback()
}

步骤2:子组件独立验证

// TaskList.vue
const taskRules = {
  name: { 
    required: true, 
    trigger: 'blur',
    message: '任务名称必填' 
  }
}

步骤3:动态prop绑定

<el-form-item 
  :prop="`tasks[${index}].name`"
  :rules="taskRules.name">
  <el-input v-model="item.name"/>
</el-form-item>

三、验证联动机制

  1. 提交时联合验证
// ProjectForm.vue
const submit = () => {
  formRef.value.validate().then(() => {
    taskListRef.value.validate().then(() => {
      // 全部验证通过
    })
  })
}

2. 实时错误反馈

// TaskList.vue
watch(() => props.modelValue, () => {
  formRef.value?.validate()
}, { deep: true })

四、异常处理方案

// 统一错误捕获
try {
  await Promise.all([
    formRef.value.validate(),
    taskListRef.value.validate()
  ])
} catch (errors) {
  console.error('验证失败:', errors)
}

一文讲清什么是A记录,CNAME记录,NS记录?

2025年5月19日 23:40

什么是A记录(Address Record)

简而言之,A记录是域名到ip地址的映射关系

例如,如果有一个网站example.sun.com,可能需要将其指向服务器的IP地址192.0.1.1。在DNS配置中添加一条A记录,将example.sun.com指向192.0.1.1即可实现

什么是CNAME记录(Canonical Name Record

也称为别名记录。它允许您为一个域名创建别名。

例如,如果您设置了example.sun.com作为www.dns.com的CNAME,这意味着所有访问example.sun.com的请求实际上都会被路由到www.dns.com。

为什么需要CNAME

简化了DNS记录的管理

例如,如果有好多个域名同时指向一个ip地址

www.cc.com → 1.1.1.1
www.xx.com → 1.1.1.1
www.kk.com → 1.1.1.1

那麽。当换服务器ip的时候,就需要对每个域名重新配置

而如果用CNAME的话呢

www.cc.com → www.cxk.com → 1.1.1.1
www.xx.com → www.cxk.com → 1.1.1.1
www.kk.com → www.cxk.com → 1.1.1.1

那么就只需要改动www.cxk.com映射的ip地址

CNAME的应用场景是什么

CDN是CNAME的主要应用场景

随着网站访问量越来越多,服务器顶不住了,就需要找CDN提供商购买CDN加速服务,这个时候他们要求你的域名做个CNAME指向他们给你的一个域名叫www.dd.cdn.com

www.dd.com → www.dd.cdn.com

当用户访问www.dd.com的时候,本地DNS会获得CDN提供的CNAME域名:www.dd.com,然后再次向DNS调度系统发出请求,通过DNS调度系统的智能解析,把离客户端地理位置最近的(或者相对负载低的,主要看CDN那边智能解析的策略)CDN提供商的服务器IP返回给本地DNS,然后再由本地DNS回给客户端,让用户就近取到想要的资源(如访问网站),大大降低了延迟。

3d66c87c40b30616357b79c0dbe75ed.jpg

本地运营商的DNS服务器怎么知道一个域名的授权(权威)服务器是哪台?这个域名应该在哪里取解析呢?

首先公司会去找运营商买域名,比如CDN公司买了cdn.com这个一级域名,那么本地运营商会做一个NS记录,即匹配到这个cdn.com后缀的域名都会到CDN服务提供商的DNS服务器做解析,即到权威服务器做解析。

什么是 NS 记录?

NS 记录,即域名服务器记录,用于指定域名应该由哪个 DNS 服务器来进行解析。简单来说,当用户在浏览器中输入一个域名时,NS 记录会告诉互联网中的 DNS 查询系统,应该向哪个 DNS 服务器询问解析结果。NS 记录中的 IP 地址即为负责解析该域名的 DNS 服务器的 IP 地址。

参考文章

Step - 3

作者 烛阴
2025年5月19日 23:33

Task

Using the max and step functions, paint only those pixels whose normalized x-coordinate is less than 0.25 or greater than 0.75.

使用maxstep函数,仅绘制那些标准化坐标下 x 坐标小于0.25或大于等于0.75

Requirements

The shader should avoid using branching or conditional statements in its code, and instead rely on the step and max functions to determine the color of each pixel.

着色器应避免在其代码中使用分支或条件语句,而是依靠stepmax函数来确定每个像素的颜色。

Theory

GLSL 中的函数max用于返回两个输入值中的最大值。它接受两个参数,并返回两个值中较大的一个。以下是该max函数在 GLSL 中的详细解释:

函数

float max(float x, float y);

  • 如果x大于或等于y,则函数返回x
  • 如果y大于x,则函数返回y

示例用法

float a = 5.0;

float b = 3.0;

float result = max(a, b); // will be 5.0

Answer

uniform vec2 iResolution;

void main() {
  // Normalized pixel coordinates (from 0 to 1)
  vec2 uv = gl_FragCoord.xy / iResolution.xy;

  vec3 color = vec3(1.0, 0.3, 0.3);
  float t1 = 1.0 - step(0.25, uv.x);
  float t2 = step(0.75, uv.x);

  gl_FragColor = vec4(color * max(t1, t2), 1.0);
}

效果

image.png

练习

Step

最后

如果你觉得这篇文章有用,记得点赞、关注、收藏,学Shader更轻松!!

抛弃form中的rule验证,利用原生js,来实现表单的验证

2025年5月19日 23:33

先上代码

<script setup>
import { reactive, computed } from "vue";

const formData = reactive({
  student: {
    name: "",
    gender: "",
    age: null,
    isValid: computed(() => {
      return (
        formData.student.name &&
        formData.student.gender &&
        formData.student.age >= 6 &&
        formData.student.age <= 25
      );
    }),
  },
  parent: {
    name: "",
    phone: "",
    isValid: computed(() => {
      return (
        formData.parent.name && /^1[3-9]\d{9}$/.test(formData.parent.phone)
      );
    }),
  },
});

const errors = reactive({
  student: {
    name: "",
    gender: "",
    age: "",
  },
  parent: {
    name: "",
    phone: "",
  },
});

const validateStudentName = () => {
  if (!formData.student.name) {
    errors.student.name = "姓名不能为空";
  } else if (formData.student.name.length < 2) {
    errors.student.name = "姓名至少2个字符";
  } else {
    errors.student.name = "";
  }
};

const validateStudentAge = () => {
  if (!formData.student.age) {
    errors.student.age = "年龄不能为空";
  } else if (formData.student.age < 6) {
    errors.student.age = "年龄不能小于6岁";
  } else if (formData.student.age > 25) {
    errors.student.age = "年龄不能大于25岁";
  } else {
    errors.student.age = "";
  }
};

const validateParentPhone = () => {
  if (!formData.parent.phone) {
    errors.parent.phone = "手机号不能为空";
  } else if (!/^1[3-9]\d{9}$/.test(formData.parent.phone)) {
    errors.parent.phone = "请输入有效的手机号";
  } else {
    errors.parent.phone = "";
  }
};

const submit = () => {
  validateStudentName();
  validateStudentAge();
  validateParentPhone();

  if (formData.student.isValid && formData.parent.isValid) {
    alert("表单验证通过");
  } else {
    alert("请检查表单填写");
  }
};
</script>

<template>
  <form @submit.prevent="submit">
    <fieldset>
      <legend>学生基本信息</legend>

      <div class="form-group">
        <label for="student-name">学生姓名:</label>
        <input
          id="student-name"
          v-model="formData.student.name"
          @blur="validateStudentName"
          placeholder="请输入学生姓名"
        />
        <span class="error">{{ errors.student.name }}</span>
      </div>

      <div class="form-group">
        <label for="student-gender">学生性别:</label>
        <select id="student-gender" v-model="formData.student.gender">
          <option value="">请选择性别</option>
          <option value="male"></option>
          <option value="female"></option>
        </select>
        <span class="error">{{ errors.student.gender }}</span>
      </div>

      <div class="form-group">
        <label for="student-age">学生年龄:</label>
        <input
          id="student-age"
          type="number"
          v-model.number="formData.student.age"
          @blur="validateStudentAge"
          placeholder="6-25岁"
        />
        <span class="error">{{ errors.student.age }}</span>
      </div>
    </fieldset>

    <fieldset>
      <legend>家长联系信息</legend>

      <div class="form-group">
        <label for="parent-name">家长姓名:</label>
        <input
          id="parent-name"
          v-model="formData.parent.name"
          placeholder="请输入家长姓名"
        />
        <span class="error">{{ errors.parent.name }}</span>
      </div>

      <div class="form-group">
        <label for="parent-phone">联系电话:</label>
        <input
          id="parent-phone"
          v-model="formData.parent.phone"
          @blur="validateParentPhone"
          placeholder="11位手机号码"
        />
        <span class="error">{{ errors.parent.phone }}</span>
      </div>
    </fieldset>

    <button type="submit">提交表单</button>
  </form>
</template>

<style scoped>
form {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}
fieldset {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 15px;
  margin-bottom: 20px;
}
legend {
  padding: 0 10px;
  font-weight: bold;
}
.form-group {
  margin-bottom: 15px;
}
label {
  display: inline-block;
  width: 100px;
  text-align: right;
  margin-right: 10px;
}
input,
select {
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  width: 200px;
}
.error {
  color: red;
  font-size: 12px;
  margin-left: 110px;
  display: block;
}
button {
  padding: 10px 20px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
button:hover {
  background: #369f6b;
}
</style>

1747669149520.jpg

本文将基于提供的学生信息表单代码,详细讲解如何使用Vue3原生方式实现表单验证功能。这个实现方案不依赖任何第三方验证库,完全使用Vue3的响应式特性和原生JavaScript实现。

表单验证的核心设计

1. 数据结构设计

表单数据使用Vue3的reactive创建响应式对象,分为学生信息和家长信息两个部分:

const formData = reactive({
  student: {
    name: "",
    gender: "",
    age: null,
    isValid: computed(() => { /* 验证逻辑 */ })
  },
  parent: {
    name: "",
    phone: "",
    isValid: computed(() => { /* 验证逻辑 */ })
  }
})

每个字段都有对应的isValid计算属性,用于实时判断该部分数据是否有效。

2. 错误信息管理

单独定义errors对象来存储验证错误信息:

const errors = reactive({
  student: {
    name: "",
    gender: "",
    age: "",
  },
  parent: {
    name: "",
    phone: "",
  }
})

这种结构与表单数据保持一致的层级关系,便于管理和访问。

验证函数实现

1. 学生姓名验证

const validateStudentName = () => {
  if (!formData.student.name) {
    errors.student.name = "姓名不能为空";
  } else if (formData.student.name.length < 2) {
    errors.student.name = "姓名至少2个字符";
  } else {
    errors.student.name = "";
  }
};

验证逻辑:

  • 非空检查
  • 最小长度检查
  • 验证通过时清空错误信息

2. 学生年龄验证

const validateStudentAge = () => {
  if (!formData.student.age) {
    errors.student.age = "年龄不能为空";
  } else if (formData.student.age < 6) {
    errors.student.age = "年龄不能小于6岁";
  } else if (formData.student.age > 25) {
    errors.student.age = "年龄不能大于25岁";
  } else {
    errors.student.age = "";
  }
};

验证逻辑:

  • 非空检查
  • 最小值检查
  • 最大值检查
  • 验证通过时清空错误信息

3. 家长手机号验证

const validateParentPhone = () => {
  if (!formData.parent.phone) {
    errors.parent.phone = "手机号不能为空";
  } else if (!/^1[3-9]\d{9}$/.test(formData.parent.phone)) {
    errors.parent.phone = "请输入有效的手机号";
  } else {
    errors.parent.phone = "";
  }
};

验证逻辑:

  • 非空检查
  • 正则表达式验证手机号格式
  • 验证通过时清空错误信息

表单提交处理

const submit = () => {
  validateStudentName();
  validateStudentAge();
  validateParentPhone();

  if (formData.student.isValid && formData.parent.isValid) {
    alert("表单验证通过");
  } else {
    alert("请检查表单填写");
  }
};

提交时执行所有验证函数,并检查各部分数据的isValid状态。

模板结构

模板使用标准的HTML表单元素,结合Vue指令:

<form @submit.prevent="submit">
  <fieldset>
    <legend>学生基本信息</legend>
    <!-- 表单字段 -->
  </fieldset>
  
  <fieldset>
    <legend>家长联系信息</legend>
    <!-- 表单字段 -->
  </fieldset>
  
  <button type="submit">提交表单</button>
</form>

每个表单字段都绑定到对应的数据属性和验证函数:

<input
  id="student-name"
  v-model="formData.student.name"
  @blur="validateStudentName"
  placeholder="请输入学生姓名"
/>
<span class="error">{{ errors.student.name }}</span>

样式设计

样式部分使用scoped CSS,确保只影响当前组件:

.form-group {
  margin-bottom: 15px;
}
.error {
  color: red;
  font-size: 12px;
  margin-left: 110px;
  display: block;
}
/* 其他样式... */

实现优势

  1. 响应式验证‌:利用Vue3的响应式系统,数据变化自动触发界面更新
  2. 即时反馈‌:通过@blur事件在用户离开输入框时立即验证
  3. 结构化设计‌:数据和错误信息采用相同结构,便于维护
  4. 计算属性‌:使用computed实现自动验证状态计算
  5. 原生实现‌:不依赖第三方库,减少项目体积和依赖

我开源了一个基于 Tiptap 实现一个和功能丰富的协同编辑器 🚀🚀🚀

作者 Moment
2025年5月19日 22:35

一个基于 TiptapNext.js 构建的现代化协同文档编辑器,集成了丰富的编辑能力与多人实时协作功能,支持插件扩展、主题切换与持久化存储。适合团队写作、教育笔记、在线文档平台等场景。

无论你是想学习或者想参与开发,你都可以添加我微信 yunmz777,我拉你进交流群中进行学习交流,我们还有很多其他不同的开源项目。

近期开始准备出一个 前端工程化实战 类的课程,如果你对前端技术迷茫,那么学习前端工程化是最好的一个进阶方案,以下是相关的实战内容大纲:

20250519222445

如果你感兴趣想参与的,可以添加我微信进行更详细的了解。

🚀 功能特性

  • 📄 富文本编辑:标题、列表、表格、代码块、数学公式、图片、拖拽等

  • 👥 实时协作:使用 Yjs + @hocuspocus/provider 实现高效协同

  • 🧩 插件丰富:基于 Tiptap Pro 多种增强功能(如表情、详情组件等)

  • 🧰 完善工具链:支持 Prettier、ESLint、Husky、Vitest 等开发工具

📦 技术栈

前端技术栈

技术 说明
Next.js 构建基础框架,支持 SSR / SSG
Tiptap 富文本编辑器,基于 ProseMirror
Yjs 协同编辑核心,CRDT 数据结构
@hocuspocus Yjs 的服务端与客户端 Provider
React 19 UI 框架,支持 Suspense 等新特性
Tailwind CSS 原子化 CSS,集成动画、表单样式等
Socket.io 协同通信通道
Prettier/ESLint 代码风格统一
Vitest/Playwright 单元测试与端到端测试支持

20250519183256

后端技术栈

分类 技术 / 工具 说明
应用框架 NestJS 现代化 Node.js 框架,支持模块化、依赖注入、装饰器和类型安全等特性
HTTP 服务 Fastify 高性能 Web 服务引擎,替代 Express,默认集成于 NestJS 中
协同编辑服务 @hocuspocus/server, yjs 提供文档协同编辑的 WebSocket 服务与 CRDT 算法实现
数据库 ORM Prisma 类型安全的数据库访问工具,自动生成 Schema、支持迁移与种子数据
数据验证 class-validator, class-transformer 请求数据验证与自动转换,配合 DTO 使用
用户鉴权 @nestjs/passport, passport, JWT, GitHub 支持本地登录、JWT 认证与 GitHub OAuth 登录
缓存与状态 ioredis 用于缓存数据、实现限流、协同会话管理或 Pub/Sub 消息推送
对象存储 minio 私有化部署的 S3 兼容存储服务,支持图片与附件上传
图像处理 sharp 图像压缩、格式转换、缩略图等操作
日志系统 winston, winston-daily-rotate-file 支持多种格式、日志分级、自动归档的日志方案
服务监控 @nestjs/terminus, prom-client 提供 /health 健康检查和 /metrics Prometheus 指标暴露接口
监控平台 Prometheus, Grafana 采集与可视化服务运行指标(已内置 Docker 部署配置)
接口文档 @nestjs/swagger 基于代码注解自动生成 Swagger UI 文档
安全中间件 @fastify/helmet, @fastify/rate-limit 添加 HTTP 安全头部、限制请求频率、防止暴力攻击等安全保护
文件上传 @fastify/multipart, @webundsoehne/nest-fastify-file-upload 支持文件流式上传,集成 Fastify 与 NestJS 的多文件上传处理

20250519183049

🚀 快速开始

1. 克隆仓库

git clone https://github.com/xun082/DocFlow.git
cd DocFlow

安装依赖

建议使用 pnpm:

pnpm install

启动本地开发环境

pnpm dev

如何部署

确保已安装以下环境:

  • Docker

  • 推荐:Linux/macOS 或启用 WSL 的 Windows 环境

1️⃣ 构建镜像

docker build -t doc-flow .

2️⃣ 启动容器

docker run -p 6001:6001 doc-flow

启动完成之后访问地址:

http://localhost:6001

🔧 常用脚本

脚本命令 作用说明
pnpm dev 启动开发服务器
pnpm build 构建生产环境代码
pnpm start 启动生产环境服务(端口 6001)
pnpm lint 自动修复所有 ESLint 报错
pnpm format 使用 Prettier 格式化代码
pnpm type-check 运行 TypeScript 类型检查
pnpm test 启动测试(如配置)

🧰 开发规范

  • 使用 Prettier 和 ESLint 保证代码风格统一

  • 配置了 Husky + lint-staged 进行 Git 提交前检查

  • 使用 Commitizen + cz-git 管理提交信息格式(支持语义化发布)

初始化 Git 提交规范:

pnpm commit

📌 未来规划(Roadmap)

项目目前已具备基础协作编辑能力,未来将持续完善并拓展更多功能,进一步提升产品的实用性与专业性:

✅ 近期目标

  • 完善现有功能体验

    • 优化协同冲突解决策略
    • 更细粒度的权限管理(只读 / 可评论 / 可编辑)
    • 增强拖拽体验与文档结构导航(大纲视图)
  • 增强文档组件系统

    • 重构基础组件体系:标题、表格、代码块等更智能、模块化
    • 增加工具栏、快捷键提示和 Markdown 快速输入支持
  • 丰富文档类型与节点支持

    • 支持更多 自定义 Tiptap 节点,如:

      • 引用评论块(Comment Block)
      • 自定义警告框 / 提示框(Tip/Warning)
      • UML/流程图嵌入(如支持 Mermaid)
      • 数据展示组件(如 TableChart、Kanban)

🚀 中期目标

  • 引入音视频实时会议能力

    • 集成 LiveKitDaily 实现嵌入式音视频会议
    • 支持多人语音 / 视频通话,结合文档协同,提升远程会议效率
    • 集成会议内共享笔记区、AI 摘要、会议录制等功能
  • 集成 AI 能力

    • 智能语法纠错、改写建议
    • 语义搜索与问答(支持上下文理解)
    • AI 总结 / 摘要生成
  • 多平台同步支持

    • PWA 支持,适配移动端和桌面离线编辑
    • 跨设备自动同步与版本恢复

🧠 长期方向

  • 插件生态系统建设

    • 引入用户可安装的第三方插件体系
    • 提供插件开发文档与市场入口
  • 文档协作平台化

    • 支持文档团队空间、多人组织结构
    • 文档看板与团队活动看板集成
  • 权限与审计系统

    • 支持操作日志记录、文档编辑历史审查
    • 审批流、编辑建议、协同讨论区等功能

License

本项目采用 MIT 开源协议发布,但包含部分 Tiptap Pro 模板代码除外

Tiptap Pro 模板版权归 Tiptap GmbH 所有,并根据 Tiptap Pro 授权协议进行授权。
详见:tiptap.dev/pro/license

如需使用本项目中涉及 Tiptap Pro 的部分,必须拥有有效的 Tiptap Pro 订阅授权。

📬 联系方式

有更多的问题或者想参与开源,可以添加我微信 yunmz777,我们这还有很多开源项目:

20250519222610

❌
❌