注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

面试官:CDN是怎么加速网站访问的?

web
做前端项目的时候,经常听到"静态资源要放CDN"这种说法: CDN到底是什么,为什么能加速? 用户访问CDN的时候发生了什么? 前端项目怎么配置CDN? 先说结论 CDN(Content Delivery Network)的核心原理:把内容缓存到离用户最近...
继续阅读 »

做前端项目的时候,经常听到"静态资源要放CDN"这种说法:



  • CDN到底是什么,为什么能加速?

  • 用户访问CDN的时候发生了什么?

  • 前端项目怎么配置CDN?


先说结论


CDN(Content Delivery Network)的核心原理:把内容缓存到离用户最近的服务器上


没有CDN时,北京用户访问美国服务器,数据要跨越太平洋。有了CDN,数据从北京的CDN节点返回,快得多。


flowchart LR
subgraph 没有CDN
U1[北京用户] -->|跨越太平洋| S1[美国服务器]
end

subgraph 有CDN
U2[北京用户] -->|就近访问| C2[北京CDN节点]
C2 -.->|首次回源| S2[美国服务器]
end

CDN的工作流程


当用户访问一个CDN加速的资源时,会经过这几步:


1. DNS解析


用户访问 cdn.example.com,DNS会返回离用户最近的CDN节点IP。


# 在北京查询
$ dig cdn.example.com
;; ANSWER SECTION:
cdn.example.com. 60 IN A 101.37.27.xxx # 北京节点

# 在上海查询
$ dig cdn.example.com
;; ANSWER SECTION:
cdn.example.com. 60 IN A 47.100.99.xxx # 上海节点

同一个域名,不同地区解析出不同IP,这就是CDN的智能调度。


2. 缓存判断


请求到达CDN节点后,节点检查本地是否有缓存:



  • 缓存命中:直接返回,速度最快

  • 缓存未命中:向源服务器获取,然后缓存起来


flowchart TD
A[用户请求] --> B[CDN边缘节点]
B --> C{本地有缓存?}
C -->|有| D[直接返回]
C -->|没有| E[回源获取]
E --> F[源服务器]
F --> G[返回内容]
G --> H[缓存到边缘节点]
H --> D

classDef hit fill:#d4edda,stroke:#28a745,color:#155724
classDef miss fill:#fff3cd,stroke:#ffc107,color:#856404
class D hit
class E,F,G,H miss

3. 缓存策略


CDN根据HTTP头决定怎么缓存:


# 典型的静态资源响应头
Cache-Control: public, max-age=31536000
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT


  • max-age=31536000:缓存1年

  • public:允许CDN缓存

  • ETag:文件指纹,用于验证缓存是否过期


为什么CDN能加速


1. 物理距离更近


光在光纤中的传播速度约为 20 万公里/秒。北京到美国往返 2 万公里,光传输就要 100ms。


CDN把内容放到离用户近的地方,这个延迟几乎可以忽略。


2. 分散服务器压力


没有CDN,所有请求都打到源服务器。有了CDN,只有第一次请求(缓存未命中)才需要回源。


假设一张图片被访问 100 万次:



  • 没有CDN:源服务器处理 100 万次请求

  • 有CDN:源服务器只处理几十次(各节点首次回源)


3. 边缘节点优化


CDN服务商的边缘节点通常有这些优化:



  • 自动压缩:Gzip/Brotli压缩

  • 协议优化:HTTP/2、HTTP/3

  • 连接复用:Keep-Alive连接池

  • 智能路由:选择最优网络路径


前端项目配置CDN


1. 构建配置


以Vite为例,配置静态资源的CDN地址:


// vite.config.js
export default defineConfig({
base: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/',
build: {
rollupOptions: {
output: {
// 文件名带hash,便于长期缓存
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})

Webpack配置类似:


// webpack.config.js
module.exports = {
output: {
publicPath: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/',
filename: 'js/[name].[contenthash].js',
}
}

2. 上传到CDN


构建完成后,把dist目录的文件上传到CDN。大多数CDN服务商都提供CLI工具或API:


# 阿里云OSS + CDN
aliyun oss cp -r ./dist oss://your-bucket/

# AWS S3 + CloudFront
aws s3 sync ./dist s3://your-bucket/

# 七牛云
qshell qupload2 --src-dir=./dist --bucket=your-bucket

3. 缓存策略配置


关键是区分两类文件:


带hash的静态资源(JS、CSS、图片):长期缓存


location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

HTML文件:不缓存或短期缓存


location ~* \.html$ {
expires 0;
add_header Cache-Control "no-cache, must-revalidate";
}

为什么这样设计?因为HTML是入口文件,它引用的JS/CSS带有hash。更新代码时:



  1. 新的JS文件会有新的hash(app.abc123.jsapp.def456.js

  2. HTML文件引用新的JS文件

  3. 用户获取新HTML后,会加载新的JS文件

  4. 旧的JS文件在CDN上继续存在,不影响正在访问的用户


4. DNS配置


把CDN域名配置成CNAME指向CDN服务商:


# 阿里云CDN
cdn.example.com. IN CNAME cdn.example.com.w.kunlunsl.com.

# Cloudflare
cdn.example.com. IN CNAME example.com.cdn.cloudflare.net.

常见问题


跨域问题


CDN域名和主域名不同,字体文件、AJAX请求可能遇到跨域。


解决方案是在CDN配置CORS头:


# CDN服务器配置
add_header Access-Control-Allow-Origin "https://example.com";
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS";

或者在源服务器的响应里加上CORS头,CDN会透传。


缓存更新问题


发布新版本后,用户还是看到旧内容?


方案一:文件名带hash(推荐)


前面已经提到,文件名带hash,新版本就是新文件,不存在缓存问题。


方案二:主动刷新缓存


// 调用CDN服务商的刷新API
await cdnClient.refreshObjectCaches({
ObjectPath: 'https://cdn.example.com/app.js\nhttps://cdn.example.com/app.css',
ObjectType: 'File'
});

方案三:URL加版本号


<script src="https://cdn.example.com/app.js?v=1.2.3"></script>

不太推荐,因为有些CDN会忽略查询参数。


HTTPS证书


CDN域名也需要HTTPS证书。大多数CDN服务商提供免费证书或支持上传自有证书。


配置方式:



  1. 在CDN控制台申请免费证书(通常是DV证书)

  2. 或上传自己的证书(用Let's Encrypt申请)


# 用certbot申请泛域名证书
certbot certonly --manual --preferred-challenges dns \
-d "*.example.com" -d "example.com"

回源优化


如果源服务器压力大,可以启用"回源加速"或"中间源":


用户 → 边缘节点 → 中间源 → 源服务器

中间源作为二级缓存,减少对源服务器的请求。多数CDN服务商默认开启这个功能。


CDN选型


CDN服务商优势适用场景
阿里云CDN国内节点多,生态完整国内业务为主
腾讯云CDN游戏加速好,直播支持强游戏、直播
Cloudflare全球节点,有免费套餐出海业务、个人项目
AWS CloudFront与AWS生态集成已用AWS的项目
Vercel/Netlify前端项目一站式部署JAMStack项目

个人项目推荐Cloudflare,免费套餐够用,全球节点覆盖好。


企业项目根据用户分布选择。面向国内用户,阿里云/腾讯云更合适;面向全球用户,Cloudflare或CloudFront。


验证CDN效果


浏览器开发者工具


打开Network面板,关注这几个指标:



  • TTFB(Time To First Byte):首字节时间,越小越好

  • 响应头中的缓存信息X-Cache: HIT 表示命中CDN缓存


命令行测试


# 查看响应头
curl -I https://cdn.example.com/app.js

# 输出示例
HTTP/2 200
cache-control: public, max-age=31536000
x-cache: HIT from CN-Beijing

在线工具



小结


CDN加速的核心原理:



  1. 就近访问:DNS智能解析,把用户引导到最近的节点

  2. 缓存机制:边缘节点缓存内容,减少回源

  3. 协议优化:HTTP/2、压缩、连接复用


前端配置CDN的关键:



  1. 构建时配置publicPath:指向CDN域名

  2. 文件名带hash:便于长期缓存

  3. HTML不缓存:确保用户能获取到最新入口

  4. 处理跨域:配置CORS头




如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:


Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):



全栈项目(适合学习现代技术栈):



  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑

  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB


作者:也无风雨也雾晴
来源:juejin.cn/post/7582438310103613486
收起阅读 »

为什么大部分程序员成不了架构师?

很多程序员初学编程那会,几乎都有一个成为架构师的梦想。 ❝ 毕竟不想当架构师的程序员不是一个好程序员。 这里有几个架构师需要具备的能力模型: ❝ 技术深度和广度: 具备深厚的技术功底,同时对相关领域非常熟悉与了解。 经验积累: 具备在某一领域,有...
继续阅读 »

很多程序员初学编程那会,几乎都有一个成为架构师的梦想。




毕竟不想当架构师的程序员不是一个好程序员。



图片


这里有几个架构师需要具备的能力模型:




技术深度和广度:



  • 具备深厚的技术功底,同时对相关领域非常熟悉与了解。


经验积累:



  • 具备在某一领域,有非常丰富的行业经验

  • 具体涉及到系统设计、性能优化、风险管理等方面。


业务理解和沟通能力:



  • 需要理解业务需求,将业务目标转化为系统设计。

  • 需要与不同角色进行高效的沟通,包括与非技术人员的沟通。


领导和管理能力:



  • 在一些情况下,架构师可能需要领导团队、制定技术方向。


学习和适应能力:



  • 需要不断学习新的技术和趋势,并将其应用到实际项目中。



其实有些程序员可能更喜欢专注于编码本身。




对于涉及更广泛系统设计和管理方面的工作不感兴趣。


他们可能更倾向于深入技术领域而非走向管理和架构方向。



不过能成为架构师还有几个点很关键:




想成为架构师至少要有一个好平台,还要有毅力钻研技术并付诸实践。



  • 而且要经历各种各样的场景。


最好还要有一个好团队一起努力,毕竟一个人的精力是有限的。



不过并非每个程序员都适合成为架构师,不同人有不同的兴趣和职业目标。


有啥其他看法,欢迎在评论区留言讨论。




想看技术文章的,可以去我的个人网站:hardyfish.top/



  • 目前网站的内容足够应付基础面试(P6)了!



每日一题


题目描述




给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。


找出那个只出现了一次的元素。



示例 1 :


ini
体验AI代码助手
代码解读
复制代码
输入:nums = [2,2,1]
输出:1

示例 2 :


ini
体验AI代码助手
代码解读
复制代码
输入:nums = [4,1,2,1,2]
输出:4

示例 3 :


ini
体验AI代码助手
代码解读
复制代码
输入:nums = [1]
输出:1

解题思路




位运算


数组中的全部元素的异或运算结果即为数组中只出现一次的数字。



代码实现


Java代码:


Java
体验AI代码助手
代码解读
复制代码
class Solution {
    public int singleNumber(int[] nums) {
        int single = 0;
        for (int num : nums) {
            single ^= num;
        }
        return single;
    }
}

Python代码:


Python
体验AI代码助手
代码解读
复制代码
class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        return reduce(lambda x, y: x ^ y, nums)

Go代码:


Go
体验AI代码助手
代码解读
复制代码
func singleNumber(nums []int) int {
    single := 0
    for _, num := range nums {
        single ^= num
    }
    return single
}

复杂度分析




时间复杂度:O(n),其中 n 是数组长度。



  • 只需要对数组遍历一次。


空间复杂度:O(1)


作者:程序员飞鱼
链接:https://juejin.cn/post/7459671967306940431
收起阅读 »

数据可视化神器Heat.js:让你的数据热起来

web
😱 我发现了一个「零依赖」的数据可视化宝藏! Hey,前端小伙伴们!今天必须给你们安利一个「让数据说话」的神器——Heat.js!这可不是一个普通的JavaScript库,而是一个能让你的数据「热」起来的魔法工具! 想象一下,当你有一堆枯燥的日期数据,想要以...
继续阅读 »

😱 我发现了一个「零依赖」的数据可视化宝藏!


Hey,前端小伙伴们!今天必须给你们安利一个「让数据说话」的神器——Heat.js!这可不是一个普通的JavaScript库,而是一个能让你的数据「热」起来的魔法工具!


image.png


想象一下,当你有一堆枯燥的日期数据,想要以直观、炫酷的方式展示出来时,Heat.js就像一个魔法师,「唰」的一下就能把它们变成色彩斑斓的热图、清晰明了的图表,甚至还有详细的统计分析!


🤩 这个库到底有什么「超能力」?


1. 「零依赖」轻量级选手,绝不拖你后腿 🦵


在这个「依赖地狱」的时代,Heat.js简直就是一股清流!它零依赖,体积小得惊人,加载速度快得飞起!再也不用担心引入一个库就拖慢整个页面加载速度了~


2. 「四种视图」任你选,总有一款适合你 🔄


Heat.js提供了四种不同的视图模式:



  • Map视图:就像GitHub贡献图一样炫酷,用颜色深浅展示日期活跃度

  • Chart视图:把数据变成专业的图表,让趋势一目了然

  • Days视图:专注于展示每一天的详细数据

  • Statistics视图:直接给你算出各种统计数据,懒人福音!


想换个姿势看数据?只需轻轻一点,瞬间切换~


3. 「51种语言」支持,真正的「世界公民」🌍


担心你的国际用户看不懂?不存在的!Heat.js支持51种语言,从中文、英文到阿拉伯语、冰岛语,应有尽有!你的应用可以轻松走向全球,再也不用为语言本地化发愁了~


4. 「数据导入导出」无所不能,数据来去自由 📤📥


想导出数据做进一步分析?没问题!Heat.js支持导出为CSV、JSON、XML、TXT、HTML、MD和TSV等多种格式,任你选择!


想导入已有数据快速生成热图?同样简单!支持从JSON、TXT、CSV和TSV导入,甚至还支持拖拽上传,简直不要太方便!


5. 「12种主题」随意切换,颜值与实用并存 💅


担心热图不好看?Heat.js提供了12种精心设计的主题,包括暗黑模式和明亮模式,让你的数据可视化既专业又美观!无论你的网站是什么风格,都能找到匹配的主题~


💡 这个神奇的库可以用来做什么?


1. 「活动追踪」,让你的用户活跃起来 📊


想展示用户的登录活跃度?想用热图展示文章的发布频率?Heat.js帮你轻松实现!就像GitHub的贡献图一样,让你的用户看到自己的「努力成果」,成就感满满!


2. 「数据分析」,让你的决策更明智 🧠


通过Heat.js的Statistics视图,你可以快速获取数据的各种统计信息,比如最活跃的月份、平均活动频率等。这些数据可以帮助你做出更明智的产品决策,优化用户体验!


3. 「趋势展示」,让你的报告更有说服力 📈


想在报告中展示某个指标的变化趋势?Heat.js的Chart视图可以将枯燥的数据变成直观的图表,让你的报告更有说服力,老板看了都说好!


🛠️ 如何用最简单的方式用上这个神器?


第一步:「把宝贝抱回家」📦


npm install jheat.js

或者直接使用CDN:


<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/williamtroup/Heat.js@4.5.1/dist/heat.js.min.css">
<script src="https://cdn.jsdelivr.net/gh/williamtroup/Heat.js@4.5.1/dist/heat.min.js"></script>

第二步:「给它找个家」🏠


<div id="heat-map" data-heat-js="{ 'views': { 'map': { 'showDayNames': true } } }">
<!-- 这里将显示你的热图 -->
</div>

第三步:「喂它数据」🍽️


// 添加日期数据
let newDateObject = new Date();
$heat.addDate("heat-map", newDateObject, "Trend Type 1", true);

// 移除日期数据(如果需要)
// $heat.removeDate("heat-map", newDateObject, "Trend Type 1", true);

三步搞定!就是这么简单!


🎯 为什么Heat.js值得你拥有?


1. 「简单易用」,小白也能轻松上手 👶


Heat.js的API设计非常友好,文档也很详细,即使是JavaScript初学者也能快速上手。几个简单的步骤,就能实现专业级的数据可视化效果!


2. 「高度定制」,满足你的各种需求 ⚙️


无论是颜色、样式,还是功能配置,Heat.js都提供了丰富的选项。你可以根据自己的需求,定制出独一无二的数据可视化效果!


3. 「响应式设计」,在任何设备上都完美展示 📱💻


Heat.js完全支持响应式设计,无论是在手机、平板还是电脑上,都能完美展示。你的数据可视化效果将在任何设备上都一样出色!


4. 「TypeScript支持」,框架党福利 🎉


如果你使用React、Angular等现代前端框架,Heat.js的TypeScript支持会让你用得更爽!类型定义清晰,代码提示完善,开发体验一流!


🚀 最后想说的话...


在这个「数据为王」的时代,如何让数据更直观、更有说服力,是每个开发者都需要面对的挑战。而Heat.js,就是帮助你征服这个挑战的绝佳工具!


它轻量级、零依赖、功能强大、易于使用,无论是个人项目还是企业应用,都能轻松胜任。最重要的是,它让数据可视化不再是一件复杂的事情,而是一种乐趣!


所以,还等什么呢?赶紧去GitHub上给Heat.js点个Star⭐,然后在你的项目中用起来吧!相信我,它一定会给你带来惊喜!


✨ 祝大家的数据可视化之路一帆风顺,让我们一起用Heat.js让数据「热」起来!✨


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

解决网页前端中文字体包过大的几种方案

web
最近想给我的博客的网页换个字体,在修复了历史遗留的一些bug之后,去google fonts上找了自己喜欢的字体,本地测试和自己的设备发现没问题后,便以为OK了。但是当我接朋友的设备打开时,发现网页依然是默认字体。这时候我才发现,我的设备能够翻墙,所以能够使用...
继续阅读 »

最近想给我的博客的网页换个字体,在修复了历史遗留的一些bug之后,去google fonts上找了自己喜欢的字体,本地测试和自己的设备发现没问题后,便以为OK了。

但是当我接朋友的设备打开时,发现网页依然是默认字体。这时候我才发现,我的设备能够翻墙,所以能够使用Google CDN服务,但是对于我的其他读者们,在大陆内是访问不了Google的,便也无法渲染字体了。

于是为了解决这个问题,我尝试了各种办法比如格式压缩,子集化(Subset),分包等等,最后考虑到本站的实际情况选用了一种比较邪门的方法,让字体压缩率达到了惊人的98.5%!于是,这篇文章就是对这个过程的总结。也希望这篇文章能够帮助到你。😊


想要自定义网站的字体,最重要的其实就是字体包的获取。大体上可以分为两种办法:在线获取网站本地部署

在线获取──利用 CDN 加速服务

CDN(Content Delivery Network) 意为内容配送网络。你可以简单理解为是一种“就近给你东西”的互联网加速服务。

传统不使用 CDN 服务的是这样的: User ←→ Server,如果相聚遥远,效果显然很差。

使用了 CDN 服务是这样的: User ←→ CDN Nodes ←→ Server,CDN 会提前把你的网站静态资源缓存到各个节点,但你需要时可以直接从最近的节点获取。

全球有多家CDN服务提供商,Google Fonts使用的CDN服务速度很快。所以如果在网络畅通的情况下,使用Google Fonts API是最简单省事的!

你可以直接在文件中导入Google fonts API:

@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Merriweather:ital,opsz,wght@0,18..144,733;1,18..144,733&family=Noto+Serif+SC:wght@500&display=swap');

这样网站它便会自动向最近的Google CDN节点请求资源。

当然,这些都是建立在网络状态畅通无阻的情况下。大陆用户一般使用不了Google服务,但并不意味着无法使用CDN服务。国内的腾讯云,阿里云同样提供高效的服务,但具体的规则我并不了解,请自行阅读研究。

本地部署

既然用不了在线的,那就只能将字体包文件一并上传到服务器上了。

这种做法不需要依赖外部服务,但缺点是字体包的文件往往很大,从进入网站到彻底加载完成的时间会及其漫长!而且这种问题尤其在中日韩(CJK)字体上体现的十分明显。

以本站为例,我主要采用了三种字体:Merriweather, Inter, Noto Serif SC. 其中每种字体都包含了Bold和Regular两种格式。前面两种都属于西文字体,每种格式原始文件大小都在200kb-300kb,但是到了思源宋体这里,仅仅一种格式的字体包大小就达到了足足14M多。如果全部加载完,恐怕从进入网站到完全渲染成功,需要耽误个2分钟。所以将原始字体包文件上传是极不可取的做法!

为了解决这个问题,我在网上查阅资料,找到了三种做法。

字体格式转换(WOFF2)

WOFF2 (Web Open Font Format 2.0) 是一种专为 Web 设计的字体文件格式,旨在提供更高的压缩率和更快的加载速度,也是是目前在 Web 上部署自定义字体的推荐标准。它本质上是一种将 TTF 或 OTF 字体数据进行高度压缩后的格式,目前已经获得了所有主流浏览器的广泛支持。

我们可以找一个在线的字体格式转化网站来实现格式的转化。本文我们以NotoSerifSC-Bold.ttf为例,转换后的NotoSerifSC-Bold.woff2文件只有5.8M左右,压缩率达到了60%!

但是,这仍旧是不够的,仅两个中文字体包加起来也已经快12M,还没有算上其他字体。这对于一个网页来说依然是灾难性的。我们必须寻找另一种方法。

子集化处理(Subset)

中国人都知道,虽然中文的字符加起来有2万多个,但是我们平常交流基本只会用到3000多个,范围再大一点,6000多个字符已经可以覆盖99%的使用场景。这意味着:

我们根本不需要保留所有字符,而只需要保留常用的几千个汉字即可。

于是这就给了我们解决问题的思路了。

首先我们可以去寻找中文常用汉字字符表,这里我获取的资源是 All-Chinese-Character-Set。我们将文件下载解压后,可以在里面找到各种各样按照字频统计的官方文件。这里我们就以《通用规范汉字表》(2013年)一级字和二级字为例。我们创建一个文档char_set.txt并将一级字和二级字的内容全部复制进去。这份文档就是我们子集化的对照表。

接着我们需要下载一个字体子集化工具,这里使用的是Python中的fonttools库,它提供了许多工具(比如我们需要的pyftsubset)可以在命令行中执行子集化、字体转化字体操作。

我们安装一下这个库和对应的依赖(在这之前确保你的电脑上安装了Pythonpip,后者一般官方安装会自带)

pip install fonttools brotli zopfli

然后找到我们字体包对应的文件夹,将原来的char_set.txt复制到该文件夹内,在该文件下打开终端,然后以NotoSerifSC-Bold.ttf为例,输入以下命令:

pyftsubset NotoSerifSC-Bold.ttf --output-file=NotoSerifSC-Bold.subset.woff2 --flavor=woff2 --text-file=char_set.txt --no-hinting --with-zopfli

过一会就能看到会输出一个NotoSerifSC-Bold.subset.woff2的文件。

font-pic-1.png 我们欣喜的发现这个文件的大小竟然只有980KB。至此,我们已经已经将压缩率达到了93%!到这一步,其实直接部署也并没有十分大问题,不过从加载进去到完全渲染,可能依然需要近十秒左右,我们依然还有优化空间。

分包处理实现动态加载

这个方法是我阅读这篇文章了解到的,但是遗憾的是我并没有在自己的网站上实现,不过失败的尝试也让我去寻找其它的方法,最终找到适用本站的一种极限字体渲染的方法,比这三种的效果还要好。下面我依然简单介绍一下这个方法的原理,想更了解可以通过看到最后通过参考资料去进一步了解。

在2017年,Google Fonts团队提出切片字体,因为他们发现:绝大部分网站只需要加载CJK字体包的小部分内容即可覆盖大部分场景。基于适用频率统计,他们将字符分成多个切片,再按 Unicode 编码对剩余字符进行分类。

怎么理解呢?他其实就是把所有的字符分成许多个小集合,每个集合里面都包含一定数量的字符,在靠前的一些集合中,都是我们常用的汉字,越到后,字形越复杂,使用频率也越低。当网页需要加载字体文件时,它是以切片为单位加载的。这意味,只有当你需要用到某个片区的字符时,这个片区才会被加载。

这种方式的好处时,能够大大加快网站加载速率。我们不用每次都一次性把全部字符加载,而是按需加载。这项技术如今已经被Noto Sans字体全面采用。

但是我们需要本地部署的话,需要多费一点功夫。这里我们利用中文网字计划的在线分包网站来实现。

我们将需要的字体上传进行分包,可以观察到输出结果是一系列以哈希值命名的woff2文件。分包其实就是做切分,把每个切分后的区域都转化为一份体积极小的woff2文件。

font-pic-2.png 下载压缩包,然后可以将里面的文件夹导入你的项目,并引用文件夹下的result.css即可。理论上,当网站需要加载渲染某个字体时,它会根据css里面的规则去寻找到对应的分包再下载。每个包的体积极小,网站加载的速度应该提升的很明显。

font-pic-3.png

我的实践──将字符压缩到极限

我的方法可以理解为子集化的一种,只不过我的做法更加的极端一些──只保留文章出现的字符

根据统计结果,截止到这篇post发布,我的文章总共出现的所有字符数不到1200个(数据来源见下文),所以我们可以做的更激进一些,只需将文章出现的中文字符全部记录下来,制成一张专属于自己网站的字符表,然后在每次发布文章时动态更新,这样我们能够保证字体完整渲染,并且处于边界极限状态!

实现这个个性化字符表char_set.txt的核心是一个提取文章中文字符的算法。这部分我是通过Gemini生成了一个update_lists.cpp文件,他能够识别_posts/下面所有文章,并输出到根目录的char_set.txt中,你可以根据代码内容进行自定义的修改:

/**
* @file update_lists.cpp
* @brief Scans Markdown files in /_posts/ and updates char_set.txt in root.
* @author Gemini
* @date 2025-11-28
*/

#include
#include
#include
#include
#include
#include
namespace fs = std::filesystem;
namespace char_collector {
const std::string kRegistryFilename = "char_set.txt";
const std::string kMarkdownExt = ".md";

const uint32_t kCJKStart = 0x4E00;
const uint32_t kCJKEnd = 0x9FFF;

bool NextUtf8Char(std::string::const_iterator& it,
const std::string::const_iterator& end,
uint32_t& out_codepoint,
std::string& out_bytes)
{
if (it == end) return false;
unsigned char c1 = static_cast<unsigned char>(*it);
out_bytes.clear();
out_bytes += c1;
if (c1 < 0x80) { out_codepoint = c1; it++; return true; }
if ((c1 & 0xE0) == 0xC0) {
if (std::distance(it, end) < 2) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
out_codepoint = ((c1 & 0x1F) << 6) | (c2 & 0x3F);
out_bytes += *(it + 1); it += 2; return true;
}
if ((c1 & 0xF0) == 0xE0) {
if (std::distance(it, end) < 3) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
unsigned char c3 = static_cast<unsigned char>(*(it + 2));
out_codepoint = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
out_bytes += *(it + 1); out_bytes += *(it + 2); it += 3; return true;
}
if ((c1 & 0xF8) == 0xF0) {
if (std::distance(it, end) < 4) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
unsigned char c3 = static_cast<unsigned char>(*(it + 2));
unsigned char c4 = static_cast<unsigned char>(*(it + 3));
out_codepoint = ((c1 & 0x07) << 18) | ((c2 & 0x3F) << 12) |
((c3 & 0x3F) << 6) | (c4 & 0x3F);
out_bytes += *(it + 1); out_bytes += *(it + 2); out_bytes += *(it + 3); it += 4; return true;
}
it++; return false;
}

bool IsChineseChar(uint32_t codepoint) {
return (codepoint >= kCJKStart && codepoint <= kCJKEnd);
}

class CharManager {
public:
CharManager() = default;

void LoadExistingChars(const std::string& filepath) {
std::ifstream infile(filepath);
if (!infile.is_open()) {

std::cout << "Info: " << filepath << " not found or empty. Starting fresh." << std::endl;
return;
}
std::string line;
while (std::getline(infile, line)) {
ProcessString(line, false);
}
std::cout << "Loaded " << existing_chars_.size()
<< " unique characters from " << filepath << "." << std::endl;
}

void ScanDirectory(const std::string& directory_path) {

if (!fs::exists(directory_path)) {
std::cerr << "Error: Directory '" << directory_path << "' does not exist." << std::endl;
return;
}

for (const auto& entry : fs::directory_iterator(directory_path)) {
if (entry.is_regular_file() &&
entry.path().extension() == kMarkdownExt) {
ProcessFile(entry.path().string());
}
}
}

void SaveNewChars(const std::string& filepath) {
if (new_chars_list_.empty()) {
std::cout << "No new Chinese characters found." << std::endl;
return;
}
std::ofstream outfile(filepath, std::ios::app);
if (!outfile.is_open()) {
std::cerr << "Error: Could not open " << filepath << " for writing." << std::endl;
return;
}
for (const auto& ch : new_chars_list_) {
outfile << ch;
}
std::cout << "Successfully added " << new_chars_list_.size()
<< " new characters to " << filepath << std::endl;
}

private:
std::unordered_set existing_chars_;
std::vector new_chars_list_;

void ProcessFile(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) return;

std::cout << "Scanning: " << fs::path(filepath).filename().string() << std::endl;
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>())
;
ProcessString(content, true);
}

void ProcessString(const std::string& content, bool track_new) {
auto it = content.begin();
auto end = content.end();
uint32_t codepoint;
std::string bytes;

while (NextUtf8Char(it, end, codepoint, bytes)) {
if (IsChineseChar(codepoint)) {
if (existing_chars_.find(bytes) == existing_chars_.end()) {
existing_chars_.insert(bytes);
if (track_new) {
new_chars_list_.push_back(bytes);
}
}
}
}
}
};

}

int main() {
char_collector::CharManager manager;
manager.LoadExistingChars(char_collector::kRegistryFilename);
manager.ScanDirectory("_posts");
manager.SaveNewChars(char_collector::kRegistryFilename);
return 0;
}

然后我们在终端编译一下再运行即可:

clang++ update_lists.cpp -o update_lists  && ./update_lists

然后我们就会发现这张独属于本站的字符表生成了!🥳 font-pic-6.png 为了方便操作,我们把原始的ttf文件放入仓库的/FontRepo/下(最后记得在.gitignore添加这个文件夹!),然后稍微修改一下之前子集化的命令就可以了:

pyftsubset /FontRepo/NotoSerifSC-Bold.ttf --output-file=/assets/fonts/noto-serif-sc/NotoSerifSC-Bold.subset.woff2 --flavor=woff2 --text-file=char_set.txt --no-hinting --with-zopfli

可以看到,最终输出的文件只有200K!压缩率达到了98.5%!

font-pic-4.png 但是这个方法就像前面说的,处于字体渲染的边界。但凡多出一个字符表中的符号,那么这个字符就无法渲染,会回退到系统字体,看起来格外别扭。所以,在每次更新文章前,我们都需要运行一下./update_lists。此外,还存在一个问题,每次更新产生新的子集化文件时,都需要把旧的子集化文件删除,防止旧文件堆积。

这些过程十分繁琐而且耗费时间,所以我们可以写一个bash脚本来实现这个过程的自动化。我这里同样是求助了Gemini,写了一个build_fonts.sh

#!/bin/bash
set -e # 遇到错误立即停止执行

# ================= 配置区域 =================
# 字体源文件目录
SRC_DIR="FontRepo"
# 字体输出目录
OUT_DIR="assets/fonts/noto-serif-sc"
# 字符列表文件
CHAR_LIST="char_set.txt"
# C++ 更新工具
UPDATE_TOOL="./updateLists"

# 确保输出目录存在
if [ ! -d "$OUT_DIR" ]; then
echo "创建输出目录: $OUT_DIR"
mkdir -p "$OUT_DIR"
fi

# ================= 第一步:更新字符表 =================
echo "========================================"
echo ">> [1/3] 正在更新字符列表..."
if [ -x "$UPDATE_TOOL" ]; then
$UPDATE_TOOL
else
echo "错误: 找不到可执行文件 $UPDATE_TOOL 或者没有执行权限。"
echo "请尝试运行: chmod +x updateLists"
exit 1
fi
# 检查 char_set.txt 是否成功生成
if [ ! -f "$CHAR_LIST" ]; then
echo "错误: $CHAR_LIST 未找到,字符表更新可能失败。"
exit 1
fi
echo "字符列表更新完成。"
# ================= 定义子集化处理函数 =================
process_font() {
local font_name="$1" # 例如: NotoSerifSC-Regular
local input_ttf="$SRC_DIR/${font_name}.ttf"
local final_woff2="$OUT_DIR/${font_name}.woff2"
local temp_woff2="$OUT_DIR/${font_name}.temp.woff2"

echo "----------------------------------------"
echo "正在处理字体: $font_name"
# 检查源文件是否存在
if [ ! -f "$input_ttf" ]; then
echo "错误: 源文件 $input_ttf 不存在!"
exit 1
fi

# 2. 调用 fonttools (pyftsubset) 生成临时子集文件
# 使用 --obfuscate-names 可以进一步减小体积,但这里只用基础参数以保证稳定性
echo "正在生成子集 (TTF -> WOFF2)..."
pyftsubset "$input_ttf" \
--flavor=woff2 \
--text-file="$CHAR_LIST" \
--output-file="$temp_woff2"
# 3. & 4. 删除旧文件并重命名 (更新逻辑)
if [ -f "$temp_woff2" ]; then
if [ -f "$final_woff2" ]; then
echo "删除旧文件: $final_woff2"
rm "$final_woff2"
fi

echo "重命名新文件: $temp_woff2 -> $final_woff2"
mv "$temp_woff2" "$final_woff2"
echo ">>> $font_name 更新成功!"
else
echo "错误: 子集化失败,未生成目标文件。"
exit 1
fi
}
# ================= 第二步 & 第三步:执行转换 =================
echo "========================================"
echo ">> [2/3] 开始字体子集化处理..."
# 处理 Regular 字体
process_font "NotoSerifSC-Regular"
# 处理 Bold 字体
process_font "NotoSerifSC-Bold"
echo "========================================"
echo ">> [3/3] 所有任务圆满完成!"

如此一来,以后每次更新完文章,都只需要在终端输入./build_fonts.sh就可以完成字符提取、字体包子集化、清除旧字体包文件的过程了。

font-pic-5.png

一点感想

在这之前另外讲个小故事,我尝试更换字体之前发现自定义的字体样式根本没有用,后来检查了很久,发现竟然是2个月前AI在我代码里加的一句font-family:'Noto Serif SC',而刚好他修改的又是优先级最高的文件,所以后面怎么修改字体都没有用。所以有时候让AI写代码前最好先搞清除代码的地位i,并且做好为AI代码后果负全责的准备。

更改网站字体其实很多时候属于锦上添花的事情,因为很多读者其实并不会太在意网站的字体。但不幸的是我对细节比较在意,或者说有种敝帚自珍的感觉吧,想慢慢地把网站装饰得舒适一些,所以才总是花力气在一些细枝末节的事情上。更何况,我是懂一点点设计的,有时候看见一些非常丑的Interface心里是很难受的。尽管就像绝大部分人理解不了设计师在细节上的别有用心一样,绝大部分人也不会在意一个网站的字体如何,但是我自己的家,我想装饰地好看些,对我来说就满足了。

更不要说,如果不去折腾这些东西,怎么可能会有这篇文章呢?如果能够帮助到一些人,也算是在世界留下一点价值了。

参考资料及附录

  1. 参考资料

    a. 网页中文字体加载速度优化

    b. 缩减网页字体大小

    c. All-Chinese-Character-Set

  2. 让Gemini生成代码时的Prompt:
---Prompt 1---
# 任务名称:创建脚本实现对字符的收集
请利用C++来完成一下任务要求:
1. 该脚本能够读取项目目录下的markdown文件,并且能够识别当中所有的中文字符,将该中文字符与`/char_test/GeneralUsedChars.txt`的字符表进行查重比较:
若该字在表中存在,则跳过,处理下一个字;
若不存在,则将该字添加到表中,然后继续处理下一个字符
2. 请设计一个高效的算法,尤其是在字符查重的过程中,你需要设计一个高效且准确率高的算法
3. 请注意脚本的通用性,你需要考虑到这个项目以后可能会继续增加更多的markdown文件,所以你不应该仅仅只是处理现有的markdown文件,还需要考虑到以后的拓展性
4. 如果可以的话,尽可能使用C++来实现,因为效率更高

---Prompt 2---
可以了,现在我要求你编写一个脚本以实现自动化,要求如下:
1. 脚本运行时,首先会调用项目根目录下的updateLists可执行文件,更新char_set.txt
2. 接着,脚本会调用fonttools工具,对路径在`/FontRepo/`下的两个文件进行ttf到woff2的子集化转化,其中这两个字体文件的名字分别为`NotoSerifSC-Regular.ttf`和`NotoSerifSC-Bold.ttf`。
3. 转化好的子集文件应该输出到 `/assets/fonts/noto-serif-sc/`文件夹下。
4. 将`/assets/fonts/noto-serif-sc/`文件夹下原本已经存在的两个字体文件`NotoSerifSC-Bold.woff2`和`NotoSerifSC-Regular.woff2`删除,然后将新得到子集化文件重新命名为这两个删除了的文件的名字。这一步相当于完成了字体文件的更新

请注意文件的命名,尤其是不要搞错字号,新子集文件和旧子集文件。
请注意在子集化步骤的bash命令,环境已经安装好fonttools及其对应依赖,你可以参考下面这个命令来使用,或者使用更好更稳定的用法:
pyftsubset --flavor=woff2 --text-file= --output-file=
(再次注意输出路径)
  1. 最终实践效果(以NotoSerifSC-Bold为例)
    处理方式字体包体积压缩率
    无处理14.462M0%
    格式转化5.776M60.06%
    子集化处理981K93.21%
    分包处理依据动态加载量而定
    我的实践216K98.5%


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

一个AI都无法提供的html转PDF方案

web
这也许就是AI无法代替人的原因,只需一行代码就可以实现纯前端 html 转矢量 pdf 的功能 // 引入 dompdf.js库 import dompdf from "dompdf.js"; dompdf(document.querySelector("#...
继续阅读 »

这也许就是AI无法代替人的原因,只需一行代码就可以实现纯前端 html 转矢量 pdf 的功能


// 引入 dompdf.js库
import dompdf from "dompdf.js";

dompdf(document.querySelector("#capture")).then(function (blob) {
//文件操作
});

实现效果(复杂表格)


企业微信截图_20251011152750.png


1. 在线体验



dompdfjs.lisky.com.cn



2. Git 仓库地址 (欢迎 Star⭐⭐⭐)



github.com/lmn1919/dom…



3. 生成 PDF


在前端生态里,把网页内容生成 PDF 一直是一个常见但不简单的需求。从报表导出、小票生成、合同下载到打印排版,很多项目或多或少都会遇到。市面上常见的方案大致有以下几类:



  • 服务端渲染 PDF(后端库如 wkhtmltopdf、PrinceXML 等)

  • 客户端将 HTML 渲染为图片(如 html2canvas + jsPDF)然后再封装为 PDF

  • 前端调用相关 pdf 生成库来生成 PDF(如 pdfmake,jspdf,pdfkit)


但是这些方案都有各自的局限性,



  • 比如服务端渲染 PDF 对服务器资源要求高,需要后端参与。

  • html2canvas + jsPDF 需要将 html 内容渲染为图片,再将图片封装为 PDF,速度会比较慢,而且生成体积会比较大,内容会模糊,限制于 canvas 生成高度,不能生成超过 canvas 高度的内容。

  • 而前端调用相关 pdf 生成库来生成 PDF 则需要对相关库有一定的了解,api 比较复杂,学习使用成本很高。


使用 jspdf 生成如图简单的 pdf


企业微信截图_20251011173729.png


就需要如此复杂的代码,如果要生成复杂的 pdf, 比如包含表格、图片、图表等内容,那使用成本就更高了。


function generateChinesePDF() {
// Check if jsPDF is loaded
if (typeof window.jspdf === "undefined") {
alert("jsPDF library has not finished loading, please try again later");
return;
}

const { jsPDF } = window.jspdf;
const doc = new jsPDF();

// Note: Default jsPDF does not support Chinese, this is just a demo
// In real projects you need to add Chinese font support

doc.setFontSize(16);
doc.text("Chinese Text Support Demo", 20, 30);

doc.setFontSize(12);
doc.text("Note: Default jsPDF does not support Chinese characters.", 20, 50);
doc.text("You need to add Chinese font support for proper display.", 20, 70);

// Draw some graphics for demonstration
doc.setFillColor(255, 182, 193);
doc.rect(20, 90, 60, 30, "F");
doc.setTextColor(0, 0, 0);
doc.text("Pink Rectangle", 25, 108);

doc.setFillColor(173, 216, 230);
doc.rect(100, 90, 60, 30, "F");
doc.text("Light Blue Rectangle", 105, 108);

doc.save("chinese-example.pdf");
}

但是现在,有了 dompdf.js,你只需要一行代码,就可以完成比这个复杂 10 倍的 PDF 生成任务,html页面所见即所得,可以将复杂的css样式转化成pdf


dompdf(document.querySelector("#capture")).then(function (blob) {
//文件操作
});

而且,dompdf.js 生成的 PDF 是矢量的,非图片式的,高清晰度的,文字可以选中、复制、搜索等操作(在支持的 PDF 阅读器环境下),区别于客户端将 HTML 渲染为图片(如 html2canvas + jsPDF)然后再封装为 PDF。



具体可以去体验 立即体验 https://dompdfjs.lisky.com.cn



4. dompdf.js 是如何实现的?


其实 dompdf.js 也是基于 html2canvas+jspdf 实现的,但是为什么 dompdf.js 生成的 pdf 文件可以二次编辑,更清晰,体积小呢?


不同于普通的 html2canvas + jsPDF 方案,将 dom 内容生成为图片,再将图片内容用 jspdf 绘制到 pdf 上,这就导致了生成的 pdf 文件体积大,无法编辑,放大后会模糊。


html2canvas 原理简介


1. DOM 树遍历
html2canvas 从指定的 DOM 节点开始,递归遍历所有子节点,构建一个描述页面结构的内部渲染队列。


2. 样式计算
对每个节点调用 window.getComputedStyle() 获取最终的 CSS 属性值。这一步至关重要,因为它包含了所有 CSS 规则(内联、内部、外部样式表)层叠计算后的最终结果。


3. 渲染模型构建
将每个 DOM 节点和其计算样式封装成渲染对象,包含绘制所需的完整信息:位置(top, left)、尺寸(width, height)、背景、边框、文本内容、字体属性、层级关系(z-index)等。


4. Canvas 上下文创建
在内存中创建 canvas 元素,获取其 2D 渲染上下文(CanvasRenderingContext2D)。


5. 浏览器绘制模拟
按照 DOM 的堆叠顺序和布局规则,遍历渲染队列,将每个元素绘制到 Canvas 上。这个过程实质上是将 CSS 属性"翻译"成对应的绘制 API 调用:


CSS 属性传统 Canvas APIdompdf.js 中的 jsPDF API
background-colorctx.fillStyle + ctx.fillRect()doc.setFillColor() + doc.rect(x, y, w, h, 'F')
borderctx.strokeStyle + ctx.strokeRect()doc.setDrawColor() + doc.rect(x, y, w, h, 'S')
color, font-family, font-sizectx.fillStyle, ctx.font + ctx.fillText()doc.setTextColor() + doc.setFont() + doc.text()
border-radiusarcTo()bezierCurveTo() 创建剪切路径doc.roundedRect()doc.lines() 绘制圆角
imagectx.drawImage()doc.addImage()

核心创新:API 替换,底层是封装了 jsPDF 的 API
dompdf.js 的关键突破在于改造了 html2canvas 的 canvas-renderer.ts 文件,将原本输出到 Canvas 的绘制 API 替换为 jsPDF 的 API 调用。这样就实现了从 DOM 直接到 PDF 的转换,生成真正可编辑、可搜索的 PDF 文件,而不是传统的图片格式。


目前实现的功能


1. 文字绘制 (颜色,大小)
2. 图片绘制 (支持 jpeg, png 等格式)
3. 背景,背景颜色 (支持合并单元格)
4. 边框,复杂表格绘制 (支持合并单元格)
5. canvas (支持多种图表类型)
6. svg (支持 svg 元素绘制)
7. 阴影渲染 (使用 foreignObjectRendering,支持边框阴影渲染)
8. 渐变渲染 (使用 foreignObjectRendering,支持背景渐变渲染)


7.使用


安装


        npm install dompdf.js --save

CDN 引入


<script src="https://cdn.jsdelivr.net/npm/dompdf.js@latest/dist/dompdf.js"></script>

基础用法


import dompdf from "dompdf.js";
dompdf(document.querySelector("#capture"), {
useCORS: true, //是否允许跨域
})
.then(function (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "example.pdf";
document.body.appendChild(a);
a.click();
})
.catch(function (err) {
console.log(err, "err");
});

写在最后


dompdf.js 让前端 PDF 生成变得前所未有的简单:无需后端、无需繁琐配置、一行代码即可输出矢量、可检索、可复制的专业文档。无论是简历、报告还是发票,它都能轻松胜任。 欢迎在你的项目中使用它 。


如果它帮到了你,欢迎去 github.com/lmn1919/dom… 点个 Star,提优化,共建项目。


作者:刘发财
来源:juejin.cn/post/7559886023661649958
收起阅读 »

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

web
我正在筹备一套前端工程化体系的实战课程。如果你在学习前端的过程中感到方向模糊、技术杂乱无章,那么前端工程化将是你实现系统进阶的最佳路径。它不仅能帮你建立起对现代前端开发的整体认知,还能提升你在项目开发、协作规范、性能优化等方面的工程能力。 ✅ 本课程覆盖构建工...
继续阅读 »

我正在筹备一套前端工程化体系的实战课程。如果你在学习前端的过程中感到方向模糊、技术杂乱无章,那么前端工程化将是你实现系统进阶的最佳路径。它不仅能帮你建立起对现代前端开发的整体认知,还能提升你在项目开发、协作规范、性能优化等方面的工程能力。


✅ 本课程覆盖构建工具测试体系脚手架CI/CDDockerNginx 等核心模块,内容体系完整,贯穿从开发到上线的全流程。每一章节都配有贴近真实场景的企业级实战案例,帮助你边学边用,真正掌握现代团队所需的工程化能力,实现从 CRUD 开发者到工程型前端的跃迁。


详情请看前端工程化实战课程


学完本课程,对你的简历和具体的工作能力都会有非常大的提升。如果你对此项目感兴趣,或者课程感兴趣,可以私聊我微信 yunmz777


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


20241223154451


我的技术栈


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


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


React


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


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


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


NextJs


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


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


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


Typescript


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


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


React Native



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



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


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


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


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


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


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


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


Nestjs


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



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



Prisma & mysql


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


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


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


Redis


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


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

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

import { isObject } from "@/utils";

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

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

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

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

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

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

return null;
}
}

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

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

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

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

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

return [];
}
}

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

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

前端工程化


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


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


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


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


20241223165138


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



  1. NextJs

  2. React Native

  3. prisma

  4. NestJs

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


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


总结


学无止境,任重道远。


最后再来提一下这两个开源项目,它们都是我们目前正在维护的开源项目:



如果你想参与进来开发或者想进群学习,可以添加我微信 yunmz777,后面还会有很多需求,等这个项目完成之后还会有很多新的并且很有趣的开源项目等着你。


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

还在用html2canvas?介绍一个比它快100倍的截图神器!

web
在日常业务开发里,DOM 截图 几乎是刚需场景。 无论是生成分享卡片、导出报表,还是保存一段精美排版内容,前端同学都绕不开它。 但问题来了——市面上的截图工具,比如 html2canvas,虽然用得多,却有一个致命缺陷:慢! 普通截图动辄 1 秒以上,大一点的...
继续阅读 »

在日常业务开发里,DOM 截图 几乎是刚需场景。


无论是生成分享卡片、导出报表,还是保存一段精美排版内容,前端同学都绕不开它。


但问题来了——市面上的截图工具,比如 html2canvas,虽然用得多,却有一个致命缺陷:慢!
普通截图动辄 1 秒以上,大一点的 DOM,甚至能直接卡到怀疑人生,用户体验一言难尽。


大家好,我是芝士,欢迎点此扫码加我微信 Hunyi32 交流,最近创建了一个低代码/前端工程化交流群,欢迎加我微信 Hunyi32 进群一起交流学习,也可关注我的公众号[ 前端界 ] 持续更新优质技术文章


最近发现一个保存速度惊艳到我的库snapDOM


这货在性能上的表现,完全可以用“碾压”来形容:



  • 👉 相比 html2canvas,快 32 ~ 133 倍

  • 👉 相比 modern-screenshot,也快 2 ~ 93 倍



以下是官方基本测试数据:


场景snapDOM vs html2canvassnapDOM vs dom-to-image
小元素 (200×100)32 倍6 倍
模态框 (400×300)33 倍7 倍
整页截图 (1200×800)35 倍13 倍
大滚动区域 (2000×1500)69 倍38 倍
超大元素 (4000×2000)93 倍 🔥133 倍

📊 数据来源:snapDOM 官方 benchmark(基于 headless Chromium 实测)。


⚡ 为什么它这么快?


二者的实现原理不同


html2canvas 的实现方式



  • 原理:
    通过遍历 DOM,把每个节点的样式(宽高、字体、背景、阴影、图片等)计算出来,然后在 <canvas> 上用 Canvas API 重绘一遍。

  • 特点:



    • 需要完整计算 CSS 样式 → 排版 → 绘制。

    • 复杂 DOM 时计算量极大,比如渐变、阴影、字体渲染都会消耗 CPU。

    • 整个过程基本是 模拟浏览器的渲染引擎,属于“重造轮子”。




所以一旦 DOM 大、样式复杂,html2canvas 很容易出现 1s+ 延迟甚至卡死。


snapDOM 的实现方式


原理:利用浏览器 原生渲染能力,而不是自己模拟


snapDOM 的 captureDOM 并不是自己用 Canvas API 去一笔一笔绘制 DOM(像 html2canvas 那样),而是:



  1. 复制 DOM 节点(prepareClone)



    • → 生成一个“克隆版”的 DOM,里面包含了样式、结构。



  2. 把图片、背景、字体都转成 inline(base64 / dataURL)



    • → 确保克隆 DOM 是完全自包含的。



  3. <foreignObject> 包在 SVG 里面



    • → 浏览器原生支持直接渲染 HTML 片段到 SVG → 再转成 dataURL。




所以核心就是:
👉 利用浏览器自己的渲染引擎(SVG foreignObject)来排版和绘制,而不是 JS 重造渲染过程


如何使用 snapDOM


snapDOM 上手非常简单, 学习成本比较低。


1. 安装


通过npm 安装:


npm install @zumer/snapdom

或者直接用 CDN 引入:


<script src="https://cdn.jsdelivr.net/npm/@zumer/snapdom/dist/snapdom.min.js"></script>

2. 基础用法


只需要一行代码,就能把 DOM 节点“变”成图片:


// 选择你要截图的 DOM 元素
const target = document.querySelector('.card');

// 导出为 PNG 图片
const image = await snapdom.toPng(target);

// 直接添加到页面
document.body.appendChild(image);


3. 更多导出方式


除了 PNG,snapDOM 还支持多种输出格式:


// 导出为 JPEG
const jpeg = await snapdom.toJpeg(target);

// 导出为 SVG
const svg = await snapdom.toSvg(target);

// 直接保存为文件
await snapdom.download(target, { format: 'png', filename: 'screenshot.png' });

4. 导出一个这样的海报图



开发中生成海报并保存到, 是非常常见的需求,以前使用html2canvas,也要写不少代码, 还要处理图片失真等问题, 使用snapDOM,真的一行代码能搞定。


 <div ref="posterRef" class="poster">
....
</div>

<script setup lang="ts">
const downloadPoster = async () => {
if (!posterRef.value) {
alert("海报元素未找到");
return;
}

try {
// snapdom 是 UMD 格式,通过全局 window.snapdom 访问
const snap = (window as any).snapdom;
if (!snap) {
alert("snapdom 库未加载,请刷新页面重试");
return;
}
await snap.download(posterRef.value, {
format: "png",
filename: `tech-poster-${Date.now()}`
});


} catch (error) {
console.error("海报生成失败:", error);
alert("海报生成失败,请重试");
}
};
</script>

相比传统方案需要大量配置和兼容性处理,snapDOM 真正做到了 一行代码,极速生成。无论是分享卡片、营销海报还是报表导出,都能轻松搞定。


await snap.download(posterRef.value, {
format: "png",
filename: `tech-poster-${Date.now()}`
});

大家好,我是芝士,欢迎点此扫码加我微信 Hunyi32 交流,最近创建了一个低代码/前端工程化交流群,欢迎加我微信 Hunyi32 进群一起交流学习,也可关注我的公众号[ 前端界 ] 持续更新优质技术文章


总结


在前端开发里,DOM 截图是一个常见但“让人头疼”的需求。



  • html2canvas 代表的传统方案,虽然功能强大,但性能和体验常常拖后腿;

  • 而 snapDOM 借助浏览器原生渲染能力,让截图变得又快又稳。


一句话:
👉 如果你还在为截图慢、卡顿、模糊烦恼,不妨试试 snapDOM —— 可能会刷新你对前端截图的认知。 🚀


作者:芝士加
来源:juejin.cn/post/7542379658522116123
收起阅读 »

10年老前端吐槽Tailwind CSS:是神器还是“神坑”?

web
作为一个老前端人比大家虚长几岁,前端技术的飞速发展,从早期的 jQuery 到现代的 React、Vue,再到 CSS 框架的演变。最近几年,Tailwind CSS 成为了前端圈的热门话题,很多人称它为“神器”,但也有不少人认为它是“神坑”。今天,我就从实际...
继续阅读 »

作为一个老前端人比大家虚长几岁,前端技术的飞速发展,从早期的 jQuery 到现代的 React、Vue,再到 CSS 框架的演变。最近几年,Tailwind CSS 成为了前端圈的热门话题,很多人称它为“神器”,但也有不少人认为它是“神坑”。今天,我就从实际项目经验出发,吐槽一下 Tailwind CSS 的弊端。


HTML 代码臃肿,可读性差


例如:


<div class="p-4 bg-white rounded-lg shadow-md text-gray-800 hover:bg-gray-100">
Content
</div>

以至于前端同学前来吐槽,这和写style有个毛线区别。
虽然tailwind提供了@apply方法,将常用的实用类代码提取到css中,来减少html的代码量


.btn {
@apply p-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600;
}

可以一个大型项目样式复杂,常常看到这样的场景:


.btn1 {
@apply p-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600;
}
.btn2 {
@apply p-6 bg-blue-500 text-blue rounded-lg hover:bg-blue-600;
}
.btn3 {
@apply p-8 bg-blue-500 text-red rounded-lg hover:bg-blue-600;
}
.btn4 {
@apply p-10 bg-blue-500 rounded-lg hover:bg-green-600;
}

读起来都费劲。


增加了前端同学的学习成本


开发是必须学习大量的tailwind的实用类,并且要花时间学习这些命名规则,例如:



  • p-4 是 padding,m-4 是 margin。

  • text-sm 是小字体,text-lg 是大字体。

  • bg-blue-500 是背景色,text-blue-500 是文字颜色。


国内的项目大家都知道,不是在赶工期,就是在赶工期的路上,好多小伙伴开发的时候直接就上style了。


当然这样也有好处,能使用JIT模式,按需生成css,减少文件的大小。避免了以前项目中好多无用的css。


动态样式支持有限


有时候需要动态生成的类会导致错误,例如:


<div class="text-{{ color }}-500 bg-{{ bgColor }}-100">
Content
</div>

如果color被复制为green,但是系统并没有定义 text-green-500 这个类。让项目变得难以调试和维护。


总结


Tailwind CSS 是一把双刃剑,它既能为开发带来极大的便利,也可能成为项目的“神坑”。作为开发者,我们需要根据项目需求,合理使用 Tailwind,并通过一些最佳实践规避它的弊端。希望这篇文章能帮助你在项目中更好地使用 Tailwind CSS,享受它带来的便利,同时避免踩坑!


如果你有更多关于 Tailwind CSS 的问题或经验分享,欢迎在评论区留言讨论! 😊


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

🔄一张图,让你再也忘不了浏览器的事件循环(Event Loop)了

web
一、前言下面纯手工画了一张在浏览器执行JavaScript代码的Event Loop(事件循环) 流程图。后文会演示几个例子,把示例代码放到这个流程图演示其执行流程。当然,这只是简单的事件循环流程,不过,却能让我们快速掌握其原理。二、概念事件循环是J...
继续阅读 »

一、前言

下面纯手工画了一张在浏览器执行JavaScript代码的Event Loop(事件循环) 流程图。

后文会演示几个例子,把示例代码放到这个流程图演示其执行流程。

当然,这只是简单的事件循环流程,不过,却能让我们快速掌握其原理。

Event Loop.png

二、概念

事件循环JavaScript为了处理单线程执行代码时,能异步地处理用户交互、网络请求等任务 (异步Web API),而设计的一套任务调度机制。它就像一个永不停止的循环,不断地检查(结合上图就是不断检查Task QueueMicrotask Queue这两个队列)并需要运行的代码。


三、为什么需要事件循环

JavaScript是单线程的,这意味着它只有一个主线程来执行代码。如果所有任务(比如一个耗时的计算、一个网络请求)都同步执行,那么浏览器就会被卡住,无法响应用户的点击、输入,直到这个任务完成。这会造成极差的用户体验。

事件循环就是为了解决这个问题而生的:它让耗时的操作(如网络请求、文件读取)在后台异步执行,等这些操作完成后,再通过回调的方式来执行相应的代码,从而不阻塞主线程

四、事件循环流程图用法演示

演示一:小菜一碟

先来一个都是同步代码的小菜,先了解一下前面画的流程图是怎样在调用栈当中执行JavaScript代码的。

console.log(1)

function funcOne() {
console.log(2)
}

function funcTwo() {
funcOne()
console.log(3)
}

funcTwo()

console.log(4)

控制台输出:

1 2 3 4

下图为调用栈执行流程

演示01.png

每执行完一个同步任务会把该任务进行出栈。在这个例子当中每次在控制台输出一次,则进行一次出栈处理,直至全部代码执行完成。

演示二:小试牛刀

setTimeout+Promise组合拳,了解异步代码是如何进入任务队列等待执行的。

console.log(1)

setTimeout(() => {
console.log('setTimeout', 2)
}, 0)

const promise = new Promise((resolve, reject) => {
console.log('promise', 3)
resolve(4)
})

setTimeout(() => {
console.log('setTimeout', 5)
}, 10)

promise.then(res => {
console.log('then', res)
})

console.log(6)

控制台输出:

1 promise 3 6 then 4 setTimeout 2 setTimeout 5

流程图执行-步骤一:

先执行同步代码,如遇到异步代码,则把异步回调事件放到后台监听对应的任务队列

image.png

  1. 执行console.log(1),控制台输出1
  2. 执行定时器,遇到异步代码,后台注册定时器回调事件,时间到了,把回调函数() => {console.log('setTimeout', 2)},放到宏任务队列等待。
  3. 执行创建Promise实例,并执行其中同步代码:执行console.log('promise', 3),控制台输出promise 3;执行resolve(4),此时Promise已经确定为完成fulfilled状态,把promise.then()的回调函数响应值设为4
  4. 执行定时器,遇到异步代码,后台注册定时器回调事件,时间未到,把回调函数() => { console.log('setTimeout', 5) }放到后台监听。
  5. 执行promise.then(res => { console.log('then', res) }),出栈走异步代码,把回调函数4 => { console.log('then', 4) }放入微任务队列等待。

流程图执行-步骤二:

上面已经把同步代码执行完成,并且把对应异步回调事件放到了指定任务队列,接下来开始事件循环

image.png

  1. 扫描微任务队列,执行4 => { console.log('then', 4) }回调函数,控制台输出then 4
  2. 微任务队列为空,扫描宏任务队列,执行() => {console.log('setTimeout', 2)}回调函数,控制台输出setTimeout 2
  3. 每执行完一个宏任务,需要再次扫描微任务队列是否存在可执行任务(假设此时后台定时到了,则会把() => { console.log('setTimeout', 5) }加入到了宏任务队列末尾)。
  4. 微任务队列为空,扫描宏任务队列,执行() => { console.log('setTimeout', 5) },控制台输出setTimeout 5

演示三:稍有难度

setTimeout+Promise组合拳+多层嵌套Promise

console.log(1)

setTimeout(() => {
console.log('setTimeout', 10)
}, 0)

new Promise((resolve, reject) => {
console.log(2)
resolve(7)

new Promise((resolve, reject) => {
resolve(5)
}).then(res => {
console.log(res)

new Promise((resolve, reject) => {
resolve('嵌套第三层 Promise')
}).then(res => {
console.log(res)
})
})

Promise.resolve(6).then(res => {
console.log(res)
})

}).then(res => {
console.log(res)
})

new Promise((resolve, reject) => {
console.log(3)

Promise.resolve(8).then(res => {
console.log(res)
})

resolve(9)
}).then(res => {
console.log(res)
})

console.log(4)

上一个演示说明了流程图执行的详细步骤,下面就不多加赘叙了,直接看图!

talk is cheap, show me the chart

image.png

上图,调用栈同步代码执行完成,开始事件循环,先看微任务队列,发现不为空,按顺序执行微任务事件:

嵌套02.png

上图,已经把刚才排队的微任务队列全部清空了。但是在执行第一个微任务时,发现还有嵌套微任务,则把该任务放到微任务队列末尾,然后接着一起执行完所有新增任务

嵌套03.png

最后微任务清空后,接着执行宏任务。到此全部事件已执行完毕!

控制台完整输出顺序:

1 2 3 4 5 6 7 8 9 10

演示四:setTimeout伪定时

setTimeout并不是设置的定时到了就马上执行,而是把定时回调放在task queue任务队列当中进行等待,待主线程调用栈中的同步任务执行完成后空闲时才会执行。

const startTime = Date.now()
setTimeout(() => {
const endTime = Date.now()
console.log('setTimeout cost time', endTime - startTime)
// setTimeout cost time 2314
}, 100)

for (let i = 0; i < 300000; i++) {
// 模拟执行耗时同步任务
console.log(i)
}

控制台输出:

1 2 3 ··· 300000 setTimeout cost time 2314

下图演示了其执行流程:

setTimeout假延时.png

演示五:fetch网络请求和setTimeout

获取网络数据,fetch回调函数属于微任务,优于setTimeout先执行。

setTimeout(() => {
console.log('setTimeout', 2)
}, 510)

const startTime = Date.now()
fetch('http://localhost:3000/test').then(res => {
const endTime = Date.now()
console.log('fetch cost time', endTime - startTime)
return res.json()
}).then(data => {
console.log('data', data)
})

下图当前Call Stack执行栈执行完同步代码后,由于fetchsetTimeout都是宏任务,所以走宏任务Web API流程后注册这两个事件回调,等待定时到后了,由于定时回调是个普通的同步函数,所以放到宏任务队列;等待fetch拿到服务器响应数据后,由于fetch回调为一个Promise对象,所以放到微任务队列。

fetch.png

经过多番刷新网页测试,下图控制台打印展示了setTimeout延时为510msfetch请求响应同样是510ms的情况下,.then(data => { console.log('data', data) })先执行了,也是由于fetch基于Promise实现,所以其回调为微任务。

b475cbb38b0161d3e7f5f97b45824b31.png

五、结语

这可能只是简单的JavaScript代码执行事件循环流程,目的也是让大家更直观理解其中原理。实际执行过程可能还会读取堆内存获取引用类型数据、操作dom的方法,可能还会触发页面的重排、重绘等过程、异步文件读取和写入操作、fetch发起网络请求,与服务器建立连接获取网络数据等情况。

但是,它们异步执行的回调函数都会经过图中的这个事件循环过程,从而构成完整的浏览器事件循环。


作者:vilan_微澜
来源:juejin.cn/post/7577395040592756746

收起阅读 »

单点登录:一次登录,全网通行

大家好,我是小悟。 想象一下你去游乐园,买了一张通票(登录),然后就可以玩所有项目(访问各个系统),不用每个项目都重新买票(重新登录)。这就是单点登录(SSO)的精髓! SSO的日常比喻 普通登录:像去不同商场,每个都要查会员卡 单点登录:像微信扫码登录,...
继续阅读 »

大家好,我是小悟。



  • 想象一下你去游乐园,买了一张通票(登录),然后就可以玩所有项目(访问各个系统),不用每个项目都重新买票(重新登录)。这就是单点登录(SSO)的精髓!


    SSO的日常比喻



    • 普通登录:像去不同商场,每个都要查会员卡

    • 单点登录:像微信扫码登录,一扫全搞定

    • 令牌:像游乐园手环,戴着就能证明你买过票


    下面用代码来实现这个"游乐园通票系统":


    代码实现:简易SSO系统


    import java.util.*;

    // 用户类 - 就是我们这些想玩项目的游客
    class User {
    private String username;
    private String password;

    public User(String username, String password) {
    this.username = username;
    this.password = password;
    }

    // getters 省略...
    }

    // 令牌类 - 游乐园手环
    class Token {
    private String tokenId;
    private String username;
    private Date expireTime;

    public Token(String username) {
    this.tokenId = UUID.randomUUID().toString();
    this.username = username;
    // 令牌1小时后过期 - 游乐园晚上要关门的!
    this.expireTime = new Date(System.currentTimeMillis() + 3600 * 1000);
    }

    public boolean isValid() {
    return new Date().before(expireTime);
    }

    // getters 省略...
    }

    // SSO认证中心 - 游乐园售票处
    class SSOAuthCenter {
    private Map<String, Token> validTokens = new HashMap<>();
    private Map<String, User> users = new HashMap<>();

    public SSOAuthCenter() {
    // 预先注册几个用户 - 办了年卡的游客
    users.put("zhangsan", new User("zhangsan", "123456"));
    users.put("lisi", new User("lisi", "abcdef"));
    }

    // 登录 - 买票入场
    public String login(String username, String password) {
    User user = users.get(username);
    if (user != null && user.getPassword().equals(password)) {
    Token token = new Token(username);
    validTokens.put(token.getTokenId(), token);
    System.out.println(username + " 登录成功!拿到游乐园手环:" + token.getTokenId());
    return token.getTokenId();
    }
    System.out.println("用户名或密码错误!请重新买票!");
    return null;
    }

    // 验证令牌 - 检查手环是否有效
    public boolean validateToken(String tokenId) {
    Token token = validTokens.get(tokenId);
    if (token != null && token.isValid()) {
    System.out.println("手环有效,欢迎继续玩耍!");
    return true;
    }
    System.out.println("手环无效或已过期,请重新登录!");
    validTokens.remove(tokenId); // 清理过期令牌
    return false;
    }

    // 登出 - 离开游乐园
    public void logout(String tokenId) {
    validTokens.remove(tokenId);
    System.out.println("已登出,欢迎下次再来玩!");
    }
    }

    // 业务系统A - 过山车
    class SystemA {
    private SSOAuthCenter authCenter;

    public SystemA(SSOAuthCenter authCenter) {
    this.authCenter = authCenter;
    }

    public void accessSystem(String tokenId) {
    System.out.println("=== 欢迎来到过山车 ===");
    if (authCenter.validateToken(tokenId)) {
    System.out.println("过山车启动!尖叫声在哪里!");
    } else {
    System.out.println("请先登录再玩过山车!");
    }
    }
    }

    // 业务系统B - 旋转木马
    class SystemB {
    private SSOAuthCenter authCenter;

    public SystemB(SSOAuthCenter authCenter) {
    this.authCenter = authCenter;
    }

    public void accessSystem(String tokenId) {
    System.out.println("=== 欢迎来到旋转木马 ===");
    if (authCenter.validateToken(tokenId)) {
    System.out.println("木马转起来啦!找回童年记忆!");
    } else {
    System.out.println("请先登录再玩旋转木马!");
    }
    }
    }

    // 测试我们的SSO系统
    public class SSODemo {
    public static void main(String[] args) {
    // 创建认证中心 - 游乐园大门
    SSOAuthCenter authCenter = new SSOAuthCenter();

    // 张三登录
    String token = authCenter.login("zhangsan", "123456");

    if (token != null) {
    // 拿着同一个令牌玩不同项目
    SystemA systemA = new SystemA(authCenter);
    SystemB systemB = new SystemB(authCenter);

    systemA.accessSystem(token); // 玩过山车
    systemB.accessSystem(token); // 玩旋转木马

    // 登出
    authCenter.logout(token);

    // 再尝试访问 - 应该被拒绝
    systemA.accessSystem(token);
    }

    // 测试错误密码
    authCenter.login("lisi", "wrongpassword");
    }
    }

    运行结果示例:


    zhangsan 登录成功!拿到游乐园手环:a1b2c3d4-e5f6-7890-abcd-ef1234567890
    === 欢迎来到过山车 ===
    手环有效,欢迎继续玩耍!
    过山车启动!尖叫声在哪里!
    === 欢迎来到旋转木马 ===
    手环有效,欢迎继续玩耍!
    木马转起来啦!找回童年记忆!
    已登出,欢迎下次再来玩!
    === 欢迎来到过山车 ===
    手环无效或已过期,请重新登录!
    请先登录再玩过山车!
    用户名或密码错误!请重新买票!

    总结一下:


    单点登录就像:



    • 一次认证,处处通行 🎫

    • 不用重复输入密码 🔑

    • 安全又方便 👍


    好的SSO系统就像好的游乐园管理,既要让游客玩得开心,又要确保安全!



单点登录:一次登录,全网通行.png


谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。


您的一键三连,是我更新的最大动力,谢谢


山水有相逢,来日皆可期,谢谢阅读,我们再会


我手中的金箍棒,上能通天,下能探海


作者:悟空码字
来源:juejin.cn/post/7577599015426228259
收起阅读 »

别让认知天花板,变成你的职业终点——技术人如何走出信息茧房

我们都活在自己编织的真相里吗? 最近参加了两位前端同事的转正答辩。他们的总结都很真诚,也很努力,但两人之间那种微妙的认知落差,却让我久久不能平静。 第一位同事踏实勤恳,技术中规中矩,对自己的评价也相对保守。他清楚自己的短板,也知道自己需要提升,但他把改变的希...
继续阅读 »

我们都活在自己编织的真相里吗?



最近参加了两位前端同事的转正答辩。他们的总结都很真诚,也很努力,但两人之间那种微妙的认知落差,却让我久久不能平静。


第一位同事踏实勤恳,技术中规中矩,对自己的评价也相对保守。他清楚自己的短板,也知道自己需要提升,但他把改变的希望寄托在别人身上——希望有人来指导他、推动他。可实际上,他并没有拿出具体的行动或方案。没有外力,他似乎就只能停在原地。看着他,我忽然想起几年前的自己:是不是也曾这样,一边焦虑,一边等待别人来拯救?


第二位同事则完全不同。他自信满满,言谈间流露出对自身能力的高度认可,甚至认为自己已经达到了“高级工程师”的水平。他在团队协作、沟通表达上也自认表现优异,列举了不少“高光时刻”。但当我站在更高的视角去看这些“成果”时,却感到一种强烈的错位——他的“高级”,建立在一个极其有限的认知框架之上。


他对跨团队协作的理解,停留在“配合顺畅”;对技术深度的衡量,止步于“功能能跑”。他看不到系统设计中的耦合隐患,意识不到架构演进背后的权衡逻辑,更缺乏对业务本质的追问。


那一刻我突然明白:一个人最深的局限,往往不是能力不足,而是根本不知道自己不知道


而更令人警觉的是,这种认知盲区,并非个例。它像一层透明的茧,包裹着我们每一个人。我们常称之为“信息茧房”,但或许更准确的说法是:认知茧房




被这个时代悄悄做局了



我们生活在一个信息爆炸的时代,但获取信息的方式,却被前所未有地“驯化”了。


算法精准推送你喜欢的内容,社交圈层不断强化你已有的观点,搜索引擎只呈现你愿意相信的答案。你读的每一篇文章、看的每一段视频、加入的每一个群聊,都在悄悄加固这层茧。


久而久之,我们开始相信:



  • 我看到的就是真实的世界;

  • 我认同的就是正确的道理;

  • 我周围人的共识就是普世的价值。


于是,偏执悄然滋生。我们变得难以倾听异见,习惯性地把不同声音归为“愚蠢”或“别有用心”。沟通不再是交换思想,而成了立场的对抗。我们活在由自己偏好构建的“回音室”里,每一次回响都让我们更加确信:我没错,错的是世界


那位自认“高级”的同事,问题不在技术本身,而在于他无法感知更高维度的存在。他没见过真正的系统复杂性,没经历过技术债务的反噬,也没体会过从0到1推动变革的艰难。他的“高级”,只是井底之蛙眼中的天空。




偏听则暗,兼听则明



信息茧房最危险的地方,在于它让人变得偏执。偏执的人很难沟通,再加上“傻子共振效应”(相似认知的人互相强化),整个世界的理解就越来越狭隘。


在这个被算法投喂的时代,你看到的,都是你想看的;你喜欢的,也被不断推给你。这不断强化你的认知,形成你的价值观,同时也压缩了你的视野、削弱了你的思辨能力,最终让你陷入一个自我闭环的逻辑牢笼


你开始以为自己了解世界的运作原理,甚至以为自己看清了本质。但最致命的是——你不觉得自己被局限了


我不禁反思:

我知道的,是不是错的?

我是不是也在自我麻痹?

我是不是一只井底之蛙,而我身边的人,也都是井底之蛙?我们彼此认同,形成联盟,却浑然不觉自己被困在同一个井里。


更可怕的是,这种“共识”会让我们坚信:我们不在茧房里。




睁眼看世界,看到的未必是真实


你看到的世界,可能是别人想让你看到的,也可能是你自己想让自己看到的。你可能并不知道真实的世界是什么样,但你以为你知道


如今的大数据和智能推荐,正不断按照你的喜好,一层层加固你的信息茧房。人类的惰性,又被海量信息投喂成“奶头乐”——不加思考,被动消费,逐渐沦为信息时代的消费品。


当然,我写下这些,也可能带着某种激进甚至偏激。但正因如此,才更要停下来问一问:

我是否也陷在这样的焦油坑里?


我是不是也自以为是、固步自封?

是不是只听自己想听的,只信自己愿意信的?

是不是被网络言论洗脑,被算法圈套套牢?


我常常做一些自以为不错的事,但别人可能完全不会那样做。这提醒我:我的“正确”,未必是世界的尺度


不要被无意义的信息重塑价值观。真正的出路,必须从内在出发。




认知决定你能活出怎样的人生



我们常说:“人赚不到认知以外的钱。”其实这句话可以更广义地理解为:人活不出认知以外的人生


你的决策、判断、人际关系、职业发展,甚至对幸福的定义,都被你当下的认知边界框定。如果你的认知是扁平的、片面的、情绪化的,那么无论多努力,你也只能在同一个维度里打转。


要破局,第一步是承认自己被困住了

这不是自我否定,而是一种清醒的自知。真正的成长,始于一个简单的念头:

“这样是不是太受限了?”


这个念头,就是打破茧房的第一道裂痕。




如何让裂痕扩大,直至破茧?



主动“反向输入”:让异见成为养分

不要只读你认同的书,只听你支持的声音。刻意去接触那些让你不适的观点:



  • 读一本政治立场与你对立的作者写的书;

  • 看一部挑战你价值观的纪录片;

  • 和一个你平时不会交往的人深入聊一次天。


重点不是说服对方,而是理解:他为什么这么想?


拓展“认知半径”:走出同类人的圈子

你周围的人,往往和你有相似的背景、价值观和信息来源。这种“同温层”会不断强化你的既有认知。试着去接触:



  • 不同行业的人(比如医生、教师、手艺人);

  • 不同年龄段的人(年轻人的焦虑,老人的智慧);

  • 不同文化背景的人(你会发现,很多你视为“理所当然”的事,在别处根本不存在)。


成年后,我们的圈子越来越小,思维越来越固化。这时候,更需要主动打破圈层的束缚。


3保持“空杯心态”:允许自己被推翻

最可怕的不是无知,而是以为自己知道。定期问自己:



  • 我三年前相信什么?现在还信吗?

  • 如果我现在的观点是错的,世界会是什么样子?

  • 有没有可能,我引以为傲的“成就”,其实只是低水平的重复?


这种自我质疑,不是自我贬低,而是一种精神上的“排毒”。




站在更高的维度看问题


当我开始带团队后,才发现下属的问题会暴露无遗。这也反过来提醒我:站得更高,才能看得更清


或许我们永远无法抵达“全知”的境界,但至少可以仰望星空。


写到这里,我也在警惕:

这会不会是另一种“认知优越感”?

我是否也在用“破茧”的叙事,构建一个新的茧?


很可能。

但关键不在于是否彻底摆脱茧房——那几乎不可能——而在于是否保有觉察和挣扎的意愿


我们无法看到全貌,但可以努力多转几个角度;

我们无法摆脱偏见,但可以学会与之共处并保持警惕;

我们可能永远都是井底之蛙,但至少可以抬头,看看那圈之外,是否还有星光。


或许,我们永远无法完全摆脱茧房



这个世界越来越复杂,而我们的认知工具却未必同步进化。

算法在固化我们,信息在淹没我们,社交在同化我们。


但人之所以为人,正是因为我们有反思的能力,有超越当下的渴望


不必追求绝对的“正确”,也不必幻想彻底的“觉醒”。

只需在每一个自以为是的瞬间,轻轻问一句:


“我是不是,又忘了抬头?”


认知的破局,不在远方,就在此刻的怀疑与开放之中


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

老板:能不能别手动复制路由了?我:写个脚本自动扫描

web
起因 周五快下班,老板过来看权限配置页面。 "这个每次都要手动输路径?" "对,现在是这样。"我打开给他看: 角色:运营专员 路由路径:[手动输入] /user/list 组件路径:[手动输入] @/views/user/List.vue "上次运营配错了,...
继续阅读 »

起因


周五快下班,老板过来看权限配置页面。


"这个每次都要手动输路径?"


"对,现在是这样。"我打开给他看:


角色:运营专员
路由路径:[手动输入] /user/list
组件路径:[手动输入] @/views/user/List.vue

"上次运营配错了,/user/list 写成 /user/lists,页面打不开找了半天。能不能做个下拉框,直接选?"


我想了想:"可以,但得先有个页面列表。"


"那就搞一个,现在页面这么多,手动输容易出错。"


确实,项目现在几十个页面,每次配置权限都要翻代码找路径,复制粘贴,还担心复制错。


解决办法


周末琢磨了一下,其实就是缺个"页面清单"。views 目录下都是页面文件,扫描一遍不就有了?


写了个 Node 脚本,自动扫描 views 目录,生成路由映射表。配置权限的时候下拉框选,还能搜索。


实现


两步:扫描文件 + 生成映射。


扫描 .vue 文件


function getAllVueFiles(dir, filesList = []) {
const files = fs.readdirSync(dir);

files.forEach((file) => {
const filePath = path.join(dir, file);

if (fs.statSync(filePath).isDirectory()) {
getAllVueFiles(filePath, filesList); // 递归子目录
} else if (file.endsWith(".vue")) {
filesList.push(filePath); // 收集 .vue 文件
}
});

return filesList;
}

生成映射文件


function start() {
const viewsDir = path.resolve(__dirname, "../views");
let files = getAllVueFiles(viewsDir);

// 兼容 Windows 路径
files = files.map(item => item.replace(/\\/g, "/"));

// 拼接成 import 映射
let str = "";
files.forEach(item => {
let n = item.replace(/.*src\//, "@/");
str += `"${n}":()=>import("${n}"),\r\n`;
});

// 写入文件
fs.writeFileSync(
path.resolve(__dirname, "../router/all.router.js"),
`export const ROUTERSDATA = {\n${str}}`
);
}

最后生成的文件大概是这样:


// src/router/all.router.js
export const ROUTERSDATA = {
"@/views/Home.vue": () => import("@/views/Home.vue"),
"@/views/About.vue": () => import("@/views/About.vue"),
"@/views/user/List.vue": () => import("@/views/user/List.vue"),
}

怎么用


权限配置页面


<template>
<el-select
v-model="selectedRoute"
filterable
placeholder="搜索并选择页面">
<el-option
v-for="(component, path) in ROUTERSDATA"
:key="path"
:label="path"
:value="path">
{{ path }}
</el-option>
</el-select>
</template>

<script setup>
import { ROUTERSDATA } from '@/router/all.router.js'

// 后台返回的权限路由配置
const permissionRoutes = [
{ path: '/user/list', component: '@/views/user/List.vue' },
{ path: '/order/list', component: '@/views/order/List.vue' }
]

// 直接从映射表取组件
const routes = permissionRoutes.map(route => ({
path: route.path,
component: ROUTERSDATA[route.component] // 这里直接用
}))
</script>

好处:



  • 下拉框自动包含所有页面

  • 支持搜索,输入 "user" 就能找到所有用户相关页面

  • 新加页面自动出现在列表里


动态路由


后台返回权限配置,前端从映射表取组件:


function generateRoutes(backendConfig) {
return backendConfig.map(item => ({
path: item.path,
component: ROUTERSDATA[item.component] // 直接用
}))
}

效果


周一把代码提上去,改了权限配置页面:


image.png


<!-- 配置页面 -->
<el-select v-model="route" filterable placeholder="搜索页面">
<el-option
v-for="(component, path) in ROUTERSDATA"
:key="path"
:label="path"
:value="path" />
</el-select>

老板过来试了一下,在下拉框输入 "user" 就搜到所有用户相关页面。


"嗯,这个好用。新加页面也会自动出现在这里吧?"


"对,每次启动项目会自动扫描。"


"行,那就这样。"


后来发现还有些意外收获:



  • 新人看这个映射表就知道项目有哪些页面

  • 后台只存路径字符串,数据库干净

  • 顺带解决了手动 import 几十个路由的问题


package.json 加个脚本:


{
"scripts": {
"dev": "node src/start/index.js && vite"
}
}

每次 npm run dev 会先扫描 views 目录,生成最新的映射表。


完整代码


// src/start/index.js
const fs = require("fs");
const path = require("path");

function getAllVueFiles(dir, filesList = []) {
const files = fs.readdirSync(dir);

files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);

if (stat.isDirectory()) {
getAllVueFiles(filePath, filesList);
} else if (file.endsWith(".vue")) {
filesList.push(filePath);
}
});

return filesList;
}

function start() {
console.log("[自动获取全部可显示页面]");

const viewsDir = path.resolve(__dirname, "../views");
let files = getAllVueFiles(viewsDir);

// 统一路径分隔符,兼容 Windows 反斜杠
files = files.map((item) => item.replace(/\\/g, "/"));

let str = "";
// 构造 import 映射:"@/views/xxx.vue": ()=>import("@/views/xxx.vue")
files.forEach((item) => {
let n = item.replace(/.*src\//, "@/");
str += `"${n}":()=>import("${n}"),\r\n`;
});

const routerFilePath = path.resolve(__dirname, "../router/all.router.js");
// 将映射写入路由聚合文件,供路由动态引用
fs.writeFileSync(
routerFilePath,
`
export const ROUTERSDATA = {
${str}
}`
,
);
console.log("[./src/router/all.router.js 写入]");
}

start();

注意事项


记得把生成的 src/router/all.router.js 加到 .gitignore,毕竟是自动生成的文件,没必要提交。


# .gitignore
src/router/all.router.js

后来


用了一个多月,运营配置权限再也没出过错。上周老板说:"这个功能不错,其他项目也加上。"


代码其实挺简单的,但确实解决了问题。


作者:码是生活
来源:juejin.cn/post/7582808491583504420
收起阅读 »

进入外包,我犯了所有程序员都会犯的错!

前言 前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。 本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。 1. 上岸折戟尘沙 本人男,安徽马鞍山人士,21年毕业于江苏某末流211,在校期间转码。 上网课期间就向往大城市,...
继续阅读 »

前言


前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。



image.png


本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。


1. 上岸折戟尘沙


本人男,安徽马鞍山人士,21年毕业于江苏某末流211,在校期间转码。

上网课期间就向往大城市,于是毕业后去了深圳,找到了一家中等IT公司(人数500+)搬砖,住着宝安城中村,来往繁华南山区。

待了三年多,自知买房变深户无望,没有归属感,感觉自己也没那么热爱技术,于是乎想回老家考公务员,希望待在宇宙的尽头。

24年末,匆忙备考,平时工作忙里偷闲刷题,不出所料,笔试卒,梦碎。


2. 误入外包


复盘了备考过程,觉得工作占用时间过多,想要找一份轻松点且离家近的工作,刚好公司也有大礼包的指标,于是主动申请,辞别深圳,前往徽京。

Boss上南京的软件大部分是外包(果然是外包之都),前几年外包还很活跃,这些年外包都沉寂了不少,找了好几个月,断断续续有几个邀约,最后实在没得选了,想着反正就过渡一下挣点钱不寒碜,接受了外包,作为WX服务某为。薪资比在深圳降了一些,在接受的范围内。


想着至少苟着等待下一次考公,因此前期做项目比较认真,遇到问题追根究底,为解决问题也主动加班加点,同为WX的同事都笑话我说比自有员工还卷,我却付之一笑。


直到我经历了几件事,正所谓人教人教不会,事教人一教就会。


3. 我在外包的二三事


有一次,我提出了自有员工设计方案的衍生出的一个问题,并提出拉个会讨论一下,他并没有当场答应,而是回复说:我们内部看看。

而后某天我突然被邀请进入会议,聊了几句,意犹未尽之际,突然就被踢出会议...开始还以为是某位同事误触按钮,然后再申请入会也没响应。

后来我才知道,他们内部商量核心方案,因为权限管控问题,我不能参会。

这是我第一次体会到WX和自有员工身份上的隔阂。


还有一次和自有员工一起吃饭的时候,他不小心说漏嘴了他的公积金,我默默推算了一下他的工资至少比我高了50%,而他的毕业院校、工作经验和我差不多,瞬间不平衡了。


还有诸如其它的团建、夜宵、办公权限、工牌等无一不是明示着你是外包员工,要在外包的规则内行事。
至于转正的事,头上还有OD呢,OD转正的几率都很低,好几座大山要爬呢,别想了。


3. 反求诸己


以前网上看到很多吐槽外包的帖子,还总觉得言过其实,亲身经历了才刻骨铭心。

我现在已经摆正了心态,既来之则安之。正视自己WX的身份,给多少钱干多少活,给多少权利就承担多少义务。

不攀比,不讨好,不较真,不内耗,不加班。

另外每次当面讨论的时候,我都会把工牌给露出来,潜台词就是:快看,我就是个外包,别为难我😔~


另外我现在比较担心的是:



万一我考公还是失败,继续找工作的话,这段外包经历会不会是我简历的污点😢



当然这可能是我个人感受,其它外包的体验我不知道,也不想再去体验了。

对,这辈子和下辈子都不想了。
附南京外包之光,想去或者不想去的伙伴可以留意一下:



image.png


作者:小鱼人爱编程
来源:juejin.cn/post/7511582195447824438
收起阅读 »

为什么 SVG 能在现代前端中胜出?

web
如果你关注前端图标的发展,会发现一个现象: 过去前端图标主要有三种方案: PNG 小图(配合雪碧图) Iconfont SVG 到了今天,大部分中大型项目都把图标系统全面迁移到 SVG。 无论 React/Vue 项目、新框架(Next/Remix/Nux...
继续阅读 »

如果你关注前端图标的发展,会发现一个现象:


过去前端图标主要有三种方案:



  • PNG 小图(配合雪碧图)

  • Iconfont

  • SVG


到了今天,大部分中大型项目都把图标系统全面迁移到 SVG。

无论 React/Vue 项目、新框架(Next/Remix/Nuxt),还是大厂的设计规范(Ant Design、Material、Carbon),基本都默认 SVG。


为什么是 SVG 胜出?

为什么不是 Iconfont、不是独立 PNG、不是雪碧图?

答案不是一句“清晰不失真”这么简单。


下面从前端实际开发的角度,把 SVG 胜出的原因讲透。




一、SVG 为什么比位图(PNG/JPG)更强?


矢量图永不失真(核心优势)


PNG/JPG 是位图,只能按像素存图。

移动端倍率屏越来越高(2x、3x、4x……),一张 24px 的 PNG 在 iPhone 高分屏里可能看起来糊成一团。


SVG 是矢量图,数学计算绘制:



  • 任意缩放不糊

  • 任意清晰度场景都不怕

  • 深色模式也不会变形


这点直接解决了前端图标领域长期存在的一个痛点:适配成本太高




体积小、多级复用不浪费


同样一个图标:



  • PNG 做 1x/2x/3x 需要三份资源

  • SVG 只要一份


而且:



  • SVG 本质是文本

  • gzip 压缩非常有效


在 CDN 下,通常能压到个位数 KB,轻松复用。




图标换色非常容易


PNG 改颜色很麻烦:



  • 设计师改

  • 重新导出

  • 重新上传/构建


Iconfont 的颜色只能统一,只能覆盖轮廓颜色,多色很麻烦。


SVG 则非常灵活:


.icon {
fill: currentColor;
}

可以跟随字体颜色变化,支持 hover、active、主题色。


深浅模式切换不需要任何额外资源。




支持 CSS 动画、交互效果


SVG 不只是图标文件,它是 DOM,可以直接加动画:



  • stroke 动画

  • 路径绘制动画

  • 颜色渐变

  • hover 发光

  • 多段路径动态控制


PNG 和 Iconfont 都做不到这种级别的交互。


很多现代 UI 的微动效(Loading、赞、收藏),都是基于 SVG 完成。




二、SVG 为什么比 iconfont 更强?


Iconfont 在 2015~2019 年非常火,但明显已经退潮了。

原因有以下几个:




① 字体图标本质是“字符”而不是图形


这带来大量问题:


● 不能多色


只能 monochrome,彩色图标很难实现。


● 渲染脆弱


在 Windows 某些字体渲染环境下会出现:



  • 发虚

  • 锯齿

  • baseline 不一致


● 字符冲突


不同项目的字体图标可能互相覆盖。


相比之下,SVG 是独立图形文件,没有这些问题。




② iconfont 需要加载字体文件,失败会出现“乱码方块”


如果字体文件没加载成功,你会看到:



☐ ☐ ☐ ☐



这在弱网、支付类页面、海外环境都非常常见。


SVG 就没有这个风险。




③ iconfont 不利于按需加载


字体文件通常包含几十甚至几百个图标:

一次加载很重,不够精细。


SVG 可以做到按需加载:



  • 一个组件一个 SVG

  • 一个页面只引入用到的部分

  • 可组合、可动态切换


对于现代构建体系非常友好。




三、SVG 为什么比“新版雪碧图”更强?


即便抛开 iconfont,PNG 雪碧图也完全被淘汰。


原因很简单:



  • 雪碧图文件大

  • 缓存粒度差

  • 不可按需加载

  • 维护复杂

  • retina 适配麻烦

  • 颜色不可动态变更


而 SVG 天生具备现代开发所需的一切特性:



  • 轻量化

  • 组件化

  • 可变色

  • 可动画

  • 可 inline

  • 可自动 tree-shaking


雪碧图本质上是为了“减少请求数”而生的产物,

但在 HTTP/2/3 中已经没有价值。


而 SVG 不是 hack,而是自然适配现代 Web 的技术方案




四、SVG 为什么能在工程体系里更好地落地?


现代构建工具(Vite / Webpack / Rollup)原生支持 SVG:



  • 转组件

  • 优化路径

  • 压缩

  • 自动雪碧(symbol sprite)

  • Tree-shaking

  • 资源分包


这让 SVG 完全融入工程体系,而不是外挂方案。


例如:


import Logo from './logo.svg'

你可以:



  • 当组件使用

  • 当资源下载

  • 当背景图

  • 动态注入


工程化友好度是它胜出的关键原因之一。




五、SVG 胜出的根本原因总结


不是 SVG “长得好看”,也不是趋势,是整个现代前端生态把它推到了最合适的位置。


1)协议升级:HTTP/2/3 让雪碧图和 Iconfont 的优势全部消失

2)设备升级:高分屏让位图模糊问题暴露得更明显

3)工程升级:组件化开发需要精细化图标

4)体验升级:动画、主题、交互都离不开 SVG


一句话总结:



SVG 不只是“更清晰”,而是从工程到体验全面适配现代前端的图标方案,因此胜出。



作者:吹水一流
来源:juejin.cn/post/7577691061034172462
收起阅读 »

软件工程师必须要掌握的泳道图

作者:面汤放盐 / uzong 在软件开发的世界里,我们习惯用代码表达逻辑,但当系统涉及多个角色、多个服务、甚至跨团队协作时,光靠代码注释或口头沟通,往往不够。这时候,一张清晰的流程图,胜过千行文档。 泳道图 :它可能不像 UML 那样“高大上”,也不如架构...
继续阅读 »

作者:面汤放盐 / uzong



在软件开发的世界里,我们习惯用代码表达逻辑,但当系统涉及多个角色、多个服务、甚至跨团队协作时,光靠代码注释或口头沟通,往往不够。这时候,一张清晰的流程图,胜过千行文档。


泳道图 :它可能不像 UML 那样“高大上”,也不如架构图那样宏观,但在梳理业务流程、厘清责任边界、排查系统瓶颈时,它真的非常实用。


1. 什么是泳道图


泳道图的核心思想很简单:把流程中的每个步骤,按执行者(人、系统、模块)分组排列,就像游泳池里的泳道一样,各走各道,互不干扰又彼此关联。



每一列就是一个“泳道”,代表一个责任主体。流程从左到右、从上到下流动,谁在什么时候做了什么,一目了然




一眼就能看出:谁干了什么,谁依赖谁,边界是什么。


与流程的差异点:



  • 流程图聚焦:“步骤顺序”,侧重 “先做什么、再做什么”,适合梳理线性业务流程;

  • 泳道图聚焦: “流转通道”,侧重 “什么东西在什么约束下通过什么路径流转”,适合拆解复杂、多路径、有规则约束的流转场景(如分布式系统数据同步、供应链物料流转、微服务请求链路等)


2. 泳道图分类


2.1. 垂直泳道图


垂直泳道图采取上下布局结构,‌主要强调职能群体。这种布局方式更适合于展示跨职能任务和流程中,‌各职能部门或角色之间的垂直关系和职能分工。



2.2. 水平泳道图


水平泳道图则采用左右布局结构,‌重点在于事件进程的展示。这种布局方式更适合于强调事件或过程的水平流动,‌以及不同阶段或部门在流程中的水平参与



3. 泳道图组成元素


泳池: 泳池是泳道图的外部框架,泳道、流程都包含于泳池内。


泳道: 泳池里可以创建多个泳道。


流程: 实际的业务流程。


部门: 通过部门或者责任来区分,明确每个部门/人/信息系统负责完成的任务环节。


阶段: 通过任务阶段来区分,明确每个阶段需要处理的任务环节。


4. 泳道图应用场景


4.1. 项目管理


展示项目从启动到完成的各个阶段,明确每个团队或成员在项目中的角色和职责,便于进行项目管理和监控,同时促进团队协作和沟通



4.2. 业务流程分析


展示业务流程的各个环节和涉及的不同部门或职能。通过分析泳道图,可以发现业务流程中的瓶颈、冗余环节或不合理之处,进而进行流程优化和改进。



4.3. 系统设计


展示系统的整体架构和各个组件之间的关系,描述系统的工作流程,包括数据的输入、处理、输出等各个环节,有助于系统开发人员更好地理解系统的功能和需求



5. 更多参考模板


故障处理多维泳道图



资源扩容泳道图



6. 最后


我刚工作时,看到导师抛出一份精致的泳道图,把一团乱麻的问题讲得透亮,心里特别佩服。直到自己多年后用上才真正体会:在面对跨部门协作、复杂故障排查、关键流程设计时,掏出这么一张图,往往就是高效沟通的开始。


作为开发者,我们常陷入“只要代码跑得通就行”的思维惯性。但软件不仅是机器执行的指令,更是人与人协作的媒介。泳道图这样的工具,本质上是在降低认知成本——让复杂的事情变得可沟通、可验证、可迭代。 其实泳道图的核心不是“画图”,而是“梳理流程、明确权责”。


在跨部门、协同需求、故障分析等关键场景使用泳道图是非常合适,并且也能把问题讲清楚。技术世界充满了抽象和复杂性,而优秀工程师的能力之一,就是创建合适的可视化工具,让复杂问题变得简单可见



本文中的大部分图片来源于 ProcessOn,ProcessOn 是一个非常不错的画图软件,功能强大,界面优美。



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

我发现很多程序员都不会打日志。。。

你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。 没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了! 你赶紧打开服务器,看着比你发量都少的报错信息: 你一脸懵逼:只有这点儿信息,我咋知道哪里...
继续阅读 »

你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。


没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了!


你赶紧打开服务器,看着比你发量都少的报错信息:



你一脸懵逼:只有这点儿信息,我咋知道哪里出了问题啊?!


你只能硬着头皮让产品经理找用户要数据,然后一条条测试,看看是哪条数据出了问题……


原本大好的摸鱼时光,就这样无了。


这时,你的导师鱼皮走了过来,问道:小阿巴,你是持矢了么?脸色这么难看?



你无奈地说:皮哥,刚才线上出了个 bug,我花了 8 个小时才定位到问题……


鱼皮皱了皱眉:这么久?你没打日志吗?


你很是疑惑:谁是日志?为什么要打它?



鱼皮叹了口气:唉,难怪你要花这么久…… 来,我教你打日志!


⭐️ 本文对应视频版:bilibili.com/video/BV1K7…


什么是日志?


鱼皮打开电脑,给你看了一段代码:


@Slf4j
public class UserService {
   public void batchImport(List<UserDTO> userList) {
       log.info("开始批量导入用户,总数:{}", userList.size());
       
       int successCount = 0;
       int failCount = 0;
       
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在导入用户:{}", userDTO.getUsername());
               validateUser(userDTO);
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 导入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       
       log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
  }
}

你看着代码里的 log.infolog.error,疑惑地问:这些 log 是干什么的?


鱼皮:这就是打日志。日志用来记录程序运行时的状态和信息,这样当系统出现问题时,我们可以通过日志快速定位问题。



你若有所思:哦?还可以这样!如果当初我的代码里有这些日志,一眼就定位到问题了…… 那我应该怎么打日志?用什么技术呢?


怎么打日志?


鱼皮:每种编程语言都有很多日志框架和工具库,比如 Java 可以选用 Log4j 2、Logback 等等。咱们公司用的是 Spring Boot,它默认集成了 Logback 日志框架,你直接用就行,不用再引入额外的库了~



日志框架的使用非常简单,先获取到 Logger 日志对象。


1)方法 1:通过 LoggerFactory 手动获取 Logger 日志对象:


public class MyService {
   private static final Logger logger = LoggerFactory.getLogger(MyService.class);
}

2)方法 2:使用 this.getClass 获取当前类的类型,来创建 Logger 对象:


public class MyService {
   private final Logger logger = LoggerFactory.getLogger(this.getClass());
}

然后调用 logger.xxx(比如 logger.info)就能输出日志了。


public class MyService {
   private final Logger logger = LoggerFactory.getLogger(this.getClass());

   public void doSomething() {
       logger.info("执行了一些操作");
  }
}

效果如图:



小阿巴:啊,每个需要打日志的类都要加上这行代码么?


鱼皮:还有更简单的方式,使用 Lombok 工具库提供的 @Slf4j 注解,可以自动为当前类生成日志对象,不用手动定义啦。


@Slf4j
public class MyService {
   public void doSomething() {
       log.info("执行了一些操作");
  }
}

上面的代码等同于 “自动为当前类生成日志对象”:


private static final org.slf4j.Logger log = 
   org.slf4j.LoggerFactory.getLogger(MyService.class);

你咧嘴一笑:这个好,爽爽爽!



等等,不对,我直接用 Java 自带的 System.out.println 不也能输出信息么?何必多此一举?


System.out.println("开始导入用户" + user.getUsername());

鱼皮摇了摇头:千万别这么干!


首先,System.out.println 是一个同步方法,每次调用都会导致耗时的 I/O 操作,频繁调用会影响程序的性能。



而且它只能输出信息到控制台,不能灵活控制输出位置、输出格式、输出时机等等。比如你现在想看三天前的日志,System.out.println 的输出早就被刷没了,你还得浪费时间找半天。



你恍然大悟:原来如此!那使用日志框架就能解决这些问题吗?


鱼皮点点头:没错,日志框架提供了丰富的打日志方法,还可以通过修改日志配置文件来随心所欲地调教日志,比如把日志同时输出到控制台和文件中、设置日志格式、控制日志级别等等。



在下苦心研究日志多年,沉淀了打日志的 8 大邪修秘法,先传授你 2 招最基础的吧。


打日志的 8 大最佳实践


1、合理选择日志级别


第一招,日志分级。


你好奇道:日志还有级别?苹果日志、安卓日志?


鱼皮给了你一巴掌:可不要乱说,日志的级别是按照重要程度进行划分的。



其中 DEBUG、INFO、WARN 和 ERROR 用的最多。



  • 调试用的详细信息用 DEBUG

  • 正常的业务流程用 INFO

  • 可能有问题但不影响主流程的用 WARN

  • 出现异常或错误的用 ERROR


log.debug("用户对象的详细信息:{}", userDTO);  // 调试信息
log.info("用户 {} 开始导入", username);  // 正常流程信息
log.warn("用户 {} 的邮箱格式可疑,但仍然导入", username);  // 警告信息
log.error("用户 {} 导入失败", username, e);  // 错误信息

你挠了挠头:俺直接全用 DEBUG 不行么?


鱼皮摇了摇头:如果所有信息都用同一级别,那出了问题时,你怎么快速找到错误信息?



在生产环境,我们通常会把日志级别调高(比如 INFO 或 WARN),这样 DEBUG 级别的日志就不会输出了,防止重要信息被无用日志淹没。



你点点头:俺明白了,不同的场景用不同的级别!


2、正确记录日志信息


鱼皮:没错,下面教你第二招。你注意到我刚才写的日志里有一对大括号 {} 吗?


log.info("用户 {} 开始导入", username);

你回忆了一下:对哦,那是啥啊?


鱼皮:这叫参数化日志。{} 是一个占位符,日志框架会在运行时自动把后面的参数值替换进去。


你挠了挠头:我直接用字符串拼接不行吗?


log.info("用户 " + username + " 开始导入");

鱼皮摇摇头:不推荐。因为字符串拼接是在调用 log 方法之前就执行的,即使这条日志最终不被输出,字符串拼接操作还是会执行,白白浪费性能。



你点点头:确实,而且参数化日志比字符串拼接看起来舒服~



鱼皮:没错。而且当你要输出异常信息时,也可以使用参数化日志:


try {
   // 业务逻辑
catch (Exception e) {
   log.error("用户 {} 导入失败", username, e);  // 注意这个 e
}

这样日志框架会同时记录上下文信息和完整的异常堆栈信息,便于排查问题。



你抱拳:学会了,我这就去打日志!


3、把控时机和内容


很快,你给批量导入程序的代码加上了日志:


@Slf4j
public class UserService {
   public BatchImportResult batchImport(List<UserDTO> userList) {
       log.info("开始批量导入用户,总数:{}", userList.size());
       int successCount = 0;
       int failCount = 0;
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在导入用户:{}", userDTO.getUsername());   
               // 校验用户名
               if (StringUtils.isBlank(userDTO.getUsername())) {
                   throw new BusinessException("用户名不能为空");
              }
               // 保存用户
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 导入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
       return new BatchImportResult(successCount, failCount);
  }
}

光做这点还不够,你还翻出了之前的屎山代码,想给每个文件都打打日志。



但打着打着,你就不耐烦了:每段代码都要打日志,好累啊!但是不打日志又怕出问题,怎么办才好?


鱼皮笑道:好问题,这就是我要教你的第三招 —— 把握打日志的时机。


对于重要的业务功能,我建议采用防御性编程,先多多打日志。比如在方法代码的入口和出口记录参数和返回值、在每个关键步骤记录执行状态,而不是等出了问题无法排查的时候才追悔莫及。之后可以再慢慢移除掉不需要的日志。



你叹了口气:这我知道,但每个方法都打日志,工作量太大,都影响我摸鱼了!


鱼皮:别担心,你可以利用 AOP 切面编程,自动给每个业务方法的执行前后添加日志,这样就不会错过任何一次调用信息了。



你双眼放光:这个好,爽爽爽!



鱼皮:不过这样做也有一个缺点,注意不要在日志中记录了敏感信息,比如用户密码。万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息。



你拍拍胸脯:必须的!


4、控制日志输出量


一个星期后,产品经理又来找你了:小阿巴,你的批量导入功能又报错啦!而且怎么感觉程序变慢了?


你完全不慌,淡定地打开服务器的日志文件。结果瞬间呆住了……


好家伙,满屏都是密密麻麻的日志,这可怎么看啊?!



鱼皮看了看你的代码,摇了摇头:你现在每导入一条数据都要打一些日志,如果用户导入 10 万条数据,那就是几十万条日志!不仅刷屏,还会影响性能。


你有点委屈:不是你让我多打日志的么?那我应该怎么办?


鱼皮:你需要控制日志的输出量。


1)可以添加条件来控制,比如每处理 100 条数据时才记录一次:


if ((i + 1% 100 == 0) {
   log.info("批量导入进度:{}/{}"i + 1, userList.size());
}

2)或者在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出:


StringBuilder logBuilder = new StringBuilder("处理结果:");
for (UserDTO userDTO : userList) {
   processUser(userDTO);
   logBuilder.append(String.format("成功[ID=%s], ", userDTO.getId()));
}
log.info(logBuilder.toString());

3)还可以通过修改日志配置文件,过滤掉特定级别的日志,防止日志刷屏:


<appender name="FILE" class="ch.qos.logback.core.FileAppender">
   <file>logs/app.log</file>
   <!-- 只允许 INFO 级别及以上的日志通过 -->
   <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
       <level>INFO</level>
   </filter>
</appender>

5、统一日志格式


你开心了:好耶,这样就不会刷屏了!但是感觉有时候日志很杂很乱,尤其是我想看某一个请求相关的日志时,总是被其他的日志干扰,怎么办?


鱼皮:好问题,可以在日志配置文件中定义统一的日志格式,包含时间戳、线程名称、日志级别、类名、方法名、具体内容等关键信息。


<!-- 控制台日志输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
   <encoder>
       <!-- 日志格式 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
   </encoder>
</appender>

这样输出的日志更整齐易读:



此外,你还可以通过 MDC(Mapped Diagnostic Context)给日志添加额外的上下文信息,比如请求 ID、用户 ID 等,方便追踪。



在 Java 代码中,可以为 MDC 设置属性值:


@PostMapping("/user/import")
public Result importUsers(@RequestBody UserImportRequest request) {
   // 1. 设置 MDC 上下文信息
   MDC.put("requestId"generateRequestId());
   MDC.put("userId", String.valueOf(request.getUserId()));
   try {
       log.info("用户请求处理完成");      
       // 执行具体业务逻辑
       userService.batchImport(request.getUserList());     
       return Result.success();
  } finally {
       // 2. 及时清理MDC(重要!)
       MDC.clear();
  }
}

然后在日志配置文件中就可以使用这些值了:


<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
   <encoder>
       <!-- 包含 MDC 信息 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
   </encoder>
</appender>

这样,每个请求、每个用户的操作一目了然。



6、使用异步日志


你又开心了:这样打出来的日志,确实舒服,爽爽爽!但是我打日志越多,是不是程序就会更慢呢?有没有办法能优化一下?


鱼皮:当然有,可以使用 异步日志


正常情况下,你调用 log.info() 打日志时,程序会立刻把日志写入文件,这个过程是同步的,会阻塞当前线程。而异步日志会把写日志的操作放到另一个线程里去做,不会阻塞主线程,性能更好。


你眼睛一亮:这么厉害?怎么开启?


鱼皮:很简单,只需要修改一下配置文件:


<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
   <queueSize>512</queueSize>  <!-- 队列大小 -->
   <discardingThreshold>0</discardingThreshold>  <!-- 丢弃阈值,0 表示不丢弃 -->
   <neverBlock>false</neverBlock>  <!-- 队列满时是否阻塞,false 表示会阻塞 -->
   <appender-ref ref="FILE" />  <!-- 引用实际的日志输出目标 -->
</appender>
<root level="INFO">
   <appender-ref ref="ASYNC" />
</root>

不过异步日志也有缺点,如果程序突然崩溃,缓冲区中还没来得及写入文件的日志可能会丢失。



所以要权衡一下,看你的系统更注重性能还是日志的完整性。


你想了想:我们的程序对性能要求比较高,偶尔丢几条日志问题不大,那我就用异步日志吧。


7、日志管理


接下来的很长一段时间,你混的很舒服,有 Bug 都能很快发现。


你甚至觉得 Bug 太少、工作没什么激情,所以没事儿就跟新来的实习生阿坤吹吹牛皮:你知道日志么?我可会打它了!



直到有一天,运维小哥突然跑过来:阿巴阿巴,服务器挂了!你快去看看!


你连忙登录服务器,发现服务器的硬盘爆满了,没法写入新数据。


你查了一下,发现日志文件竟然占了 200GB 的空间!



你汗流浃背了,正在考虑怎么甩锅,结果阿坤突然鸡叫起来:阿巴 giegie,你的日志文件是不是从来没清理过?


你尴尬地倒了个立,这样眼泪就不会留下来。



鱼皮叹了口气:这就是我要教你的下一招 —— 日志管理。


你好奇道:怎么管理?我每天登服务器删掉一些历史文件?


鱼皮:人工操作也太麻烦了,我们可以通过修改日志配置文件,让框架帮忙管理日志。


首先设置日志的滚动策略,可以根据文件大小和日期,自动对日志文件进行切分。


<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
   <maxFileSize>10MB</maxFileSize>
   <maxHistory>30</maxHistory>
</rollingPolicy>

这样配置后,每天会创建一个新的日志文件(比如 app-2025-10-23.0.log),如果日志文件大小超过 10MB 就再创建一个(比如 app-2025-10-23.1.log),并且只保留最近 30 天的日志。



还可以开启日志压缩功能,进一步节省磁盘空间:


<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   <!-- .gz 后缀会自动压缩 -->
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>


你有些激动:吼吼,这样我们就可以按照天数更快地查看日志,服务器硬盘也有救啦!


8、集成日志收集系统


两年后,你负责的项目已经发展成了一个大型的分布式系统,有好几十个微服务。


如今,每次排查问题你都要登录到不同的服务器上查看日志,非常麻烦。而且有些请求的调用链路很长,你得登录好几台服务器、看好几个服务的日志,才能追踪到一个请求的完整调用过程。



你简直要疯了!


于是你找到鱼皮求助:现在查日志太麻烦了,当年你还有一招没有教我,现在是不是……


鱼皮点点头:嗯,对于分布式系统,就必须要用专业的日志收集系统了,比如很流行的 ELK。


你好奇:ELK 是啥?伊拉克?


阿坤抢答道:我知道,就是 Elasticsearch + Logstash + Kibana 这套组合。


简单来说,Logstash 负责收集各个服务的日志,然后发送给 Elasticsearch 存储和索引,最后通过 Kibana 提供一个可视化的界面。



这样一来,我们可以方便地集中搜索、查看、分析日志。



你惊讶了:原来日志还能这么玩,以后我所有的项目都要用 ELK!


鱼皮摆摆手:不过 ELK 的搭建和运维成本比较高,对于小团队来说可能有点重,还是要按需采用啊。


结局


至此,你已经掌握了打日志的核心秘法。



只是你很疑惑,为何那阿坤竟对日志系统如此熟悉?


阿坤苦笑道:我本来就是日志管理大师,可惜我上家公司的同事从来不打日志,所以我把他们暴打了一顿后跑路了。


阿巴 giegie 你要记住,日志不是写给机器看的,是写给未来的你和你的队友看的!


你要是以后不打日志,我就打你!


更多


💻 编程学习交流:编程导航 📃 简历快速制作:老鱼简历 ✏️ 面试刷题神器:面试鸭


作者:程序员鱼皮
来源:juejin.cn/post/7569159131819753510
收起阅读 »

分库分表正在被淘汰

前言 “分库分表这种架构模式会逐步的被淘汰!” 不知道在哪儿看到的观点 如果我们现在在搭建新的业务架构,如果说你们未来的业务数据量会达到千万 或者上亿的级别 还在一股脑的使用分库分表的架构,那么你们的技术负责人真的就应该提前退休了🙈 如果对未来的业务非常有...
继续阅读 »

前言



“分库分表这种架构模式会逐步的被淘汰!” 不知道在哪儿看到的观点



如果我们现在在搭建新的业务架构,如果说你们未来的业务数据量会达到千万 或者上亿的级别 还在一股脑的使用分库分表的架构,那么你们的技术负责人真的就应该提前退休了🙈


如果对未来的业务非常有信心,单表的数据量能达到千万上亿的级别,请使用NewSQL 数据库,那么NewSQL 这么牛,分布库分表还有意义吗?



今天虽然写的是一篇博客,但是更多的是抱着和大家讨论的心态来的,所以大家目前有深度参与分库分表,或者NewSQL 的都可以在评论区讨论!



什么是NewSQL


NewSQL 是21世纪10年代初出现的一个术语,用来描述一类新型的关系型数据库管理系统(RDBMS)。它们的共同目标是:在保持传统关系型数据库(如Oracle、MySQL)的ACID事务和SQL模型优势的同时,获得与NoSQL系统类似的、弹性的水平扩展能力


NewSQL 的核心理念就是 将“分库分表”的复杂性从应用层下沉到数据库内核层,对上层应用呈现为一个单一的数据库入口,解决现在 分库分表的问题;


分库分表的问题


分库分表之后,会带来非常多的问题;比如需要跨库联查、跨库更新数据如何保证事务一致性等问题,下面就来详细看看分库分表都有那些问题



  1. 数据库的操作变得复杂



    • 跨库 JOIN 几乎不可行:原本简单的多表关联查询,因为表被分散到不同库甚至不同机器上,变得异常困难。通常需要拆成多次查询,在应用层进行数据组装,代码复杂且性能低下。

    • 聚合查询效率低下COUNT()SUM()GR0UP BYORDER BY 等操作无法在数据库层面直接完成。需要在每个分片上执行,然后再进行合并。

    • 分页问题LIMIT 20, 10 这样的分页查询会变得非常诡异。你需要从所有分片中获取前30条数据,然后在应用层排序后取第20-30条。页码越大,性能越差。



  2. 设计上需要注意的问题



    • 分片键(Sharding Key)的选择:如果前期没有设计好,后期数据倾斜比较严重

    • 全局唯一ID需要提前统一设计,规范下来

    • 分布式事务问题,需要考虑使用哪种方式去实现(XA协议,柔性事务)




选择TiDB还是采用mysql 分库分表的设计


数据量非常大,需要满足OLTP (Online Transactional Processing)OLAP (Online Analytical Processing)HTAP预算充足(分布式数据库的成本也是非常高的这一点非常的重要),并且是新业务新架构落地 优先推荐使用TiDB
当然实际上选择肯定是需要多方面考虑的,大家有什么观点都可以在评论区讨论。


可以看看一个资深开发,深度参与TiDB项目,他对TiDB的一些看法:


3150c08d-9372-41aa-9cf4-7aafbea0c149.png


efe93ca3-12ef-47fe-aab4-16e191894a01.png


f3e3a4a7-c0f1-47bf-b524-e9863458cff0.png


1 什么是TiDB?


TiDB是PingCAP公司研发的开源分布式关系型数据库,采用存储计算分离架构,支持混合事务分析处理(HTAP) 。它与MySQL 5.7协议兼容,并支持MySQL生态,这意味着使用MySQL的应用程序可以几乎无需修改代码就能迁移到TiDB。


🚀目标是为用户提供一站式 OLTP (Online Transactional Processing)、OLAP (Online Analytical Processing)、HTAP 解决方案。TiDB 适合高可用、强一致要求较高、数据规模较大等各种应用场景。



官方文档:docs.pingcap.com/zh/tidb/dev…



TiDB五大核心特性


TiDB之所以在分布式数据库领域脱颖而出,得益于其五大核心特性



  • 一键水平扩容或缩容:得益于存储计算分离的架构设计,可按需对计算、存储分别进行在线扩容或缩容,整个过程对应用透明。

  • 金融级高可用:数据采用多副本存储,通过Multi-Raft协议同步事务日志,只有多数派写入成功事务才能提交,确保数据强一致性。

  • 实时HTAP:提供行存储引擎TiKV和列存储引擎TiFlash,两者之间的数据保持强一致,解决了HTAP资源隔离问题。

  • 云原生分布式数据库:通过TiDB Operator可在公有云、私有云、混合云中实现部署工具化、自动化。

  • 兼容MySQL 5.7协议和生态:从MySQL迁移到TiDB无需或只需少量代码修改,极大降低了迁移成本。


2 TiDB与MySQL的核心差异


虽然TiDB兼容MySQL协议,但它们在架构设计和适用场景上存在根本差异。以下是它们的详细对比:


2.1 架构差异


表1:TiDB与MySQL架构对比


特性MySQLTiDB
架构模式集中式架构分布式架构
扩展性垂直扩展,主从复制水平扩展,存储计算分离
数据分片需要分库分表自动分片,无需sharding key
高可用机制主从复制、MGRMulti-Raft协议,多副本
存储引擎InnoDB、MyISAM等TiKV(行存)、TiFlash(列存)

2.2 性能表现对比


性能方面,TiDB与MySQL各有优势,主要取决于数据量和查询类型:



  • 小数据量简单查询:在数据量百万级以下的情况下,MySQL的写入性能和点查点写通常优于TiDB。因为TiDB的分布式架构在少量数据时无法充分发挥优势,却要承担分布式事务的开销。

  • 大数据量复杂查询:当数据量达到千万级以上,TiDB的性能优势开始显现。一张千万级别表关联查询,MySQL可能需要20秒,而TiDB+TiKV只需约5.57秒,使用TiFlash甚至可缩短到0.5秒。

  • 高并发场景:MySQL性能随着并发增加会达到瓶颈然后下降,而TiDB性能基本随并发增加呈线性提升,节点资源不足时还可通过动态扩容提升性能。


2.3 扩展性与高可用对比


MySQL的主要扩展方式是一主多从架构,主节点无法横向扩展(除非接受分库分表),从节点扩容需要应用支持读写分离。而TiDB的存储和计算节点都可以独立扩容,支持最大512节点,集群容量可达PB级别。


高可用方面,MySQL使用增强半同步和MGR方案,但复制效率较低,主节点故障会影响业务处理[]。TiDB则通过Raft协议将数据打散分布,单机故障对集群影响小,能保证RTO(恢复时间目标)不超过30秒且RPO(恢复点目标)为0,真正实现金融级高可用。


2.4 SQL功能及兼容性


虽然TiDB高度兼容MySQL 5.7协议和生态,但仍有一些重要差异需要注意:


不支持的功能包括:



  • 存储过程与函数

  • 触发器

  • 事件

  • 自定义函数

  • 全文索引(计划中)

  • 空间类型函数和索引


有差异的功能包括:



  • 自增ID的行为(TiDB推荐使用AUTO_RANDOM避免热点问题)

  • 查询计划的解释结果

  • 在线DDL能力(TiDB更强,不锁表支持DML并行操作)


3 如何选择:TiDB还是MySQL?


选择数据库时,应基于实际业务需求和技术要求做出决策。以下是具体的选型建议:


3.1 选择TiDB的场景


TiDB在以下场景中表现卓越:



  1. 数据量大且增长迅速的OLTP场景:当单机MySQL容量或性能遇到瓶颈,且数据量达到TB级别时,TiDB的水平扩展能力能有效解决问题。

    例如,当业务数据量预计将超过TB级别,或并发连接数超过MySQL合理处理范围时。

  2. 实时HTAP需求:需要同时进行在线事务处理和实时数据分析的场景。

    传统方案需要OLTP数据库+OLAP数据库+ETL工具,TiDB的HTAP能力可简化架构,降低成本和维护复杂度。

  3. 金融级高可用要求:对系统可用性和数据一致性要求极高的金融行业场景。

    TiDB的多副本和自动故障转移机制能确保业务连续性和数据安全。

  4. 多业务融合平台:需要将多个业务数据库整合的统一平台场景。

    TiDB的资源管控能力可以按照RU(Request Unit)大小控制资源总量,实现多业务资源隔离和错峰利用。

  5. 频繁的DDL操作需求:需要频繁进行表结构变更的业务。

    TiDB的在线DDL能力在业务高峰期也能平稳执行,对大表结构变更尤其有效。


3.2 选择MySQL的场景


MySQL在以下情况下仍是更合适的选择:



  1. 中小规模数据量:数据量在百万级以下,且未来增长可预测。

    在这种情况下,MySQL的性能可能更优,且总拥有成本更低。

  2. 简单读写操作为主:业务以点查点写为主,没有复杂的联表查询或分析需求。

  3. 需要特定MySQL功能:业务依赖存储过程、触发器、全文索引等TiDB不支持的功能。

  4. 资源受限环境:硬件资源有限且没有分布式数据库管理经验的团队。

    MySQL的运维管理相对简单,学习曲线较平缓。


3.3 决策参考框架


为了更直观地帮助决策,可以参考以下决策表:


考虑因素倾向TiDB倾向MySQL
数据规模TB级别或预计快速增长GB级别,增长稳定
并发需求高并发(数千连接以上)低至中等并发
查询类型复杂SQL,多表关联简单点查点写
可用性要求金融级(RTO<30s,RPO=0)常规可用性要求
架构演进微服务、云原生、HTAP传统单体应用
运维能力有分布式系统管理经验传统DBA团队

4 迁移注意事项


如果决定从MySQL迁移到TiDB,需要注意以下关键点:



  1. 功能兼容性验证:检查应用中是否使用了TiDB不支持的MySQL功能,如存储过程、触发器等。

  2. 自增ID处理:将AUTO_INCREMENT改为AUTO_RANDOM以避免写热点问题。

  3. 事务大小控制:注意TiDB对单个事务的大小限制(早期版本限制较严,4.0版本已提升到10GB)。

  4. 迁移工具选择:使用TiDB官方工具如DM(Data Migration)进行数据迁移和同步。

  5. 性能测试:迁移前务必进行充分的性能测试,特别是针对业务关键查询的测试。


5 总结


TiDB和MySQL是适用于不同场景的数据库解决方案,没有绝对的优劣之分。MySQL是优秀的单机数据库,适用于数据量小、架构简单的场景;数据量大了之后需要做分库分表。而TiDB作为分布式数据库,专注于解决大数据量、高并发、高可用性需求下的数据库瓶颈问题,但是成本也是非常的高



本人没有使用过NewSQL ,还望各位大佬批评指正



作者:提前退休的java猿
来源:juejin.cn/post/7561245020045918249
收起阅读 »

告别终端低效,10个让同事直呼卧槽的小技巧

在 IDE 横行的今天,我们这些程序员依然需要跟终端打交道,三五年下来,谁还没踩过一些坑,又或者自己琢磨出一些能让效率起飞的小窍门呢? 今天不聊那些 ls -la 比 ls 好用之类的基础知识,只分享那些真正改变我工作流、甚至让旁边同事忍不住探过头来问“哥们,...
继续阅读 »

在 IDE 横行的今天,我们这些程序员依然需要跟终端打交道,三五年下来,谁还没踩过一些坑,又或者自己琢磨出一些能让效率起飞的小窍门呢?


今天不聊那些 ls -lals 好用之类的基础知识,只分享那些真正改变我工作流、甚至让旁边同事忍不住探过头来问“哥们,你这手速没单身30年练不下来吧”的实战技巧。



快速定位系统性能瓶颈


服务器或者自己电脑突然变卡,得快速知道是谁在捣鬼。


# 查看哪个目录最占硬盘空间(只看当前目录下一级)
du -ah --max-depth=1 | sort -rh | head -n 10

# 按 CPU 使用率列出排名前 10 的进程
ps aux --sort=-%cpu | head -n 11

# 按内存使用率列出排名前 10 的进程
ps aux --sort=-%mem | head -n 11

环境和配置管理?交给专业的来


以前,管理本地开发环境简直是一场灾难。一会儿要配 PHP,一会儿又要弄 Node.js,还得装个 Python。各种环境变量、数据库配置写在 .bashrc.zshrc 里,像这样:


# 老办法:用函数切换环境
switch_env() {
if [ "$1" = "proj_a" ]; then
export DB_HOST="localhost"
export DB_PORT="3306"
echo "切换到项目 A 环境"
elif [ "$1" = "proj_b" ]; then
export DB_HOST="127.0.0.1"
export DB_PORT="5432"
echo "切换到项目 B 环境"
fi
}

这种方式手动维护起来很麻烦,项目一多,配置文件就变得特别臃肿,切换起来也容易出错。


但是,时代变了,朋友们。现在我处理本地开发环境,都用 ServBay。


请注意,ServBay不是命令行工具,而是一个集成的本地开发环境平台。它把程序员常用的语言,比如 PHP、Node.js、Python、Go、Rust 都打包好了,需要哪个版本点一下就行,完全不用自己去折腾编译和环境变量。


CleanShot 2025-11-18 at <a href=18.00.15@2x.png" loading="lazy" src="https://www.imgeek.net/uploads/article/20251214/459b28b5fdb4e57a34956d28d1655e18.jpg"/>


数据库也一样,无论是 SQL(MySQL, PostgreSQL)还是 NoSQL(Redis, MongoDB),都给你准备得妥妥的。而且它支持一键部署本地 AI,适合vibe coder。


用了 ServBay 之后,上面那些复杂的环境切换脚本我早就删了。所有环境和配置管理都通过一个清爽的图形界面搞定,我变强了,也变快了。


一行搞定网络调试


简单测试一下端口通不通,或者 API 能不能访问,完全没必要打开 Postman 那么重的工具。


# 检查本地 3306 端口是否开放
nc -zv 127.0.0.1 3306

# 快速给 API 发送一个 POST 请求
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{"username":"test","role":"admin"}'

目录间的闪转腾挪:pushdpopd


还在用一连串的 cd ../../.. 来返回之前的目录吗?那也太“复古”了。试试目录栈吧。


# 你当前在 /Users/me/workspace/project-a/src
pushd /etc/nginx/conf.d
# 这时你瞬间移动到了 Nginx 配置目录,并且终端会记住你来的地方

# 在这里查看和修改配置...
vim default.conf

# 搞定之后,想回去了?
popd
# “嗖”的一下,你又回到了 /Users/me/workspace/project-a/src

pushd 可以多次使用,它会把目录一个个地压入一个“栈”里。可以用 dirs -v 查看这个栈,然后用 pushd +N 跳到指定的目录。对于需要在多个不相关的目录之间反复横跳的场景,这就是大杀器。


文件操作的骚操作


cp 复制大文件时,看着光标一动不动,你是不是也曾怀疑过电脑是不是死机了?


# 安装 rsync (macOS 自带,Linux 大部分也自带)
# 复制文件并显示进度条
rsync -avh --progress source-large-file.zip /path/to/destination/

查找文件,find 命令固然强大,但参数复杂得像咒语。我更推荐用 fd,一个更快、更友好的替代品。


# 安装 fd (brew install fd / apt install fd-find)
# 查找所有 tsx 文件
fd ".tsx$"

# 查找并删除所有 .log 文件
fd ".log$" --exec rm {}

批量重命名文件,也不用再写复杂的脚本了。


# 比如把所有的 .jpeg 后缀改成 .jpg
for img in *.jpeg; do
mv "$img" "${img%.jpeg}.jpg"
done

历史命令的魔法:!!!$


这个绝对是手残党和健忘症患者的良药。最常见的场景就是,刚敲了一个需要管理员权限的命令,然后……


# 信心满满地创建目录
mkdir /usr/local/my-app
# 得到一个冷冰冰的 "Permission denied"

# 这时候别傻乎乎地重敲一遍,优雅地输入:
sudo !!
# 这行命令会自动展开成:sudo mkdir /usr/local/my-app

!! 代表上一条完整的命令。而 !$ 则更精妙,它代表上一条命令的最后一个参数。


# 创建一个藏得很深的项目目录
mkdir -p projects/a-very-long/and-nested/project-name

# 紧接着,想进入这个目录
cd !$
# 是的,它会自动展开成:cd projects/a-very-long/and-nested/project-name

# 或者,想在那个目录下创建一个文件
touch !$/index.js

自从熟练掌握了这两个符号,我的键盘方向上键和 Ctrl+C 的使用频率都降低了不少。


进程管理不用抓狂


以前杀个进程,得先 ps aux | grep xxx,然后复制 PID,再 kill -9 PID,一套操作下来黄花菜都凉了。现在,我们可以更直接一点。


# 按名字干掉某个进程
pkill -f "gunicorn"

# 优雅地请所有 Python 脚本进程“离开”
pkill -f "python.*.py"

对于我们这些经常和端口打交道的开发者来说,端口被占用的问题更是家常便饭。下面这个函数,我把它写进了我的 .zshrc 里,谁用谁知道。


# 定义一个函数,专门用来释放被占用的端口
free_port() {
lsof -i tcp:$1 | grep LISTEN | awk '{print $2}' | xargs kill -9
echo "端口 $1 已释放"
}

# 比如,干掉占用 8000 端口的那个“钉子户”
free_port 8000

给 Git 整个外挂


把这些别名(alias)加到 ~/.gitconfig 文件里,每天能省下无数次敲击键盘的力气。


[alias]
co = checkout
br = branch
ci = commit -m
st = status -sb
lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset'

# 把暂存区的修改撤销回来
unstage = reset HEAD --

# 彻底丢弃上一次提交,但保留代码改动
undo = reset --soft HEAD~1

# 一键推送当前分支到远程
pushup = "!git push --set-upstream origin $(git rev-parse --abbrev-ref HEAD)"

还有一个我特别喜欢的,一键清理已经合并到主干的本地分支,分支列表干净清爽。


git branch --merged main | grep -v "*|main|develop" | xargs -n 1 git branch -d

文本处理,快准狠


从日志里捞个邮箱,或者快速格式化一坨 JSON,都是日常操作。


# 从文件里提取所有 URL
grep -oE 'https?://[a-zA-Z0-9./-]+' access.log

# 格式化粘贴板里的 JSON (macOS)
pbpaste | jq .

# 从 API 响应中只提取需要的字段
curl -s 'https://api.github.com/users/torvalds' | jq '.name, .followers'

把重复劳动变成自动化脚本


真正拉开效率差距的,是把那些每天都在重复的操作,变成一个可以随时呼叫的函数或脚本。


# 比如,创建一个新前端项目的完整流程
new_react_project() {
npx create-react-app "$1" && cd "$1"
git init && git add .
git commit -m "🎉 Initial commit"
# 自动在 VS Code 中打开
code .
}

# 比如,在执行危险操作前,快速打包备份当前目录
backup() {
local fname="backup-$(date +%Y%m%d-%H%M).tar.gz"
tar -czvf "$fname" . --exclude-from=.gitignore
echo "备份完成: $fname"
}

把这些函数写进 .bashrc.zshrc,下次再做同样的事情时,只需要敲一个命令就搞定了。


写在最后


这些技巧本身并不复杂,但它们就像肌肉记忆,一旦养成习惯,就能在日常工作中节省大量时间。对程序员来说,时间就是头发。


作者:该用户已不存在
来源:juejin.cn/post/7573988811916017714
收起阅读 »

做中国人自己的视频编辑UI框架,WebCut正式开源

web
项目地址:github.com/tangshuang/… 朋友们晚上好啊,这段时间我在忙着完成最新的开源项目WebCut。这个项目是我这小半年来唯一的开源新项目,这对比我过去几年的一些开源事迹来说,真的是一段低产荒。不过这是正常的,没有任何人可以长时间的一直...
继续阅读 »



项目地址:github.com/tangshuang/…



朋友们晚上好啊,这段时间我在忙着完成最新的开源项目WebCut。这个项目是我这小半年来唯一的开源新项目,这对比我过去几年的一些开源事迹来说,真的是一段低产荒。不过这是正常的,没有任何人可以长时间的一直发布新项目,这种沉寂,正是因为我把时间和精力都投入在其他事情上,所以其实是好事。之所以要发起和开源这个项目,说起来还是有些背景,下面我会来聊一聊关于这个项目的一些背景,以及过程中在技术上的一些探索。


没有合适的视频编辑UI框架😭


过去半年,我连续发布了多款与视频相关的产品,这些产品或多或少都需要用户参与视频编辑操作,比如给视频添加字幕、对视频进行裁剪、给视频配音等等,这些在视频编辑器中常见的视频处理工具,在Web端其实需求非常巨大,特别是现在AI领域,制作各种个样的视频的需求非常多。而这些需求,可能并不需要在产品中载入一整个视频编辑器,而是只需要几个简单的部件来实现编辑功能。然而回望开源市场,能够支持这种编辑能力的项目少之又少,虽然有一些项目呈现了视频编辑的能力,然而,要么项目太大,一个完整的视频编辑器甩在开发者脸上,要么过于底层,没有UI界面。如果我只是想有一个视频预览,再有轨道和几个配置界面,就没法直接用这些项目。包括我自己在内,每次有与视频相关的功能,都要把之前在另外一个产品中实现的编辑能力移植到新产品中,而且要调的细节也很多。正是这种求而不得的现状,促使我打算自己写一个视频编辑器项目。


初始想法💡:拼积木


我可不是从0开始的,因为我已经开发过很多次视频编辑相关的功能了。我还在Videa项目中完整实现了一个视频编辑器。因此,我的想法是把我之前做过的功能,整理一遍,就可以得到想要的组件或逻辑。有了这个工具包之后,我只需要在将来的新产品中复用这些代码即可。于是我建立了一个独立的npm包,来把所有功能集中放在一起。随着持续的迭代,我发现其实这里面是有规律的。


视频编辑器我们都用过,像剪映一样,有各种功能,细节也很多。但是,当我们把视频编辑的功能放到某个具体的产品中时,我们是不可能直接把整个编辑器给用户的。实际上,我们最终呈现的产品形态,基本上都是剪映的子集,而且是很小很小的子集,可能只是整个剪映1%的功能,最终给到用户操作可能只是非常简单的一次性操作,而页面也很轻量,属于用户即用即走,用完就关再也不会来第二次的那种。正是这种看似功能点很小,但实际上需要为它单独定制,技术上的成本可以用巨大来描述的场景,让我觉得这是一个需要认真对待的点。


我的计划是采用组件化的思想,把一个视频编辑器拆成一个一个的组件,把一个完整的剪映编辑器,拆成一个按钮一个按钮的积木。当我们面对产品需求时,就从这些积木中挑选,然后组合成产品经理所描述的功能,同时,具体这些积木怎么布局,则根据设计稿调整位置,还可以用CSS来覆盖组件内部的样式,达到与设计稿媲美的效果。



上面是我用AI做的一张示意图,大概就是这个意思,把一个编辑器拆的细碎,然后要什么功能就把对应的组件拿来拼凑一下。比如要对视频进行静音裁剪,就只要把预览区和轨道区拿出来,然后自己再增加一些能力上去。这样,开发者在面对各种各样的需求时,就能快速搭建起界面效果,而不需要从头实现界面和视频处理能力。



通过不同的组合方式,配合开发者自己的布局,就可以创建符合需求的视频编辑界面。


没那么容易😥:外简内繁的接口艺术


虽然想法很容易有,但是要做成成品,并且发布为可用的库可没那么轻松。要知道视频编辑器的功能非常多,多到我们无法用简单的文字描述它们。那么,如何才能站在开发者的角度,让这件事变得简单呢?


开发库永远面临着一个矛盾:灵活性和规范性之间的矛盾。灵活性就是暴露又多又细的接口,让开发者可以用不同参数,玩出花活,例如知名项目echarts早期就是这种。这种对于刚上手的开发者而言,无言是一场灾难,他们甚至不知道从哪里开始,但是一旦完全掌握,就相当于拥有了一个武器库,可以为所欲为。而规范性则是强制性规则比较多,只能暴露少量接口,避免开发者破坏这种规范,例如很多前端UI组件库就是这样。这两者之间的矛盾,是每一个库或框架开发者最难平衡的。


在视频编辑器上,我认为有些东西是固定的,几乎每一个需求都会用到,例如视频预览播放器、控制按钮等,但是有些功能是低频功能,例如媒体库、字幕编辑等。因此,对于高频和低频的功能,我用两种态度去处理。


高频的功能,拆的很细,拆到一个按钮为止,例如视频播放器,看上去已经很单一了,但是,我还要把预览屏幕和播放按钮拆开,屏幕和按钮如何布局就可以随意去处理。类似的还有导出按钮、分割片段按钮等等。把这些工具拆分为一个一个最小的单元,由开发者自由布局,这样就可以很好的去适配产品需求。


对于低频功能,则直接导出为大组件,这样就可以在一个组件内完成复杂的逻辑,减少开发这些组件时需要跨组件控制状态的心智成本。比如媒体库,里面包含了媒体的管理、上传、本地化等等,这些逻辑还是很复杂的,如果还是按照细拆思路,那么实现起来就烦得要死。因此,这类工具我都是只暴露为一个大的组件。


同时,为了方便开发者在某些情况下快捷接入,我会设计一些自己看到的不错的编辑器UI,然后用上诉的工具把它们搭出来,这样,开发者如果在产品需求中发现仓库里已经有功能一致的组件,就不需要自己去组合,直接使用对应组件即可。


以模仿剪映为例,开发者只需要接入WebCutEditor这个组件即可:


<script setup>
import { WebCutEditor } from 'webcut';
import 'webcut/esm/style.css';
</script>

<template>
<WebCutEditor />
</template>

这样就可以得到一个界面接近于剪映的视频编辑器。


数据驱动,视频编辑的DSL


经过我的研究,发现对于单纯的视频编辑而言,编辑器其实只需要两份数据,就可以解决大部分场景下的需求。一份是编辑的素材数据,一份是视频的配置数据。


素材数据


它包含素材文件本身的信息、素材的组织信息、素材的属性信息。


文件信息


我通过opfs将文件存在本地,并且在indexedDB中存储每一个文件的关联信息。包含文件的类型、名称、大小等。在一个域名下,每一个文件的实体(也就是File对象)只需要一份,通过file-md5值作为索引存在opfs中。而一个文件可能会在多处被使用,indexedDB中则是记录这些信息,多个关联信息可同时链接到同一个File。另外,基于indexedDB的特性,还可以实现筛选等能力。


素材组织信息


主要是指当把素材放在视频时间轨道中时,所需要的数据结构。包含轨道列表、素材所对应的文件、素材对应时间点、播放时的一些属性等等信息。这些信息综合起来,我们就知道,在视频的某一个时刻,应该播放什么内容。


素材属性信息


在播放中,素材以什么方式呈现,如文本的样式、视频音频的播放速度、动画、转场等。


配置数据


主要指视频本身的信息,在导出时这些配置可以直接体现出来,例如视频的分辨率、比例、速率等,视频是否要添加某些特殊的内容,例如水印等。


基于素材数据和配置数据,我们基本上可以完整的知道当前这个视频的编辑状态。通过数据来恢复当前的编辑状态,变得可行,这可以抵消用户在浏览器中经常执行“刷新”操作带来的状态丢失。同时,这份数据也可以备份到云端,实现多端的同步(不过需要同时同步File,速度肯定会受影响)。而且由于数据本身是纯序列化的,因此,可以交给AI来进行处理,例如让AI调整一些时间、样式等可基于纯序列化数据完成的功能。这就让我们的编辑器变得有更多的玩法。


发布上线🌏


经过几天的工作,我终于把代码整理完整,经过调试之后,基本可用了,便迫不及待的准备与大家分享。现在,你可以使用这个项目了。



由于底层是由Vue3作为驱动的,因此,在Vue中使用有非常大的优势,具体如下:


npm i webcut

先安装,安装之后,你就可以在Vue中如下使用。


<script setup>
import { WebCutEditor } from 'webcut';
import 'webcut/esm/style.css';
</script>

<template>
<WebCutEditor />
</template>

或者如果你的项目支持typescript,你可以直接从源码进行引入,这样就不必主动引入css:


<script setup lang="ts">
import { WebCutEditor } from 'webcut/src';
</script>

如果是非Vue的项目,则需要引用webcomponents的构建产物:


import 'webcut/webcomponents';
import 'webcut/webcomponents/style.css';

export default function Some() {
return <webcut-editor></webcut-editor>;
}

如果是直接在HTML中使用,可以直接引入webcomponents/bundle,这样包含了Vue等依赖,就不需要另外构建。


<script src="https://unpkg.com/webcut/webcomponents/bundle/index.js"></script>
<link rel="stylesheet" href="https://unpkg.com/webcut/webcomponents/bundle/style.css" />

<webcut-editor></webcut-editor>

如果是想自己布局,则需要引入各个很小的组件来自己布局。在这种情况下,你必须引入WebCutProvider组件,将所有的子组件包含在内。


<webcut-provider>
<webcut-player></webcut-player>
<webcut-export-button></webcut-export-button>
</webcut-provider>

未来展望


当前,WebCut还是处于很初级的阶段,实现了最核心的能力,我的目标是能够为开发者们提供一切需要的组件,并且不需要复杂的脚本处理就可以获得视频编辑的全部功能。还有很多功能没有实现,在计划中:



  • 历史记录功能,包含撤销和重做功能

  • 内置样式的字体

  • 花字,比内置样式更高级的文本

  • 轨道里的素材吸附能力

  • 视频的轨道分离(音频分离)

  • 音视频的音量调节

  • 单段素材的下载导出

  • 整个视频导出时可以进行分辨率、码率、速率、编码、格式的选择,支持只导出音频


以上这些都是编辑的基本功能。还有一些是从视频编辑定制化的角度思考的能力:



  • 动画(帧)支持

  • 转场过渡效果支持

  • 扩展功能模块,这部分可能会做成收费的,下载模块后,通过一个接口安装一下,就可以支持某些扩展功能

  • AI Agent能力,通过对话来执行视频编辑,降低视频编辑的门槛

  • 视频模板,把一些流行的视频效果片段做成模板,在视频中直接插入后,与现有视频融合为模板中的效果

  • 基于AI来进行短剧创作的能力


要实现这些能力,需要大量的投入,虽然现在AI编程非常火热,但是真正能够完美实现的,其实还比较少,因此,这些工作都需要小伙伴们的支持,如果你对这个项目感兴趣,可以通过DeepWiki来了解项目代码的底层结构,并fork项目后,向我们提PR,让我们一起共建一个属于我们自己的视频编辑UI框架。


最后,你的支持是我前进的动力,动动你的小手,到我们的github上给个start,让我们知道你对此感兴趣。


作者:否子戈
来源:juejin.cn/post/7579819594270900262
收起阅读 »

程序员越想创业,越不要急着动手

昨天晚上,我和老婆聊了一个创业点子。 一个能源方面的前辈找到我,希望通过我把一些人工的工作 AI 自动化。 能源方面我不懂,找 Gemini 聊完发现这个是可以复制的,非常兴奋。我跟老婆说,这个项目做好以后可以做成平台,推广到其他公司,你就等着做总裁夫人吧! ...
继续阅读 »

昨天晚上,我和老婆聊了一个创业点子。


一个能源方面的前辈找到我,希望通过我把一些人工的工作 AI 自动化。


能源方面我不懂,找 Gemini 聊完发现这个是可以复制的,非常兴奋。我跟老婆说,这个项目做好以后可以做成平台,推广到其他公司,你就等着做总裁夫人吧!


她听完以后跟我说,这个项目还是太定制化,和我之前做的一个项目很像。


那个项目一开始,我和朋友设想的是可以做完一个以后再推到不同的学校,但最后没有达到期望。不同的甲方想要的和我们做的差异很大,没办法推广,都得定制。


但我觉得这次不一样,她说了好几遍,发现我一直“冥顽不灵”,就不再说了。


今天休息,在写完专栏《转型 AI 工程师》第二篇以后,我想起之前老婆分享给我的一些抖音视频还没看,打开看了以后,发现她分享的都是一些赚钱妙招,有些真的很打破我的认知,让我大受震撼。


好些项目是我头一次见到,在这之前脑子里完全没有概念。这时我才明白她昨天晚上跟我说的话。对比别人讲的这些,我想做的的确是太小众、太定制了。


为什么我听不进去?


冯新(原真格基金投资合伙人)说过:创业企业的成长本质是创始人认知边界的突破,而『不知道自己不知道』的认知茧房,正是创始人成长的最大障碍


过去八九年,我两耳不闻窗外事、一心只想搞技术,对商业机会的认知非常少。如今想要参与进 AI 这波浪潮,却不知道从何做起。


在前辈跟我说了诉求后,我像落水的人抓住绳子一样,脑子里都是那个新项目的画面:问题都有哪些、怎么解决、做完怎么推广到更多公司等等。这些画面在我脑子里不断循环,占据了全部的注意力。


而老婆说的"大规模复制的模式",在我脑子里没有画面,所以就完全听不进去。就像戴着VR眼镜,你只能看到眼镜里的世界,别人跟你说外面的世界是什么样,你根本想象不出来。


我想这也是为什么很多孩子你跟他讲话他不听,很多年轻人长辈跟他讲话他也不听。不是他们不想听,而是他们脑子里只能看到当前自己见到的、听到的、想到的一些事。


我不是第一个犯这种错的人


后来我查了一下,发现我这种情况不是个例。


哈佛商学院有个研究发现,90% 的创业者倒在头 18 个月。“最大的敌人不是市场或竞争对手,而是创业者自己"。


具体来说,就是对「快速试错」和「尽早动手」等观点的误解,导致在错误道路上浪费了太多资源。这种情况被叫做「错误的起步」——省略初期的全面审慎思考,直接进入执行阶段。


现在这个能源项目,如果我按照昨晚的想法继续推进,很可能又会掉进一样的坑。


在写下这篇文章的时候,我想明白了动手之前要调研的情况:



  1. 这个需求是不是真的普遍存在?

  2. 不同公司的差异有多大?

  3. 推广的成本和难度是什么?

  4. 有没有更标准化的切入点?


如何拓宽认知边界?


老婆分享的那几个抖音视频,讲的赚钱方式,有些我从来没想到过,也没接触过这些行业,脑子里完全没有画面。但看完以后,我突然明白了一件事:原来赚钱的方式有这么多种,而我一直在自己的一亩三分地里打转。


对于像我这样想要创业的人来说,今天最大的感悟是:一定要多看,一定要先知道「猪是怎么跑的」,哪怕吃不到猪肉,至少知道猪是怎么跑的,心里有了一个概念


具体怎么做?我目前想到这些,欢迎你评论区留言:


1、主动搜索不同行业的赚钱案例


不是为了照搬,而是为了拓宽认知边界。


我在日历里加上了日程---每周花30分钟,专门看别人有什么小众赚钱模式,他们是怎么发现机会的,怎么切入市场的,怎么实现标准化的。抖音、小红书、知乎、即刻,都是很好的信息源。


关键是要看那些你从来没想到过的行业。比如我今天看到的几个案例:直播切片、广告媒介采买。


2、用 AI 筛选出适合自己的机会


看得多了,就会发现很多机会。接下来需要进行筛选。


我的优势是 AI 技术,所以我会重点关注那些「传统行业+AI」的机会。不是去做通用大模型,而是找那些可以用 AI 提升效率的垂直领域。


3、经常问自己三个问题


为了避免再次陷入「执行陷阱」,我给自己设了一个自我检查清单,每周问自己三个问题:



  1. 我现在做的事,是在拓宽认知边界,还是重复的经验复用?

  2. 我看到的机会,受众有多少,其中有多少人愿意付费?

  3. 这个项目如果一年内没有成果,我会坚持吗?坚持的原因是什么


4、搭建一个「商机捕获系统」


这几天我一直在想,能不能用 AI 搭建一个系统,自动帮我捕获各种赚钱商机?


我试了几个方案,发现真的可行。


这个系统的核心不是找到一个具体的项目,而是持续拓宽认知边界,让自己能看到更多的可能性。具体怎么搭建,我准备放到 转型 AI 工程师专栏 的最后大作业部分,这个点子比之前想的「深度研究助手」更有价值。


最后想说的


今天突然有这个感想,没想到越写越多,差不多了收个尾吧。


像我这样两耳不闻窗外事、一心只想搞技术的老程序员,想要创业不要急着动手。


不是说不要行动,而是说在行动之前,先拓宽自己的认知边界。


如果你脑子里只有一种赚钱方式,那你只能在这一种方式里打转。如果你脑子里有十种、一百种赚钱方式,你才能找到最适合自己的那一种。


我自己的经历就是教训。那个学校项目失败了,现在这个项目如果不调整思路,很可能又会失败。


但好在,我现在意识到了这个问题。


希望这篇文章对你也有启发。


以上。


作者:张拭心
来源:juejin.cn/post/7582854603061379114
收起阅读 »

我为什么放弃了XMind和亿图,投向了这款开源绘图工具的怀抱?

web
关注我的公众号:【编程朝花夕拾】,可获取首发内容。 01 引言 思维导图、流程图应该是每个程序员都会用到的绘图工具。Xmind和亿图曾是我的首选工具,但是免费版功能受限,高级功能需付费,用起来总是差点意思。虽然通过其他方式正常使用(大家都懂得),但是软件的更...
继续阅读 »

关注我的公众号:【编程朝花夕拾】,可获取首发内容。



01 引言


思维导图、流程图应该是每个程序员都会用到的绘图工具。Xmind和亿图曾是我的首选工具,但是免费版功能受限,高级功能需付费,用起来总是差点意思。虽然通过其他方式正常使用(大家都懂得),但是软件的更新根本不敢动,一旦更新就会前功尽弃......而且两款工具总会来回切换,虽已习惯,但稍显麻烦!


直到不久前逛GitHub发现了一款开源的且可以在线使用的工具:Drawnix。界面简约,满足日常基本绘图需求,且支持 mermaid 语法转流程图等,用起来非常丝滑。整理一下分享给大家!


02 简介



2.1 名称的由来


Drawnix ,源于绘画( Draw)与凤凰( Phoenix )的灵感交织。凤凰象征着生生不息的创造力,而 Draw代表着人类最原始的表达方式。在这里,每一次创作都是一次艺术的涅槃,每一笔绘画都是灵感的重生。


创意如同凤凰,浴火方能重生,而 Drawnix 要做技术与创意之火的守护者。


2.2 框架


Drawnix 的定位是一个开箱即用、开源、免费的工具产品,它的底层是 Plait框架,Plait 是作者公司开源的一款画图框架,代表着公司在知识库产品上的重要技术沉淀。


Drawnix 是插件架构,与前面说到开源工具比技术架构更复杂一些,但是插件架构也有优势,比如能够支持多种 UI 框架(AngularReact),能够集成不同富文本框架(当前仅支持 Slate框架),在开发上可以很好的实现业务的分层,开发各种细粒度的可复用插件,可以扩展更多的画板的应用场景。


GitHub地址:


2.3 特性



  • 💯 免费 + 开源

  • ⚒️ 思维导图、流程图

  • 🖌 画笔

  • 😀 插入图片

  • 🚀 基于插件机制

  • 🖼️ 📃 导出为 PNG, JSON(.drawnix)

  • 💾 自动保存(浏览器缓存)

  • ⚡ 编辑特性:撤销、重做、复制、粘贴等

  • 🌌 无限画布:缩放、滚动

  • 🎨 主题模式

  • 📱 移动设备适配

  • 📈 支持 mermaid 语法转流程图

  • ✨ 支持 markdown 文本转思维导图(新支持 🔥🔥🔥)


2.4 安装


提供Docker部署:


docker pull pubuzhixing/drawnix:latest

也可以在线使用:


地址:


03 最佳实践


实践我们采用在线方式,如果注重安全或者本地使用可以使用Docker部署。


3.1 界面说明


通过https://drawnix.com/进入之后,所有功能如图:



中间工具栏分别表示:



  • 拖拽

  • 选中

  • 思维导图

  • 文字

  • 手绘

  • 箭头

  • 流程图

  • 插入图片

  • 格式转换


功能都比较简单,思维导图、流程图以及格式转化是常用的工具。



3.2 思维导图


思维导图的使用方式和Xmind的用法极为相似。选中节点,然后Tab键就可以添加同级子节点,也可以利用+号添加。-号并不是删除,而是合并。删除也非常简单,选中直接Delete即可。



3.3 流程图


流程图的图形相对来说比较简单,只有七项。



选中之后直接绘制即可,图形之间使用箭头链接即可。


3.4 格式转化


目前支持两种格式转化成流程图:



Mermaid



仅支持流程图、序列图和类图


Markdown



仅支持思维导图。


04 小结


Drawnix的极简设计满足日常绘图的需要,可能在很多功能上不如Xmind或者亿图,但是它确实两者的结合。是一款不错的绘图软件。


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

作为前端你必须要会的CICD

web
前言 这是一篇属于面向前端的关于CICD的入门文章,其旨在: 入门掌握CI CD的用法 学习CI和CD的含义及其实现细节 基于GitLab展示如何给自己手上的项目添加CICD的流程 学习本文你需要注意的事情 你的项目必须是支持Node版本 16.20.0...
继续阅读 »

前言


这是一篇属于面向前端的关于CICD的入门文章,其旨在:



  1. 入门掌握CI CD的用法

  2. 学习CICD的含义及其实现细节

  3. 基于GitLab展示如何给自己手上的项目添加CICD的流程


学习本文你需要注意的事情



  1. 你的项目必须是支持Node版本 16.20.0

  2. 笔者的CentOS安装Node18以上的版本底层库不支持,如果你想安装高版本的Node请先解决CentOS版本低的问题

  3. 本文采用的是CentOSLinux操作系统

  4. 本文的操作系统版本截图在下方


image.png


OK 如果你已经明白了我上面说的注意事项 那我们事不宜迟,直接开始本文的内容吧。


安装Node


(第一种)安装 NVM


请注意 如果使用这种安装方式在后面执行runner的时候在gitlab-runner账户会报错找不到Node 这是因为Linux的系统的用户环境隔离问题



curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash


更新环境变量

source ~/.bashrc


验证NVM是否安装成功

command -v nvm


使用NVM安装Node.js:

nvm install node


安装指定版

nvm install 16.20.0


使用 NVM 切换到安装的 Node.js 版本

nvm use node


nvm use node 16.20.0


验证安装:

node -v
npm -v
npx -v


(第二种)使用系统级 Node.js 安装

wget https://nodejs.org/dist/v16.20.0/node-v16.20.0-linux-x64.tar.xz


解压

tar xf node-v12.9.0-linux-x64.tar.xz


复制

cp -rf /root/node-v16.20.0-linux-x64 /usr/local/node


打开编辑配置文件

vim /etc/profile


在文件的最后一行加上这句话

export PATH=$PATH:/usr/local/node/bin


重载系统配置文件

source /etc/profile


这次在切换用户到gitlab-runner就可以查看到你安装的版本了

安装 Nginx


安装相关依赖


  • zlib 开启 gzip 需要

  • openssl 开启 SSL 需要

  • pcre rewrite模块需要

  • gcc-c++ C/C++ 编译器


yum -y install gcc-c++ zlib zlib-devel openssl openssl-devel pcre pcre-devel


下载压缩包

wget https://nginx.org/download/nginx-1.18.0.tar.gz


解压

tar -zxvf nginx-1.18.0.tar.gz
cd ./nginx-1.18.0
./configure
make
make install

查看安装路径

whereis nginx


编辑环境变量

vim /etc/profile


在文件最下面添加这两行

export NGINX_HOME=/usr/local/nginx


export PATH=$NGINX_HOME/sbin:$PATH


更新配置文件

source /etc/profile


查看nginx是否安装完成

nginx -v


开放 80 端口 如果不想一次性一个一个的放行端口,可以关闭防火墙

firewall-cmd --permanent --zone=public --add-port=80/tcp


查看防火墙的状态

systemctl status firewalld.service


关闭防火墙的状态

systemctl stop firewalld.service


查看已经放行的端口

firewall-cmd --zone=public --list-ports


重载防火墙

firewall-cmd --reload


启动

nginx


安装 GitLab


安装 GitLab,需要的时间比较长

yum -y install https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-14.3.0-ce.0.el7.x86_64.rpm


编辑配置文件

vim /etc/gitlab/gitlab.rb


修改配置文件

image.png


重载配置文件,

gitlab-ctl reconfigure


开放 1874 端口

firewall-cmd --permanent --zone=public --add-port=1874/tcp


查看已经放行的端口

firewall-cmd --zone=public --list-ports


重载防火墙

firewall-cmd --reload


修改gitlab的root用户密码

1.进入到gitlab控制面板中 gitlab-rails console -e production
2.执行命令: user = User.where(id: 1).first,此 user 则表示 root 用户

3、修改密码
执行命令:user.password = '12345678'修改密码
再次执行 user.password_confirmation = '12345678' 确认密码

4、保存密码
执行命令: user.save!

5、退出控制台
执行命令: exit

image.png


验证是否修改成功


http://192.168.80.130:1874/users/sign_in 把Ip换成自己的Ip 输入root的用户名和密码尝试进行登录,正常创建项目进行测试



配置 CI/CD


下载

wget -O /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64


分配运行权限

chmod +x /usr/local/bin/gitlab-runner


创建用户

useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash


安装 在 /usr/local/bin/gitlab-runner 这个目录下

gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner


运行

gitlab-runner start


新建 runner


注册 runner

gitlab-runner register


输入 gitlab 的访问地址

http://192.168.80.130:1874


输入 runner token

打开 http://192.168.80.130:1874/admin/runners 页面查看


image.png


runner 描述,随便填

测试vue项目部署


runner tag

ceshi


Enter optional maintenance note for the runner:

直接回车走过


输入(选择) shell 最后一步选择执行的脚本

shell


image.png


注册完成后,就可以在 http://192.168.80.130/admin/runners 里面看到创建的 runner。


nginx 配置项目访问地址


创建目录

mkdir -pv /www/wwwroot/dist


分配权限 如果后面执行脚本命令提示没有权限那就是这个地方有问题

chown gitlab-runner /www/wwwroot/dist/


(备用)如果权限有问题可以使用这个命令单独给这个目录设置上gitlab-runner用户权限

sudo chown -R gitlab-runner:gitlab-runner /www/wwwroot/


开放 3001 端口

firewall-cmd --permanent --zone=public --add-port=3001/tcp


重载防火墙 .

firewall-cmd --reload


打开 nginx 配置文件

vim /usr/local/nginx/conf/nginx.conf


在第一个 server 下方 (nginx 默认的,端口为80),加上下面的内容

server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
#新增开始
server {
listen 3001;
server_name localhost;
location / {
root /www/wwwroot/dist;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
#新增结束

重新加载配置文件

nginx -s reload


编写 .gitlab-ci.yml 文件


# 阶段
stages:
- build
- deploy

# 缓存 node_modules 减少打包时间,默认会清除 node_modules 和 dist
cache:
paths:
- node_modules/

# 拉取项目,打包
build:
stage: build
tags:
- ceshi
before_script:
# - export PATH=/usr/local/bin:$PATH
- node --version
- npm --version
- echo "开始构建"
script:
- cd ${CI_PROJECT_DIR}
- npm install
- npm run build
only:
- main
artifacts:
paths:
- dist/

# 部署
deploy:
stage: deploy
tags:
- ceshi
script:
- rm -rf /www/wwwroot/dist/*
- cp -rf ${CI_PROJECT_DIR}/dist/* /www/wwwroot/dist/
only:
- main

浏览器打开3001端口

提交代码到main分支上,如果想改为提交到其他分支上也可以进行自动部署就需要改only参数为分支名称


作者:青晚舟
来源:juejin.cn/post/7546420270421999642
收起阅读 »

一年多三次考试,总算过了系统架构师

前言 2024/12/27更新:实体证书也出来啦,如下: 2024/12/20更新:电子证书出来啦,如下: 算上这次,我其实已经参加了三次考试,先贴上这三次的成绩,相信大家也能感受到我的心情: 虽然这次总算通过了考试,但看到综合知识的成绩还是心有余悸,由于...
继续阅读 »

前言


2024/12/27更新:实体证书也出来啦,如下:


image.png


2024/12/20更新:电子证书出来啦,如下:


image.png


算上这次,我其实已经参加了三次考试,先贴上这三次的成绩,相信大家也能感受到我的心情:


20241214160256.png虽然这次总算通过了考试,但看到综合知识的成绩还是心有余悸,由于前两次考试的打击(都是差一门案例没有通过,上半年只差了两分),这次考试前只写了两套半的综合知识真题和在考前一天准备了大半天(背论文模板和知识点集锦的 pdf),而综合知识也是自己信心最足的一门,结果这次压线通过,所以还是建议大家只要报名了考试,还是要认真准备,至少把往年综合知识的真题刷一刷,避免最后发现只有综合知识未通过而追悔莫及。


我也在Github上分享了几个我备考用到的文档资料,大家自行取用。


PS:这次考试通过还要感谢我女友的祝福,我们在今年5.23相识(上半年考试前两天,然后考试也是差两分),再加上这次的压线通过,感受到了冥冥之中自有天意(❁´◡`❁)。


考试注意点


从去年下半年开始,软考统一由笔试改为机考,虽然不用再担心写字速度太慢或者不美观导致论文扣分,但要注意的是键盘只能使用考场提供的,因此很多人可能不太习惯。就我这几次的考试经验来说,两个小时写论文还是比较紧凑的,剩余时间都不超过10分钟,还要用这点时间去通读检查一遍论文有没有什么错别字,因此在考前准备一个论文模板还是十分必要的,这样就可以在写模板内容的同时去构思正文,对于时间充分的小伙伴来说,也可以计时去练习写几篇论文。另外需要注意从2024年开始,系统架构师改为一年两考(不通过也可以趁热打铁立刻准备下一次的考试了),上午考综合知识和案例(总共四小时,分别两小时,综合知识写完可提前半小时交卷去写案例),下午考论文(两个小时),考试时间安排如下:


考试时间考试科目
8:30—12:30综合知识、案例分析
14:30—16:30论文

备考经历


第一次(2 ~ 3个月):看完某赛视频全集(无大数据相关)+ 某赛知识点集锦 + 写完历年综合知识真题 + 案例论文对着答案看一遍(写了几道质量属性和数据库相关的案例题)+ 准备并背诵一个论文模板。


第二次(0.5 ~ 1个月):这次将上次的看视频改为了看教材(把考试重点的几个章节内容都混了个眼熟),然后其他准备都差不多,只是准备时间有相应减少。


第三次(1 ~ 2天):两套半综合知识真题 + 大致浏览一遍知识点集锦 + 背诵论文模板。


备考主要有以下注意点:



  1. 视频课不管是哪一家都无所谓,但需要注意架构师考试在22年12月更新了考试大纲,所以需要留意视频的版本不可太老,然后就是不管是在B站、闲鱼还是原价购买都不会有什么差别,只需保证视频内容完整即可。

  2. 各个机构的模拟题不要过多在意,尤其是考纲之外的题目,可作为对个人学习情况的测试。

  3. 近三次的考试由于是机考,只能在网上找到部分回忆版,不再有完整版真题,这个可自行搜索了解。

  4. 如果是第一次备考,建议还是至少 2 ~ 3 个月,除非基础特别好,不然还是建议将视频课看完(至少看完核心内容,计算机基础部分的优先级最低),这样至少可以保证综合知识问问拿下,还有就是真题特别特别特别重要


备考方式


综合知识


就我的经验来讲,我觉得综合知识是最可控的部分,只需将视频课 + 重要知识点集锦 + 历年综合知识真题过一遍,综合知识是完全不需要担心的。还有就是遇到考纲之外的真题,比如今年有一道题是:一项外观设计专利里面相似设计最多有几个,像这种基本无再考可能的题,只需要看到答案后混个眼熟就可以。除此之外还有一部分反复考的知识点:构件、4 + 1视图、ABSD、DSSA、架构评估(质量属性)、系统架构风格、项目时间和成本计算以及软件测试,这些内容需要格外留意,有时间的话,可以把教材上相关知识的内容过一遍。除此之外,一定要记得考试时相信自己的第一感觉,不确定的题目不要修改答案


案例分析


案例分析的题型变化比较大,更考验平时的技术积累,不过第一道必选题近几年都是和质量属性相关(除了23年下半年是大数据),然后就是 Redis 的考频也比较高,近三次考试有两次涉及(以往也有涉及),在24年上半年甚至精确到了命令的考察。此外,近几次案例也都考到了关于技术架构图的填空题,所以建议练习一下往年的相关题型,再到 ProcessOn 之类的平台找几个技术架构图看看。



案例考察的范围比较广,因此建议在高频考点上多加复习和准备。然后遇到不熟悉的知识点也不要慌,更不要空着不写,可以分点试着写一些或者硬凑一些相关的内容,能得一分是一分。如果时间充足,还是建议把往年的案例真题按照时间由近到远认真看一看,即使是一些视频中说的考试概率很低的知识点(Hibernate和设计模式)在前两次的考试和论文中也都有涉及,尤其是项目和技术经验不是那么丰富的小伙伴(比如我自己)需要注意这点。



论文


虽然看到很多小伙伴都说论文难写还会卡分,但因为我三次考试也都只有案例未过,论文虽然分数不高,但也都过了合格线,这里也分享一下我的写作经验。


我觉得写论文只需要记住真实项目 + 技术点讨论 + 论点点题并结合项目分析 + 项目中遇到的问题点这几点即可,即使内容有点流水账也无伤大雅,最重要的是写的让项目看起来真实,是自己做的,除了摘要和开头结尾可以找模板进行参考,正文部分还是需要自己结合论点去写,不能全是理论而没有一点技术点(使用到的各种工具和服务也都可以说,例如代码评审使用和项目管理相关的)的讨论。就以我这次的论文结构为例,首先是摘要部分(250字以内):



​ 2022年12月,我所在公司承接了某区xxx的开发项目。我在该项目中担任系统架构设计师的职务,负责需求分析和系统的架构设计等工作。该项目主要提供xxx、xxx和xxx功能。本文将结合作者的实践,以xxx项目为例,论述xxx在系统开发中的具体应用。在xxx模块使用了xxx,解决了xxx问题。在xxx模块使用了xxx,解决了xxx问题。在xxx模块使用了xxx,解决了xxx问题。实践证明,采用xxx,提升了软件的开发效率和质量。整个项目历时一年多,于今年6月正式上线运行,整个系统运行稳定,达到了预期的目标的要求。



然后是开头和结尾(800字左右):



​ ......。(项目背景,150字左右)


​ 正是在这一背景下,2022年12月,我们公司承接了xxx项目,在本项目中,我担任系统架构师的职务,负责需求分析和系统的架构设计等工作。经过对项目的调研和对用户需求的分析,我们确认了系统应当具有以下功能:xxx,xxx,xxx。基于以上的需求,我们采用xxx解决了xxx问题。(300字左右,这部分介绍功能的部分可以和摘要内容有重合)


​ 经过团队的共同努力,本项目按时交付,于今年6月顺利交付并上线,到目前运行稳定,不管是xxx使用xxx,还是xxx使用xxx都反馈良好。但在实施的过程中也遇到了一些问题,xxx。而如何让xxx更xxx是一项长期的工作,还有很多问题需要在实践中不断探索,在理论中深入研究并加以解决。只有这样,xxx才能不断地优化和发展,xxx。(350字左右)



最后是正文,由于我写的是软件维护(具体包含完善性维护、预防性维护、改正性维护、适应性维护),所以我首先用200 ~ 300字描述了这四种维护的具体含义(可以用自己的语言去描述,不需要和书上完全一致)。然后针对每种维护,再分四段用250 ~ 300字去结合项目和技术点具体去讨论我在每种维护中所做的工作。


当然上面只是我的一些论文写作经验,至少最近三次都是按照这个模板和套路去写,也都通过了。不过大家还是要结合自己的项目去做一些修改,建议多找几个论文综合一下,然后结合自己的语言去写一个属于自己的模板( •̀ ω •́ )✧。


感想


经过这三次的备考和考试经历,我觉得除了一些实力外,运气也占了一部分。就像这次的案例考了我熟悉的也简单的质量属性和 Cache Aside 缓存策略,前两次都有涉及到大数据这个我不熟悉的相关知识,也是我挂在案例的原因之一,所以大家如果考试遇到不熟悉的题或者分数还差一点,不妨再试一两次,相信自己可以的(●'◡'●)。如果大家有什么问题,也可以留言交流讨论。


作者:庄周de蝴蝶
来源:juejin.cn/post/7449570539884265524
收起阅读 »

当一个前端学了很久的神经网络...👈🤣

web
前言最近在学习神经网络相关的知识,并做了一个简单的猫狗识别的神经网络,结果如图。虽然有点绷不住,但这其实是少数情况,整体的猫狗分类正确率已经来到 90% 了。本篇文章是给大家介绍一下我是如何利用前端如何做神经网络-猫狗训练的。如果觉得这篇文章有些复杂,那么也可...
继续阅读 »

前言

最近在学习神经网络相关的知识,并做了一个简单的猫狗识别的神经网络,结果如图。

虽然有点绷不住,但这其实是少数情况,整体的猫狗分类正确率已经来到 90% 了。

本篇文章是给大家介绍一下我是如何利用前端如何做神经网络-猫狗训练的。如果觉得这篇文章有些复杂,那么也可以看看我的上一篇更简单的鸢尾花分类

步骤概览

还是掏出之前那个步骤流程,我们只需要按照这个步骤就可以训练出自己的神经网络

  1. 处理数据集
  2. 定义模型

    1. 神经网络层数
    2. 每层节点数
    3. 每层的激活函数
  3. 编译模型
  4. 训练模型
  5. 使用模型

最终的页面是这样的

处理数据集

  1. 首先得找到数据集,本次使用的是这个 http://www.kaggle.com/datasets/li… 2000 个猫图,2000 个狗图,足够我们使用(其实我只用了其中 500 个,电脑跑太慢了)
  2. 由于这些图片大小不一致,首先我们需要将其处理为大小一致。这一步可以使用 canvas 来做,我统一处理成了 128 * 128 像素大小。
const preprocessImage = (img: HTMLImageElement): HTMLCanvasElement => {
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;

const ctx = canvas.getContext("2d");
if (!ctx) return canvas;

// 保持比例缩放并居中裁剪
const ratio = Math.min(128 / img.width, 128 / img.height);
const newWidth = img.width * ratio;
const newHeight = img.height * ratio;

ctx.drawImage(
img,
(128 - newWidth) / 2,
(128 - newHeight) / 2,
newWidth,
newHeight
);
return canvas;
};

这里可能就有同学要问了:imooimoo,你怎么返回了 canvas,不应该返回它 getImageData 的数据点吗。我一开始也是这样想的,结果 ai 告诉我,tfjs 是可以直接读取 canvas 的,牛。

tf.browser.fromPixels() // 可以接受 canvas 作为参数

  1. 将其处理为 tfjs 可用的对象
  // 加载单个图片并处理为 tfjs 对应格式
const loadImage = async (category: "cat" | "dog", index: number): Promise<ImageData> => {
const imgPath = `src/pages/cat-dog/image/${category}/${category}.${index}.jpg`;
const img = new Image();
img.src = imgPath;

await new Promise((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
});

return {
path: imgPath,
element: img,
tensor: tf.browser.fromPixels(preprocessImage(img)).div(255), // 归一化
label: category === "cat" ? 0 : 1,
};
};

// 加载全部图片
const loadDataset = async () => {
const images: ImageData[] = [];

for (const category of ["cat", "dog"]) {
for (let i = 1000; i < 1500; i++) { // 这里只使用了后 500 张,电脑跑不动
try {
const imgData = await loadImage(category, i);
images.push(imgData);
} catch (error) {
console.error(`加载${category === "cat" ? "猫" : "狗"}图片失败: ${category}.${i}.jpg`, error);
}
}
}
return images;
};

定义模型 & 编译模型

由于我们的主题是图片识别,图片识别一般会需要用到几个常用的层

  1. 最大池化层:用于缩小图片,节约算力。但也不能太小,否则很糊会提取不出东西。
  2. 卷积层:用于提取图片特征
  3. 展平层:将多维的结果转为一维

有同学可能想问为什么会有多维。首先是三维的颜色,输入就有三维;卷积层的每一个卷积核,都会使结果增加维度,所以后续的维度会很高。这张图比较形象,最后就只会剩下一维,方便机器进行计算。

  // 创建卷积神经网络模型
const createCNNModel = () => {
const model = tf.sequential({
layers: [
// 最大池化层:降低特征图尺寸,增强特征鲁棒性
tf.layers.maxPooling2d({
inputShape: [128, 128, 3], // 输入形状 [高度, 宽度, 通道数]
poolSize: 2, // 池化窗口尺寸 2x2
strides: 2, // 滑动步长:每次移动 n 像素,使输出尺寸减小到原先的 1/n
}),

// 卷积层:用于提取图像局部特征
tf.layers.conv2d({
filters: 32, // 卷积核数量,决定输出特征图的深度
kernelSize: 3, // 卷积核尺寸 3x3
activation: "relu", // 激活函数:修正线性单元,解决梯度消失问题
padding: "same", // 边缘填充方式:保持输出尺寸与输入相同
}),

// 展平层:将多维特征图转换为一维向量
tf.layers.flatten(),

// 全连接层(输出层):进行最终分类
tf.layers.dense({
units: 2, // 输出单元数:对应猫/狗两个类别
activation: "softmax", // 激活函数:将输出转换为概率分布
}),
],
});

// 编译模型,参数基本写死这几个就对了
model.compile({
optimizer: "adam",
loss: "categoricalCrossentropy",
metrics: ["accuracy"],
});

console.log("模型架构:");
model.summary();

return model;
};

这里实际上只需要额外注意两点:

  1. 卷积层的激活函数activation: "relu",这里理论上是个非线性激活函数就行。但是我个人更喜欢 relu,函数好记,速度和效果又不错。
  2. 输出层的激活函数activation: "softmax",由于我们做的是分类,最后必须是这个。

训练模型

训练模型可以说的就不多了,也就是提供一下你的模型、训练集就可以开始了。这里有俩参数可以注意下

  • epochs: 训练轮次
  • validationSplit: 验证集比例,用于测算训练好的模型准确程度并优化下一轮的模型
  // 训练模型
const trainModel = async (
model: tf.Sequential,
xData: tf.Tensor4D,
yData: tf.Tensor2D
) => {
setTrainingLogs([]); // 清空之前的训练日志

await model.fit(xData, yData, {
epochs: 10, // 训练轮数
batchSize: 4,
validationSplit: 0.4,
callbacks: {
onEpochEnd: (epoch, logs) => {
if (!logs) return;
setTrainingLogs((prev) => [
...prev,
{
epoch: epoch + 1,
loss: Number(logs.loss.toFixed(4)),
accuracy: Number(logs.acc.toFixed(4)),
},
]);
},
},
});
};

整体页面

基本就是这样了,稍微写一下页面,基本就完工了

总结

别慌,神经网络没那么可怕,核心步骤就那几步,冲冲冲。

源码:github.com/imoo666/neu…


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

一天 AI 搓出痛风伴侣 H5 程序,前后端+部署通吃,还接入了大模型接口(万字总结)

web
自我介绍 大家好,我是志辉,10 年大数据架构,目前专注 AI 编程 1、背景 这个很早我就想写了 App 了,我也是痛风患者,好多年,深知这里面的痛呀,所以我想给大家带来一个好的通风管家的体验,但宏伟目标还是从小点着手,那么就有了今天的主角,痛风伴侣 H5。...
继续阅读 »

自我介绍


大家好,我是志辉,10 年大数据架构,目前专注 AI 编程


1、背景


这个很早我就想写了 App 了,我也是痛风患者,好多年,深知这里面的痛呀,所以我想给大家带来一个好的通风管家的体验,但宏伟目标还是从小点着手,那么就有了今天的主角,痛风伴侣 H5。


目录大纲


前面都是些开胃小菜,看官们现在我们就正式开始正文,那么整体目前是分的 6 个阶段。


第零阶段:介绍


第一阶段:需求


第二阶段:数据准备


第三阶段:开发+联调+部署


第四阶段:部署+上线


第五阶段:运营维护+推广


第六阶段:成本计算


前四个阶段可以分为一个大家的阶段,就完成了你的产品工作


最后就是收尾工作,以及后续的维护。


废话少说,就正式开始吧。


第零阶段:介绍


产品开发流程图


img


这是一个传统的软件开发流程,从需求的讨论开始到最后的产品上线,总共需要的六大步骤,包括后续的迭代升级维护。


成本测算


这里面其实最大的就是投入的人力成本,还不算使用的电脑、软件这些,还包括最大的就是时间成本。


我们按按照基本公司业务项目的项目来迭代看



  • 人力成本



    • 产品:1~2 人,有些大项目合作的会更多,跨大部门合作的。

    • UI :1 人

    • 研发:



      • 前端:1~2人

      • 后端:2~3人



    • 测试:1~2 人

    • 合计:这里面最少都是 6 人



  • 时间成本



    • 这里不用多少,大家如果有经验的,基本公司项目一般的需求都是至少一个月才能上线一个版本,小需求快的也就是半个月上线。



  • 沟通成本



    • 这个就用说了,大家都是合作项目,产品和 UI,产品和研发,研发和测试,这就是为啥会有那么多会的缘故,不同的工种,面对的是不同




img


时间成本感受


个人创业感想


那这里你就可能要较真了,你这个功能简单,哪能跟公司的项目比了。


那我就想起我之前跟我同学一起创业搞 app 的时候,那个时候我不会 app、也不会前端,我是主战大数据的,其实对后端有些框架不也太熟。


那会儿我们四个人,1 个 app、1 个后端+前端、1 个产品,也是足足搞了 1 个多月才勉强上了第一个小版本。


但是我们花的时间很多,虽然一个月,但那一个月我没睡过觉,不会就得学呀,哪像现在不会你找个 AI 帮手帮你搞,你就盯着就行,那会一边学前端,一遍写代码,遇到问题只能搜索引擎查,要么就是硬看源码去找思路解决。


想想就是很痛苦。


公司工作感想


本职工作是大数据架构,设计的都是后端复杂的项目通信,整体底层架构设计,但是也需要去做一些产品的事情。


但是大数据产品就不像业务系统配比那么豪华,一整个公司就两三个人,那么有时候就的去做后端服务、前端界面,就为了把我们的产品体验做好。


每天下班疯狂学习前端框架,从最基本的 html、css、js 学起,不然问题解决不了,花了大量的时间,并且做项目还要学习各种框架,不然报错了你都不知道咋去搜索。


这样能做大功能的事情很少,也就是修修补补做些小功能。


产品感想


这也是我最近用了 AI 编程后的感想,最近公司的数据产品项目,我基本都是 AI 编程搞定。


以前复杂的画布拖拉拽,我基本搞不定到上线的质量,现在咔咔的一下午就搞定开发,再结合 AI 的部署模板,一天就基本完成功能。效率太快。


也是这样,我在现在的公司的产出一周顶的上以前的一个月(这真不是吹牛,开会的半个小时,大数据的首页的 landing page 我就做好了🤦♂️) ,但是时间完全不用一周(偷偷的在这了讲,老板知道了,就。。。所以我要多做一些,让老板留下我)。


我现在感想的就是现在更加需要的就是你的创意、你的想法,现在的 AI 能力让更多的人提效的同时,也降低了普通人实现自己产品的可能性。这在以前是无法想象的,毕竟很多门槛是无法跨越,是需要时间磨练的。


效果展示


然后再多来几张美美的截图(偷偷告诉你,这就是我的背景图片工具做出来的。)


img


第一阶段:需求


1、需求思考


做产品最开的就是需求了,如果你是产品经理,那么我理解这一阶段是不需要 AI 来帮你忙的。


虽然大家基本对产品或多或少都有一些理解,那么专业性肯定比不了,那么我们就需要找专业的帮忙了。


所以我这里找的是 ChatGPT,大家找 DeepSeek,或者是 Gemin,或者是 Claude 都可以的。


我目前准备为痛风患者开发一个拍照识别食物嘌呤的h5应用,我的需求如下:

1. 这个h5底部有3个tab: 识别、食物、我的
2. 在【识别】页面,用户可以选择拍照或者选择相册上传,然后AI识别食物,并且给到对应的嘌呤的识别结果和建议。
3. 在【食物】页面,用于展示不同升糖指数的常见食物,顶部有一个筛选,用户可以筛选按嘌呤含量高低的食物,下方显示食物照片/名称/嘌呤/描述
4. 【我的】页面顶部有一个折线图,用户记录用户的尿酸历史;下方显示近3次的尿酸数据:包括平均、最低、最高的数据;还有记录尿酸和历史记录的列表。

在技术上,【我的】页面尿酸历史记录保存在本地localStorage中,【食物】的筛选也是通过本地筛选,拍照识别食物嘌呤的功能,采用通义千问的vl模型。

请你参考我的需求,帮我编写一份对应的需求文档。

发给 ChatGPT


img


这样就给我们回复了。


2、思考


你说我不会像你写那么多好的提示词,一个我也是借鉴别人的,一个就是继续找 AI 帮你搞定,比如你不知道 localstoreage 是什么,没关系,这个都是可以找 AI 问出来的。


img


或者是说你只有一个想法,而不知道这个产品要做成什么,也可以问 AI。


GPT 会告诉你每个阶段该做哪些功能,这样看看哪些对你合适,然后通过不断的多轮对话,来让他输出最后的需求文档。


img


3、创建需求工作空间


我们在电脑新建个目录,用来存放暂时的需求文档和一些前置工作的文件


Step 1: 在电脑的某个目录下创建前期我们需要的工作项目的目录,这里我叫 h5-food-piaoling


Step 2: Cursor 打开这个目录


Step 3: 创建 docs 目录


Step 4: docs 目录下创建 prd.md 文件,把刚刚 GPT 生成的需求文档拷贝过来。



我这里是后截图的,所以文件很多,不要受干扰了



img


4、重要的一步


到这里需求文档就创建好了,那么我们是不是马上就可以开发了,哦,NO,这里还有很重要的一步。


那么就是需要仔细看这个 GPT 给我们生成的需求文档,还是需要人工审核下的,避免一些小细节的词语、或者影响的需要修改的。


比如这里,我已经恢复不出来了,这里原来有些 “什么一页的文章,适合老年人的这些文字”,这些其实不符合我那会儿想的需求的,所以我就删除了。


img


比如这里用到的一些技术,如果你懂的话,就可以换成你懂的技术,也是需要考虑到后面迭代升级的一些事情。


img


总结:其实这里就是需要人工审查下,避免一些很不符合你想的那些,是需要修改/删除的,这个会影响后面生成 UI/交互的逻辑。



不过这个步骤不做问题也不大,这一步也是需要长久锻炼出来,后面等真实的页面出来后,你再去修改也行。



第二阶段:数据准备


这里的一步也是我认为比较特别的点,这个步骤的点可以借鉴到其他场景里面。


1、哪里找数据


你的产品里的数据的可信度在哪里?特别是关乎于健康的,网上的信息纷繁复杂,大家很难分清哪些是真的,哪些是假的。


我之前查食物的嘌呤的时候,就遇见了,同样一个食物,app 上看到的,网上看到的都不一样,我就黑人问号了???


所以,这里就涉及到数据的权威性、真实性了。那么权威机构发布的可信度会更强。


所以我找到了卫健委颁发的数据。


地址:http://www.nhc.gov.cn/sps/c100088…


img



另外还可以看到不止痛风的资料有,还有青少年、肥胖、肾病的食养指南。


这些病其实都是慢性病,不是吃药就能马上好起来,需要长期靠饮食、运动来恢复的。


可以把这些数据用起来,后面挖掘更多需求。



2、下载数据


这一步周就是把数据下载下来,直接点击上面的


img


下载来后是个pdf 的文件,那么这一步我们就准备好了。


这里我附带一份,大家可以作为参考


暂时无法在飞书文档外展示此内容


3、处理数据


这一步是为什么了,是因为目前在所有的 AI 编程工具里面,pdf 是读取不了的,特别是 Cursor 里面。


目前能够读取的是 markdown 格式的数据



markdown 格式的数据很简单,就是纯文本,加上一些符号,就可以做成标题显示


不懂的可以直接问题 AI 工具就行了。



这里就可以看到大模型给我们的解释了。


插曲

我不懂 markdown 是什么,帮我解释下,我一点都不懂这个

在 Cursor 里面使用 ask 模式来提问


img


下面就是一个回答的截图,如果你对里面的文字不清楚的,那么就继续问 AI 就可以了。多轮对话。


img


处理数据

这里就是需要把 pdf 转为 markdown 的数据


这里推荐使用:mineru.net/


重点在于免费,直接登录注册进来后,点击上传我们刚下载的 pdf。


img


等待上传转换完成,下一步就是在文件里面,看到转换的文件了。


点击右侧下载,就是 markdown 格式。


img


把下载好的 markdown 文件放入到项目里面的 data 目录,待会儿会需要数据处理。


img


4、修正需求文档


那么让 Cursor 给我们重新生成需求文档,这样食物的分类,还有统计,会更准确,因为现在是基于权威数据来的。


食物数据库目前是存储在 json 文件里,请根据 @成人高尿酸血症与痛风食养指南2024 年版).md 的食物嘌呤数据,再根据 @prd.md 里面的食物数据结构,生成一份数据,并获取对应的 image 图片,保存在 imgs 目录下

img


5、生成数据文件


前面我们不是讲到了。食物列表的数据需要存储在本地,也就是客户端,形式我们就采用 json 的形式



同样你不知道 json 是个啥的话,找 AI 问,或者直接 Cursor 里面提问就行了。



左边是提示词,右侧就是创建的 json 文件


食物数据库目前是存储在 json 文件里,请根据 @成人高尿酸血症与痛风食养指南2024 年版).md 的食物嘌呤数据,再根据 @prd.md 里面的食物数据结构,生成一份数据,并获取对应的 image 图片,保存在 imgs 目录下

imgimg


结果:


img


6、继续调整文件


上一步骤发现,其实只给我们列觉了 53 种食物,并不全


我需要全部的数据,那么继续


总结的有 53 种食物,但是我看 @成人高尿酸血症与痛风食养指南2024 年版).md 下的“表1-2 常见食物嘌呤含量表” 应该不止这么多,请再次阅读然后补全数据到 @foods.json 文件里

img


最后发现,总文档里总结了 180 种的食物


img


最后生成的数据文件如下:


img


6、图片问题


不过这里有个问题就是,食物对应的图片是没有办法在这里一次性完成的


我也尝试了在 Cursor 里让他帮我完成,结果些了一大堆的代码,下来的图片还对应不上。


尝试了很多方案,都不太理想。


那你说了,去搜索引擎下载了,我也想到了,不过想起来你要去搜索,然后找图片,下载,有 180 多种了,还要命名好图片名字,最后保存到目录。


想到这里,我就头大,索性干脆自己写一个,其他流程系统都帮我搞定,暂时目前只需要我人工确认图片,保证准确性。


Claude Code + 爬虫碰撞什么样的火花,3 小时搞定我的数据需求



这个小系统也还是有很大的挖掘潜力,后面也还可以做很多事情



到这里基本需求阶段就完成了,数据也准备的差不多了,下面就是进入开发阶段了。


不要看前面的文字多,那都是前戏,下面就是正戏,坐稳扶好,开奔。


第三阶段:开发+联调+测试


这里是主要的开发、联调、测试阶段,也就是在传统开发流程中会占据大部分的时间,基本一个软件/系统的开发大部分的时间都在这个里面,所以我们看看结合 AI 它的速度将会达到什么样。


== 1、前端 ==


步骤一:bolt 开发



说下为什么采用 bolt 工具来做第一步工作。


其实线下 v0、bolt、lovable 很多这种前端设计工具,那么他与 Cursor 的区别在哪里了?


1、首先通过简单的提示词,它生成的功能和 UI 基本都是很完善的,UI 很美、交互也很舒服。这种你在 Curosr 里面从零开始些是很难的。


2、这种工具一般都可以选择界面的上的元素(比如 div、button,这个就比较难),然后进行你的提示词修改,很精准,这个你在 Cursor 里面比较难做。


3、还有一个点就是前端开发的界面的定位这些大模型很难听得懂你在说啥的,所以我感觉也是这块的难度采用了上面那么多的类似的工具的诞生。



当然,如果不用这些工具,直接让 Cursor 给你出 ui 设计,然后使用 UI 设计出前端代码也可以的。


这个我看看后面用其他例子来讲解。


把上面的需求步骤的 prd.md 的需求直接粘贴到提示词框里。没问题,就可以直接点击提交了。



小技巧:看左下角有个五角星的图标,是可以美化提示词的,这个目前倒是 bolt 都有的功能。


另外还可以通过 Github 或者 Figma 来生成项目图片。



img


下面就是嘎嘎开始干活了。


img


等他写完,就可以在界面的右侧看到写完的H5程序。


界面很简单,左侧就是对话区域,右侧就是产品的展示区域


小细节:在使用移动端展示的时候,还可以选择对应的手机型号


img


步骤二:调整


1、错误修复

这个交互我觉得做的特别好,不用粘贴错误,直接就在界面上点击“Attempt fix”就可以了,这真的是纯 vibe coding ,粘贴复制都不用了。🤦♂️


如果有错误,继续就可以了。


img


2、UI 调整:主题

刚开始其实 UI 并不是太好看,我的主题色是绿色的,所以我也不知道让它弄什么样的好看。


再帮我美化下 UI 界面

就输入了上面一句话,刚开始的 UI 如下图


img


最后看下对比效果


左边是最开始生成的,右边是我让他优化后的样子。还是有很多细节优化的。


img


3、UI 修复方式一:截图

另外如果样式有问题,可以截图粘贴到对话框,然后输入提示词修改。


img


4、UI 修复方式二:选择元素

这里就是我要说的可以选择界面上的元素,然后针对某些元素进行改写



bolt 的方式这几输入提示词


v0 比较高级,选择后,可以直接修改 div 的一些样式参数,比如:宽高、字体、布局、背景、阴影。精准调节。(低代码+vibe coding)



img


经过多轮修复,觉得功能差不多了,就可以转战本地 Cursor 就继续下一步了。


步骤三:本地 Cursor 修改


1、同步代码到 Github

点击右上角的「Integrations」里的 Github。


img


下面就会提示你链登录 Github


img


接着授权就可以


img


然欧输入你需要创建的项目名称


img


2、本地下载代码

使用 git 工具把代码下载到本地



git 就类似游戏的存档工具,每一个步骤都可以存档,当有问题的时候,就可以从某个存档恢复了。


当然:这里需要提前安装好 Git,如果有不懂的可以联系我,我来帮你解决。这你就不多说了



打开你的 Github 仓库页面,复制 HTTPS 的地址


img


然后使用下面的命令,就可以下载到本地了。


git clone 你的代码仓库地址

img


下一步就是安装代码的依赖包



这里需要 nodejs 环境,同样就不多说了,不懂的可以私聊



img


下一步就是启动


img


接着就是浏览器打开上面的地址:http://localhsot:3000,就可以看见上面写好的页面。


默认打开是按照 pc 的全屏显示的,可能看着有些别扭


img


我们打开 F12,打开调试窗口,如下图


点击右侧类似电脑手机的按钮,就可以调到移动端模式显示了,还可以选择对应的机型。


img


xx 小插曲 xx


原本不想放这里的,结果还是放一下吧,刚好是解决了一个很大的问题


刚开始在 bolt 上面修改的时候,修改后一直报个错误,结果修复了很多次,还是没有解决。


img


没办法,我就在本地 Cursor 上仔细看了下代码,发现是个引号的问题。


img


我就在本地 Cursor 中快速修复了下


img


但是后面惊悚的事情来了,我去 bolt 上调整了下界面样式,结果又给我写成了引号的问题


最后我就发现,可能 bolt 目前对这类的错误还是没有意识。并且看它界面的代码,每次都是从头开始写(难怪要等好一段时间才弄完,究竟是什么设计了?)


最后索性,我仔细看了下代码,删除掉了,没啥大的影响。


目前来看 bolt 这种工具还是有点门槛,解决错误的能力还是没有 Cursor 强大,一不小心页面上的错误就在一起存在,你也不知道它改了啥。


这就需要你对代码还是有基本的认识。


步骤四:使用本地数据


首先就是把前面下载准备好的图片放到 imgs 目录下


img


在 Cursor 中让从 imgs 目录中显示图片。


img


不过这里 Cursor 还是很智能的,访问后都是 404


img


那么就直接告诉 Cursor 让他解决这个问题。


结果他一下子就找到问题所在了,需要放在 public 目录下,这个放以前你需要去搜索引擎里面找问题,并且有时候你拿让 AI 解决的问题,去搜索引擎找,基本都是牛头不对马嘴的回答。


最后还要去找官方文档看资料,不断的尝试。


imgimg


** 前端小结 **


到这里,基本前端的事情就搞完了


1、识别:识别流程,现在都是走前端模拟的流程


2、食物:这里目前应该是很全的功能了,读取本地的 json 数据,有分类标识,还有图片的展示


3、我的:个人中心有尿酸的记录,有曲线图,还有基本的体重指数记录。


img


== 2、后端 ==


步骤零:阿里云大模型准备


背景:需要使用大模型来识别图片,然后返回嘌呤的含量,所以我们需要选择一个大模型的 API 来对接。


这里选择阿里的 qwen 来对接。


登录百炼平台:bailian.console.aliyun.com/


访问API-kEY 的地址:bailian.console.aliyun.com/?tab=model#…


创建一个 API-kEY,并保存好你的 key 信息。


img


步骤一:创建必要的配置


先访问找到通义千问 API 的文档的地方


img


这里我们采用直接复制上面页面的内容,保存到项目下的 docs 目录在的 qwen.md 里面


img



这里顺便把之前的 prd.md 文档从之前的项目目录拷贝过来了



步骤二:创建后端服务模板代码


直接使用下面的提示词,就可以创建一个后端的服务



这里要想为什么要创建后端服务,


一方面主要是需要调用大模型的 API,用到一些KEY 信息,这些是需要保密的,不能在前端被人看到了。


另外一方面,后面如果需要一些登录注册服务,还有食物数据都是需要后端来存储,提供给前端。



请在项目的根目录下创建 backend 目录,在这个 backend 目录下创建一个基于fastify框架的server,保证服务没有问题


同样的不知道什么fastify技术的,找大模型聊就行。



imgimgimg


步骤三:API 文档+后端业务服务开发


重点来了,这里我就写到一个提示词里面,让他完成的


帮我接入图像理解能力,参考 @qwen.md  :
1. 现在在 @/backend 的后端服务器环境中调用ai能力,
2. 使用 .env 文件保存API_KEY,并使用环境变量中的DASHSCOPE_API_KEY.并且.env文件不能提交到git上,提交到git的可以用.env.example文件作为举例供供用户参考
3. 要求使用openai的sdk,并且前端上传base64的图片
4. 后端返回值要求返回json格式,返回的数据能够渲染识别结果中的字段,包括:食物/嘌呤值/是否适合高尿酸患者/食用建议/营养成分估算
5. 在 @/backend 目录下创建 api.md 文件,记录后端接口文档

这里我把 api.md 高亮了,这个是关键,是后面前后端联调的关键,不然 Cursor 是不知道请求字段和响应字段该怎么对接的,到时候数据不对,再来调试就比较麻烦。


所以接口文档务必保证 100% 准确,后面的调试就会很容易。


截图如下:


imgimgimg


很贴心的完成功能后,最后帮我们些了 api.md 接口文档,还进行了一些列测试,保证功能是完整的。


这里放出来,Cursor 看是怎么帮我们写这个代码的



  • 帮我们组装好了提示词

  • 根据 qwen.md 的接口文档,组装请求数据和返回数据,字段都我们的项目符合


img


== 3、联调 ==


其实这里的联调很简单了。就是一句话的事情。


因为之前的前端的拍照图片都是走的模拟的接口,没有真正的调用后端的接口,所以需要换成真正的后端接口。


刚好前面的后端服务写好了 api.md 接口文档


前端修改点,前端目录是当前根目录
1. 也需要加入请求后端的 url 的环境变量,本地调试就默认使用 localhost,线上发布的时候设置环境变量后,前端服务从环境变量获取 url 然后请求到对应的后端服务
2. 食物识别的接口参考 @api.md 文档,请修改需要适配的地方,食物识别的代码在 @identify-page.tsx代码中。

imgimgimg


这里要说的是:前面的 api.md 接口文档些的非常准备,这一步的前端请求后端接口,基本都是一遍过,所以后端提供的接口文档一定要准确,这样前端就可以很准确的调用接口传参和取返回值了。


== 4、测试 ==


其实到这里,基本测试的工作也就完成了。


基本的流程到现在都是跑通的。


不过还是需要多实际测试,这里下面的例子就是,我上传了「黄瓜」的照片,结果没识别,按理说不应该呀。


这里上了点专业的技巧,通过 F12 的调试窗口,看下接口返回的数据。



按照以往经验来说,估计是字段对应不上



img


所以我就直接和 Cursor 说,可能是字段对应不上。请帮我修复。


测试黄光的食物的时候,后台接口返回的数据是 "purine_level": "low(低嘌呤<50mg)",但是 @getPurineLevel() @getPurineLevelText 没有识别到,请帮我修复

最后从前后端都给我做了修复,字段的匹配对应上了。


img


最后的总结如下:


img


== 4、总结 ==


其实到这里基本功能就完成了。



  1. 前端使用 bolt 工具等生成,快速生成漂亮的 UI 界面和基本完整的前端功能



    1. bolt工具调整样式、UI 等细节(擅长的)

    2. Cursor 精修前端小细节



  2. Cursor 开发完整后端功能



    1. 写清楚需求,如果知道具体技术栈是最好的

    2. 写好接口文档,最好人工校验下



  3. 前后端联调



    1. @使用后端的接口文档,最好写改动的接口的地方,前后精准对接

    2. 学会使用浏览器的 F12 调试窗口,特备是接口的请求参数和响应值的学习。




就目前来看,如果你是零基础,那么基本的术语不明白的话,有些问题可能会不好解决



  1. 寻求 AI 的帮助,遇事不决问 AI,它可以帮你搞定

  2. 寻求懂行的人来帮助你,比如环境的事情、按照的事情有时候一句话就可以给你讲明白的。


第四阶段:部署+上线


部署这一块其实对普通人门槛还比较高的,问题比较多。



  • 域名问题

  • 服务器问题

  • 如何部署,如何配置


这里我们采用云厂商的部署服务,简化配置文件和部署的流程


但是域名申请还是需要提前准备好的,不过现在我们用的这个云服务暂时现在没有的域名,也有临时域名可以先用。


到这里,其实如果你只是本地看的话,就已经可以了,那么这里我们教一个上线部署的步骤,傻瓜式的,不需要各种配置环境。


我相信大家如果搞独立开发的 Vercel 肯定都熟悉了。这里也介绍下类似的工具,railway.com/,他不仅可以部署前端静态页面,还有后端服务,PostgreSQL、Redis 等数据库也支持一键部署。


img


1、项目的配置文件


railway 部署是需要一些配置文件的,当然我们可以让 Cursor 帮我们搞定。


直接告诉 Cursor 我们需要部署到 railway 上,看还需要什么工作可以做的。


后端


@/backend 这个后端项目现在需要在railway上去部署,请帮我看看需要哪些部署配置

imgimgimg


前端


也是一样,让 Cursor 给我们生成部署的配置文件


当前目录是前端目录,也需要添加railway的部署相关配置

imgimgimg


Cursor 会帮我们创建需要的配置文件,那么就可以进入下一步部署了。


2、提交代码


记得要提交代码,在 Cursor 的页面添加提交代码,推送代码到 Github 上,这样 railway 才可以拉取到代码。


提交代码的时候,可以使用 AI 生成提交信息,也可以自己填写信息


imgimg


记得还要同步更改


imgimg


3、railway 页面操作


现在会有赠送的额度,并且免费就用也有 512M 的内存机器使用。对于当前下的足够了。


注册登录后,选择 Dashboard 后,点击添加,就可以看到如下的页面,


添加 Github 项目,后续就会授权等操作,继续完成就可以。


img


下一步就一个你的项目


然后就会跳转到工作区间,会自动部署。


img


记得不要忘记环境变量


img


就是在「Variables」标签下,直接添加变量就行。


添加完记得需要重新部署下。


后端环境变量


img


前端环境变量


img


当然不过你有错误,可以把 log 里面的错误复制,粘贴到 Cursor 里面,让他解决,我之前部署的项目有个就有问题,通过这个方式,帮我解决了。


4、大功告成


部署完成后怎么访问了,切换到 settings 页面,有个 Networking 部分,可以生成一个 railway 自带的域名,用这个域名就可以访问了,如果你有自己的域名还可以添加一个自己的域名,添加完以后就可以自己访问了。


img


5、总结


很开心,跟我走到了这里,基本到这里,算是完成一大步,也就是我们的 MVP 完成了。


现在我们再来总结下前面整体的步骤


1、前端我们通过 bolt 来生成代码,加速前端的设计,让 bolt 这种工具提供我们更多的能力,发挥他的有点


2、后端使用 Cursor 来开发,纯业务逻辑通过提示词还是很好的达到效果。


3、前后端联调,写好接口文档,让 Cursor 必须阅读接口文档,前端再写接口


4、部署配置文件也可以通过 Cursor 来搞定,无所不能


5、中间有任何问题,有任何不懂的都可以找 Cursor 使用 ask 模式搞定。


第五阶段:运营维护+推广


分了「优化」「安全」「推广」三个部分来说这个事情。



  • 其实到这里是后续的常态,你不要不断的推广你的产品,去增加访问量。

  • 另外就是不断的迭代优化你的功能,提升用户体验,加强本身产品的竞争力。

  • 最后、最后、最后就是安全,这个不要忘记了,后面我也会加强后,然后去推广下我的产品,安全很重要,提前做好可以更保护你的服务器和大模型的 API-KEY。


优化


这个是上线了后发现的,就是使用手机拍的照片,一般都比较大,这张图片请求后端的时候,数据量比较大,接口超时了。


那么解决办法:


1、增加后端的请求体的大小


2、压缩图片,然后再请求后端接口


安全


其实这里还是蛮重要的,因为你的服务,还有你的大模型的 KEY,如果服务器被攻击是要付出代价的,最重要的是花掉你的钱呀。


所以这块我还在做,目前想的就是让 Cursor 正题 revivew 代码,看下有什么安全隐患,给我一些解决方案。


推广


如果你的产品上线后,需要写文章、发小红书去推广,首先从你的种子用户开始,你的微信群,你的朋友圈都是可以的。


后面积极听取用户心声,持续解决痛点需求,满足用户的痛点,产品就会越来越好。


第六阶段:成本计算


时间成本


从开始到结束上线,手机使用正式的域名访问,大概就是整一天的时间,从早上开始,忙到晚上我就开启了测试,晚上搞完还去外面遛弯了一大圈回来的。


我们就算:10 小时


人力成本


哈哈哈哈,很清楚,就我一个人


软件成本


bolt:20 元优惠包月(海鲜市场),就算正式渠道,20 刀一个月,当然有免费额度,调整不多,基本够用


Cursor:150教育优惠(海鲜市场),就算正式渠道,20 刀一个月,足足够用


域名:32首年


我们就算满的,折算成人民币,也就是 300 块


想想 300 块一天你就做出来一个系统(前后端+部署),何况软件都是包月的,一个月你可以产出很多东西,不止这个一个系统。


对比公司开发,一个月的成本前后端两个人,毕业生也的上万了吧,何况还是 5 年经验开发的(市面上的抢手货)。


总结


能走到这里的,我希望你给自己一个掌声,确实不容易。


我希望你也有可以通过编程来实现自己的想法和创意。


虽然目前编程对于零基础的人来说确实可能会有些吃劲,但是你我差距也不大,我现在遇到了很多在搞 AI 编程的都是程序员,有房地产行业的、也有产品的。


遇事不决,问 AI


我希望你可以记住这句话,自己的创意+基本问题找 AI,你基本就可以解决 99% 的问题,剩下的 1% 你基本遇不到,遇到了,也不要慌,身边这么多牛人总会有人知道。


作者:志辉AI编程
来源:juejin.cn/post/7517496354244067339
收起阅读 »

一行生成绝对唯一 ID:别再依赖 Date.now() 了!

web
在前端开发中,“生成唯一 ID” 是高频需求 —— 从列表项标识、表单临时存储,到数据缓存键值,都需要一个 “绝对不重复” 的标识符。但看似简单的需求下,藏着很多容易踩坑的实现方式,稍有不慎就会引发数据冲突、逻辑异常等问题。 今天我们就来拆解常见误区,带你掌握...
继续阅读 »

在前端开发中,“生成唯一 ID” 是高频需求 —— 从列表项标识、表单临时存储,到数据缓存键值,都需要一个 “绝对不重复” 的标识符。但看似简单的需求下,藏着很多容易踩坑的实现方式,稍有不慎就会引发数据冲突、逻辑异常等问题。


今天我们就来拆解常见误区,带你掌握真正可靠的唯一 ID 生成方案。


一、为什么 “唯一 ID” 比想象中难?


唯一 ID 的核心要求是 “全局不重复”,但前端环境的特殊性(无状态、多标签页、高并发操作),让很多看似合理的方案在实际场景中失效。


下面两种常见实现,其实都是 “伪唯一” 陷阱。


❌ 误区 1:时间戳 + 随机数(Date.now() + Math.random())


很多开发者会直觉性地将 “时间唯一性” 和 “随机唯一性” 结合,写出这样的代码:


// 错误示例:看似合理的“伪唯一”方案
function generateNaiveId() {
// 时间戳转36进制(缩短长度)+ 随机数截取
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// 示例输出:l6n7f4v2am50k9m7o4

这种方案的缺陷在高并发场景下会暴露无遗:



  • 时间戳精度不足Date.now() 的精度是毫秒级(1ms),如果同一毫秒内调用多次(比如循环生成、高频接口回调),ID 的 “时间部分” 会完全重复;

  • 伪随机性风险Math.random() 生成的是 “非加密级随机数”,其算法可预测,在短时间内可能生成重复的序列,进一步增加冲突概率。


结论:仅适用于低频次、非核心场景(如临时展示用 ID),绝对不能用于生产环境的核心数据标识


❌ 误区 2:全局自增计数器


另一种思路是维护一个全局变量自增,看似能保证 “有序唯一”:


// 错误示例:自增计数器方案
let counter = 0;
function generateIncrementId() {
return `id-${counter++}`;
}
// 示例输出:id-0、id-1、id-2...

但在浏览器环境中,这个方案的缺陷更致命:



  • 无状态丢失:页面刷新、路由跳转后,counter 会重置为 0,之前的 ID 序列会重复;

  • 多标签页冲突:用户打开多个相同页面时,每个页面的 counter 都是独立的,会生成完全相同的 ID(比如两个页面同时生成 id-0)。


结论:浏览器环境中几乎毫无实用价值,仅能用于单次会话、单页面的临时标识。


二、王者方案:一行代码实现绝对唯一 —— crypto.randomUUID()


既然简单方案不可靠,我们需要借助浏览器原生提供的 “加密级” 能力。crypto.randomUUID() 就是 W3C 标准推荐的官方解决方案,彻底解决 “唯一 ID” 难题。


1. 用法:一行代码搞定


crypto 是浏览器内置的全局对象(无需引入任何库),专门提供加密相关能力,randomUUID() 方法可直接生成符合 RFC 4122 v4 规范 的 UUID(通用唯一标识符):


// 正确示例:生成绝对唯一ID
const uniqueId = crypto.randomUUID();
// 示例输出:3a6c4b2a-4c26-4d0f-a4b7-3b1a2b3c4d5e

2. 为什么它是 “绝对唯一” 的?


crypto.randomUUID() 的可靠性源于三个核心优势:



  • 极低碰撞概率:v4 UUID 由 122 位随机数构成,组合数量高达 2^122(约 5.3×10^36),相当于 “在地球所有沙滩的沙粒中,选中某一颗特定沙粒” 的概率,实际场景中碰撞概率趋近于 0;

  • 加密级随机性:基于 “密码学安全伪随机数生成器(CSPRNG)”,随机性远优于 Math.random(),无法被预测或破解,避免恶意伪造重复 ID;

  • 跨环境兼容:生成的 UUID 是全球通用标准格式(8-4-4-4-12 位字符),前端、后端(Node.js、Java 等)、数据库(MySQL、MongoDB)都能直接识别,无需格式转换。


3. 兼容性:覆盖所有现代环境


crypto.randomUUID() 的支持范围已经非常广泛,完全满足绝大多数新项目需求:



  • 浏览器:Chrome 92+、Firefox 90+、Safari 15.4+(2022 年及以后发布的版本);

  • 服务器:Node.js 14.17+(LTS 版本均支持);

  • 框架:Vue 3、React 18、Svelte 等现代框架无任何兼容性问题。


三、兼容性兜底方案(针对旧环境)


如果需要兼容旧浏览器(如 IE11)或低版本 Node.js,可以使用第三方库 uuid(轻量、无依赖),其底层逻辑与 crypto.randomUUID() 一致:


安装依赖:


npm install uuid
# 或 yarn add uuid

使用方式:


// 旧环境兜底方案
import { v4 as uuidv4 } from 'uuid';
const uniqueId = uuidv4();
// 示例输出:同标准UUID格式

四、总结:唯一 ID 生成的 “最佳实践”



对于 2023 年后的新项目,直接使用 crypto.randomUUID() 即可 —— 一行代码、零依赖、绝对可靠,彻底告别 “ID 重复” 的烦恼!


作者:大知闲闲i
来源:juejin.cn/post/7561781514922688522
收起阅读 »

前端终于不用再写html,可以js一把梭了,我的ovs(不写html,兼容vue)的语法插件终于上线了

web
OVSJS 语法预览 语法是这样的: 项目资源 GitHub 地址: ovsjs 示例代码 (hello.ovs) VS Code 语法提示插件: ovs-vscode-client 下载 模仿的kotlin的语法 欢迎大家体验交流! 卫星 (We...
继续阅读 »

OVSJS 语法预览


语法是这样的:


语法示例图


项目资源





模仿的kotlin的语法


image.png


欢迎大家体验交流!



  • 卫星 (WeChat): alamhubb


如果你对这个语法感兴趣欢迎和我交流,谢谢


dc35feef2cb4dae6c8678cd89571378e.png


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

为何前端圈现在不关注源码了?

web
大家好,我是双越。前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP。我的代表作有: wangEditor 开源 web 富文本编辑器,GitHub 18k star,npm 周下载量 20k 划水AI Node 全栈 AIGC 知识库,包括 AI 写作、...
继续阅读 »

大家好,我是双越。前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP。我的代表作有:



  • wangEditor 开源 web 富文本编辑器,GitHub 18k star,npm 周下载量 20k

  • 划水AI Node 全栈 AIGC 知识库,包括 AI 写作、多人协同编辑。复杂业务,真实上线。

  • 前端面试派 系统专业的面试导航,刷题,写简历,看面试技巧,内推工作。开源免费。


开始


大家有没有发现一个现象:最近 1-2 年,前端圈不再关注源码了。


最近 Vue3.6 即将发布,alien-signal 不再依赖 Proxy 可更细粒度的实现响应式,vapor-model 可以不用 vdom 。


Vue 如此大的内部实现的改动,我没发现多少人研究它的源码,我日常关注的那些博客、公众号也没有发布源码相关的内容。


这要是在 3 年之前,早就开始有人研究这方面的源码了,博客一篇接一篇,跟前段时间的 MCP 话题一样。


还有前端工具链几乎快让 Rust 重构一遍了,rolldown turbopack 等产品使得构建效率大大提升。这要是按照 3 年之前对 webpack 那个研究态度,你不会 rust 就不好意思说自己是前端了。


不光是这些新东西,就是传统的 Vue React 等框架源码现在也没啥热度了,我关注每日的热门博客,几乎很少有关于源码的文章了。


这是为什么呢?


泡沫


看源码,其实是一种泡沫,现在破灭了。所谓泡沫,就是它的真实价值之前一直被夸大,就像房地产泡沫。


前几年是互联网发展的红利期,到处招聘开发人员,大家都拿着高工资,随便跳槽就能涨薪 20% ,大家就会误以为真的是自己的能力值这么多钱。


而且,当年面试时,尤其是大公司,为了筛选出优秀的候选人(因为培训涌入的人实在太多),除了看学历以外,最喜欢考的就是算法和源码。


确实,如果一个技术人员能把算法和源码看明白,那他肯定算是一个合格的程序员,上限不好说,但下限是能保证的。就像一个人名牌大学毕业的,他的能力下限应该是没问题的。


大公司如此面试,其他公司也就跟风,面试题在网络上传播,各位程序员也就跟风学习,很快普及到整个社区。


所以,如果不经思考,表面看来:就是因为我会算法、会源码,有这些技能,才拿到一个月几万甚至年薪百万的工资。


即,源码和算法价值百万。


现状


现在泡沫破灭了。业务没有增长了,之前是红利期,现在是内卷期,之前大量招聘,现在大量裁员。


你看这段时间淘宝和美团掐架多严重,你补贴我补贴,你广告我也广告。如果有新业务增长,他们早就忙着去开疆拓土了,没公司在这掐架。


面试少了,算法和源码也就没有发挥空间了。关键是大家现在才发现:原来自己会算法会源码,也会被裁员,也拿不到高工资了。


哦,原来之前自己的价值并不是算法和源码决定的,最主要是因为市场需求决定的。哪怕我现在看再多的源码,也少有面试机会,那还看个锤子!


现在企业预算缩减,对于开发人员的要求更加返璞归真:降低工资,甚至大量使用外包人员代替。


所以开发人员的价值,就是开发一些增删改查的日常 web 或 app 的功能,什么算法和框架源码,真实的使用场景太少。


看源码有用吗?


答案当然是肯定的。学习源码对于提升个人技术能力是至关重要的,尤其是对于初学者,学习前辈经验是个捷径。


但我觉得看 Vue react 这些源码对于开发提升并不会很直接,它也许会潜移默化的提升你的“内功”,但无法直接体现在工作上,除非你的工作就是开发 Vue react 类的框架。


我更建议大家去看一些应用类的源码,例如 UI 组件库的源码看如何封装复杂组件,例如 vue-admin 看如何封装一个 B 端管理后台。


再例如我之前学习 AI Agent 开发,就看了 langChain 提供的 agent-chat-ui 和 Vercel 提供的 ai-chatbot 这两个项目的源码,我并没有直接看 langChain 的源码。


找一些和你实际开发工作相关的一些优秀开源项目,学习他们的设计,阅读他们的源码,这是最直接有效的。


最后


前端人员想学习全栈 + AI 项目和源码,可关注我开发的 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线。


作者:前端双越老师
来源:juejin.cn/post/7531888067218800640
收起阅读 »

我们需要前端架构师这个职位吗?

前端架构师,这个岗位,在我的技术体系的认知中,是不需要这样的一个岗位的。但是市面上,我发现,在招聘的需求中,很多企业,依然在招聘前端架构师这个岗位。 为什么市场中,依然会有这个岗位呢? 真实的场景中,这个岗位的设立,是否真的是合理的呢? 作为前端出身的同学,这...
继续阅读 »

前端架构师,这个岗位,在我的技术体系的认知中,是不需要这样的一个岗位的。但是市面上,我发现,在招聘的需求中,很多企业,依然在招聘前端架构师这个岗位。


为什么市场中,依然会有这个岗位呢?


真实的场景中,这个岗位的设立,是否真的是合理的呢?


作为前端出身的同学,这是我一直在思考的一个问题,因为这个问题,关乎一个人的职业发展,代表了我们前端的同学,职业的道路的天花板,到底在哪里。


如果前端架构师,这个岗位,本身是不应该出现的,那么我们前端的职业道路,就不应该走纯技术路线。真正应该走的是,技术兼管理的路线,也就是应该走前端leader的角色,再往上就是技术总监/CTO的职业角色。


如果前端架构师,这个岗位,是应该出现,那么前端的职业道路,其实完全可以走纯技术路线,从开发到前端架构师,然后持续深耕。


不同的方向,对人的要求是不一样的。


走纯技术路线,对一个人的更大要求是专注,持续的技术学习力,持续的自我技术进步,其他方面,作为辅助,协助你的技术能力在职场中进行发挥,你就能在这个行业和岗位上,有一定的立足之地。


走leader的路线,对一个人的要求是全面,关注点在技术,人,事务,项目,管理等等一系列比较杂乱的事情上,它注定了不能过于专注,而是站在高点,俯瞰整个大盘,才能真正的把事情做好。


作为自己,一直以来,努力的方向,也是走技术leader的路线。


真实的市场场景是什么样?


有一句很经典的话,世界是由一帮草台班子组成的。


这句话反馈到真实的工作场景中,就变成了这样的现状。


大多数的人的技术水准,真的让人一言难尽,很多人,真的也就是把技术,当作一份吃饭的差事,大多数人对技术,并没有追逐的热情。


我们这个行业,有太多的小公司了,小公司招聘人的标准,和大公司比,也是真的一言难尽,我见过有些公司,用四五千的薪资,招聘了一些人做事,真的是啥也不会。


大量的中小型企业,招聘技术人员,薪资大概给1万左右,这类研发人员,大多数的水准,大概就是能把项目做出来,会一些框架,仅此而已。


但是我们日常中,遇到的业务问题,往往是复杂的,比如前端的复杂表单问题,不同环境的运行容器差异问题,各种各样的兼容问题,复杂的数据处理和渲染问题等等。这类问题,其实在日常的开发中,很常见,但是往往很多人对此,难以处理。


这个时候,很多企业,潜意识就觉得,招聘一个技术更厉害的人,这个时候,前端架构师这个岗位,其实就出现了,而这也是市场中,需要这个岗位的现实情况。


不同规模企业的前端组织架构到底有哪些差异?


在大公司,我看到的更多的是,前端技术leader的岗位,一般而言,是由一个技术leader,带领团队,完成业务。


比如阿里,比较有钱,一般一个团队中,p8是前端leader,p6/p7是做事的主力,配备部分p4/p5的同学,一起完成业务。整体大概是,一个leader负责统筹全局,团队中真正做事的同学,完全由能力驾驭自己的业务。


再中小型公司,我看到的是,因为成本的原因,招聘的工程师偏于初中级,然后团队中,有那么一两位高级工程师作为主力成员。有些时候,这些高级工程师还会担任leader的角色。


但是这类团队有一些问题,就是因为技术能力问题,无法真正的做到,对企业的业务负责。


1、针对业务场景,无法给出合理的方案/方法。经常性的在日常工作中,这类团队,会提到这个需求改动太大,这个方案没法实现,这个东西做不了等等。但是其实正常的场景中,业务方提出一个需求,本身是有一定的运营目标/目的,这个时候,应该从业务的角度出发,再结合我们互联网技术,针对这个运营目标,提出我们的产品/技术方案,然后执行。


2、无法做出合理的判断。在大多数的场景中,我们对代码的要求是,高内聚和低耦合,代码的结构要清晰,可维护要高。但是还应该有技术之外的一些判断,我们完成一项业务,应该团队配备什么样的成员,市场中哪类程序员好招聘,技术的选型和业务是不是最优解,在人员、技术,业务、成本,这类问题中,如何达到更高的效率最优。


在中小型公司,大家习惯性的把问题简单化,做不了,判断不了,以为招聘一个技术更厉害的人,就能解决当下问题。


那到底需要前端架构师吗?


这个答案很显然,其实当下的市场中,市场有这样的职位诉求,原因就是一些复杂问题,很多企业搞不定。(但是这个问题的解决之道,不在于招聘前端架构师这么一个岗位,而是在于团队内在的一些问题,这些问题恰恰是需要一个前端leader来解决的)


从职业发展的角度来说,其实是不需要前端架构师的。


我们从技术的角度,来分析一下为什么不需要前端架构师。


前端的职责,是对UI负责,我们的工作,主要是针对,不同的容器环境(浏览器、手机app、桌面端内嵌h5等等)、不同的技术(RN、react、小程序等等),实现不同的端上的产品(App、小程序、网页、桌面端应用),我们通过接口协议,同后端进行数据通信。


端上的页面,主体是运行在用户端的设备上,最大的障碍是加载/渲染性能。接口方面,是和用户的网络环境相关。


这些东西,其实从技术的层面上,属于开发的职责。我们不得不承认,前端开发的层面,入手会比后端简单一些,但是做到一定程度,其实要求是要比后端更高的。


所以前端架构师,到底在架构什么东西?它不过就是,针对每一种端上的技术,使用的比普通开发,更好一些。


相比而言,后端有太多的策略性的东西了,哪些数据是业务数据,哪些数据是缓存数据,哪些数据要支持实时查询,哪些数据支持统计查询,后端的基础组件也多,不同的组件,擅长的事情也不一样,适用的场景其实也有差异,kafka、redis这些东西,数据分库和分表,按什么维度分,机器的运行性能,支持的量到底有多大等等。


这些就是后端架构师的存在责任,同一块代码,在不同的场景下是完全不同的。在某些场景中可能是最优解,换一个场景,可能就是最差的代码。


这就是我一直推崇的价值观,前端和后端,不一样,前端不需要架构师。


合理的团队架构到底长什么样?


如我个人所评判的那样,一个团队中,是不需要前端架构师这个角色的,那么对于那些中小型公司的人来说,到底什么样的结构,符合自身最优价值的团队结构呢?


答案是这样的,一位专家级别的前端,带领一两个干活的主力,再加上多个初中级的工程师。专家级别的工程师-也就是leader,保障我们的技术和方案,是行业内标准方案,方向是最优的,一两个高级工程师,辅助这个leader把事情能落地下去,其他所有初中级工程师,就是干活的苦力,也就是团队内的技术体力劳动者。


这也就是,我认可的职业发展中,前端需要leader的原因,但是不需要架构师。


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

别再死磕框架了!你的技术路线图该更新了

先说结论: 前端不会凉,但“只会几个框架 API”的前端,确实越来越难混 这两年“前端要凉了”“全栈替代前端”的声音此起彼伏,本质是门槛重新洗牌: 简单 CRUD、纯样式开发被低代码、模板代码和 AI 模型快速蚕食; 复杂业务、工程体系、跨端体验、AI 能力...
继续阅读 »

先说结论:


前端不会凉,但“只会几个框架 API”的前端,确实越来越难混

这两年“前端要凉了”“全栈替代前端”的声音此起彼伏,本质是门槛重新洗牌:



  • 简单 CRUD、纯样式开发被低代码、模板代码和 AI 模型快速蚕食;

  • 复杂业务、工程体系、跨端体验、AI 能力集成,反而需要更强的前端工程师去撑住。


如果你对“前端的尽头是跑路转管理”已经开始迷茫,那这篇就是给你看的:别再死磕框架版本号,该更新的是你的技术路线图。




一、先搞清楚:2025 的前端到底在变什么?


框架红海:从“会用”到“用得值”


React、Vue、Svelte、Solid、Qwik、Next、Nuxt……Meta Framework 一大堆,远远超过岗位需求。

现在企业选型更关注:



  • 生态成熟度(如 Next.js 的 SSR/SSG 能力)

  • 框架在应用生命周期中的角色(渲染策略、数据流转、SEO、部署)


趋势:



  • 框架 Meta 化(Next.js、Nuxt)将路由、数据获取、缓存策略整体纳入规范;

  • 约定优于配置,不再是“一个前端库”,而是“一套完整解决方案”。



以前是“你会 Vue/React 就能干活”,现在是“你要理解框架在整个应用中的角色”。





工具有 AI,开发方式也在变


AI 工具(如 Cursor、GitHub Copilot X)可以显著提速,甚至替代重复劳动。

真正拉开差距的变成了:



  • 你能给 AI 写出清晰、可实现的需求描述(Prompt);

  • 你能判断 AI 生成代码的质量、潜在风险、性能问题;

  • 你能基于生成结果做出合理抽象和重构。



AI 不是来抢饭碗,而是逼你从“码农”进化成“架构和决策的人”。





业务侧:前端不再是“画界面”,而是“做体验 + 做增长”



  • B 端产品:交互工程师 + 低代码拼装师 + 复杂表单处理专家;

  • C 端产品:与产品运营深度捆绑,懂 A/B 测试、埋点、Funnel 分析、广告投放链路;

  • 跨平台:Web + 小程序 + App(RN/Flutter/WebView)混合形态成为常态。



那些还在喊“切图仔优化 padding”的岗位确实在消失,但对“懂业务、有数据意识、能搭全链路体验”的前端需求更高。





二、别再死磕框架 API:2025 的前端核心能力长什么样?


基石能力:Web 原生三件套,得真的吃透


重点不是“会用”,而是理解底层原理:



  • JS:事件循环、原型链、Promise 执行模型、ESM 模块化;

  • 浏览器:渲染流程(DOM/CSSOM/布局/绘制/合成)、HTTP/2/3、安全防护(XSS/CSRF)。



这块扎实了,你在任何框架下都不会慌,也更能看懂“框架为什么这么设计”。





工程能力:从“会用脚手架”到“能看懂和调整工程栈”


Vite、Rspack、Turbopack 等工具让工程构建从“黑魔法”变成“可组合拼装件”。

你需要:



  • 看懂项目的构建配置(Vite/Webpack/Rspack 任意一种);

  • 理解打包拆分、动态加载、CI/CD 流程;

  • 能排查构建问题(路径解析、依赖冲突)。



如果你在团队里能主动做这些事,别人对你的“级别判断”会明显不一样。





跨端和运行时:不只会“写 Web 页”


2025 年前端视角的关键方向:



  • 小程序/多端框架(Taro、Uni-app);

  • 混合方案(RN/Flutter/WebView 通信机制);

  • 桌面端(Electron、Tauri)。


建议:



  • 至少深耕一个“跨端主战场”(如 Web + 小程序 或 Web + Flutter)。




数据和状态:从“会用 Vuex/Redux”到“能设计状态模型”


现代前端复杂度 70% 在“数据和状态管理”。

进阶点在于:



  • 设计合理的数据模型(本地 UI 状态 vs 服务端真相);

  • 学会用 Query 库、State Machine 解耦状态与视图。



当你能把“状态设计清楚”,你在复杂业务团队里会非常吃香。





性能、稳定性、可观测性:高级前端的硬指标


你需要系统性回答问题,而不是“瞎猜”:



  • 性能优化:首屏加载(资源拆分、CDN)、运行时优化(减少重排、虚拟列表);

  • 稳定性:错误采集、日志上报、灰度发布;

  • 工具:Lighthouse、Web Vitals、Session Replay。



这块做得好的人往往是技术骨干,且很难被低代码或 AI 直接替代。





AI 时代的前端:不是“写 AI”,而是“让 AI 真正跑进产品”


你需要驾驭:



  • 基础能力:调用 AI 平台 API(流式返回处理、增量渲染);

  • 产品思维:哪些场景适合 AI(智能搜索、文档问答);如何做权限控制、错误兜底。




三、路线图别再按“框架学习顺序”排了,按角色来选


初中级:从“会用”到“能独立负责一个功能”


目标:



  • 独立完成中等复杂度模块(登录、权限、表单、列表分页)。


建议路线:



  • 夯实 JS + 浏览器基础;

  • 选择 React/Vue + Next/Nuxt 做完整项目;

  • 搭建 eslint + prettier + git hooks 的开发习惯。




进阶:从“功能前端”到“工程前端 + 业务前端”


目标:



  • 优化项目、推进基础设施、给后端/产品提技术方案。


建议路线:



  • 深入构建工具(Webpack/Vite);

  • 主导一次性能优化或埋点方案;

  • 引入 AI 能力(如智能搜索、工单回复建议)。




高级/资深:从“高级前端”到“前端技术负责人”


目标:



  • 设计技术体系、推动长期价值。


建议路线:



  • 明确团队技术栈(框架、状态管理、打包策略);

  • 主导跨部门项目、建立知识分享机制;

  • 评估 AI/低代码/新框架的引入价值。




四、2025 年不要再犯的几个错误



  1. 只跟着热点学框架,不做项目和抽象



    • 选一个主战场 + 一个备胎(React+Next.js,Vue+Nuxt.js),用它们做 2~3 个完整项目。



  2. 完全忽略业务,沉迷写“优雅代码”



    • 把重构和业务迭代绑一起,而不是搞“纯技术重构”。



  3. 对 AI 持敌视和逃避态度



    • 把重复劳动交给 AI,把时间投到架构设计、业务抽象上。



  4. 把“管理”当成唯一出路



    • 做前端架构、性能优化平台、低代码平台的技术专家,薪资和自由度不输管理岗。






五、一个现实点的建议:给自己的 2025 做个“年度规划”


Q1:



  • 选定主技术栈(React+Next 或 Vue+Nuxt);

  • 做一个完整小项目(登录、权限、列表/详情、SSR、部署)。


Q2:



  • 深入工程化方向(优化打包体积、搭建监控埋点系统)。


Q3:



  • 选一个业务场景引入 AI 或配置化能力(如智能搜索、低代码表单)。


Q4:



  • 输出和沉淀(写 3~5 篇技术文章、踩坑复盘)。




最后:别问前端凉没凉,先问问自己“是不是还停在 2018 年的玩法”



  • 如果你还把“熟练掌握 Vue/React”当成简历亮点,那确实会焦虑;

  • 但如果你能说清楚:

    • 在复杂项目里主导过哪些工程优化;

    • 如何把业务抽象成可复用的组件/平台;

    • 如何在产品里融入 AI/多端/数据驱动;

      那么,在 2025 年的前端市场,你不仅不会“凉”,反而会成为别人眼中的“稀缺”。




别再死磕框架了,更新你的技术路线图,从“写页面的人”变成“打造体验和平台的人”。这才是 2025 年前端真正的进化方向。


作者:纯爱掌门人
来源:juejin.cn/post/7573694361474629659
收起阅读 »

别再吹性能优化了:你的应用卡顿,纯粹是因为产品设计烂🤷‍♂️

web
大家好! 最近面试,我发现一个很有意思的事情。几乎每个高级前端的简历上,都专门开辟了一栏,叫性能优化。 里面写满了各种高大上的名词😖: 使用Virtual List(虚拟列表)优化长列表渲染... 使用Web Worker把复杂计算移出主线程... 使用WA...
继续阅读 »

image.png


大家好!


最近面试,我发现一个很有意思的事情。几乎每个高级前端的简历上,都专门开辟了一栏,叫性能优化


里面写满了各种高大上的名词😖:



使用Virtual List(虚拟列表)优化长列表渲染...


使用Web Worker把复杂计算移出主线程...


使用WASM重写核心算法...



看着这些,我通常会问一个问题:


你为什么要渲染一个有一万条数据的列表?用户真的看得过来吗?


候选人通常会愣住,然后支支吾吾地说:“呃...这是我们产品经理要求的🤷‍♂️。”


这就是今天我想聊的话题:


在2025年的今天,前端领域90%的所谓性能瓶颈,根本不是技术问题,而是产品问题。


我们这群工程师,拿着最先进的前端技术(Vite, Rust, WASM),却在日复一日地给一坨屎💩(糟糕的产品设计)雕花。




我们正在解决错误的问题


让我们还原一个经典的性能优化现场吧👇。


场景:一个中后台的超级表格(默认大家应该比较熟悉🤔)。


产品经理说需求:这个表格要展示所有订单,大概有50列,每页要展示500条,而且要支持实时搜索,还要支持列拖拽,每个单元格里可能还有下拉菜单...


image.png


开发者的第一反应(技术视角)



  • 50列 x 500行 = 25000个DOM节点,浏览器肯定卡死。

  • 快!上虚拟滚动(Virtual Scroll)!

  • 快!上防抖(Debounce)!

  • 快!上Memoization(缓存)!


我们为了这个需求,引入了复杂的 第三方库,写了晦涩难懂的优化代码,甚至为了解决虚拟滚动带来的样式问题(比如高度坍塌、定位异常),又打了一堆补丁。


最后,页面终于不卡了。我们觉得自己很牛逼,技术很强。


但我们从来没问过那个最核心的问题:



人类的视网膜和大脑,真的能同时处理50列 x 500行的数据吗?



答案是:不能。


当屏幕上密密麻麻挤满了数据时,用户的认知负荷已经爆表了。他根本找不到他要看的东西。他需要的不是高性能的渲染,他需要的是筛选搜索


我们用顶级的技术,去实现了一个反人类的设计。 这不是优化,这是叫作恶😠。




真正的优化,是从砍需求开始


我曾经接手过一个类似的项目,页面卡顿到FPS只有10。前任开发留下了几千行用来优化渲染的复杂代码,维护起来生不如死。


我接手后,没有改一行渲染代码。


我直接去找了产品总监,把那个页面投在大屏幕上,问了他三个问题:


1.你看这一列 订单原始JSON日志,平均长度3000字符,你把它全展示在表格里,谁会看?


砍掉!改成一个查看详情的按钮,点开再加载。DOM节点减少20%。


2.这50列数据,用户高频关注的真的有这么多吗?


默认只展示核心的8列。剩下的放在自定义列里,用户想看自己勾选。DOM节点减少80%。


3.我就不知道为什么🤷‍♂️ 要一次性加载500条?用户翻到第400条的时候,他还记得第1条是什么吗?


赶紧砍掉!改成标准的分页,每页20条。DOM节点减少96%。


做完这三件事,我甚至把之前的虚拟滚动代码全删了,回退到了最朴素的<table>标签。


结果呢?



  • 页面飞一样快(因为DOM只有原来的1%)。

  • 代码极其简单(维护就更简单了🤔)。

  • 用户反而更开心了(因为界面清爽了,信息层级清晰了)。


这才是最高级的性能优化:不仅优化了机器的性能,更优化了人的体验。




技术自负的陷阱


为什么我们总是陷在技术优化的泥潭里出不来呢?😒


因为我们有技术自负


作为工程师,我们潜意识里觉得:承认这个需求做不了(或者做不好),是因为我技术不行。


产品经理要五彩斑斓的黑,我就得给他做出来!


产品经理要在这个页面跑3D地球,我就得去学Three.js!


我们试图用技术去弥补产品逻辑上的懒惰!(非常有触感😖)


因为产品经理懒得思考信息的层级 ,所以他把所有信息一股脑扔给前端,让你去搞懒加载。


技术不是万能的。


浏览器的渲染能力是有上限的,JS的主线程是单核的,移动端的电量是有限的。更重要的是,用户的注意力是极其有限的。


当你发现你需要用极其复杂的新技术才能勉强让一个页面跑起来的时候


请停下来!


stOpStopstopstOpstOp.gif


这时候,问题的根源通常不在代码里,而可能是在 PRD(需求文档) 里。




说了那么多,该怎么做呢?


下次,当你再面对一个导致卡顿的需求时,别急着打开Profiler分析性能。


请试着做以下几步:


我们真的需要在前端处理10万条数据吗?能不能在后端聚合好,只给我返回结果?


这个图表真的需要实时刷新吗?用户真的能看清1毫秒的变化吗?改成5秒刷新一次行不行?


在这个弹窗里塞个完整地图太卡了。能不能改成:点击缩略图,跳转到专门的地图页面?


你要告诉产品经理: 性能本身,也是一个产品功能。


如果为了塞下更多的功能,牺牲了流畅度这个最核心的功能,那是丢了西瓜捡芝麻。




最好的代码,是 没有代码(No Code)


同理,最好的性能优化,是没有需求


作为高级工程师,你的价值不仅仅体现在你会写Virtual List,更体现在你敢不敢在需求评审会上,拍着桌子说:


这个设计怎么这么反人类😠!我们能不能换个更好的方式?🤷‍♂️


别再给屎山💩雕花了。把那座山推了,才是真正的优化。


关于这个观点你们怎么看?


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

别再滥用 Base64 了——Blob 才是前端减负的正确姿势

web
一、什么是 Blob? Blob(Binary Large Object,二进制大对象)是浏览器提供的一种不可变、类文件的原始数据容器。它可以存储任意类型的二进制或文本数据,例如图片、音频、PDF、甚至一段纯文本。与 File 对象相比,Blob 更底层,Fi...
继续阅读 »

一、什么是 Blob?


Blob(Binary Large Object,二进制大对象)是浏览器提供的一种不可变、类文件的原始数据容器。它可以存储任意类型的二进制或文本数据,例如图片、音频、PDF、甚至一段纯文本。与 File 对象相比,Blob 更底层,File 实际上继承自 Blob,并额外携带了 namelastModified 等元信息 。


Blob 最大的特点是纯客户端、零网络:数据一旦进入 Blob,就活在内存里,无需上传服务器即可预览、下载或进一步加工。




二、构造一个 Blob:一行代码搞定


const blob = new Blob(parts, options);

参数说明
parts数组,元素可以是 StringArrayBufferTypedArrayBlob 等。
options可选对象,常用字段:
type MIME 类型,默认 application/octet-stream
endings 是否转换换行符,几乎不用。

示例:动态生成一个 Markdown 文件并让用户下载


const content = '# Hello Blob\n> 由浏览器动态生成';
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = url;
a.download = 'hello.md';
a.click();

// 内存用完即弃
URL.revokeObjectURL(url);



三、Blob URL:给内存中的数据一个“临时地址”


1. 生成方式


const url = URL.createObjectURL(blob);
// 返回值样例
// blob:https://localhost:3000/550e8400-e29b-41d4-a716-446655440000

2. 生命周期



  • 作用域:仅在当前文档、当前会话有效;页面刷新、close()、手动调用 revokeObjectURL() 都会使其失效 。

  • 性能陷阱:不主动释放会造成内存泄漏,尤其在单页应用或大量图片预览场景 。


最佳实践封装:


function createTempURL(blob) {
const url = URL.createObjectURL(blob);
// 自动 revoke,避免忘记
requestIdleCallback(() => URL.revokeObjectURL(url));
return url;
}



四、Blob vs. Base64 vs. ArrayBuffer:如何选型?


场景推荐格式理由
图片回显、<img>/<video>Blob URL浏览器可直接解析,无需解码;内存占用低。
小图标内嵌在 CSS/JSONBase64减少一次 HTTP 请求,但体积增大约 33%。
纯计算、WebAssembly 传递ArrayBuffer可写、可索引,适合高效运算。
上传大文件、断点续传Blob.slice流式分片,配合 File.prototype.slice 做断点续传 。



五、高频实战场景


1. 本地图片/视频预览(零上传)


<input type="file" accept="image/*" id="uploader">
<img id="preview" style="max-width: 100%">

<script>
uploader.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
preview.src = url;
preview.onload = () => URL.revokeObjectURL(url); // 加载完即释放
};
</script>

2. 将 Canvas 绘图导出为 PNG 并下载


canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'snapshot.png';
a.click();
URL.revokeObjectURL(url);
}, 'image/png');

3. 抓取远程图片→Blob→本地预览(跨域需 CORS)


fetch('https://i.imgur.com/xxx.png', { mode: 'cors' })
.then(r => r.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
document.querySelector('img').src = url;
});

若出现图片不显示,99% 是因为服务端未返回 Access-Control-Allow-Origin 头 。




六、踩坑指南与性能锦囊


坑点解决方案
内存暴涨每次 createObjectURL 后,务必在合适的时机 revokeObjectURL
跨域失败确认服务端开启 CORS;fetch 时加 {credentials: 'include'} 如需 Cookie。
移动端大视频卡顿避免一次性读完整文件,使用 blob.slice(start, end) 分段读取。
旧浏览器兼容IE10+ 才原生支持 Blob;如需更低版本,请引入 Blob.js 兼容库。



七、延伸:Blob 与 Stream 的梦幻联动


当文件超大(GB 级)时,全部读进内存并不现实。可以借助 ReadableStream 把 Blob 转为流,实现渐进式上传:


const stream = blob.stream(); // 返回 ReadableStream
await fetch('/upload', {
method: 'POST',
body: stream,
headers: { 'Content-Type': blob.type }
});

Chrome 85+、Edge 85+、Firefox 已经支持 blob.stream(),能以流式形式边读边传,内存占用极低。




八、总结:记住“三句话”



  1. Blob = 浏览器端的二进制数据仓库,File 只是它的超集。

  2. Blob URL = 指向内存的临时指针,用完后必须手动或自动释放。

  3. 凡是“本地预览、零上传、动态生成下载”的需求,优先考虑 Blob + Blob URL 组合。


用好 Blob,既能提升用户体验(秒开预览),又能降低服务端压力(无需中转),是每一位前端工程师的必备技能。


作者:404星球的猫
来源:juejin.cn/post/7573521516324896795
收起阅读 »

亲历外企裁员:上午还在写代码,下午工位就空了

引子:不再是“主动的选择” 记得 2018 年的时候,年轻与互联网的兴起,只要简历挂在求职 APP 上,立马就会有猎头或者 HR 来联系。在那时,离职通常伴随着的是薪资增长和职级的跃升。 那时候的我们,仿佛是在草原上逐水草而居的游牧民族,哪里水草丰美就去哪里,...
继续阅读 »

jimeng-2025-12-09-9499-现代办公室内部,冷色调,蓝色和灰色为主,空荡荡的工位,几台亮着的显示器,窗外是阴..._cleanup.png


引子:不再是“主动的选择”


记得 2018 年的时候,年轻与互联网的兴起,只要简历挂在求职 APP 上,立马就会有猎头或者 HR 来联系。在那时,离职通常伴随着的是薪资增长和职级的跃升。


那时候的我们,仿佛是在草原上逐水草而居的游牧民族,哪里水草丰美就去哪里,从未想过草原也会有枯黄的一天。


然而今天,我第一次以旁观者的身份,见证了一场并非出于自愿的离别。当大环境不再安定、当时代的红利退潮,裸泳的不仅是企业,还有每一个身处其中的个体。


现场:两小时的“消失术”


早在一个月前,空气中就已经弥漫着不安的味道。或者说早在 HC(Headcount)冻结时,就已经埋下了伏笔。


11月的时候,一些原本该续签的合同被搁置,那时候我们就知道,暴风雨要来了。但我没想到,它来得如此迅猛且安静。


早上 9 点,无意间瞥见 Leader 们聚在一间会议室中开会。原以为是他们的例会,没曾想会是裁员的开始键:


Leader 谈话 -> 确认赔偿 -> 签字 -> 交还电脑 -> 离开。


这场“斩首行动”持续了不到两个小时,迅速且安静。整个过程持续到了上午 11 点,尘埃落定。整个部门合计少了四分之一的人。


外企的体面在这一刻展现得淋漓尽致,同时也冷酷得令人心惊。没有多余的废话,只有流程和结果。前一分钟还在和我说要修 CI 错误的同事,后一分钟工位就已经回来和我们告别了。


jimeng-2025-12-09-6938-咖啡厅的一角,桌上放着两杯咖啡,模糊的背景是繁华的城市CBD,透过玻璃窗看外面,..._cleanup.png


午餐:焦虑的泡沫


中午,“幸存”下来的同事不约而同地一起去吃饭。


话题不再是往日的“最近哪个 AI 技术栈很火”、“哪个 Node.js 的库可以用到项目里”,而是变成了“现在出去了能做什么”、“还会有下一波吗”。


谈到赔偿,由于不方便透露,只能算是“中规中矩”。即不会像佳能那样上新闻,也不会闹到仲裁。


在六七年前,这笔钱可能是一笔快乐的旅游基金;而在当下的环境中,它更像是一笔“过冬费”,是面对未知的漫长寒冬时,手里仅有的一点余粮。


吃完饭后,大家也是心照不宣地散着步。专门挑了太阳最大的地方走了好久,仿佛可以驱散身上的寒气。


但我们都清楚,我们这些留下来的人,也没有太多庆幸。更多的是一种“兔死狐悲”的无力感。谁也不知道,下一次名单上会不会有自己的名字。


下午:沉默的办公室


回到工位,工作还得继续。代码还在那里,Bug 还没修完,PR 还等着 Merge。


但当看到 PR 中那些点了 Approve 或者留下 Comments 的“前”同事的头像,竟有一丝不想 Merge 的冲动。


发生了这么大的事情后,办公室的氛围自然变了。往常到了下班点,大家可能还会为了赶进度再多待一会儿或者一起聊会天。但今天,一到时间,Leader 们就示意大家早点回去,不要再加班了。


这不仅是一种体恤,更像是一种无声的宣告:当“努力”已经无法对抗“趋势”时,大家默契地选择了“节能模式”。


jimeng-2025-12-09-2436-一个人孤独地站在高楼的落地窗前,背影,俯瞰着下面繁忙但渺小的城市车流,夕阳西下或..._cleanup.png


结语


古人云:“山雨欲来风满楼”。今天的这场裁员,或许只是时代大潮中的一朵浪花。


外企曾经是我们心中的避风港,意味着高薪、体面、Work-Life Balance。但今天的一切告诉我们,在这个充满不确定性的时代,没有绝对的安全岛。


对于我们每一个技术人来说,或许是时候重新审视自己的核心竞争力了。当大潮退去,我们手里握着的,究竟是可以随时变现的技能,还是一张随时可能失效的工牌?


最后的最后,我想说我们组本来人也不多,大家关系也都很好,平时说说笑笑也很开心。衷心祝愿他们能在后面的日子顺利。


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

让网页在 PC 缩放时“纹丝不动”的 4 个技巧

web
记录一次把「标题、描述、背景图」全部做成“流体响应式”的踩坑与经验 背景 最近给 LUCI OS 官网做首屏改版,需求只有一句话: “PC 端浏览器随意缩放,首屏内容要像海报一样,几乎看不出形变。” 听起来简单,但「缩放不变形」+「多端自适应」本质上是...
继续阅读 »

记录一次把「标题、描述、背景图」全部做成“流体响应式”的踩坑与经验





背景


最近给 LUCI OS 官网做首屏改版,需求只有一句话:



“PC 端浏览器随意缩放,首屏内容要像海报一样,几乎看不出形变。”



听起来简单,但「缩放不变形」+「多端自适应」本质上是矛盾的。

经过 3 轮迭代,我们把问题拆成了 4 个小目标,并给出了最简洁的解法。




1. 文本:用 clamp() 一把梭


传统写法给 3~4 个断点写死字号,窗口稍微拉一下就会跳变。

CSS 4 级函数 clamp(MIN, VAL, MAX) 天生就是解决“跳变”的:



  • 标题:text-[clamp(28px,6vw,48px)]

  • 描述:text-[clamp(14px,1.2vw,18px)]


一行代码实现「最小值保底、最大值封顶、中间平滑变化」。

浏览器缩放时,字号随 vw 线性变化,肉眼几乎察觉不到阶梯感。




2. 容器:限宽 + 居中 = “锁死”水平形变


再漂亮的字号,如果容器宽度跟着窗口无限拉伸,一样会崩。

做法简单粗暴:


css


复制


max-w-6xl mx-auto


  • max-w-6xl 把最大内容宽度锁死在 1152px;

  • mx-auto 保证左右留白始终对称。


窗口继续拉大,两侧只是等比留空,内容区不再变形。




3. 图片(或背景):固定尺寸 + 背景定位


背景图不能跟着 100% 拉伸,否则人物/产品会被拉长。

我们把背景拆成两层:



  • 外层:全屏 div,只做黑色渐变遮罩;

  • 内层:真正的背景图用


    css


    复制


    background: url(...) 50% / cover no-repeat;
    max-width: 1280px;
    max-height: 800px;

    只要窗口没超过 1280×800,背景图始终保持原始比例,居中裁剪。





4. 布局:断点内“锁死”,断点外才变化


Tailwind 的 md:flex-row 之类前缀只在跨断点时生效。

同一断点内 我们故意:



  • 用固定 gap-32px 而非百分比;

  • 用固定图片宽 md:w-75md:h-47

  • items-center 保证垂直居中。


=> 浏览器宽一点点、窄一点点,所有尺寸都不变,自然看不出变化。

直到窗口拉到下一个断点阈值,布局一次切换,干净利落。




最终代码(最简可读版)


tsx


复制


<section className="relative flex items-center justify-center min-h-[400px] md:h-[800px]">
{/* 1. 背景层:固定尺寸 + 居中 */}
<div
className="absolute inset-0 mx-auto"
style={{
maxWidth: 1280,
maxHeight: 800,
background:
'linear-gradient(180deg,rgba(2,2,2,0) 60%,#020202 99%), url(/unlocking_vast_data_potential.png) 50%/cover no-repeat',
}}
/>

{/* 2. 内容层:限宽 + 居中 + clamp */}
<div className="relative z-10 w-full max-w-6xl px-4 text-center">
<h1 className="font-bold text-white text-[clamp(28px,6vw,48px)]">
Unlocking Vast Data Potential
</h1>
<p className="mt-4 mx-auto max-w-5xl text-[clamp(14px,1.2vw,18px)] text-[#8C8B95]">
LUCI OS is powered by Mavi's video understanding engine
</p>
</div>
</section>



效果



  • 1440px 与 1920px 两档分辨率下,标题、描述、背景图的视觉差异 < 2%

  • 字号、行宽、图片比例在鼠标拖拽窗口时线性变化,无跳变

  • 移动端仍保持完美自适应,无需额外代码。




写在最后


把「响应式」做细,核心就是 “在需要的范围内平滑,在不需要的范围内锁死”。

希望这 4 个小技巧也能帮你把“缩放不变形”真正落地。


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

如果产品经理突然要你做一个像抖音一样流畅的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
收起阅读 »

前端开发人员:以下是如何充分利用 Cursor😍😍😍

web
前言 我相信作为程序员Cuesor大家一定都很熟悉了,它是一款代码编辑工具,位于 Claude AI、o3、Gemini-3.5-Pro 和 GPT-4.1 等其他顶级 AI 模型之上,帮助我们提高工作效率以及体验。 Cursor AI 入门 导航到 curs...
继续阅读 »

前言


我相信作为程序员Cuesor大家一定都很熟悉了,它是一款代码编辑工具,位于 Claude AI、o3、Gemini-3.5-Pro 和 GPT-4.1 等其他顶级 AI 模型之上,帮助我们提高工作效率以及体验。


Cursor AI 入门


导航到 cursor.com 时,可以下载 IDE:


image.png
下载并打开代码编辑器后,你将看到以下内容:


image.png
在上面的界面中,我们观察到一些事情:



  • 代理模式 每当编码时,您都需要使用代理模式。它有助于处理端到端任务

  • Ask 如果单击代理模式下拉列表:


image.png
你会看到“提问”选项——当你想提出问题时使用此选项,就像使用 ChatGPT 一样。



  • Model selection( 模型选择 ) 
    此下拉列表显示要从中选择的最喜欢的模型列表

  • Context integration(上下文集成) “添加上下文”按钮,用于使用 @ 符号(@docs、@web 等)

  • Chat interface(聊天界面) 
    “新聊天”,用于开始新的或后续的对话。


如何利用这些功能


传统的 Web 开发项目遵循以下工作流程:


image.png
但是在进行 vibe 编码时,您的工作流程将看起来更像这样:


image.png
而cursor可以帮你完成设计这一步骤,使用 Cursor AI 生成界面 。首先创建一个新项目;我们将在本例中使用 Next.js。


image.png
使用快捷键 Command+K,这非常适合您忘记基本命令


image.png
现在我们已经安装了项目,是时候进行提示了。


构建示例项目


使用聊天进行设计提示


导航到新建聊天
选择光标代理 ,然后选择您喜欢的模型。对我来说,我更喜欢 Claude 3.5


image.png


现在你可以提示它了。这是我的提示的样子:“在我的 page.tsx 中,重新创建 Logrocket 的登录页面。”


这是我们得到的:


image.png
接受更改,并使用 npm run dev 运行应用程序
如果生成的和我们所设想的效果不太一样,我们可以通过多种方式使用图像上下文使其更接近。


Attaching an image  附加图像


导航到聊天,单击图像图标,然后附加图像。这应该是您想要复制的设计图像。来不断迭代来达到我们想要的效果。


在迭代时,可能会有时ai误解了我们的意思,或者Cursor 发现这个问题有点难以修复。在这种情况下,我们可以通过单击恢复检查点轻松恢复到以前的设计。这可以在之前的聊天下找到:


image.png


模型上下文协议 (MCP) 服务器


MCP 是一种开放标准,使开发人员能够在其数据源和 AI 驱动的工具之间构建安全的双向连接。


Framelink.ai 为 Figma 构建了一个 MCP 服务器,它允许您直接访问和处理 Cursor 中的设计文件。按照本指南轻松设置。


充分利用 Cursor 的更多策略


利用 AI 代理模式


使用光标时,您最常使用 AI 代理模式。这是一个强大的功能,可以:



  • 自动安装依赖项(不再需要手动包管理)

  • 在整个项目中创建和修改文件

  • 在确认后运行终端命令

  • 处理复杂的多文件重构

  • 端到端实现完整功能


使用 @ 符号进行上下文管理


了解上下文管理可以提升您使用 Cursor 的编码体验。 @ 符号是您在帮助您时告诉 Cursor 要查看什么或优先考虑什么的方式。主要的  @  类型是:



  • @code – 您的整个项目

  • @web – 搜索互联网

  • @docs – 该工具或库的文档甚至可以是一个框架

  • @files 和文件夹 – 特定文件

  • 图像 – 拖放屏幕截图/设计

  • @cursor 规则 – 甚至您过去的聊天记录


“新聊天”策略


根据经验,我建议您始终为新功能或项目开始新的聊天。当您在旧对话之上提示某些内容时,可能会分散 AI 的注意力并降低响应质量。


Cursor 添加了一项有用的功能,允许您使用旧聊天的摘要开始新的聊天,为您提供两全其美的体验 - 新鲜的上下文而不会丢失重要信息。


使用上面的  “添加 ”按钮创建新聊天,单击上下文选项的  @ ,滚动以选择  “最近的更改”,  单击它,然后继续提示:


image.png


总结


Cursor AI 的定价似乎偏高。但其功能确实还行,不过我们也可以选择trae来替代,毕竟Cursor也不比trae强多少


作者:狂炫一碗大米饭
来源:juejin.cn/post/7545725771315478554
收起阅读 »

脱裤子放屁 - 你们讨厌这样的页面吗?

web
前言 平时在逛掘金和少数派等网站的时候,经常有跳转外链的场景,此时基本都会被中转到一个官方提供的提示页面。 掘金: 知乎: 少数派: 这种官方脱裤子放屁的行为实在令人恼火。是、是、是、我当然知道这么做有很多冠冕堂皇的理由,比如: 防止钓鱼攻击 增强用户...
继续阅读 »

前言


平时在逛掘金和少数派等网站的时候,经常有跳转外链的场景,此时基本都会被中转到一个官方提供的提示页面。


掘金:


site-juejin.png


知乎:


site-zhihu.png


少数派:


site-sspai.png


这种官方脱裤子放屁的行为实在令人恼火。是、是、是、我当然知道这么做有很多冠冕堂皇的理由,比如:



  • 防止钓鱼攻击

  • 增强用户意识

  • 品牌保护

  • 遵守法律法规

  • 控制流量去向


(以上5点是 AI 告诉我的理由)


但是作为混迹多年的互联网用户,什么链接可以点,什么最好不要点(悄悄的点) 我还是具备判断能力的。


互联网的本质就是自由穿梭,一个 A 标签就可以让你在整个互联网翱翔,现在你每次起飞的时候都被摁住强迫你阅读一次免责声明,多少是有点恼火的。


解决方案


这些中转站的实现逻辑基本都是将目标地址挂在中转地址的target 参数后面,在中转站做免责声明,然后点击继续跳转才跳到目标网站。


掘金:


https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fcn%2Fdesign%2Fhuman-interface-guidelines%2Fapp-icons%23macOS/


少数派:
https://sspai.com/link?target=https%3A%2F%2Fgeoguess.games%2F


知乎:
https://link.zhihu.com/?target=https%3A//asciidoctor.org/


所以我们就可以写一个浏览器插件,在这些网站中,找出命中外链的 A 标签,替换掉它的 href 属性(只保留 target 后面的真实目标地址)。


核心函数:



function findByTarget() {
if (!hostnames.includes(location.hostname)) return;
const linkKeyword = "?target=";
const aLinks = document.querySelectorAll(
`a[href*="${linkKeyword}"]:not([data-redirect-skipper])`
);
if (!aLinks) return;
aLinks.forEach((a) => {
const href = a.href;
const targetIndex = href.indexOf(linkKeyword);
if (targetIndex !== -1) {
const newHref = href.substring(targetIndex + linkKeyword.length);
a.href = decodeURIComponent(newHref);
a.setAttribute("data-redirect-skipper", "true");
}
});
}

为此我创建了一个项目仓库 redirect-skipper ,并且将该浏览器插件发布在谷歌商店了 安装地址


安装并启用这个浏览器插件之后,在这些网站中点击外链就不会看到中转页面了,而是直接跳转到目标网站。


因为我目前明确需要修改的就是这几个网站,如果大家愿意使用这个插件,且有其他网站需要添加到替换列表的,可以给 redirect-skipper 仓库 提PR。


如果需要添加的网站的转换规则是和 findByTarget 一致的,那么仅需更新 sites.json 文件即可。


如果需要添加的网站的转换规则是独立的,那么需要更新插件代码,合并之后,由我向谷歌商店发起更新。


为了后期可以灵活更新配置(谷歌商店审核太慢了),我默认将插件应用于所有网站,然后在代码里通过 hostname 来判断是否真的需要执行。


{
"$schema": "https://json.schemastore.org/chrome-manifest.json",
"name": "redirect-skipper",
"manifest_version": 3,
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["./scripts/redirect-skipper.js"],
"run_at": "document_end"
}
],
}

在当前仓库里维护一份 sites.json 的配置表,格式如下:


{
"description": "远程配置可以开启 Redirect-Skipper 插件的网站 (因为谷歌商店审核太慢了,否则无需通过远程配置,增加复杂性)",
"sites": [
{
"hostname": "juejin.cn",
"title": "掘金"
},
{
"hostname": "sspai.com",
"title": "少数派"
},
{
"hostname": "www.zhihu.com",
"title": "知乎"
}
]
}

这样插件在拉取到这份数据的时候,就可以根据这边描述的网站配置,决定是否执行具体代码。


插件完整代码:


function replaceALinks() {
findByTarget();
}

function observerDocument() {
const mb = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
if (mutation.addedNodes.length) {
replaceALinks();
}
}
}
});
mb.observe(document, { childList: true, subtree: true });
}

// 监听路由等事件
["hashchange", "popstate", "load"].forEach((event) => {
window.addEventListener(event, async () => {
replaceALinks();
if (event === "load") {
observerDocument();
await updateHostnames();
replaceALinks(); // 更新完数据后再执行一次
}
});
});

let hostnames = ["juejin.cn", "sspai.com", "www.zhihu.com"];

function updateHostnames() {
return fetch(
"https://raw.githubusercontent.com/dogodo-cc/redirect-skipper/master/sites.json"
)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok");
})
.then((data) => {
// 如果拉到了远程数据,就用远程的
hostnames = data.sites.map((site) => {
return site.hostname;
});
})
.catch((error) => {
console.error(error);
});
}

// 符合 '?target=' 格式的链接
// https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fcn%2Fdesign%2Fhuman-interface-guidelines%2Fapp-icons%23macOS/
// https://sspai.com/link?target=https%3A%2F%2Fgeoguess.games%2F
// https://link.zhihu.com/?target=https%3A//asciidoctor.org/

function findByTarget() {
if (!hostnames.includes(location.hostname)) return;
const linkKeyword = "?target=";
const aLinks = document.querySelectorAll(
`a[href*="${linkKeyword}"]:not([data-redirect-skipper])`
);
if (!aLinks) return;
aLinks.forEach((a) => {
const href = a.href;
const targetIndex = href.indexOf(linkKeyword);
if (targetIndex !== -1) {
const newHref = href.substring(targetIndex + linkKeyword.length);
a.href = decodeURIComponent(newHref);
a.setAttribute("data-redirect-skipper", "true");
}
});
}


更详细的流程可以查看 redirect-skipper 仓库地址


夹带私货



标题历史



  • 浏览器插件之《跳过第三方链接的提示中转页》


作者:甜甜的泥土
来源:juejin.cn/post/7495977411273490447
收起阅读 »

当了leader才发现,大厂最想裁掉的,不是上班总迟到的,也不是下班搞失联的,而是经常把这3句话挂在嘴边的

“当了 leader 才发现,公司最想裁掉的,不是上班总迟到的,也不是下班搞失联的,而是经常把这 3 句话挂在嘴边的” 这是最近在职场社区里又被聊热起来的一个老话题。 作为一个在职场上混迹了近 9 年的程序员,一路走来亲眼目睹和经历了程序员职场里的各种风雨。...
继续阅读 »

“当了 leader 才发现,公司最想裁掉的,不是上班总迟到的,也不是下班搞失联的,而是经常把这 3 句话挂在嘴边的”



这是最近在职场社区里又被聊热起来的一个老话题。


作为一个在职场上混迹了近 9 年的程序员,一路走来亲眼目睹和经历了程序员职场里的各种风雨。从一开始的大头兵到后来负责一个独立的小团队,从一个所谓的 leader 的视角上来看问题,对这个事情的理解似乎又有了一些变化。


在我刚成为小团队负责人时,和许多职场人一样,我以为那些显而易见的职业毛病——比如习惯性迟到、到点就消失联系不上——才是最让管理者头疼的。直到自己坐上这个位置,真实去参与招人、考核、决策,才明白有些表象问题在 leader 眼中其实根本不算什么。


回到文首的话题,说的似乎有些夸张,但在真实的职场里确实是存在的,工作能力当然重要,但是“沟通艺术”和“向上管理”同样也不容忽视。


今天,我们就来聊聊这 3 句看似平常却极具“杀伤力”的话,相信我们平时可能也听过。


1、“这个需求做不了”


这句话相信在平时的需求会或者评审会上经常会听到。


当接到一个新需求或任务时,有些同学的第一反应是:“这个需求做不了”。


当员工说出“这个需求做不了”时,背后的含义有可能是“我不想做”、“我不会做”或“我觉得没必要做”。无论是哪种情况,传递出来的都是拒绝和封闭的态度。


因为现代职场没有绝对“做不了”的事情,只有尚未找到的解决方案或者不够充分的资源支持而已。


如果面对平级的产品经理这样说倒还无所谓,如果面对大领导这样表达那就多少有些欠妥了。


所以遇到这类问题大可不必当场上头去否定,别急着说‘做不了’,我们不妨换一个方式来表达


表达变换 Tips:



  • “这个需求很有挑战,我需要先调研看看”

  • “这个需求很有挑战,我需要XX资源和XX支持来实现它”

  • “目前有几点困难,我需要先评估一下”

  • “我需要XX资源和XX条件的支持,能否帮我协调”


2、“我以前都是这么做的”


当员工不断强调“以前都是这么做的”,他实际上是在拒绝创新,拒绝适应变化,拒绝接受新思想。


在 IT 互联网行业,方法和工具迭代速度极快。之前曾经高效的方法,现在可能已经落后了,而对过往的经验和标签的一味固守往往会阻碍人进行第二层、第三层的进一步思考。


所以面对这种表达情形,我们不妨也可以换一种表达方式


表达变换 Tips:



  • “我过去是这样做的,但不确定是否适合现在的情况,需要先确定一下”

  • “过去我们是用XX方法做的,效果是XX。我们也可以探讨一下新方法,看看会不会更好”

  • “我能分享一下过去做这类项目的经验教训,供大家参考”

  • “我对这个新方法还不太熟,需要一点时间学习一下”


3、“这个不归我负责”


我认为这句话可能是这三句中最致命的一句,因为它通常反映的是一种界限感和团队协作意识的缺失。


“各人自扫门前雪”的心态在某些特殊的场景下确实“杀伤力”极大,尤其如果在上级领导问责面前这么说,那基本真就 gg 了。。


在团队工作中,职责边界清晰本是好事,但过度强调“不归我管”则是一种危险的信号。


而且有时候模糊地带的问题往往就是自己能创造价值的机会,而且即便自己不想插手,也完全没必要这样去表达,我们不妨也换一些表达方式


表达变换 Tips:



  • “这个事情谁更熟悉?我可以协助”

  • “这个事情的负责人是xxx,我可以了解之后协助解决”

  • “我不太熟悉这个领域,但可以了解一下,谁能给我指点”

  • “我们需要先明确一下这个问题的负责人,我可以暂时跟进一下”


所以以上这三点沟通的艺术也是程序员职场生活里切实可能会遇到的问题。当你下次准备说出这三句话中的任何一句时,不妨先停顿三秒,换个表达方式。


虽说向上管理和沟通艺术是被很多程序员所诟病和瞧不上的,但是有时候因为一句上头的话或者表达而导致了某些通道的关闭那也实在是太可惜了,这很不公平,但这往往就是职场的现实。


程序员作为一个有个性的创造性群体要专注精进技术这本身没错,但是职场毕竟也是一个充满人情世故的江湖,掌握一些通用的职场规则和沟通艺术也是十分有必要的,所以埋头赶路的同时也不要忘记看看周围的环境和机会


在这个快速变化的时代,唯一不变的就是需要不停地适应和成长。共勉。


好了,那以上就是今天的内容分享了,希望能对大家有所启发,我们下篇见。



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



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

我本是写react的,公司让我换赛道搞web3D

web
当你在会议室里争论需求时, 智慧工厂的数字孪生正同步着每一条产线的脉搏; 当你对着平面图想象空间时, 智慧小区的三维模型已在虚拟世界精准复刻每一扇窗的采光。 当你在CAD里调整参数时, 数字孪生城市的交通流正实时映射每辆车的轨迹; 当你等待客户确认方案...
继续阅读 »

当你在会议室里争论需求时,

智慧工厂的数字孪生正同步着每一条产线的脉搏;




当你对着平面图想象空间时,

智慧小区的三维模型已在虚拟世界精准复刻每一扇窗的采光。




当你在CAD里调整参数时,

数字孪生城市的交通流正实时映射每辆车的轨迹;

当你等待客户确认方案时,

机械臂的3D仿真已预演了十万次零误差的运动路径;




当你用二维图纸解释传动原理时,

可交互的3D引擎正让客户‘拆解’每一个齿轮;

当你担心售后维修难描述时,

AR里的动态指引已覆盖所有故障点;




当你用PS拼贴效果图时,

VR漫游的业主正‘推开’你设计的每一扇门;

当你纠结墙面材质时,

光影引擎已算出了午后3点最温柔的折射角度;



从前端到Web3D,

不是换条赛道,

而是打开新维度。



韩老师说过:再牛的程序员都是从小白开始,既然开始了,就全心投入学好技术。



🔴 工具


所有的api都可以通过threejs官网的document,切成中文,去搜:


image.png


🔴 平面


⭕️ Scene 场景


场景能够让你在什么地方什么东西来交给three.js来渲染,这是你放置物体灯光摄像机地方


image.png


import * as THREE from "three";

// console.log(THREE);

// 目标:了解three.js最基本的内容

// 1、创建场景
const scene = new THREE.Scene();

⭕️ camera 相机


示例:threejs.org/examples/?q…


image.png


import * as THREE from "three";

// console.log(THREE);

// 目标:了解three.js最基本的内容

// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75, // 相机的角度
window.innerWidth / window.innerHeight, // 相机的宽高比
0.1, // 相机的近截面
1000 // 相机的远截面
);

// 设置相机位置
camera.position.set(0, 0, 10); // 相机位置 (X轴坐标, Y轴坐标, Z轴坐标)
scene.add(camera); // 相机添加到场景中

⭕️ 物体 cube


import * as THREE from "three";

// console.log(THREE);

// 目标:了解three.js最基本的内容

// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75, // 相机的角度
window.innerWidth / window.innerHeight, // 相机的宽高比
0.1, // 相机的近截面
1000 // 相机的远截面
);

// 设置相机位置
camera.position.set(0, 0, 10); // 相机位置 (X轴坐标, Y轴坐标, Z轴坐标)
scene.add(camera); // 相机添加到场景中

// 添加物体
// 创建几何体
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1); // 创建立方体的几何体 (长, 宽, 高)
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); // MeshBasicMaterial 基础网格材质 ({ color: 0xffff00 }) 颜色
// 根据几何体和材质创建物体
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial); // 创建立方体的物体 (几何体, 材质)
// 将几何体添加到场景中
scene.add(cube); // 物体添加到场景中

⭕️ 渲染 render


// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染的尺寸大小 (窗口宽度, 窗口高度)
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement); // 将webgl渲染的canvas内容添加到body

// 使用渲染器,通过相机将场景渲染进来
renderer.render(scene, camera); // 使用渲染器,通过相机将场景渲染进来 (场景, 相机)

⭕️ 效果


效果是平面的:


image.png


到这里,还不是3d的,如果要加3d,要加一下控制器


🔴 3d


⭕️ 控制器


添加轨道。像卫星☄围绕地球🌏,环绕查看的视角:


// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

// 目标:使用控制器查看3d物体

// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement); // 创建轨道控制器 (相机, 渲染器dom元素)
controls.enableDamping = true; // 设置控制器阻尼,让控制器更有真实效果。

function render() {
renderer.render(scene, camera); // 浏览器每渲染一帧,就重新渲染一次
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render); // 浏览器渲染下一帧的时候就会执行render函数,执行完会再次调用render函数,形成循环,每秒60次
}

render();

⭕️ 加坐标轴辅助器


// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5); // 坐标轴(size轴的大小)
scene.add(axesHelper);

1.gif


⭕️ 设置物体移动


// 设置相机位置
camera.position.set(0, 0, 10);
scene.add(camera);

1.gif


cube.position.x = 3;

// 往返移动
function render() {
cube.position.x += 0.01;
if (cube.position.x > 5) {
cube.position.x = 0;
}
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

⭕️ 缩放


cube.scale.set(3, 2, 1); // xyz, x3倍, y2倍

单独设置


cube.position.x = 3;

⭕️ 旋转


cube.rotation.set(Math.PI / 4, 0, 0, "XZY"); // x轴旋转45度

单独设置


cube.rotation.x = Math.PI / 4;

⭕️ requestAnimationFrame


function render(time) {
// console.log(time);
// cube.position.x += 0.01;
// cube.rotation.x += 0.01;

// time 是一个不断递增的数字,代表当前的时间
let t = (time / 1000) % 5; // 为什么求余数,物体移动的距离就是t,物体移动的距离是0-5,所以求余数
cube.position.x = t * 1; // 0-5秒,物体移动0-5距离

// if (cube.position.x > 5) {
// cube.position.x = 0;
// }
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

⭕️ Clock 跟踪事件处理动画


// 设置时钟
const clock = new THREE.Clock();
function render() {
// 获取时钟运行的总时长
let time = clock.getElapsedTime();
console.log("时钟运行总时长:", time);
// let deltaTime = clock.getDelta();
// console.log("两次获取时间的间隔时间:", deltaTime);
let t = time % 5;
cube.position.x = t * 1;

renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

大概是8毫秒一次渲染时间.


⭕️ 不用算 用 Gsap动画库


gsap.com/


// 导入动画库
import gsap from "gsap";

// 设置动画
var animate1 = gsap.to(cube.position, {
x: 5,
duration: 5,
ease: "power1.inOut", // 动画属性
// 设置重复的次数,无限次循环-1
repeat: -1,
// 往返运动
yoyo: true,
// delay,延迟2秒运动
delay: 2,
onComplete: () => {
console.log("动画完成");
},
onStart: () => {
console.log("动画开始");
},
});
gsap.to(cube.rotation, { x: 2 * Math.PI, duration: 5, ease: "power1.inOut" });

// 双击停止和恢复运动
window.addEventListener("dblclick", () => {
// console.log(animate1);
if (animate1.isActive()) {
// 暂停
animate1.pause();
} else {
// 恢复
animate1.resume();
}
});

function render() {
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

⭕️ 根据尺寸变化 实现自适应


// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();

// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});


⭕️ 用js控制画布 全屏 和 退出全屏


window.addEventListener("dblclick", () => {
const fullScreenElement = document.fullscreenElement;
if (!fullScreenElement) {
// 双击控制屏幕进入全屏,退出全屏
// 让画布对象全屏
renderer.domElement.requestFullscreen();
} else {
// 退出全屏,使用document对象
document.exitFullscreen();
}
// console.log(fullScreenElement);
});

⭕️ 应用 图形 用户界面 更改变量


// 导入dat.gui
import * as dat from "dat.gui";

const gui = new dat.GUI();
gui
.add(cube.position, "x")
.min(0)
.max(5)
.step(0.01)
.name("移动x轴")
.onChange((value) => {
console.log("值被修改:", value);
})
.onFinishChange((value) => {
console.log("完全停下来:", value);
});

//   修改物体的颜色
const params = {
color: "#ffff00",
fn: () => {
// 让立方体运动起来
gsap.to(cube.position, { x: 5, duration: 2, yoyo: true, repeat: -1 });
},
};
gui.addColor(params, "color").onChange((value) => {
console.log("值被修改:", value);
cube.material.color.set(value);
});
// 设置选项框
gui.add(cube, "visible").name("是否显示");

var folder = gui.addFolder("设置立方体");
folder.add(cube.material, "wireframe");
// 设置按钮点击触发某个事件
folder.add(params, "fn").name("立方体运动");

image.png




🔴 结语



前端的世界,

不该只有VueReact——

还有WebGPU里等待你征服的星辰大海。"



“当WebGL成为下一代前端的基础设施,愿你是最早站在三维坐标系里的那个人。”


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

🔥 放弃 vw!我在官网大屏适配中踩了天坑,用 postcss-px-to-viewport-8-plugin 实现了 Rem 终极方案

web
引言:我的大屏适配“翻车”现场 领导拍板,1天内完成官网所有界面的响应式适配,我想了下,这还不简单?postcss-px-to-viewport 安排上,vw 单位一把梭!直到测试同事幽幽地说了句:‘这个屏… 4K 分辨率下好像被拉扁了?’ 我心头一紧,打开 ...
继续阅读 »

引言:我的大屏适配“翻车”现场


领导拍板,1天内完成官网所有界面的响应式适配,我想了下,这还不简单?postcss-px-to-viewport 安排上,vw 单位一把梭!直到测试同事幽幽地说了句:‘这个屏… 4K 分辨率下好像被拉扁了?’
我心头一紧,打开 3840px 宽的显示器一看——所有图片、文字被无限拉宽,布局直接崩坏。vw 方案的致命缺陷暴露无遗:它只负责缩放,不负责限制最大宽度。  我们的内容在超过 1920px 的屏幕上经历了‘拉伸灾难’。必须寻找一个既能自动缩放,又能优雅限制最大宽度的 终极方案


一、为什么 VW 不是大屏适配的银弹?



  1. vw 的本质1vw 等于视口宽度的 1%。视口越宽,元素尺寸越大。

  2. 理想的适配效果



    • 小于 1920px:等比例缩小。

    • 等于 1920px:完美还原设计稿。

    • 大于 1920px内容不再无限放大,而是居中显示,两侧留白(类似 max-width: 1920px; margin: 0 auto; 的效果)。



  3. vw 的困境:它无法实现第三点。在 3840px 的 4K 屏上,一个 100vw 的元素会宽达 3840px,远远超出设计预期,导致布局稀疏、元素被拉扁,体验极差。


二、终极方案的选型:REM 王者归来


我们的需求其实有两个:



  1. 动态缩放:在不同尺寸下,元素能等比缩放。

  2. 最大限制:有一个绝对单位作为基准,限制最大尺寸。


Rem (Root Em) 单位完美契合!



  • 1rem 等于根元素 (<html>) 的 font-size 大小。

  • 我们可以通过 JavaScript 动态计算并设置 <html> 的 font-size

  • 同时,我们可以用 CSS 媒体查询或 JS 逻辑,为 font-size 设置一个 最大值,比如 16px。这样,当屏幕宽超过 1920px 时,布局宽度就会稳定在 1920px 的对应尺寸,实现居中留白。


思路转变:从 px -> vw 变为 px -> rem


三、核心实战:逆向工程与插件配置


我们的目标是:继续使用高效的 postcss-px-to-viewport-8-plugin 自动将设计稿的 px 转换为 rem,但要破解它的默认公式。


1. 插件的“固执”公式

该插件默认用于转换 vw,它有一个强制逻辑:


// 插件内部大概是这样计算的
function fixedTo(number, unitPrecision) {
// 公式: (px / viewportWidth) * 100
return (number / viewportWidth * 100).toFixed(unitPrecision) + 'vw';
}

我们要把输出单位改成 rem,但公式没变,它依然会套用 (px / viewportWidth) * 100


2. 逆向计算,破解公式

我们的目标是:让 1920px 的设计稿上,1rem 恰好等于 16px



  • 设:设计稿上一个元素的宽度为 100px

  • 我们希望插件输出:100px -> Y rem

  • 我们希望在实际 1920px 宽的屏幕下:Y rem = 100px

  • 因为 1rem = 16px,所以 Y = 100 / 16 = 6.25rem


现在,我们反向推导插件内部的公式:

插件计算:Y = (100 / viewportWidth) * 100


让两个 Y 相等:


(100 / viewportWidth) * 100 = 100 / 16

两边同时除以 100


100 / viewportWidth = 1 / 16

解得


viewportWidth = 100 * 16 = 1600

结论:  将插件的 viewportWidth 设置为 1600viewportUnit 设置为 rem,它就能输出符合我们需求的 rem 值!


3. 最终 PostCSS 配置


// nuxt.config.ts / vite.config.ts / postcss.config.js
export default {
// ... other config
postcss: {
plugins: [
require('postcss-px-to-viewport-8-plugin')({
// 【核心逆向配置】通过计算得出,让 1920px 设计稿下 1rem = 16px
viewportWidth: 1600, // 设计稿视口宽度(逆向计算值)
viewportHeight: 1080, // 设计稿视口高度(根据实际情况设置,主要用于高宽都固定的元素)
unitToConvert: 'px', // 要转换的单位
unitPrecision: 5, // 转换后的精度
propList: ['*'], // 可以从 px 转为 rem 的属性列表,* 代表所有属性
viewportUnit: 'rem', // 【核心】转换后的单位,我们选择 rem
fontViewportUnit: 'rem', // 字体转换后的单位
selectorBlackList: ['.container-max-width', ''], // 指定不转换的类名
minPixelValue: 1, // 小于 1px 不转换
mediaQuery: true, // 允许在媒体查询中转换
replace: true, // 直接替换值而不添加备用属性
include: [/src/, /node_modules[\/]element-plus/], // 只转换 src 和 element-plus 下的文件
// exclude: [/node_modules/] // 忽略 node_modules
}),
],
},
}

四、动态控制:Nuxt 插件设置根字体大小


光有 rem 还不够,我们需要动态设置 <html> 的 font-size


// plugins/rem.client.ts
export default defineNuxtPlugin((nuxtApp) => {
const setRem = () => {
const designWidth = 1920 // 我们的设计稿宽度
const baseSize = 16 // 我们希望的最大基准值 (1rem = 16px)
const clientWidth = document.documentElement.clientWidth

// 核心计算公式:缩放比例 = 当前视宽 / 设计稿宽度
const remSize = (clientWidth / designWidth) * baseSize

// 【关键】设置字体大小,并限制其在 12px 到 16px 之间
// 这意味着:屏幕小于 1920px 时会缩小,大于 1920px 时根字体大小稳定在 16px
document.documentElement.style.fontSize = `${Math.min(Math.max(remSize, 12), 16)}px`
}

let timer: NodeJS.Timeout | null = null
const setRemDebounced = () => {
if (timer) clearTimeout(timer)
timer = setTimeout(setRem, 250) // 防抖优化
}

// App 挂载后设置并监听 resize
nuxtApp.hook('app:mounted', () => {
setRem()
window.addEventListener('resize', setRemDebounced)
})

// App 卸载前清理
nuxtApp.hook('app:beforeMount', () => {
window.removeEventListener('resize', setRemDebounced)
if (timer) clearTimeout(timer)
})
})

五、收尾工作:CSS 最大宽度容器


最后,别忘了创建一个最大宽度容器,这是完美收尾的关键。


/* assets/css/global.css */
.container-max-width {
max-width: 1920px; /* 限制最大宽度 */
margin: 0 auto; /* 居中显示 */
}

/* 在布局组件或App.vue中应用 */
<!-- app.vue -->
<template>
<div id="app" class="container-max-width">
<RouterView />
</div>
</template>

总结与展望



  • 方案优势



    • 完美适配:实现了“小屏缩放、大屏留白”的理想效果。

    • 开发高效:延续了 postcss-px-to-viewport 的书写习惯,开发时直接写 px

    • 体验优雅:彻底杜绝超宽屏下的布局崩坏问题。



  • 注意事项



    • 注意 selectorBlackList 要把 container-max-width 加进去,防止它的 max-width: 1920px 被转换成 rem

    • baseSize 和 mediaQuery 结合,可以实现更复杂的响应式逻辑。




这个方案是我们团队从 vw 的坑里爬出来后,不断探索得出的最优解,目前已在生产环境稳定运行。如果对你有启发,欢迎点赞、收藏、关注!


作者:郭少
来源:juejin.cn/post/7540877562265911332
收起阅读 »

就因为package.json里少了个^号,我们公司赔了客户十万块

web
写这篇文章的时候,我刚通宵处理完一个P0级(最高级别)的线上事故,天刚亮,烟灰缸是满的🚬。 事故的原因,说出来你可能不信,不是什么服务器宕机,也不是什么黑客攻击,就因为我们package.json里的一个依赖项,少写了一个小小的^(脱字符号) 。 这个小小的失...
继续阅读 »

image.png


写这篇文章的时候,我刚通宵处理完一个P0级(最高级别)的线上事故,天刚亮,烟灰缸是满的🚬。


事故的原因,说出来你可能不信,不是什么服务器宕机,也不是什么黑客攻击,就因为我们package.json里的一个依赖项,少写了一个小小的^(脱字符号)


这个小小的失误,导致我们给客户A的数据计算模块,在一次平平无奇的依赖更新后,全线崩溃。而我们,直到客户的业务方打电话来投诉,才发现问题。


等我们回滚、修复、安抚客户,已经是7个小时后。按照合同的SLA(服务等级协议),我们公司需要为这次长时间的服务中断,赔付客户十万块


老板在事故复盘会上,倒没说什么重话,只是默默地把合同复印件放在了桌上。


满脸写着无奈.gif


今天,我不想抱怨什么,只想把这个价值 十万块 的教训,原原本本地分享出来,希望能给所有前端、乃至所有工程师,敲响一个警钟。




事故是怎么发生的?


我们先来复盘一下事故的现场。


我们有一个给客户A定制的Node.js数据处理服务。它依赖了我们内部的另一个核心工具库@internal/core


在项目的package.json里,依赖是这么写的:


{
"name": "customer-a-service",
"dependencies": {
"@internal/core": "1.3.5",
"express": "^4.18.2",
"lodash": "^4.17.21"
// ...
}
}

注意看,expresslodash前面,都有一个^符号,而我们的@internal/core没有


这个^代表什么?它告诉npm/pnpm/yarn:“我希望安装1.x.x版本里,大于等于1.3.5最新版本。”


而没有^,代表什么?它代表:我安装1.3.5这一个版本,锁死它,不许变。


问题就出在这里。


上周,core库的同事,修复了一个严重的性能Bug,发布了1.3.6版本,并且在公司群里通知了所有人。


我们组里负责这个项目的同学,看到了通知,也很负责任。他想:core库升级了,我也得跟着升。


于是,他看了看package.json,发现项目里用的是1.3.5。他以为,只要他去core库的仓库,把1.3.5这个tag删掉,然后把1.3.6的tag打上去,CI/CD在下次部署时,重新pnpm install,就会自动拉取到最新的代码。


他错了!




最致命的锁死版本


因为我们的依赖写的是"1.3.5",而不是"^1.3.5",所以我们的pnpm-lock.yaml文件里,把这个依赖的解析规则,彻底锁死在了1.3.5


无论core库的同事怎么发布1.3.61.3.7,甚至2.0.0...


只要我们不去手动修改package.json,我们的CI/CD流水线,在执行pnpm install时,永远、永远,都只会去寻找那个被写死的1.3.5版本。


然后,灾难发生了。


core库的同事,在发布1.3.6后,为了保持仓库整洁,就把1.3.5那个旧的git tag删掉了


然后,客户A的项目,某天下午需要做一个常规的文案更新,触发了部署流水线。


流水线执行到pnpm install时,pnpm拿着lock文件,忠实地去找@internal/core@1.3.5这个包...


“Error: Package '1.3.5' not found.”


流水线崩溃了。一个本该5分钟完成的文案更新,导致了整个服务7个小时的宕机😖




十万块换来的血泪教训


事故复盘会上,我们所有人都沉默了。我们复盘的,不是谁的锅,而是我们对依赖管理这个最基础的认知,出了多大的偏差。


^ (Caret) 和 ~ (Tilde) 不是选填,而是必填



  • ^ (脱字符)^1.3.5 意味着 1.x.x (x >= 5)。这是最推荐的写法。它允许我们自动享受到所有 非破坏性 的小版本和补丁更新(比如1.3.6, 1.4.0),这也是npm install默认的行为。

  • ~ (波浪号)~1.3.5 意味着 1.3.x (x >= 5)。它只允许补丁更新,不允许小版本更新。太保守了,一般不推荐。

  • (啥也不写)1.3.5 意味着锁死。除非你是reactvue这种需要和生态强绑定的宿主,否则,永远不要在你的业务项目里这么干!


我们团队现在强制规定,所有package.json里的依赖,必须、必须、必须使用^


关于lock文件


我们以前对lock文件(pnpm-lock.yaml, package-lock.json)的理解太浅了,以为它只是个缓存。


现在我才明白,package.json里的^1.3.5,只是在定义一个规则。


而pnpm-lock.yaml,才是基于这个规则,去计算出的最终答案。


lock文件,才是保证你同事、你电脑、CI服务器,能安装一模一样的依赖树的唯一路径。它必须被提交到Git


依赖更新,是一个主动的行为,不是被动的


我们以前太天真了,以为只要依赖发了新版,我们就该自动用上。


这次事故,让我们明白:依赖更新,是一个严肃的、需要主动管理和测试的行为。


我们现在的流程是:


image.png



  1. 使用pnpm update --interactivepnpm会列出所有可以安全更新的包(基于^规则)。

  2. 本地测试:在本地跑一遍完整的测试用例,确保没问题。

  3. 提交PR:把更新后的pnpm-lock.yaml文件,作为一个单独的PR提交,并写清楚更新了哪些核心依赖。

  4. CI/CD验证:让CI/CD在staging环境,用这个新的lock文件,跑一遍完整的E2E(端到端)测试。




这十万块,是技术Leader(我)的失职,也是我们整个团队,为基础不牢付出的最昂贵的一笔学费。


一个小小的^,背后是整个npm生态的依赖管理的核心。


分享出来,不是为了博眼球,是真的希望大家能回去检查一下自己的package.json


看看你的依赖前面,那个小小的^,它还在吗?😠


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

不容易,35岁的我还在小公司苟且偷生

前言 前几天和前同事闲时聚餐,约了两个月的小聚终于达成了,程序员行业聚少离多,所幸大家的发量还坚挺着。 期间不可避免地聊到了自己的公司、行业状况以及对未来的看法,几杯老酒之后,大家畅所欲言,其中一位老哥侃起了他的职业生涯,既坎坷又无奈,饭后想起来挺有代表性的,...
继续阅读 »

前言


前几天和前同事闲时聚餐,约了两个月的小聚终于达成了,程序员行业聚少离多,所幸大家的发量还坚挺着。

期间不可避免地聊到了自己的公司、行业状况以及对未来的看法,几杯老酒之后,大家畅所欲言,其中一位老哥侃起了他的职业生涯,既坎坷又无奈,饭后想起来挺有代表性的,征得他同意故记录在此。

以下是老哥的历程。



cold.jpg


程序员的前半生


我今年35岁,有房有贷有妻女有老父母。


出生在90年代的农村,从小中规中矩,不惹事不喧哗不突出,三好学生没有我,德智体美没有全面发展。学习也算努力,不算小题做题家,因为只考了个本科。


大学学费全靠助学贷款,勤工俭学补贴日用,埋头苦干成绩也只在年级中等偏下水平。有些同学早早就定下了大学的目标,比如考研、比如出国、比如考公,到了大三的时候大家基本都有了自己的目标。而我的目标就是尽早工作,争取早日还完贷款,因此早早就开始准备找工作。

也许是上天眷顾,不知道怎么就被华为看重了(那会华为还没现在的如日中天,彼时是BAT的天下),稀里糊涂的接受了offer,没想到却是改变了后面十年的决定。


2013年,深圳的夏天阳光明媚,热气扑鼻,提着一个简单的箱子进入了坂田基地。

刚开始,工作上的一切都很新鲜,每个人都在忙碌,虽然不知道他们在忙什么,但感觉很高级的样子。同期入职的同事都比较厉害,很快就适应了工作,而自己还是没完全应对工作内容,于是下班之后继续留在公司学习,顺便蹭饭。

就这样,很快就一年过去了,自己也慢慢熟悉了工作节奏,但是加班也越来越多了。对于自己来说,为了过节点,6点是晚饭时间,9点是下班时间,12点正式下班。

平凡的日子没什么值得留恋,过一天、一个月、一年、四年都没什么两样,四年里学习到了不少的知识,也数了很多次深圳凌晨的路灯数。


作为深漂,没有遇到深圳爱情故事,也对高昂的房价绝望,于是决定回到二线城市,成为一名蓉漂。
2017年,还是和四年前一样的行李箱,出现在了老家的省会城市,只是那时的我没有了助学打款,怀里也攒下了一些血汗钱。

那时互联网行业发展还是如火如荼,前端的需求量也很大,也得益于华为公司发展越来越好,自己的华为经历很快就拿到了几个offer,选了一家初创公司,幻想着能有一番成就。


2018年底,眼看着房价越长越高,某链中介不断地灌输再不买明天就是另一个价了,错过这个村就没这个店了,也许是想有个家,也许是想着父母能到省会里一起住,拿出自己做牛马几年的积蓄加上父母一辈子辛苦攒的小十万的养老钱购买了城区里的新房,那会儿的价格已经比前两年涨了一倍多,妥妥的高位站岗,不过想着自己是刚需也不会卖,因此咬咬牙掏出了全部的积蓄怒而背上了三十年的房贷。


房子的事暂时落定了,全身心的投入到工作中,没想到老板只想骗投资人的钱,产品没弄好投资人不愿跟进了,坚持了三年,期间各种断臂求生,最终还是落了个司破人走的境地。


2020年,30岁的我第一次被动失业了,幸运的是也找到了另一半。为了尽可能节省支出,房子装修的事我们都是亲力亲为,最后花了十多万终于将房子装好了,虽然很简单但毕竟是自己在大城市里的第一套房子,那一刻,感觉十年的付出都是值得的。

背着沉重的房贷,期望能找到一份薪资稍微过得去的工作,于是在简历上优势那行写了:“可加班”。依稀记得有些HR对我进行了灵魂拷问:结婚了吗?有小孩了吗?你都30岁了还能加班吗?。我斩钉截铁地说:只要公司有需要,我定会全力以赴!


2022年,我们的孩子出世了,队友辞去了工作全心全意带小孩,而我更加努力了,毕竟有了四脚吞金兽,不得不肝。

虽然工作很努力,但成果一般,不是公司的技术担当,也不会是技术洼地。


2023年的某一天,和之前的364天一样的平淡,在座位上解Bug的我突然感觉到一阵心悸,呼吸不畅,实在不行了呼唤同事叫了120,去医院一套检查下来没发现什么大问题。医生询问是不是工作压力太大,平时加班很多?我说还好,平时也就加班到9点。医生笑了笑说你这种年轻人我见多了,都是压力大的毛病,平时工作不要久坐盯着屏幕多站起来走走。他让我回家多休息,回去后观察了几天还是偶尔会有心悸,再去了另一个医院进行检查,也是没有明确的诊断结果,只是说可能是这个问题,又可能是另一个问题。

过了1个月后,身体上的问题不见好转,我辞去了工作。


2023年末,找了一家小公司,也就是我现在的公司,工资没有涨,仔细算起来还变相下降了。

还是做的业务需求,也没有领导什么人,管好自己就行,直属上级还是个工作几年的小伙。这家公司主要的特点是不加班,技术难度不高,能做多少就是多少,前提是要报风险,领导也不会强迫加班。


就这样到了2024,神奇的是我已经很久没有心悸的感觉了,不知道是不加班还是心态转变的原因。
家里的小朋友也长大了,会说话了。我现在每天下班最温馨的的是她开着门期待我回家的那一刻,她的期盼的眼神就是我回家的动力。


公司在2024年也裁了不少人,领导也找我谈过问问我的想法,我说:我还是能胜任这份工作的。领导说:公司觉得你年级大了一些,工资虽然不是最高,但不太符合行情,你懂的。我说:我懂,可以接受适当的降薪。
就这样,我挺过了2024,然而过了一周领导走了。


2025年,我35周岁了。
现在的我已经彻底接受自己的平庸的事实了。在学生时代,从来都不出色,也不会垫底,就是那类最容易被忽略的人。在工作时代,不是技术大牛,也不是完全的水货,就是普普通通的程序员。


如果说上半生吃到了什么红利,只能说入坑了计算机这行业,技术给我带了收入,有了糊口的基础。没进股市,却被房价狠狠割了一道。


35岁的我,没有彻底躺平摆烂,也没有足够奋发进取。

35岁的我,有着24年的房贷,还好61岁的时候我还在工作,应该还能还房贷。

35岁的我,不吃海鲜不喝酒,尿酸500+。

35岁的我,人体工学椅也挽救不了腰椎间盘突出。

35岁的我,头发依然浓密,只是白发越来越多。

35岁的我,已经不打游戏,只是会看这各种小说聊以慰藉。

35岁的我,两点一线,每天挤着地铁,看众生百态。

35岁的我,早睡早起,放空自己。

35岁的我,暂时还没有领取毕业大礼包,希望今年还能苟过。

35岁的我,希望经济能够好起来,让如我一般平凡的人能够有活下去的勇气。


诸君,下一年再会~祝你平安喜乐,万事顺遂!


太极分两仪,有程序员也有程序媛:30岁的程序媛,升值加薪与我无缘


作者:小鱼人爱编程
来源:juejin.cn/post/7457567782470385705
收起阅读 »

我删光了项目里的 try-catch,老板:6

web
相信我们经常这样写bug(不是 👇: try { const res = await api.getUser() console.log('✅ 用户信息', res) } catch (err) { console.error('❌ 请求失败',...
继续阅读 »

相信我们经常这样写bug(不是 👇:

image_90.gif


try {
const res = await api.getUser()
console.log('✅ 用户信息', res)
} catch (err) {
console.error('❌ 请求失败', err)
}

看似没问题



  • 每个接口都要 try-catch,太啰嗦了!

  • 错误处理逻辑分散,不可控!

  • 代码又臭又长💨!


image_432.gif


💡 目标:不抛异常的安全请求封装


我们希望实现这样的调用👇:


const [err, data] = await safeRequest(api.getUser(1))
if (err) return showError(err)
console.log('✅ 用户信息:', data)


是不是清爽多了?✨

没有 try-catch,却能同时拿到错误和数据。




🧩 实现步骤


1️⃣ 先封装 Axios 实例


// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'

const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
})

// 🧱 请求拦截器
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
},
(error) => Promise.reject(error)
)

// 🧱 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data
if (res.code !== 0) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message || '请求失败'))
}
return res.data
},
(error) => {
ElMessage.error(error.message || '网络错误')
return Promise.reject(error)
}
)

export default service

拦截器的作用:



  • ✅ 统一处理 token;

  • ✅ 统一处理错误提示;

  • ✅ 保证业务层拿到的永远是“干净的数据”。




2️⃣ 封装一个「安全请求函数」


// src/utils/safeRequest.js
export async function safeRequest(promise) {
try {
const data = await promise
return [null, data] // ✅ 成功时返回 [null, data]
} catch (err) {
return [err, null] // ❌ 失败时返回 [err, null]
}
}

这就是关键!

它让所有 Promise 都变得「温柔」——不再抛出异常,而是返回结构化结果。




3️⃣ 封装 API 模块


// src/api/user.js
import request from '@/utils/request'

export const userApi = {
getUser(id) {
return request.get(`/user/${id}`)
},
updateUser(data) {
return request.put('/user', data)
},
}



4️⃣ 在业务层优雅调用


<script setup>
import { ref, onMounted } from 'vue'
import { userApi } from '@/api/user'
import { safeRequest } from '@/utils/safeRequest'

const user = ref(null)

onMounted(async () => {
const [err, data] = await safeRequest(userApi.getUser(1))
if (err) return showError(err)
console.log('✅ 用户信息:', data)
})
</script>

是不是很优雅、数据逻辑清晰、不需要 try-catch、 错误不崩溃。


老板说:牛🍺,你小子有点东西


image_443.gif


🧱 我们还可以进一步优化:实现自动错误提示


我们可以给 safeRequest 增加一个选项,让错误自动提示:


// src/utils/safeRequest.js
import { ElMessage } from 'element-plus'

export async function safeRequest(promise, { showError = true } = {}) {
try {
const data = await promise
return [null, data]
} catch (err) {
if (showError) {
ElMessage.error(err.message || '请求失败')
}
return [err, null]
}
}

使用时👇:


const [err, data] = await safeRequest(userApi.getUser(1), { showError: false })

这样你可以灵活控制是否弹出错误提示,

比如某些静默请求就可以关闭提示。




🧠 进阶:TypeScript 支持(超丝滑)


如果你用的是 TypeScript,可以让返回类型更智能👇:


export async function safeRequest<T>(
promise: Promise<T>
): Promise<[Error | null, T | null]> {
try {
const data = await promise
return [null, data]
} catch (err) {
return [err as Error, null]
}
}

调用时:


const [err, user] = await safeRequest<User>(userApi.getUser(1))
if (user) console.log(user.name) // ✅ 自动提示类型

老板:写得很好,下次多写点,明天你来当老板


20240809104358583cfe95dc770206b95ebe14e51a0737.png


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

VSCode 推出 绿色版!更强!更智能!

最近,VSCode 再次被推上热门,各大开发社区、推特/X 上都在讨论一个话题—— “VSCode 绿色版(Insiders)功能太强了!” 事实上,VSCode Insiders 并不是一个新名字,但随着微软不断注入 AI 能力、终端增强、Git 大升级、...
继续阅读 »

最近,VSCode 再次被推上热门,各大开发社区、推特/X 上都在讨论一个话题—— “VSCode 绿色版(Insiders)功能太强了!”



事实上,VSCode Insiders 并不是一个新名字,但随着微软不断注入 AI 能力、终端增强、Git 大升级、新 UI,它已经变成了真正意义上的:VSCode 超强增强版!



本文将带你全面认识 绿色版都强在哪?为什么开发者都在推荐?你是否应该安装?


🎯 什么是 VSCode 绿色版(Insiders)?


一句话:VSCode 的“更新抢先体验版”,比正式版更快看到新功能!



它和 Stable(正式版)可以共存使用,不会覆盖不冲突,非常适合爱折腾、追新功能的开发者。


🟢 终端大升级:智能提示终于来了!


VSCode 终端一直是“能用但不好用”;而绿色版直接把它提升到“智能终端”级别。



1. 命令自动补全(智能建议)


例如你在终端输入:


git ch

它会自动建议:


git checkout

更厉害的是:



  • 命令 flag 会被分类展示

  • 参数智能提示

  • 路径补全更精确


2. AI 执行命令时使用“真终端渲染器”



绿色版已将 Copilot Agent 的终端输出升级为 xterm.js 真终端渲染器




  • ANSI 颜色正常显示

  • 表格行列对齐准确

  • 输出清晰结构化


终端从此不再只是“命令窗”,而是智能交互环境。


🌳 Git 管理体验提升:Stash 终于可视化了!


以前 VSCodestash 支持几乎没有,只能敲命令。


这次绿色版直接补上短板!


设置以下选项:



  • scm.repositories.explorer:true

  • scm.repositories.selectionMode:single



可视化 Stash 管理器




  • 查看所有 stash

  • 一键 apply / pop / delete

  • 自动显示时间戳

  • 相同时间段用竖线对齐,界面更清爽


这功能对于频繁切分支保存临时改动的开发者来说简直是 质的提升


🤖 AI 全面进化:Copilot Agent 模式上线!


这是绿色版最轰动的更新之一。


1. Copilot 从“智能补全” → “自动执行任务代理”



它可以自动:



  • 修改多个文件

  • 执行并分析终端命令

  • 管理项目结构

  • 重构代码

  • 搜索 / 替换文件内容

  • 构建 / 运行项目


你只需要一句话:



“帮我给项目增加用户登录功能。”



然后它会自动拆解步骤,帮你完成。


2. Notebook(Markdown + 代码)也能用 AI 自动编辑



例如:



  • 自动创建 Code Cell

  • 总结文章内容

  • 插入示例代码

  • 自动修复错别字

  • 做学习笔记


科研、学习、数据分析用户狂喜!


🖥 UI / UX 更现代、更便捷


绿色版对各种细节都做了体验升级




  • Git 同步按钮更直观

  • 悬停信息更清晰

  • 状态指示更精准

  • 布局与配色有实验性改进


整体观感比正式版更轻盈、简洁、顺滑。


🔧 扩展生态更强:插件作者必装


很多新的 API(如 Terminal APINotebook APIPanel API)都会:


👉 先出现在 Insiders
👉 过几个月再进入正式版


如果你要开发或测试 VSCode 扩展,绿色版是必备。


📥 如何安装 VS Code 绿色版?


安装非常简单:


👉 官方下载安装https://code.visualstudio.com/insiders


优势:



  • 可与正式版并存

  • 不影响原环境

  • 卸载不留痕

  • 随时体验最新功能


✔️ 谁适合使用 VS Code 绿色版?


如果你属于下面任意一个人,请立即安装:



  • 喜欢尝鲜

  • 重度命令行用户

  • Copilot 用户

  • 插件开发者

  • 关注效率 & 新技术

  • 想第一时间体验 VSCode 未来方向


🎉 结语:绿色版不是“内测玩具”,而是更强大的 VSCode


总结一下绿色版带来的增强:


功能方向提升
🔥 终端智能提示 + 真终端渲染
🌳 Git可视化 Stash 管理
🤖 AICopilot 进入自动化时代
🖥 UI更现代、更细腻
🧩 插件最新 API 先体验

一句话:VSCode Insiders 是真正意义上的“VS Code Pro 版”。


如果你还没体验,现在就装一个吧,你会爱上它。



作者:前端开发爱好者
来源:juejin.cn/post/7580683234437267519
收起阅读 »

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

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

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


这个问题问得比较扎心:


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



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


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


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


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


就像盖大楼一样,公司需要做一个 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
收起阅读 »

一个超级真实的Three.js树🌲生成器插件

web
前言 分享一个基于Three.js封装的树生成器插件,可以实现创建不同类型且渲染效果真实的3D树 说实话,第一次在这个插件官网看到这个效果时我一度以为这只是一个视频,树的内容不仅仅是动态的而且整体的渲染效果也十分真实。 在three.js中使用起来也是非常的...
继续阅读 »

前言


分享一个基于Three.js封装的树生成器插件,可以实现创建不同类型且渲染效果真实的3D树


11111111111111111111111.gif


说实话,第一次在这个插件官网看到这个效果时我一度以为这只是一个视频,树的内容不仅仅是动态的而且整体的渲染效果也十分真实。


在three.js中使用起来也是非常的简单的仅仅需几行代码就可以搞定,下面给大家简单的介绍一下。


安装


通过 npm/pnpm 安装到项目本地即可


npm i @dgreenheck/ez-tree

pnpm add @dgreenheck/ez-tree


使用


使用起来也是非常简单的,只需要将插件import 引入然后在 new 实例化出来 在添加到 场景中就可以了


最后在一个requestAnimationFrame 动画函数中更新的内容就行了


import { Tree } from '@dgreenheck/ez-tree';

createTree(){

const tree = new Tree();
tree.generate();
// 设置一下位置
tree.position.set(0, 0, 0);
// 设置一下大小缩放
tree.scale.set(0.1, 0.1, 0.1);
// 添加到场景中
this.scene.add(tree);

}

sceneAnimation(): void {
// 确保动画循环持续进行
this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation());

// 更新时钟
const elapsedTime = this.clock.getElapsedTime();


// 更新控制器 如果当前是第一人称控制器则不更新
if (!this.pointerLockControls) {
this.controls.update();
}

// 更新 Tree 动态效果(风动效果等)
if (this.tree) {
this.tree.update(elapsedTime);
}
// 渲染场景
this.renderer.render(this.scene, this.camera);
}


本地项目效果


因为本地项目对光照等参数没有专门调试所以和官网展示的效果有一定的差距


image.png


将相机放大查看树渲染的效果细节处理个人觉得是非常nice的,十分真实


image.png


参数


该插件还提供了创建不同类型树的方法,通过官网的在线调试就可以看到效果了


创建一个别的类型树


image.png


修改树枝的方向


image.png


树叶的多少


image.png


项目地址


该项目插件是一个外国大佬开发,如果你的项目或者个人网站需要丰富一下页面内容,那么这个插件或许是个不错的选择


官网:http://www.eztree.dev/


项目地址:github.com/dgreenheck/…


作者:答案answer
来源:juejin.cn/post/7573588675638099983
收起阅读 »

PAC 2025:在算力风暴中淬炼的国产力量

2025年的夏天虽已远去,然而PAC 2025的热血余温未散:算力的涌动、屏幕的闪烁、代码的狂奔……那份拼搏与激情,仿佛仍在空气中炽烈燃烧,未曾褪色。顶尖战队齐聚第21届CCF HPC China 2025的PAC决赛现场,展开正面交锋,将激情与实力尽数倾注 ...
继续阅读 »

2025年的夏天虽已远去,然而PAC 2025的热血余温未散:算力的涌动、屏幕的闪烁、代码的狂奔……那份拼搏与激情,仿佛仍在空气中炽烈燃烧,未曾褪色。

顶尖战队齐聚第21届CCF HPC China 2025的PAC决赛现场,展开正面交锋,将激情与实力尽数倾注 “优化” 与 “应用” 两大赛道,现场氛围燃至顶峰。

赛场的热度,不止是代码奔涌时的风扇轰鸣,更是年轻人拼尽全力时的心跳共振。正是这股激情与执着,凝聚成推动国产计算驶向未来的核心动力。终场哨响,PAC2025并行应用挑战赛圆满收官。


鲲鹏撑腰,满格开战

本届大赛全面采用鲲鹏计算平台作为核心硬件底座。以ARM架构为技术核心,其集成的众核架构、向量/矩阵扩展、片上内存高带宽等硬件特性,成为参赛团队挖掘极致性能的核心载体,也标志着国产CPU平台正式成为高性能计算技术探索的关键阵地。


技术亮点回顾“硬件-软件-应用”的全栈突破

硬件架构特性的深度挖掘:以鲲鹏 ARM 为核心,释放国产 CPU 潜力

ARM 技术的规模化应用:特等奖获得者清华大学深圳国际研究生院团队(简称清华团队)充分发挥矩阵运算可伸缩向量扩展的优势,通过循环重排与数据预取优化GEMM与HPCG性能,最大化鲲鹏CPU的向量计算吞吐。在INT8低精度计算与Attention算子这一核心挑战上,清华、浙大、山大团队均依托鲲鹏平台的矩阵算力,实现了“向量→矩阵”的计算单元升级。例如,清华团队利用矩阵运算单指令完成 Tile 级乘加,大幅降低指令数量与寄存器压力;浙江大学团队则验证“矩阵运算+片上内存”组合的优势,将鲲鹏CPU的带宽与矩阵吞吐拉至接近GPU量级,减少CPU与加速器的数据搬运延迟。

鲲鹏硬件优势的协同验证:山东大学团队在应用赛道中,基于鲲鹏新一代CPU的多核并行与高带宽优势,实现了 20 亿原子体系的分子动力学模拟。在弱扩展8倍、强扩展 4 倍的条件下仍保持80%并行效率,直接证明了国产CPU在超大规模科学计算中的端到端性能,已具备与GPU相当的竞争力。

PAC2025上机现场


软件优化创新:硬件特性与软件策略的深度协同

精细化内存与计算调度:清华团队采用二维 Tiling 策略,浙江大学团队针对K维度切分以充分利用HPC缓存,均将关键数据留驻L1/L2缓存,减少对内存带宽的依赖,适配鲲鹏的缓存架构设计。此外,清华基于 Pthreads 自建线程池,规避操作系统调度开销,实现鲲鹏多核间的任务均衡分配,并行效率较传统方案提升显著。

精度与性能的平衡优化:针对混合精度计算需求,浙大提出“fp32保存中间变量 + svzip 转化为 fp16”的方法,避免了纯 fp16 的指数溢出问题;山大则提出“全流程混合精度向量化”,并自研 ARM 向量化超越函数库,进一步适配鲲鹏平台的指令集特性,在保证计算正确性的前提下,效率提升 20%-30%。

算子级优化突破:山东大学团队在优化赛道中,针对 INT8GEMM 与 Attention 算子提出“数值扩展+算子融合”全栈方案——基于SVSUMOPA/SVMOPA指令实现2路/4路矩阵外积乘法,结合FlashAttention融合策略,减少中间结果访存开销与线程竞争,使大Batch训练与大模型推理的稳定性提升40%以上,为鲲鹏平台的AI算子库建设提供直接技术参考。

PAC2025答辩现场


应用落地突破:覆盖 AI 与科学计算的多领域验证

AI 计算:清华团队的矩阵运算加速与山大的算子融合成果,可直接应用于鲲鹏生态的 AI 芯片与 CPU,为大模型推理(如语音识别、视觉计算)与中小规模训练提供高性能算子支撑,有效解决国产平台“AI计算性能不足”的核心痛点。

科学计算:清华团队的 HPCG 优化与山大的分子动力学模拟,验证了鲲鹏平台在气象、天文、流体力学、药物研发等领域的适用性——如山东大学团队的成果可直接复用至新能源材料设计与复杂流体计算,为国产高性能计算的行业落地提供技术范本。


PAC的意义:从赛场到未来

PAC大赛的成果不是单点的创新打法,而是真正能走出赛场、落到产业的技术。无论是算子优化,还是大规模科学计算模拟,都已具备直接赋能科研与产业的潜力。

PAC 2025的意义,在于夯实国产算力生态,让以鲲鹏为核心的国产 CPU 走向成熟,打破“高性能依赖国外架构”的偏见;在于推动“硬件—软件—应用”的全栈融合,让协同优化成为可复制的范式;更在于将成果带入产业与人才的长远布局,既赋能 AI、大模型、分子动力学等应用场景,也培养出一批能够横跨硬件、软件与应用的青年力量。

从 ARM 架构的深度挖掘,到软硬件的协同优化,再到端到端的应用突破,PAC 2025 让国产算力不再只是“能用”,而是真正“好用”。它证明了我们不再只是被动追赶,而是已能与前沿并肩而行,正全力奔向属于中国的高性能计算未来。


收起阅读 »

从JS到Python:一个前端开发者的丝滑转型之路

“我永远记得第一次用Python写出自动化脚本的那个深夜——原来编程语言间的鸿沟,远没有想象中那么深。” 作为一名纯前端出身的开发者,我曾以为JavaScript就是编程世界的全部。直到被迫接手一个数据分析项目,才在焦虑中踏上了Python学习之路。今天分享...
继续阅读 »

u=2680496620,3323051261&fm=253&fmt=auto&app=138&f=JPEG.webp



“我永远记得第一次用Python写出自动化脚本的那个深夜——原来编程语言间的鸿沟,远没有想象中那么深。”



作为一名纯前端出身的开发者,我曾以为JavaScript就是编程世界的全部。直到被迫接手一个数据分析项目,才在焦虑中踏上了Python学习之路。今天分享我的真实转型经验,带你避开我踩过的坑。




我的认知颠覆时刻


初学JS时,我以为死记语法是王道:


// 曾经的我:疯狂背诵语法
const arrowFn = (a, b) => a + b;
const promise = new Promise((res) => setTimeout(res, 1000));

转学Python后才发现:


# 现在的我:理解概念重于记忆
add = lambda a, b: a + b
await asyncio.sleep(1)

核心顿悟:编程语言的本质是表达逻辑的工具,掌握变量/函数/循环这些通用概念,比记住特定语法重要十倍。




为什么JS开发者必学Python?


当我的项目经理扔来一份Python需求时,我发现了它的不可替代性:


场景JavaScriptPython我的选择
数据可视化Chart.jsMatplotlib后者交互更专业
爬虫开发PuppeteerScrapy效率差5倍不止
自动化脚本Node脚本PyAutoGUI写文件操作真香
机器学习TensorFlow.jsPyTorch生态碾压性优势

最打动我的点:用Python写算法时,代码就像伪代码一样直白:


# 快速实现斐波那契数列
def fib(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b



我的语法转换血泪史


这些细节坑惨了我这个JS老手:


1. 花括号→缩进的地狱


// JS习惯:靠{}划分作用域
if (loggedIn) {
const user = fetchUser()
console.log(user)
}

# Python的缩进陷阱(初学者的噩梦)
if logged_in:
user = fetch_user()
print(user) # 这里居然还能访问user!

我的教训:安装Pylint插件强制4空格缩进,避免混合制表符


2. 变量命名的文化冲突


// JS:驼峰式
const currentUser = { firstName: "John" };

# Python:蛇形命名
current_user = { "first_name": "John" }

适应技巧:在VSCode中设置自动转换规则


3. 空值判断的深坑


// JS的魔幻三兄弟:null, undefined, false
let value = undefined;
if (!value) { /* 会触发 */ }

# Python的明确哲学
value =
if value is : # 必须显式判断
print("空值")



让我惊艳的Python特性


列表推导式(List Comprehensions)


# 一行完成JS需要5行的操作
squares = [x**2 for x in range(10) if x % 2 == 0]

# 等效JS代码:
# const squares = [];
# for(let x=0; x<10; x++) {
# if(x%2===0) squares.push(x**2)
# }

解构赋件的优雅


# 交换变量值
a, b = b, a

# JS等价操作:
# let temp = a;
# a = b;
# b = temp;

类型提示的救赎


def greet(name: str) -> str:
return f"Hello, {name}"

# 作为TS爱好者狂喜!



我的学习路线图



  1. 第一周:语法转换训练



    • 把常写的JS工具函数用Python重写

    • 在Leetcode上用Python刷简单题



  2. 第二周:生态征服计划


    pip install pandas requests matplotlib


    • 用Pandas处理Excel数据

    • 用Requests爬取网页内容



  3. 第三周:项目实战



    • 自动化日报邮件发送脚本

    • Django搭建简易博客后台



  4. 持续进阶



    • 深入理解Python垃圾回收机制

    • 掌握asyncio异步编程模型






那些我希望早点知道的技巧


# 1. 虚拟环境是救命稻草
python -m venv .venv
source .venv/bin/activate

# 2. 使用pathlib代替os.path
from pathlib import Path
config = Path.home() / ".config" / "myapp.json"

# 3. 活用f-string
print(f"{user.name=} {user.age=}") # 输出:user.name='John' user.age=30



转型后的真实感受


开发体验变化



  • ✅ 少了npm install的依赖地狱

  • ✅ 错误信息更人性化

  • ❌ 前端调试体验不如Chrome DevTools


心态转变



“以前觉得Python是‘其他语言’,现在明白它和JS是互补的左右手——JS构建用户界面,Python处理背后逻辑。”



给同行的建议:不要试图“转行”,而要“扩列”。我的GitHub个人主页现在 proudly displays:

JavaScript | Python | 持续学习者





最后忠告:当你在Python中写import this,输出的禅意哲学,正是这门语言的精髓——优美胜于丑陋,明了胜于晦涩。这大概就是我从JS的“灵活”走向Python的“优雅”时,最深的共鸣。



faedab64034f78f03144443a3c310a55b2191ca8.jpg


红中老大 快醒吧。 我们要成啦!!!


作者:CF14年老兵
来源:juejin.cn/post/7538329980636479498
收起阅读 »

当上组长一年里,我保住了俩下属

前言 人类的悲喜并不相通,有人欢喜有人愁,更多的是看热闹。 就在上周,"苟住"群里的一个小伙伴也苟不住了。 在苟友们的"墙裂"要求下,他分享了他的经验,以他的视角看看他是怎么操作的。 1. 组织变动,意外晋升 两年前加入公司,依然是一线搬砖的码农。 干到一...
继续阅读 »

前言


人类的悲喜并不相通,有人欢喜有人愁,更多的是看热闹。


就在上周,"苟住"群里的一个小伙伴也苟不住了。



image.png


在苟友们的"墙裂"要求下,他分享了他的经验,以他的视角看看他是怎么操作的。


1. 组织变动,意外晋升


两年前加入公司,依然是一线搬砖的码农。

干到一年的时候公司空降了一位号称有诸多大厂履历的大佬来带领研发,说是要给公司带来全新的变化,用技术创造价值。

大领导第一件事:抓人事,提效率。

在此背景下,公司不少有能力的研发另谋出处,也许我看起来人畜无害,居然被提拔当了小组长。


2. 领取任务,开启副本


当了半年的小组长,我的领导就叫他小领导吧,给我传达了大领导最新规划:团队需要保持冲劲,而实现的手段就是汰换。

用人话来说就是:



当季度KPI得E的人,让其填写绩效改进目标,若下一个季度再得到E,那么就得走人



我们绩效等级是ABCDE,A是传说中的等级,B是几个人有机会,大部分人是C和D,E是垫底。

而我们组就有两位小伙伴得到了E,分别是小A和小B。

小领导意思是让他们直接走得了,大不了再招人顶上,而我想着毕竟大家共事一场,现在大环境寒气满满,我也是过来人,还想再争取争取。

于是分析了他们的基本资料,他俩特点还比较鲜明。


小A资料:




  1. 96年,单身无房贷

  2. 技术栈较广,技术深度一般,比较粗心

  3. 坚持己见,沟通少,有些时候会按照自己的想法来实现功能



小B资料:




  1. 98年,热恋有房贷

  2. 技术基础较薄弱,但胜在比较认真

  3. 容易犯一些技术理解上的问题



了解了小A和小B的历史与现状后,我分别找他们沟通,主要是统一共识:




  1. 你是否认可本次绩效评估结果?

  2. 你是否认可绩效改进的点与风险点(未达成被裁)?

  3. 你是否还愿意在这家公司苟?



最重要是第三点,开诚布公,若是都不想苟了,那就保持现状,不要浪费大家时间,我也不想做无用功。

对于他们,分别做了提升策略:


对于小A:




  1. 每次开启需求前都要求其认真阅读文档,不清楚的地方一定要做记录并向相关人确认

  2. 遇到比较复杂的需求,我也会一起参与其中梳理技术方案

  3. 需求开发完成后,CR代码看是否与技术方案设计一致,若有出入需要记录下来,后续复盘为什么

  4. 给足时间,保证充分自测



对于小B:




  1. 每次需求多给点时间,多出的时间用来学习技术、熟悉技术

  2. 要求其将每个需求拆分为尽可能小的点,涉及到哪些技术要想清楚、弄明白

  3. 鼓励他不懂就要问,我也随时给他解答疑难问题,并说出一些原理让他感兴趣的话可以继续深究

  4. 分配给他一些技术调研类的任务,提升技术兴趣点与成就感



3. 结束?还是是另一个开始?


半年后...


好消息是:小A、小B的考核结果是D,达成了绩效改进的目标。

坏消息是:据说新的一轮考核算法会变化,宗旨是确保团队血液新鲜(每年至少得置换10%的人)。


随缘吧,我尽力了,也许下一个是我呢?



image-20250730002436026.png


作者:小鱼人爱编程
来源:juejin.cn/post/7532334931021824034
收起阅读 »

第一个成功在APP store 上架的APP

web
XunDoc开发之旅:当AI医生遇上家庭健康管家 当我在生活中目睹家人为管理复杂的健康数据、用药提醒而手忙脚乱时,一个想法冒了出来:我能否打造一个App,像一位贴心的家庭健康管家,把全家人的健康都管起来?它不仅要能记录数据,还要够聪明,能解答健康疑惑,能主动提...
继续阅读 »

XunDoc开发之旅:当AI医生遇上家庭健康管家


当我在生活中目睹家人为管理复杂的健康数据、用药提醒而手忙脚乱时,一个想法冒了出来:我能否打造一个App,像一位贴心的家庭健康管家,把全家人的健康都管起来?它不仅要能记录数据,还要够聪明,能解答健康疑惑,能主动提醒。这就是 XunDoc App。


1. 搭建家庭的健康数据中枢


起初,我转向AI助手寻求架构指导。我的构想很明确:一个以家庭为单位,能管理成员信息、记录多种健康指标(血压、血糖等)的系统。AI很快给出了基于SwiftUI和MVVM模式的代码框架,并建议用UserDefaults来存储数据。


但对于一个完整的应用而言,我马上遇到了第一个问题:数据如何在不同视图间高效、准确地共享? 一开始我简单地使用@State,但随着功能增多,数据流变得一团糟,经常出现视图数据不同步的情况。


接着在Claude解决不了的时候我去询问Deepseek,它一针见血地指出:“你的数据管理太分散了,应该使用EnvironmentObject配合单例模式,建立一个统一的数据源。” 这个建议成了项目的转折点。我创建了FamilyShareManagerHealthDataManager这两个核心管家。当我把家庭成员的增删改查、健康数据的录入与读取都交给它们统一调度后,整个应用的数据就像被接通了任督二脉,立刻流畅稳定了起来。


2. 请来AI医生:集成Moonshot API


基础框架搭好,接下来就是实现核心的“智能”部分了。我想让用户能通过文字和图片,向AI咨询健康问题。我再次找到AI助手,描述了皮肤分析、报告解读等四种咨询场景,它很快帮我写出了调用Moonshot多模态API的代码。


然而,每件事都不能事事如意的。文字咨询很顺利,但一到图片上传就频繁失败。AI给出的代码在处理稍大一点的图片时就会崩溃,日志里满是编码错误。我一度怀疑是网络问题,但反复排查后,我询问Deepseek,他告诉我:“多模态API对图片的Base64编码和大小有严格限制,你需要在前端进行压缩和校验。”


我把他给我的建议给到了Claude。claude帮我编写了一个“图片预处理”函数,自动将图片压缩到4MB以内并确保编码格式正确。当这个“关卡”被设立后,之前桀骜不驯的图片上传功能终于变得温顺听话。看着App里拍张照就能得到专业的皮肤分析建议,那种将前沿AI技术握在手中的感觉,实在令人兴奋。


3. 打造永不遗忘的智能提醒系统


健康管理,贵在坚持,难在记忆。我决心打造一个强大的医疗提醒模块。我的想法是:它不能是普通的闹钟,而要像一位专业的护士,能区分用药、复查、预约等不同类型,并能灵活设置重复。


AI助手根据我的描述,生成了利用UserNotifications框架的初始代码。但很快,我发现了一个新问题:对于“每周一次”的重复提醒,当用户点击“完成”后,系统并不会自动创建下一周的通知。这完全违背了“提醒”的初衷。


“这需要你自己实现一个智能调度的逻辑,在用户完成一个提醒时,计算出下一次触发的时间,并重新提交一个本地通知。” 这是deepseek告诉我的,我把这个需求告诉给了Claude。于是,在MedicalNotificationManager中, claude加入了一个“重新调度”的函数。当您标记一个每周的用药提醒为“已完成”时,App会悄无声息地为您安排好下一周的同一时刻的提醒。这个功能的实现,让XunDoc从一个被动的记录工具,真正蜕变为一个主动的健康守护者。


4. 临门一脚:App Store上架“渡劫”指南


当XunDoc终于在模拟器和我的测试机上稳定运行后,我感觉胜利在望。但很快我就意识到,从“本地能跑”到“商店能下”,中间隔着一道巨大的鸿沟——苹果的审核。证书、描述文件、权限声明、截图尺寸……这些繁琐的流程让我一头雾水。


这次,我直接找到了DeepSeek:“我的App开发完了,现在需要上传到App Store,请给我一个最详细、针对新手的小白教程。”


DeepSeek给出的回复堪称保姆级,它把整个过程拆解成了“配置App ID和证书”、“在App Store Connect中创建应用”、“在Xcode中进行归档打包”三大步。我就像拿着攻略打游戏,一步步跟着操作:



  • 创建App ID:在苹果开发者后台,我按照说明创建了唯一的App ID com.[我的ID].XunDoc

  • 搞定证书:最让我头疼的证书环节,DeepSeek指导我分别创建了“Development”和“Distribution”证书,并耐心解释了二者的区别。

  • 设置权限:因为App需要用到相机(拍照诊断)、相册(上传图片)和通知(医疗提醒),我根据指南,在Info.plist文件中一一添加了对应的权限描述,确保审核员能清楚知道我们为什么需要这些权限。


一切准备就绪,我在Xcode中点击了“Product” -> “Archive”。看着进度条缓缓填满,我的心也提到了嗓子眼。打包成功!随后通过“Distribute App”流程,我将我这两天的汗水上传到了App Store Connect。当然不是一次就通过上传的。


image.png


5. 从“能用”到“好用”:三次UI大迭代的觉醒


应用上架最初的兴奋感过去后,我陆续收到了一些早期用户的反馈:“功能很多,但不知道从哪里开始用”、“界面有点拥挤,找东西费劲”。这让我意识到,我的产品在工程师思维里是“功能完备”,但在用户眼里可能却是“复杂难用”。


我决定重新设计UI。第一站,我找到了国产的Mastergo。我将XunDoc的核心界面截图喂给它,并提示:“请为这款家庭健康管理应用生成几套更现代、更友好的UI设计方案。”


Mastergo给出的方案让我大开眼界。它弱化了我之前强调的“卡片”边界,采用了更大的留白和更清晰的视觉层级。它建议将底部的标签栏导航做得更精致,并引入了一个全局的“+”浮动按钮,用于快速记录健康数据。这是我第一套迭代方案的灵感来源:从“功能堆砌”转向“简洁现代”


image.png
然而,Mastergo的方案虽然美观,但有些交互逻辑不太符合iOS的规范。于是,第二站,我请来了Stitch。我将完整的产品介绍、所有功能模块的说明,以及第一版的设计图都给了它,并下达指令:“请基于这些材料,完全重现XunDoc的完整UI,但要遵循iOS Human Interface Guidelines,并确保信息架构清晰,新用户能快速上手。”等到他设计好了后 我将我的设计图UI截图给Claude,让他尽可能的帮我生成。


image.png
(以上是我的Stitch构建出来的页面)
Claude展现出了惊人的理解力。它不仅仅是在画界面,而是在重构产品的信息架构。它建议将“AI咨询”的四种模式(皮肤、症状、报告、用药)从并列排列,改为一个主导航入口,进去后再通过图标和简短说明让用户选择。同时,它将“首页”重新定义为真正的“健康概览”,只显示最关键的数据和今日提醒,其他所有功能都规整地收纳入标签栏。这形成了我的第二套迭代方案从“简洁现代”深化为“结构清晰”


image.png


拿着Claude的输出,我结合Mastergo和Stitch的视觉灵感,再让Cluade一步一步的微调。我意识到,颜色不仅是美观,更是传达情绪和功能的重要工具。我将原本统一的蓝色系,根据功能模块进行了区分:健康数据用沉稳的蓝色,AI咨询用代表智慧的紫色,医疗提醒用醒目的橙色。图标也设计得更加线性轻量,减少了视觉负担。(其实这是Deepseek给我的建议)这就是最终的第三套迭代方案在清晰的结构上,注入温暖与亲和力


image.png
这次从Stitch到Claude的UI重塑之旅,让我深刻意识到,一个成功的产品不仅仅是代码的堆砌。它是一次与用户的对话,而设计,就是这门对话的语言。通过让不同的AI助手在我的引导下“协同创作”,我成功地让XunDoc从一個工程师的作品,蜕变成一个真正为用户着想的产品。


现在这款app已经成功上架到了我的App store上 大家可以直接搜索下来进行使用和体验,我希望大家可以在未来可以一起解决问题!


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