写给小公司前端的 UI 规范
写给小公司前端的 UI 规范大部分小公司前端开发是不是都会有个困扰,一天到晚做的都是后台管理系统,而且百分之 80 都是表格的增删改查,导致领导觉得前端简单的很,所以连个 UI 都没有,但是每次写完页面又被老大吐槽没有审美,而且如果整个系统多个人开发,每个的风格都不一样,导致整个系统看起来很乱,想要统一又不知道从何入手,所以今天我给大家分享一下我们团队的针对后台管理系统的 UI 规范,希望对大家有帮助。叠个甲:这个只是我们遵循的规范,因为交互和设计这种东西每个人的感受不一样,你觉得你这个规范我觉得不合理,没关系,你可以按照自己的想法来也可以,只要遵循一个原则,那就是整个系统风格保持一致性即可,所以这个规范仅供参考。大家都知道后台管理主要就是几部分:表格、表单、弹窗,所以我们的 UI 规范也是围绕这三部分来的。顺便吆喝一句,技术大厂[机会],前后端测试捞人,待遇还可以,感兴趣可以试一试。表格自适应形式最小页面宽度+自适应的综合运用,最小自适应页面宽度 1366px(小于此宽度则不再自适应,超出页面的内容使用滚动条查看),单元格字段过长一行展示不下时,不换行并出现省略号,鼠标移入,提示框显示完整字段。表格内行高场景字号行高适用情况紧凑模式12px42px数据量大的表格标准模式14px48px默认推荐大字号模式16px56px无障碍/老年版表格内对齐方式表头和文字内容:采用左对齐表头和普通数字:采用左对齐表头和具有比较场景的数字: 采用右对齐表头和操作项: 采用左对齐分页器分页器元素:数据总量、单页面展示数量、翻页部分对齐方式:数据总量左对齐,单页面展示数量&翻页部分右对齐(依次顺序为数据总量、单页面展示数量、翻页部分)分页器位置:表格为页面全部内容时,表格超出一页分页器固定及底,表格未超出一页分页器跟随表格下方表格仅为页面部分内容时,分页器跟随表格下方表格操作区按钮类型:新增数据类按钮 &面向已有数据类的操作按钮(包含可合并类操作按钮)。对齐方式:操作区居右,主按钮居右。视觉样式:新增数据类按钮样式-【文字+主色底】或【文字+图标+主色底文字+中性色线框】面向已有数据类的操作按钮样式-【文字+主色底】或【文字+图标+主色底文字+中性色线框】按钮个数:小于等于 4 个全部展示大于四个时候,最后一个按钮为【更多】按钮,点击【更多】按钮后,下拉展示全部按钮表格筛选区域一行最多四个筛选条件,不超过四个的时候查询按钮和重置按钮跟在筛选项后超过四个筛选条件时,出现【展示更多】按钮,点击【展示更多】按钮后,下拉展示全部筛选条件,【展示更多】变成【收起更多】按钮如果是时间范围 比如开始时间和结束时间 他独占两个筛选项默认 label 最多四个字 建议两个字或者四个字筛选项的宽度建议 200px表格滑块表格为页面全部内容时:内容无超出:滚动不出现横向内容有超出:表格内横向滚动条,且默认固定操作和标题列纵向内容有超出:页面滚动条,表头到达页面顶部,吸顶固定表格仅为页面部分内容时:表格区最大高度为 10 行数据+分页器,当 10 行数据+分内器超出页面容器时,表格区最大高度将限制为页面容器的 90%内容无超出:滚动不出现横向内容有超出:表格内横向滚动条,且默认固定操作项和标题列纵向内容有超出:表格内滚动条,固定表头&分页器表单表单项 Label 与控件的对齐方式&必填标识表单项 Label 和控件对齐方式当表单项 Label 过长(6 个中文字符以上),采用顶对齐方式布局。当表单项 Label 在 6 个中文字符以内,使用右对齐方式布局。 对齐布局;当表单项在 15 个以下时,采用单列布局方式,当表单项在 15 个以上时,采用多列顶对齐方式布局。表单项 Label 和必填标识对齐方式表单项 Label 右对齐布局时,必填示识放在标题前;表单项 Label 顶对齐布局时,必填示识放在标题后;提交/取消操作按钮位置&对齐方式操作按钮统一采用左对齐的方式,表单域未超出一屏时跟在表单项下方,若超出一屏则固定吸底。表单页自适应方式&lnput 框长度当只有单列时,表单域左对齐且定宽 、输入框&选择框长度为 480px、文本框长度为 640px,支持表单项 Label 顶对齐和右对齐。当出现双列时,表单域左对齐,表单域宽度为页面容器宽度的 80%,自适应布局,仅支持表单项 Label 顶对齐。当出现三列时,表单域占满整个页面容器,自适应布局,仅支持表单项 Label 顶对齐。弹窗弹窗尺寸:在未达到弹窗最大尺寸(弹窗最大宽高[removed]
Vite 底层彻底换血,尤想要控制整个前端生态?
Hello,大家好,我是 Sunday。最近,尤发了一篇非常关键的文章,宣布 Vite 正式引入 Rust 写的打包器 Rolldown,并将逐步替代现有的 Rollup 成为 默认打包器。该文章发布在 尤 新公司 void(0),文章链接:https://voidzero.dev/posts/announcing-rolldown-vite虽然这篇文章的内容并不长,但是内部做出的改成确实非常大的,可以毫不夸张的说:尤把整个 vite 的心脏都换掉了!所以,咱们今天这篇文章,我不打算重复发布会上的内容,而是一起来看看这波 “换心脏” 的背后逻辑:为什么 Rust 能上位?真实速度到底快了多少?尤到底在下一盘什么棋?跳板→技术大厂,前后端or测试捞人,待遇还可以,感兴趣可以试一试。01:Vite 正在全面 Rust 化很多人看到这次更新,可能会说:“Rolldown 不就是个性能更好的打包器吗?用不用都行吧?”说实话,这种理解可能有些过于表面了。这次更新的不仅仅是一个工具,而是把整个的 vite 底层都重写了一遍:Vite 的打包器,从 JS 写的 Rollup,换成了 Rust 写的 Rolldown配套的 Babel、Terser、ESLint,也被 Rust 实现的 Oxc 接管整个构建链路,从解析到压缩,从转换到分析,全都 Rust为什么要这么干呢?很简单,因为:JS 写的构建工具已经摸到天花板了。不管你怎么做缓存、怎么压缩 AST、怎么优化 Plugin 顺序,JS 就是做不到 Rust 那种级别的执行效率。而现代前端的项目体积正在变得越来越大,早就不是之前只写几个静态页面的事情了!目前 微前端、组件库、国际化、权限系统……每加一个功能,构建时间就会变得越来越长,特别是很多公司在配合 CI/CD 的逻辑,每构建跑一次可能就得跑 2 分钟,而如果换成 Rolldown 那么就只要 15 秒上下了,你算算整个团队每天省下多少时间?因此,这样的替换 “势在必行”,同时这也标记着:Vite 已经不再是一个 JS 写的现代前端工具了,而是一个由 Rust 驱动的、高性能构建内核。02:真实表现到底快了多少?咱们先来看官方数据:这些官方给出的数据看上去是不是非常炸裂!但是,根据前端圈的历史特性:所有官方性能对比,都只能作为参考,而不能作为实际的决策依据。为啥呢?因为,实际开发中,环境不同、项目结构不同、依赖链不同、构建目标不同,变量太多了。很多的 demo 是干净环境下跑的,而你实际项目里,插件、polyfill、非预构建依赖一大堆,所以 官方数据,仅供参考!但我要说的是:哪怕实际操作中,只能做到官方数据的一半,这件事也值得我们去尝试下。就拿我自己接触的几个中大型项目来说,生产环境下的 Vite 构建时间基本都在 30 秒到 2 分钟之间浮动,特别是:多语言、主题、子应用拆包场景下,Rollup 明显吃力babel + terser 的组合在压缩阶段特别耗 CPU内存比较小的,如果在启动其他的任务(你电脑总得开其他的软件吧),那速度就更慢了换句话说,如果 Rolldown 真能在这些环节上带来 哪怕 30% 的性能提升,对于团队的持续集成、构建稳定性、开发反馈体验,都是实打实的收益。03:尤在下一盘大棋很多同学可能会说:Vite 已经“遥遥领先”了,为啥还非要换底层呢?多麻烦呀!如果你有这种疑惑的话,那么可能是因为你对 vite 使用到的这些新工具还不太了解,如果你了解一下背后的发布方,就知道这件事没那么简单。Rolldown 是谁发布的?不是 Vue,也不是 Vite 核心团队,而是尤创办的新公司 —— VoidZero (也叫做 void(0) ) 。这家公司刚一出手就连放两个大招:第一个是 Oxc :这是一个全新的 Rust 实现 JS 工具链(parser、transform、minifier、linter、formatter,全都自己造)第二个就是 Rolldown:Vite 打包器的 Rust 替代方案,目标直接瞄准 Rollup、Babel、Terser 这整条传统链路而这次 Vite 接入 Rolldown,正是 void(0) 把自家工具「回注入」开源生态的第一步。所以这不是在“优化 Vite”,而是想要 “替换整条构建基础设施”你可以这么理解 void(0) 的策略路径:Vue 站稳前端框架圈核心位置Vite 用 Rollup 起家,成为构建工具主流选择void(0) 作为新公司登场,切入工具链底层,用 Rust 重写一整套生态再反哺 Vite,用 Rolldown 替代原来的 JS 构建方案最终形成:Vue + Vite + void(0) 工具链 的闭环这其实是一个很聪明、很清晰的长期路线图:不再被 Babel、Terser、ESLint 等“生态外依赖”所绑定,而是自己控制工具底层、性能节奏、开发体验。尤本人也在社区里反复提过:Vite 的未来,不只是“构建工具”,而是下一代工程化的“前端开发基建平台”。而这张底牌,就是 Rolldown + Oxc。你可以想想看,如果:Vue 生态已经在试水 RolldownVite 即将全面接入 RolldownVite 插件作者必须适配 Rolldown(否则未来会不兼容)那就意味着:无论你是 Vue、React、Svelte,还是用 Vite 的任何框架,都必须配合这次 “Rust 工具链” 的迁移。 否则将有可能会被踢出前端生态。而想要参与,就必须要使用 Void(0) 的产品。这样,尤就可以很成功的让 Void(0) 变成整个前端生态的标准了!——转载自:程序员Sunday#畅聊专区#
这样的SQL太吓人了
很多小伙伴看到了能够快速发现问题,当 company_id 为 null 的时候,会导致全表更新。但是也有小伙伴不解,自己平时就是这么写的呀,也没什么问题,如果有问题,那么上面的 SQL 该怎么改呢?松哥来和大家简单聊几句。一 防止全表更新如果在生产环境中使用 UPDATE 语句更新表数据,此时如果忘记携带本应该添加的 WHERE 条件,那么后果不堪设想。那么怎么避免这个问题呢?二 sql_safe_updatessql_safe_updates 是 MySQL 数据库中的一个参数,它的作用是增强数据安全性,防止因误操作导致的数据丢失或破坏。具体来说,当 sql_safe_updates 设置为 ON(启用)时,MySQL 将阻止执行没有明确 WHERE 子句的 UPDATE 或 DELETE 语句。这意味着如果试图运行一个不包含 WHERE 条件来限定更新或删除范围的 DML 语句,MySQL 会抛出一个错误。而当 sql_safe_updates 设置为 OFF(禁用)时,MySQL 不会对此类无条件更新或删除操作进行特殊限制,允许它们按常规方式执行这个参数可以配置在会话级别或全局级别。在会话级别,可以通过执行 SET sql_safe_updates = 1; 命令来启用,这只对当前连接有效。在全局级别,可以通过 SET GLOBAL sql_safe_updates = 1; 命令或在 MySQL 配置文件中设置,这会影响服务器上所有新的会话,但是这个配置不会修改当前会话。启用 sql_safe_updates 参数可以减少因人为失误引发的重大数据事故,尤其适合开发环境和对数据完整性要求严格的生产环境。我们可以先执行 SHOW VARIABLES LIKE '%sql_safe_updates%'; 查看当前配置:然后执行 SET sql_safe_updates = 1; 去更新,更新之后再去查看配置,发现 sql_safe_updates 就已经开启了:这个时候,假设我们执行如下 SQL:
UPDATE user set username='javaboy';
就会报一个错误:需要注意的是,启用 sql_safe_updates 参数可能会影响现有应用程序的正常运行,特别是那些依赖于无条件更新或删除操作的程序,因此在生产环境中启用之前,必须确保所有相关的应用程序代码已经过严格审查和适配。顺便吆喝一句,技术大厂,前后端测试[捞人][捞人],待遇还可以,感兴趣可以试试。三 SQL 插件MyBatis-Plus 提供了一个非法 SQL 拦截插件叫做 IllegalSQLInnerInterceptor。这是 MyBatis-Plus 框架中的一个安全控制插件,用于拦截和检查非法 SQL 语句。这个插件主要提供了四方面的功能:识别并拦截特定类型的 SQL 语句,如全表更新、删除等高风险操作。确保在执行查询时使用索引,以提高性能并避免全表扫描。防止未经授权的全表更新或删除操作,减少数据丢失风险。对包含 not、or 关键字或子查询的 SQL 语句进行额外检查,以防止逻辑错误或性能问题。插件用法也简单,配置一个 Bean 即可:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加非法SQL拦截器
interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
return interceptor;
}
}
配置完成后,如果执行了不带 where 条件的 update 或者 delete 语句,就会报如下错误。但是!!!如果你的 SQL 后面有个 where 1=1,那么这样的 SQL 是不会被 IllegalSQLInnerInterceptor 插件识别并拦截的。四 IDEA 插件利用 IDEA 的一些插件,也可以检测到有风险的 SQL,比如松哥常用的这个:不过这些插件不一定能检测出来文章一开始所提出的问题。五 Code Review日常的 Code Review 也不可少,很多问题都是在 CR 的时候发现的。六 问题解决除了上面提到的各种办法之外,对于本文一开始提出的问题,这个有问题的 SQL 还可以做哪些修改呢?欢迎小伙伴们评论区给出自己的答案~松哥也会在评论区给出我的看法!——转载自:江南一点雨#畅聊专区#
如何医治一条慢SQL ?
前言"苏工,订单列表又崩了!"接到电话时,我对着监控大屏上999ms的SQL响应时间哭笑不得。几年来,我发现一个定律:所有SQL问题都是在凌晨三点爆发!今天抽丝剥茧,教你用架构师的思维给慢SQL开刀手术。希望对你会有所帮助。1 术前检查:找准病灶1.1 EXPLAIN 查看执行计划使用EXPLAIN查看SQL语句的执行计划,相当于给SQL拍了张X光。下面是一个典型的SQL问题,它是某电商平台历史订单查询的SQL语句:SELECT *
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN products p ON o.product_id = p.id
WHERE o.create_time > '2023-01-01'
AND u.vip_level > 3
AND p.category_id IN (5,8)
ORDER BY o.amount DESC
LIMIT 1000,20;
使用EXPLAIN关键字查看执行计划的结果如下:+----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+
| id | select_type | table | type | possible_keys | key | rows | Extra| key_len |
+----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+
| 1 | SIMPLE | o | ALL | idx_user_time | NULL | 1987400 | Using where; Using filesort |
| 1 | SIMPLE | u | ALL | PRIMARY | NULL | 100000 | Using where |
| 1 | SIMPLE | p | ALL | PRIMARY | NULL | 50000 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+
诊断报告:全表扫描三连击(type=ALL)filesort暴力排序(内存警告)索引全军覆没 技术大厂[跳板][跳板],前后端测试捞人,待遇还可以,感兴趣可以试试~2 手术方案:精准打击2.1 单表代谢手术如果通过执行计划查到是索引有问题,我们就需要单独优化索引。病根:JSON字段索引失效错误用法:ALTER TABLE users ADD INDEX idx_extend ((extend_info->'$.is_vip'));
extend_info字段是JSON类型的字段,即使创建了索引,索引也会丢失。正解姿势(MySQL 8.0+):ALTER TABLE users ADD INDEX idx_vip_level (vip_level);
ALTER TABLE orders ADD INDEX idx_create_user (create_time, user_id) COMMENT '组合索引覆盖查询';
创建组合索引覆盖查询。2.2 血管疏通术卡点分析:原始join顺序是:orders → users → products
优化后的方案:(子查询过滤users) → products → orders
调整执行顺序,用小表驱动大表。重写后的SQL:SELECT o.*
FROM products p
INNER JOIN (
SELECT o.id, o.amount, o.create_time
FROM orders o
WHERE o.create_time > '2023-01-01'
) o ON p.id = o.product_id
INNER JOIN (
SELECT id
FROM users
WHERE vip_level > 3
) u ON o.user_id = u.id
WHERE p.category_id IN (5,8)
ORDER BY o.amount DESC
LIMIT 1000,20;
术后效果:先扫小表(users过滤后只有100条)消除冗余字段传输减少Join时临时表生成2.3 开颅手术通过执行计划锁定了问题,走错索引了,该怎么处理呢?可以通过FORCE INDEX强制指定索引:SELECT /*+ INDEX(o idx_create_user) */
o.id, o.amount
FROM orders o FORCE INDEX (idx_create_user)
WHERE o.create_time > '2023-01-01';
使用衍生表加速:SELECT *
FROM (
SELECT id, amount
FROM orders
WHERE create_time > '2023-01-01'
ORDER BY amount DESC
LIMIT 1020
) tmp
ORDER BY amount DESC
LIMIT 1000,20;
医嘱:警惕OR导致的索引失效用覆盖索引避免回表查询CTE表达式谨慎使用2.4 生命体征监测查看索引使用:SHOW INDEX FROM orders;
监控索引使用率:SELECT object_schema, object_name, index_name,
count_read, count_fetch
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL;
——转载自:苏三说技术#畅聊专区#
为了不让领导看到我的屏幕,我写了一个 Chrome 插件
那天下午,我正在浏览一些到岛国前端技术文档,突然听到身后传来脚步声。我下意识地想要切换窗口,但已经来不及了——我的同事小张已经站在了我身后。"咦,你在看什么?"他好奇地问道。我尴尬地笑了笑,手忙脚乱地想要关闭页面。那一刻,我多么希望有一个快捷键,能瞬间让整个屏幕变得模糊,这样就不会有人看到我正在浏览的内容了。于是乎我想:为什么不开发一个 Chrome 插件,让用户能够一键模糊整个网页呢?这样不仅能保护隐私,还能避免类似的尴尬情况。开发过程说干就干,我开始了 Web Blur 插件的开发。这个插件的核心功能很简单:一键切换:使用快捷键(默认 Ctrl+B)快速开启/关闭模糊效果可调节的模糊程度:根据个人喜好调整模糊强度记住设置:自动保存用户的偏好设置跳板实现技术大厂,前/后端or测试多地捞人,待遇给的还可以,想看机会的哥们可以试试。技术实现1.首先,我们需要在 manifest.json 中声明必要的权限:
"manifest_version": 3,
"name": "Web Blur",
"version": "1.0",
"permissions": [
"activeTab",
"storage",
"commands"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"128": "images/icon.png"
}
},
"commands": {
"toggle-blur": {
"suggested_key": {
"default": "Ctrl+Shift+B"
},
"description": "Toggle blur effect"
}
}
}
2. 实现模糊效果
function applyBlur(amount) {
const style = document.createElement('style');
style.id = 'web-blur-style';
style.textContent = `
body {
filter: blur(${amount}px) !important;
transition: filter 0.3s ease;
}
`;
document.head.appendChild(style);
}
// 移除模糊效果
function removeBlur() {
const style = document.getElementById('web-blur-style');
if (style) {
style.remove();
}
}
3. 快捷键控制
if (command === 'toggle-blur') {
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, {action: 'toggleBlur'});
});
}
});
4. 用户界面
5. 设置持久化
function saveSettings(settings) {
chrome.storage.sync.set({settings}, () => {
console.log('Settings saved');
});
}
// 加载设置
function loadSettings() {
chrome.storage.sync.get(['settings'], (result) => {
if (result.settings) {
applySettings(result.settings);
}
});
}
以后可以愉快的学技术辣——转载自:想想肿子会怎么做#畅聊专区#
Tailwind 到底是设计师喜欢,还是开发者在硬撑?
我们最近刚把一个后台系统从 element-plus 切成了完全自研组件,CSS 层统一用 Tailwind。全员同意设计稿一致性提升了,但代码里怨言开始冒出来。这篇文章不讲原理,直接上代码对比和团队真实使用反馈,看看是谁在享受,谁在撑着。1.组件内样式迁移原先写法(BEM + scoped):
Tailwind 重写:
优点:组件直接可读,不依赖 class 定义样式即结构,调样式时不用来回翻缺点:设计稿变了?全组件搜索 text-sm 改成 text-base?无法抽象:多个地方复用 .text-label 变成复制粘贴2.复杂交互样式纯 CSS(原写法)
Tailwind 写法
问题来了:✅ 简单 hover/active 很方便❌ 多态样式(如 disabled + dark mode + hover 同时组合)就很难读:
调试时需要反复阅读 class 字符串,不能直接 Cmd+Click 查看样式来源。新跳板机会技术大厂,前/后端or测试前/后端or测试 多地HC,至少待遇给的还可以。3.统一样式封装,复用方案混乱原写法:统一样式变量 + class
复制代码
$border-color: #eee;
.panel {
border: 1px solid $border-color;
border-radius: 8px;
}
Tailwind 使用中经常出现的写法:
问题来了:设计稿调整了主色调或边框粗细,如何批量更新?BEM 模式下你只需要改一个变量,Tailwind 下必须靠 @apply 或者手动替换所有 .border-gray-200。于是我们项目里又写了一堆“语义类”去封装 Tailwind:
/* 自定义 utilities */
@layer components {
.app-border {
@apply border border-gray-200;
}
.app-card {
@apply p-4 rounded-lg shadow-sm bg-white;
}
}
最后导致的问题是:我们重新“造了个 BEM”,只不过这次是基于 Tailwind 的 apply 写法。🧪 实测维护成本:100+组件、多人协作时的问题我们项目有 110 个组件,4 人开发,统一用 Tailwind,协作两个月后出现了这些反馈:👨💻 A 开发:写得很快,能复制设计稿的 class 直接粘贴🧠 B 维护:改样式全靠人肉找 .text-sm、.p-4,没有结构命名层🤯 C 重构:统一调整圆角半径?所有 .rounded-md 都要搜出来替换所以我们内部的结论是:Tailwind 写得爽,维护靠人背。它适合“一次性强视觉还原”,不适合“结构长期型组件库”。🔧 我们后来的解决方案:Tailwind + token 化抽象我们仍然使用 Tailwind 作为底层 utilities,但同时强制使用语义类抽象,例如:
@layer components {
.text-label {
@apply text-sm text-gray-500;
}
.btn-primary {
@apply bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded;
}
.card-container {
@apply p-4 bg-white rounded-lg shadow;
}
}
模板中统一使用:
标题
内容
这种方式保留了 Tailwind 的构建优势(无 tree-shaking 问题),但代码结构有命名可依,后期批量维护不再靠搜索。📌 最终思考Tailwind 是给设计还原速度而生的,不是给可维护性设计的。 设计师爱是因为它像原子操作; 开发者撑是因为它把样式从结构抽象变成了“字串组合游戏”。如果你的团队更在意开发效率,样式一次性使用,那 Tailwind 非常合适。 如果你的组件系统是要长寿、要维护、要被多人重构的——你最好在 Tailwind 之上再造一层自己的语义层,或者别用。分享完毕,谢谢大家🙂——转载自:ErpanOmer
Cursor,2天,一个小程序!
前言一直想用 Cursor 做点什么,却苦于没有灵感。笔记应用?记账工具?倒数日?这些同质化严重的应用早已泛滥成灾,做了也不过是为互联网增加一份数字垃圾。直到某个深夜,灵光一闪,欸,不如做个电子木鱼?翻遍微信小程序,搜索排名靠前的几款电子木鱼应用,发现它们如出一辙:相似的界面设计,平庸的视觉审美,还有那些无处不在的广告。既然如此,何不自己动手,做一个更优雅的版本?设计稿篇都说 Claude 3.7 Sonnet 的 UI 审美相当在线。作为 Cursor 的深度用户,在这个模型上线的第一天,我就已经领教过它的强大。(小声哔哔:Claude4 已经发布了文章还没写完发布,这该死的拖延症)随手在网上找了段关于 Cursor 生成设计稿的提示词,稍作修改后粘贴到对话框,"啪"的一声,很快啊。
你是一位全栈开发工程师,同时精通产品规划和UI设计。
我现在想要开发一个 "电子木鱼" 微信小程序,需要输出一套完整的APP原型图,请按照下面的要求执行:
- 模拟真实用户使用 "电子木鱼" 小程序的真实场景和需求
- 结合用户需求,以产品经理的视角去规划APP的功能、页面和交互
- 结合产品规划,以设计师的视角去输出完整的高保真UI/UX
- 以上全部页面都在同一个html文件中平铺展示,一行最多显示3个原型页面,且将每个原型页面用类手机边框包裹
- 页面引入tailwindcss来完成样式编写,图片使用unsplash,小图标使用fontawesome
这是第一版生成的设计稿,于我而言,水准已经超出预期了。唯一的缺点是木鱼皮肤素材过于小众,Unsplash 无法提供合适的图片资源。不过,令人惊喜的是,AI 不仅完成了基础功能设计,还贴心地加入了数据统计、成就系统、自动敲击等高级功能。此刻,产品经理、UI 设计师、程序员的内心:天塌了!??有了设计稿,开发工作便可正式启动。在实际开发中,我选择了从 0 到 1 的渐进式开发方式。因此对原有提示词进行了优化,增加了功能范围限定(专注于敲木鱼、换肤、换声音、换文案、自动敲击等核心功能)。+.大厂跳板待遇还可以,考虑技术大厂的弟兄,前后端测试,多地捞人~感兴趣可以试试~前端代码实现UI/UX 设计的输出是一个包含所有原型的 index.html 文件。确定前端技术栈后,便可以开始将设计原型转换为可执行的代码。通过文件关联功能选择 UI/UX 的 HTML 文件,再使用 Agent 功能,告知技术栈信息和开发需求,并提供参考文件。AI 开始马不停蹄地编码......后端架构后端方面,我有一个现成的 Express 服务,正在运行我开源的 EsChatPro 项目——这是一个开箱即用且极易上手的类 ChatGPT/DeepSeek 商业应用,支持接入任何 OpenAI 兼容的大语言模型。项目地址:github.com/isnl/EsChat…既然后端服务已经就绪,我只需要专注于 API 接口的开发。数据表结构设计交给 AI 来完成,最终整理成以下提示词:
## 前置说明
我有前端微信小程序:电子木鱼,需要当前项目为其提供 api 接口
请阅读这个项目源代码,完成需求开发
## 微信小程序登录
在 routes/mp 文件夹中,做微信的登录鉴权
appid 为 wxexxxxxxxxxxxxx2
appsecret 为 8exxxxxxxxxxxxxxxxe9
jwt 鉴权逻辑可参考 src/routes/wechat.ts 文件中公众号登录方案的实现
## 数据表创建
在 src/schema 中创建相关的数据库表
- mp_users 小程序用户相关
字段:openid、name、avatar、role(默认为 0)
- mp_unlocks 资源解锁相关
id: string;
openid: string;
resource_type: string;
resource_key: string;
unlock_time: string;
expire_time: string;
cost: number;
exchange_time: string;
- mp_skins 木鱼皮肤
key: string
name: string
description: string
imageUrl: string
- mp_audio 木鱼音色
key: string
name: string
description: string
audioUrl: string
- mp_text 木鱼文案
key: string,
name: string
content: string
description: string
为什么不选择小程序云开发?诚然,小程序云开发在微信鉴权方面的体验确实无可比拟,但每月 20 元的费用对于个人开发者来说并不便宜——这可是两顿外卖的钱。既然服务器现成,后端服务现成,API 接口又能通过 AI 快速生成,何必多此一举?正所谓:骑自行车去酒吧,该省省该花花。调试与优化调试阶段需要人工介入,仔细检查功能完整性、交互细节、用户体验是否符合预期,排查潜在的 Bug 和性能问题。上手体验木鱼时刻.png(为防广告被删文章,就不放二维码啦,大家自行搜索体验即可)总结这次开发经历让我深刻体会到 AI 辅助编程的强大威力。从设计稿生成到代码实现,从前端到后端,AI 几乎承担了 90% 的工作量。作为开发者,我们的角色正在从"代码编写者"转变为"需求定义者"和"质量把控者"。在这个 AI 时代,学会与 AI 协作,比单纯提升编程技能更加重要。一个周末的时间,一个完整的微信小程序就此诞生。这不仅是技术的胜利,更是创意与效率的完美结合。——转载自:极客密码
如何优雅的进行日期处理!过于优雅了!
前言在我们的日常工作中,需要经常处理各种格式,各种类似的的日期或者时间。比如:2025-04-21、2025/04/21、2025年04月21日等等。有些字段是String类型,有些是Date类型,有些是Long类型。如果不同的数据类型,经常需要相互转换,如果处理不好,可能会出现很多意想不到的问题。这篇文章跟大家一起聊聊日期处理的常见问题,和相关的解决方案,希望对你会有所帮助。顺便吆喝一句,技术大厂,前后端/测试[HC] ,待遇还可以,感兴趣试一试。一、日期的坑1.1 日期格式化陷阱在文章的开头,先给大家列举一个非常经典的日期格式化问题:
// 旧代码片段(线程不安全的经典写法)
public class OrderService {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");*
public void saveOrder(Order order) {
// 线程A和线程B同时进入该方法
String createTime = sdf.format(order.getCreateTime());
// 可能出现"2023-02-30 12:00:00"这种根本不存在的日期
orderDao.insert(createTime);**
}
}
问题复现场景:高并发秒杀场景下,10个线程同时处理订单。每个线程获取到的order.getCreateTime()均为2023-02-28 23:59:59。由于线程调度顺序问题,某个线程执行sdf.format()时。内部Calendar实例已被其他线程修改为非法状态。最终数据库中出现2023-02-30这类无效日期。问题根源:SimpleDateFormat内部使用了共享的Calendar实例,多线程并发修改会导致数据污染。1.2 时区转换我们在处理日期的时候,还可能会遇到夏令时转换的问题:
// 错误示范:简单加减8小时
public Date convertToBeijingTime(Date utcDate) {
Calendar cal = Calendar.getInstance();
cal.setTime(utcDate);
cal.add(Calendar.HOUR, 8); // 没考虑夏令时切换问题
return cal.getTime();
}
夏令时是一种在夏季期间将时间提前一小时的制度,旨在充分利用日光,病节约能源。在一些国家和地区,夏令时的开始和结束时间是固定的。而在一些国家和地区,可能会根据需要调整。在编程中,我们经常需要处理夏令时转换的问题,以确保时间的正确性。隐患分析:2024年10月27日北京时间凌晨2点会突然跳回1点,直接导致订单时间计算错误二、优雅方案的进阶之路2.1 线程安全重构在Java8之前,一般是通过ThreadLocal解决多线程场景下,日期转换的问题。例如下面这样:
// ThreadLocal封装方案(适用于JDK7及以下)
public class SafeDateFormatter {
private static final ThreadLocal[removed] THREAD_LOCAL = ThreadLocal.withInitial(() ->
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static String format(Date date) {
return THREAD_LOCAL.get().format(date);
}
}
线程安全原理:每个线程第一次调用format()方法时会通过withInitial()初始化方法创建独立的DateFormat实例后续该线程再次调用时直接复用已有实例线程销毁时会自动清理ThreadLocal存储的实例原理揭秘:通过ThreadLocal为每个线程分配独立DateFormat实例,彻底规避线程安全问题。2.2 Java8时间API革命在Java8之后,提供了LocalDateTime类对时间做转换,它是官方推荐的方案。例如下面这样:
// 新时代写法(线程安全+表达式增强)
public class ModernDateUtils {
public static String format(LocalDateTime dateTime) {
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
public static LocalDateTime parse(String str) {
return LocalDateTime.parse(str, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}
黑科技特性:288种预定义格式器支持ISO-8601/ZonedDateTime等国际化标准不可变对象天然线程安全最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。转载自:苏三说技术
出了兼容性问题……
背景项目上线后跑了应该有两三个月了,接到生产报事,页面进不去了,用户设备是iPhone8 iOS13.1,用户很气愤,领导也很不乐意,我也很气愤,刚来这项目组就被报事,。但是要解决呀,怎么办?研究以前的代码,加配置呗。浏览器兼容性问题是什么?浏览器兼容性问题通常是指网页或 Web 应用在不同浏览器或版本中表现不一致的问题。说白了无非就是 css不兼容,JS Api在旧版本浏览器中不兼容。解决思路明白目标浏览器范围找个插件将现代 JS 转到 ES5处理一下CSS的兼容性问题解决方案通过定义 .browserslistrc 明确目标浏览器范围使用 Babel 将现代 JS 转到 ES5使用 Autoprefixer 给 CSS 加厂商前缀好了,开搞.browserslistrc文件是什么.browserslistrc 文件是一个配置文件,用于定义目标浏览器和Node.js版本的兼容性列表。这个文件被多个前端工具链和库所使用,如Babel、Autoprefixer、ESLint等,可以帮助我们确定需要转译或添加兼容性前缀的JavaScript和CSS代码版本。通过配置 .browserslistrc,我们可以精确地控制代码应该兼容哪些浏览器和设备,从而优化构建输出和减少最终包的大小。.browserslistrc文件中可以配置的内容浏览器名称和版本:例如,last 2 Chrome versions 表示最新的两个Chrome浏览器版本。市场份额:如 > 1% in US 表示在美国市场份额超过1%的浏览器。年份:since 2017 表示自2017年以来发布的所有浏览器版本。特定浏览器:not IE 11 表示不包括IE 11浏览器。个人项目中使用.browserslistrc配置在个人日常办公项目中 .browserslistrc 文件配置如下:
> 0.2%
last 2 versions
Firefox ESR
not dead
IE 11
这个配置的含义是:支持全球使用率超过0.2%的浏览器。支持最新的两个浏览器版本。支持Firefox的Extended Support Release(ESR)版本。排除所有已经不被官方支持(dead)的浏览器。额外包含IE 11浏览器,尽管它可能不在其他条件内Babel是什么Babel 是一个广泛使用的 JavaScript 编译器/转译器,其核心作用是将 高版本 JavaScript(如 ES6+)转换为向后兼容的低版本代码(如 ES5),以确保代码能在旧版浏览器或环境中正常运行。Babel的主要作用1. 语法转换(Syntax Transformation)将现代 JavaScript 语法(如 let/const、箭头函数、类、模板字符串、解构赋值等)转换为等价的 ES5 语法,以便在不支持新特性的浏览器中运行。2. Polyfill 填充新 API通过插件(如 @babel/polyfill 或 core-js),为旧环境提供对新增全局对象(如 Promise, Array.from, Map, Set)的支持。3. 按需转换(基于目标环境)结合 .browserslistrc 配置,@babel/preset-env 可根据指定的目标浏览器自动决定哪些特性需要转换,哪些可以保留原样。4. 支持 TypeScript 和 JSXBabel 提供了对 TypeScript(通过 @babel/preset-typescript)和 React 的 JSX 语法(通过 @babel/preset-react)的解析与转换能力,无需依赖其他编译工具。5. 插件化架构,高度可扩展Babel 支持丰富的插件生态,开发者可以自定义语法转换规则,比如:按需引入 polyfill(@babel/plugin-transform-runtime)移除调试代码(@babel/plugin-transform-remove-console)支持装饰器、私有属性等实验性语法[顺便吆喝一句,技术大厂,前后端/测试 多地空位,待遇还可以,感兴趣可以试试]@babel/preset-env的核心配置@babel/preset-env 的参数项数量很多,但大部分我们都用不到。我们只需要重点掌握四个参数项即可:targets、useBuiltIns、modules 和 corejs。@babel/preset-env 的 targets 参数该参数项的写法和.browserslistrc 配置是一样的,主要是为了定义目标浏览器。如果我们对 targets 参数进行了设置,那么就不会使用 .browserslistrc 配置了,为了减少多余的配置,我们推荐使用 .browserslistrc 配置。@babel/preset-env 的 useBuiltIns 参数useBuiltIns 项取值可以是usage、 entry 或 false。如果该项不进行设置,则取默认值 false。设置成 false 的时候会把所有的 polyfill 都引入到代码中,整个体积会变得很大。设置成 entry 则是会根据目标环境引入所需的 polyfill,需要手动引入;设置成 usage 则是会根据目标环境和代码的实际使用来引入所需的 polyfill。 此处我们推荐使用:useBuiltIns: usage 的设置。@babel/preset-env 的 corejs 参数该参数项的取值可以是 2 或 3,没有设置的时候取默认值为 2。这个参数只有 useBuiltIns 参数为 usage 或者 entry 时才会生效。在新版本的Babel中,建议使用 core-js@3。@babel/preset-env 的 modules 参数指定模块的输出方式,默认值是 "auto",也可以设置为 "commonjs"、"umd"、"systemjs" 等。个人项目中使用Babel的配置在个人日常办公项目中 .babel.config.js 文件配置如下:
module.exports = {
plugins: [
// 适配某些构建流程中的模块元信息访问方式
() => ({
visitor: {
MetaProperty(path) {
path.replaceWithSourceString('process');
},
},
})
],
presets: [
[
'@babel/preset-env', {
// targets: { esmodules: false, }, // 通过配置browserslist,来使用 browserslist 的配置
useBuiltIns: "usage", // 配置按需引入polyfill
corejs: 3
}
],
'@babel/preset-typescript'
],
};
Autoprefixer 的使用在vite.config.ts文件中css的部分,添加 autoprefixer 的配置。
css: {
postcss: {
plugins: [
postCssPxToRem({
// 这里的rootValue就是你的设计稿大小
rootValue: 37.5,
propList: ['*'],
}),
autoprefixer({
overrideBrowserslist: [
'Android 4.1',
'iOS 7.1',
'ff > 31',
'Chrome > 69',
'ie >= 8',
'> 1%'
]
}),
],
},
},
总结主要通过配置 .browserslistrc 明确目标浏览器范围,使用 Babel 将现代 JS 转到 ES5,主要用到的插件是 @babel/preset-env ,最后再使用 Autoprefixer 插件给 CSS 加厂商前缀。——转载自:页面仔Dony
MySQL同步ES的6种方案!
引言在分布式架构中,MySQL与Elasticsearch(ES)的协同已成为解决高并发查询与复杂检索的标配组合。然而,如何实现两者间的高效数据同步,是架构设计中绕不开的难题。这篇文章跟大家一起聊聊MySQL同步ES的6种主流方案,结合代码示例与场景案例,帮助开发者避开常见陷阱,做出最优技术选型。方案一:同步双写场景:适用于对数据实时性要求极高,且业务逻辑简单的场景,如金融交易记录同步。在业务代码中同时写入MySQL与ES。代码如下:
@Transactional
public void createOrder(Order order) {
// 写入MySQL
orderMapper.insert(order);
// 同步写入ES
IndexRequest request = new IndexRequest("orders")
.id(order.getId())
.source(JSON.toJSONString(order), XContentType.JSON);
client.index(request, RequestOptions.DEFAULT);
}
痛点:硬编码侵入:所有涉及写操作的地方均需添加ES写入逻辑。性能瓶颈:双写操作导致事务时间延长,TPS下降30%以上。数据一致性风险:若ES写入失败,需引入补偿机制(如本地事务表+定时重试)。顺便推个机会,技术大厂,前、后端/测试空位,工资待遇还行,偏中上。方案二:异步双写场景:电商订单状态更新后需同步至ES供客服系统检索。我们可以使用MQ进行解耦。代码示例如下:
// 生产者端
public void updateProduct(Product product) {
productMapper.update(product);
kafkaTemplate.send("product-update", product.getId());
}
// 消费者端
@KafkaListener(topics = "product-update")
public void syncToEs(String productId) {
Product product = productMapper.selectById(productId);
esClient.index(product);
}
优势:吞吐量提升:通过MQ削峰填谷,可承载万级QPS。故障隔离:ES宕机不影响主业务链路。缺陷:消息堆积:突发流量可能导致消费延迟(需监控Lag值)。顺序性问题:需通过分区键保证同一数据的顺序消费。方案三:Logstash定时拉取场景:用户行为日志的T+1分析场景。该方案低侵入但高延迟。配置示例如下:
input {
jdbc{
jdbc_driver=>"com.mysql.jdbc.Driver"
jdbc_url=>"jdbc:mysql://localhost:3306/log_db"
schedule=>"*/5 * * * *"# 每5分钟执行
statement=>"SELECT * FROM user_log WHERE update_time > :sql_last_value"
}
}
output{
elasticsearch{
hosts=>["es-host:9200"]
index=>"user_logs"
}
}
适用性分析:优点:零代码改造,适合历史数据迁移。致命伤:分钟级延迟(无法满足实时搜索)全表扫描压力大(需优化增量字段索引)方案四:Canal监听Binlog场景:社交平台动态实时搜索(如微博热搜更新)。 技术栈:Canal + RocketMQ + ES该方案高实时,并且低侵入。关键配置:
# canal.properties
canal.instance.master.address=127.0.0.1:3306
canal.mq.topic=canal.es.sync
避坑指南:数据漂移:需处理DDL变更(通过Schema Registry管理映射)。幂等消费:通过_id唯一键避免重复写入。方案五:DataX批量同步场景:将历史订单数据从分库分表MySQL迁移至ES。该方案是大数据迁移的首选。配置文件如下:
{
"job":{
"content":[{
"reader":{
"name":"mysqlreader",
"parameter":{"splitPk":"id","querySql":"SELECT * FROM orders"}
},
"writer":{
"name":"elasticsearchwriter",
"parameter":{"endpoint":"http://es-host:9200","index":"orders"}
}
}]
}
}
性能调优:调整channel数提升并发(建议与分片数对齐)启用limit分批查询避免OOM方案六:Flink流处理场景:商品价格变更时,需关联用户画像计算实时推荐评分。该方案适合于复杂的ETL场景。代码片段如下:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.addSource(new CanalSource())
.map(record -> parseToPriceEvent(record))
.keyBy(event -> event.getProductId())
.connect(userProfileBroadcastStream)
.process(new PriceRecommendationProcess())
.addSink(new ElasticsearchSink());
优势:状态管理:精准处理乱序事件(Watermark机制)维表关联:通过Broadcast State实现实时画像关联总结:对于文章上面给出的这6种技术方案,我们在实际工作中,该如何做选型呢?下面用一张表格做对比:方案实时侵入复杂度适用阶段同步双写秒级高低小型单体项目MQ异步秒级中中中型分布式系统Logstash分钟级无低离线分析Canal毫秒级无高高并发生产环境DataX小时级无中历史数据迁移Flink毫秒级低极高实时数仓苏三的建议:若团队无运维中间件能力 → 选择Logstash或同步双写需秒级延迟且允许改造 → MQ异步 + 本地事务表追求极致实时且资源充足 → Canal + Flink双保险——转载自:苏三说技术
数据库建表时才知道我多菜
最近建库设计表,弄得有点不自信了,好久没干过这种细活了,真是家狗吃不了细糠不是; 下面咱就说道说道这个编码格式及所需字节数之间的关系,一起坐好,上课;(以下内容均来自于网络学习及ai回答,没深入细节,没深入细节,没深入细节,不对的地方,大佬们一定要指正) 数据库编码格式首先说下,咱们目前常用的数据库编码格式,其他字符咱没用过也没见过就不瞎说了。 ISO 8859-1 GB2312 GBK GB18030 UTF8 UTF16 UTF32 还有个啥UTF8mb4;太多了,是不是,先过滤下。 看下u8家族的,当我们在设计数据库时,Unicode码与我们的数据库并不是一一对应的,直接看结果: UTF8数据库中UTF8(实际叫utf8mb3)不等于UTF-8,"utf8"只支持每个字符最多3个字节, 对于超过3个字节的字符就会出错,而我们的**汉字虽然通常在utf8的情况下占三个字节**,但是存在占用四个字节的情况,且某些特殊符号也是四个字节,所以utf8淘汰。 UTF-8UTF-8支持1-4个字节,其最小单元是1个字节,也有说它支持**最大6个字节**; utf16utf16的每个字符必须是2个字节或者4个字节,而*UTF编码在最小单元为多字节中存在字节顺序的问题*, 所以UTF-8没这个困扰,但是utf16最小是2字节,所以我们也pass掉吧,费神不是; utf32utf32呢直接一个字符四个字节,但是呢我们的库表并不需要简单粗暴的定长,而是尽量最优使用存储空间(可以参考oracle);utf8mb4数据库里的utf8mb4有说他就是纯正的UTF-8,特性类似于UTF-8;(我以前根本不懂这玩意,就在哪看过说utf8mb4支持emoj我就用它了,没想到是对的); **那最终我们mysql层面u8家族的就剩一个utf8mb4能打了。** 再说一下我们的utf8mb4什么时候是一个字节呢,就是内容在ASCII编码范围内(就是128个字母数字符号)的时候是一个字符;下面这几个一般在oracle上用了(如果mysql也用就当我没说过这句话)ISO 8859-1占用一个字节,不支持汉字等其他字符,所以直接淘汰 GB2312(国标)汉字占用2个字节,非汉字字符(如字母、数字、标点符号等)占用1个字节主要覆盖简体汉字,(对汉字支持不够全面)所以直接淘汰; GBK(国标扩展)兼容GB2312,所需字节数与GB2312一样,GB2312中的字符在GBK中有相同的编码,相对于GB2312添加了繁体字,生僻字,东亚其他文字的支持;(有时我们会使用它)GB18030ai给的评价是基本覆盖了中国所有的汉字(包括少数民族文字)和常用字符需求;(我的想法是正常普通业务不需要这么大的,如果你喜欢当我没说)好了,不同编码格式存储数据所需要的字节数我们差不多知道了吧,下面我们再看看mysql那些讨人厌的字段类型各自的字节数。[顺便吆喝一个,就是有看机会的哥们可以看看,技术大厂,前后端测试捞人,待遇还可以,不妨一试。]数据库字段类型我直接复制网上一份过来 MySQL 字段类型可以简单分为三大类- 数值类型:整型(TINYINT、SMALLINT、MEDIUMINT、INT 和 BIGINT)、浮点型(FLOAT 和 DOUBLE)、定点型(DECIMAL)- 字符串类型:CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT、TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB 等,最常用的是 CHAR 和 VARCHAR。- 日期时间类型:YEAR、TIME、DATE、DATETIME 和 TIMESTAMP 等。DATETIME 和 TIMESTAMP先捡简单的说:日期类型其实我们常用的就两个DATETIME 和 TIMESTAMP,其他三个就是字面意思年份、时间、日期; 这两个说实话大差不差哈,平时我们都用,整体区别就是DATETIME占八个字节,而 TIMESTAMP占四个字节,DATETIME表示的时间范围更广,TIMESTAMP能表示到2038年,但其可以随时区变化; 接着看我们的字符串类型,我此处捡常用的说char 和 varchar简单说就是char是不可变长度,但是varchar是可变长度,这么看好像没啥区别,比如说我们数字类型的字典,那我给varchar(1)岂不是更方便,然后我一顿捣鼓,终于发现存储的区别,**varchar会用字节空间来记录字符长度**,而char是定长的,不需要记录,这就会让mysql在sql优化的时候会考虑这种情况,所以总能看到前人的总结,固定字符数的用char,字符数不固定就用varchar,*有人说char属性的字段如果字符不够会空格填充,又有人说填充仅限于oracle*;注意哈,虽然char(1)表示一个字符空间,但是存储依然只能存储一个值哈,简单理解就是它叫字符个数,varchar同理一样;总结下哈,定长(char),可变长(varchar);整型这里TINYINT、SMALLINT、MEDIUMINT、INT 和 BIGINT我就不拆开说了,下图简单看就是一个字节能存储范围是255,那两个就是255*255,依次类推那我们经常定义整型字段后面那个位数是啥比如int(M)的M是啥,怎么说呢,你就当它毫无用处吧,因为有说他们表示的是**显示宽度**,但是mysql8又不推荐了,所以咱们就当不存在,总结就是这里选择的话看你要表示的范围选取合适的,不用管数据库建表时的长度配置;浮点型给我说我就是不推荐,反驳的理由就是精度无法保证,想要精度就别用他,不在乎精度更没必要用它,当然,如果你就说普通的精度控制其实也可以用,但是给我我不会废这个脑子去思考,其实这两个的精度控制在mysql中我欣赏不来,可能是我navicat问题,我还是喜欢在plsql上操作oracle中double的感觉。举例double(M,D)中 M=整数位+小数位,D=小数位;DECIMAL我也是第一次知道这个叫定点数,DECIMAL(M,D)表示M是最大位数(精度)(整数位+小数位+小数点),范围是1到65。可不指定,默认值是10。 D是小数点右边的位数(小数位)。范围是0到30,并且不能大于M,可不指定,默认值是0, ——转载自:小红帽的大灰狼
Cursor最佳工作方法!!!
我正在使用Cursor,所以我在想我是否应该先了解一下使用它的最佳工作方法。这是为了确保我在处理项目时不会被复杂鶥惧憝疖初性压垮。1、使用Cursor之前,让Claude用Markdown 创建一个清晰详细的计划(让它提出一些澄清性问题,然后批评自己的计划,然后重新生成)。将其添加到Instructions.md文件中(这样你就可以经常让Cursor 参考它)。告诉 ChatGPT我想要创建什么,然后让它为另一个负责编码的 AI提供指令。然后我将所有内容粘贴Cursor Composer Agent 中.ChatGPT 基本上增加了另一层规划,从而降低了遇到问题的几率。在一个项目中,Cursor遇到了一些问题,无论如何都无法解决。浪费了几个小时,陷入了循环。然后我从头开始,但这次我让 ChatGPT o1为另一个编码 AI编写了清晰的指令。它非常有效。 2.、使用.cursorrules(它们总是在AI上下文中)来定义大致的规则。请参阅https:/cursor.directory。例如:先编写测试,然后编写代码,然后运行测试并更新代码,直到测试通过。3、让agent以小段“编辑·测试”循环的方式逐步编写代码。定义一个小任务进行增量。编写(或让AI编写)一个在此增量中失败的测试用例。指示AI(通常在agent模式下)编写代码以通过测试。如果测试失败,AI将分析故障原因并尝试修复代码,环回到步4。一旦测试通过,开发者审查更改。4、在prompt中鼓励链式思维5、当你遇到问题时,让Cursor写一份报告,列出所有文件及其功能,并描述遇到的问题,发给Caude或ChatGPT要解决方案。6、使用gitinsect.com以便将所有脚本配置和相关文件(可按扩展名选)整合到一个页面中。7、https:/context7.com/用于参考最新文档。[removed]~8、请经常使用git进行版本控制,避免出现太多未提交的更改。通过@明确添加文件来保持上下文简短。上下文越长,AI提供的细节就越多。。当上下文边长时,开始新聊天频繁地重新同步 /index 代码使用.cursorsignore 来排除不相关的文件。9、使用 /Reference 打开的编辑器快速将它们添加到上下文中记事本是常用的提示符10、示例:启用Yolo模式以伊它编写测试允许任何测试,例如vitest、npmtest、nr test等,也可以使用基本构建命令,例如 build、tsc等也始终允许创建文件和目录,如touch,mkdir等可选:11、在光标设置中的“AI规则”中设置系统提示保持prompt简洁明了使用替代词汇避免不必要的解释优先考虑技术细节而不是通用建议——转载自:行痴·知无畏
程序员必看:两个思想优化90%的代码
概览在软件开发过程中,代码的可读性和可维护性往往是衡量代码质量的重要指标。本文将介绍两个能够显著提升代码质量的设计原则:组合函数模式(Composed Method Pattern)和抽象层次一致性原则(Single Level of Abstraction Principle, SLAP),并通过实例说明如何在实际开发中应用这些原则。组合函数模式组合函数模式最早由 Kent Beck 在《Smalltalk Best Practice Patterns》一书中提出。这是一个简单易懂且实用的编程原则,能够对代码的可读性和可维护性产生立竿见影的效果。组合函数模式要求:所有公有函数(入口函数)应当读起来像一系列执行步骤的概要具体实现细节应当封装在私有函数中这种模式有助于保持代码精炼并易于复用。阅读这样的代码就像在看一本书,入口函数是目录,指向各自的私有函数,而具体内容则在私有函数中实现。示例代码不良实践: public void processOrder(Order order) { // 验证订单 if (order == null) { throw new IllegalArgumentException("Order cannot be null"); } if (order.getItems().isEmpty()) { throw new IllegalArgumentException("Order must contain at least one item"); } // 计算总价 double total = 0; for (OrderItem item : order.getItems()) { total += item.getPrice() * item.getQuantity(); } // 应用折扣 if (total > 1000) { total *= 0.9; // 10% 折扣 } else if (total > 500) { total *= 0.95; // 5% 折扣 } // 更新订单状态 order.setTotal(total); order.setStatus(OrderStatus.PROCESSED); orderRepository.save(order); // 发送确认邮件 String message = "Your order #" + order.getId() + " has been processed. Total: $" + total; emailService.sendEmail(order.getCustomerEmail(), "Order Confirmation", message); } 良好实践(应用组合函数模式): public void processOrder(Order order) { validateOrder(order); double total = calculateTotal(order); total = applyDiscount(total); updateOrderStatus(order, total); sendConfirmationEmail(order, total); } private void validateOrder(Order order) { if (order == null) { throw new IllegalArgumentException("Order cannot be null"); } if (order.getItems().isEmpty()) { throw new IllegalArgumentException("Order must contain at least one item"); } } private double calculateTotal(Order order) { double total = 0; for (OrderItem item : order.getItems()) { total += item.getPrice() * item.getQuantity(); } return total; } private double applyDiscount(double total) { if (total > 1000) { return total * 0.9; // 10% 折扣 } else if (total > 500) { return total * 0.95; // 5% 折扣 } return total; } private void updateOrderStatus(Order order, double total) { order.setTotal(total); order.setStatus(OrderStatus.PROCESSED); orderRepository.save(order); } private void sendConfirmationEmail(Order order, double total) { String message = "Your order #" + order.getId() + " has been processed. Total: $" + total; emailService.sendEmail(order.getCustomerEmail(), "Order Confirmation", message); } 抽象层次一致性原则(SLAP)抽象层次一致性原则与组合函数密切相关。它要求函数体中的内容必须在同一个抽象层次上。如果高层次抽象和底层细节杂糅在一起,代码会显得凌乱,难以理解。按照组合函数和 SLAP 原则,我们应当:在入口函数中只显示业务处理的主要步骤通过私有方法封装具体实现细节确保一个函数中的抽象在同一个水平上,避免高层抽象和实现细节混杂代码金字塔结构满足 SLAP 实际上是构筑了代码结构的金字塔。金字塔结构是一种自上而下的,符合人类思维逻辑的表达方式。在构筑金字塔的过程中,要求金字塔的每一层要属于同一个逻辑范畴、同一个抽象层次。示例: // 顶层抽象 - 业务流程 public void registerNewUser(UserRegistrationRequest request) { User user = createUserFromRequest(request); validateUser(user); saveUser(user); sendWelcomeEmail(user); } // 中层抽象 - 具体步骤 private User createUserFromRequest(UserRegistrationRequest request) { User user = new User(); mapBasicInfo(user, request); mapAddressInfo(user, request); mapPreferences(user, request); return user; } // 底层抽象 - 实现细节 private void mapBasicInfo(User user, UserRegistrationRequest request) { user.setUsername(request.getUsername()); user.setEmail(request.getEmail()); user.setFirstName(request.getFirstName()); user.setLastName(request.getLastName()); // 其他基本信息映射 } 顺便吆喝一句,技术大厂[机遇][机遇],前后端测试捞人,待遇还可以如何进行抽象1. 寻找共性抽象的过程是合并同类项、归并分类和寻找共性的过程。将有内在逻辑关系的事物放在一起,然后给这个分类进行命名,这个名字就代表了这组分类的抽象。示例:重构重复代码 public void processCustomerA(Customer customer) { System.out.println("Processing customer: " + customer.getName()); double discount = customer.getTotal() * 0.1; customer.applyDiscount(discount); notifyCustomer(customer); } public void processVipCustomer(Customer customer) { System.out.println("Processing customer: " + customer.getName()); double discount = customer.getTotal() * 0.2; customer.applyDiscount(discount); notifyCustomer(customer); } // 抽象后的代码 public void processCustomer(Customer customer, double discountRate) { System.out.println("Processing customer: " + customer.getName()); double discount = customer.getTotal() * discountRate; customer.applyDiscount(discount); notifyCustomer(customer); } public void processRegularCustomer(Customer customer) { processCustomer(customer, 0.1); } public void processVipCustomer(Customer customer) { processCustomer(customer, 0.2); } ——转载自:Lvan
熬夜写了个开源项目,同事看了直呼救命
从 CSS 地狱到开源救赎作为一个被丑陋后台界面折磨到想砸电脑的前端狗,我曾幻想有个管理后台能让我既省心又不辣眼睛。现实呢?公司的后台系统 UI 像 90 年代网页,按钮点下去没反馈,产品经理还吐槽“能不能好看点”。更别提手写 CRUD 页面,调 CSS 调到怀疑人生,部署时还因为配置文件路径炸了锅。终于有一天,我受够了,怒开 VS Code,熬夜到凌晨,敲秃了键盘,搞出了这个开源后台系统——Art Design Pro。结果呢?同事试用后直呼“救命,这界面也太香了!”有个后端小哥甚至默默点了 star,说“终于不用看默认 Element UI 了”。今天,我就来跟大家聊聊这个项目,顺便吐槽一下开发路上的坑。准备好了吗?先点个赞暖暖场!痛点:后台开发的“四大天王”做过后台开发的,都懂那种崩溃:UI 辣眼睛:默认的 Element UI 蓝白配色,看久了像得了视觉疲劳症,产品经理嫌弃“太程序员风”。交互像便秘:按钮没反馈,页面切换卡顿,用户体验差到让客户想跑路。重复造轮子:用户管理、角色分配、数据表格,每次从零写,CSS 调到手抽筋,效率低到想哭。响应式灾难:PC 上好好的界面,手机上一打开直接崩,媒体查询写到凌晨。我在上一家公司就踩了这些坑,领导还要求“两天上线,美观一点”。于是我下定决心,搞一个开源后台系统,目标是“两小时搭建,UI 美到飞起,交互丝滑”,让程序员从 CRUD 和 CSS 地狱中解脱!Art Design Pro:你的后台救星废话不多说,Art Design Pro 是一个基于 Vue3 + TypeScript + Element Plus + Vite 的开源后台管理系统,专为不想被丑 UI 和重复代码折磨的程序员设计。以下是它的杀手锏:美到冒泡的 UI:抛弃 Element UI 的“程序员风”,采用精心调配的配色和动效,页面清爽,产品经理看了都想加鸡腿。丝滑的交互:每个按钮、每处切换都有反馈,操作起来像在“和界面谈恋爱”,再也不怕用户吐槽体验差。模块化神器:用户管理、权限控制、数据表格全内置,组件可自由定制,省下 80% 的重复代码量。全响应式适配:从 PC 到手机,界面自动适配,媒体查询?不存在的!拒绝过度封装:代码简洁透明,想改样式或加功能?直接上手,零学习成本。项目已在 GitHub 收获 1.2k star,被某初创公司用于内部管理系统,产品经理直呼“比商业软件还好看”。顺便吆喝一声,技术大厂机会机会,前后端测试捞人,待遇还可以~三步上手 Art Design Pro想试试 Art Design Pro?三步就能跑起来!克隆项目: git clone https://github.com/Daymychen/art-design-pro.git 安装依赖: cd art-design-pro pnpm install 启动项目: 复制代码 pnpm dev 访问 http://localhost:3006,就能看到美到冒泡的后台界面!来吐槽你的后台故事!你们用过哪些后台系统?遇到过啥奇葩 UI 或交互?评论区吐槽吧!顺手点个 star 支持下 Art Design Pro 呗~演示地址:www.lingchen.kim/art-design-…GitHub 地址:github.com/Daymychen/a…——转载自:琢磨先生TT
new Map 这么好用,你为什么不用?是不喜欢吗?
最近项目闲了,领导有事儿没事儿就进行代码评审。看完我的代码后,领导问我,你的代码怎么全是对Object 增删查改审,你怎么不用`new Map()`呢,我都审美疲劳了!我没用过,但我要装作我知道,于是我赶忙解释,对直接进行对象操作写的代码直观易懂啊!领导还真被我唬住了,哈哈。后来浅浅研究一下,没想到`new Map()`用起来确实更优雅!好用,爱用! 为什么要用 `new Map`?当我第一次认真用 `Map`,有点像发现了新大陆。`Map` 是 JavaScript 提供的一种键值对集合。在处理键值对时,它的这些优点真的令我上头:- 键可以是任何类型不像普通对象的键只能是字符串或 symbol,`Map` 的键可以是对象、数组、函数,甚至是 NaN!const map = new Map(); map.set({}, 'hello'); // 对象也能做 key(注意,你没有对象)! - 顺序可控,操作清晰`Map` 保留插入顺序,遍历也很优雅:for (const [key, value] of map) { console.log(key, value); } 比起 Object 还要 `Object.keys()` + 再去 index 取值,`Map` 简直是优雅代名词 💅- 一行就能「映射」数组const map = new Map(arr.map(item => [item.id, item.value])); 这是不是比用 reduce + object 拼键值对方便多了?为什么我们以前不用 Map?我相信有部分人一定和我一样,不用new Map完全是因为不了解这个东西,也不知道它的使用场景!没关系,我来帮你梳理一下。常用方法- 创建一个 Mapconst map = new Map(); 也可以通过数组初始化:const map = new Map([ ['name', 'Alice'], ['age', 25] ]); - `set(key, value)`添加或更新键值对。map.set('city', 'Beijing'); map.set(123, 'number key'); map.set({ id: 1 }, 'object key'); 返回值仍是该 `Map` 对象,可以链式调用:map.set('a', 1).set('b', 2); - `get(key)`获取对应键的值,找不到返回 `undefined`。map.get('city'); // "Beijing" map.get('unknown'); // undefined - `has(key)`判断是否存在某个键:map.has('city'); // true map.has('unknown'); // false - `delete(key)`删除指定键值对:map.delete('city'); // true map.has('city'); // false - `clear()`清空所有键值对:map.clear(); map.size; // 0 - `size`返回当前 `Map` 的元素数量(只读):const m = new Map(); m.set('x', 10); m.set('y', 20); console.log(m.size); // 2 顺便吆喝一声[机会][机会],技术大厂,前/后端/测试,待遇还可以~遍历方法- `keys()`返回一个**可迭代对象**,包含所有键:for (let key of map.keys()) { console.log(key); } - `values()`返回所有值:for (let value of map.values()) { console.log(value); } - `entries()`返回所有 `[key, value]` 对:for (let [key, value] of map.entries()) { console.log(key, value); } `Map` 本身也是可迭代对象,等价于 `entries()`:for (let [k, v] of map) { console.log(k, v); } - `forEach(callback[, thisArg])`与数组类似,支持 `forEach`:map.forEach((value, key) => { console.log(key, value); }); 转换为数组使用扩展运算符:const arr = [...map]; // [[key, value], ...] 仅键数组:const keys = [...map.keys()]; 值数组:const values = Array.from(map.values()); 实用场景- 去重,但保留顺序const map = new Map(); arr.forEach(item => map.set(item.id, item)); const result = [...map.values()]; - 根据对象某字段快速查找const userMap = new Map(users.map(u => [u.id, u])); const user = userMap.get(101); 是不是比用 `find()` 好用很多?- 用作缓存池const cache = new Map(); function fetchData(id) { if (cache.has(id)) return cache.get(id); const data = loadFromServer(id); cache.set(id, data); return data; } 总结不夸张地说,`Map` 就是更强大的对象升级版。所以嘛——你要是还没用 `new Map()`,赶紧优雅起来吧😉 ——转载自:快乐就是哈哈哈
__init__.py 是个啥,为什么深受大厂程序员偏爱?
👋 朋友们,今天我们来聊聊 Python 里一个低调却至关重要的文件 ——`__init__.py`。 说实话,这玩意儿刚开始学 Python 时,很多人(包括当年的我)都是一脸懵:“这啥?删了会咋样?”有些人可能听说过它是 “包的标志”,也有人觉得它 “没啥大用,可以忽略”,更有甚者以为它 “只是个装样子的文件”😂。今天,我们就来彻底搞清楚 `__init__.py` 到底是干啥的,以及它如何影响 Python 项目的结构和运行。 彩蛋惊喜:521 人生小满胜万全,[程序员脱单大作战][程序员脱单大作战] 🏗️ 先搞懂 Python 模块(module)在聊 `__init__.py` 之前,我们得先弄清楚 Python 里的 ** 模块 ** 和 ** 包 ** 这两个概念。📌 ** 模块(module)** :简单来说,就是一个 `.py` 文件,里面写了一些函数、类或者变量。 比如,有个叫 `math_tools.py` 的文件,里面有一堆数学工具函数,那它就是个模块。# math_tools.py def add(a, b): return a + b def subtract(a, b): return a - b 然后,我们可以在别的 Python 文件里这样用它:import math_tools print(math_tools.add(3, 5)) # 输出 8 这就是 ** 模块的基本用法 **,没啥难的,对吧?📦 Python 包(package)是啥?如果你写的模块越来越多,代码量越来越大,就得想办法组织它们。这时候,Python 里的 ** 包(package)** 就派上用场了。📌 ** 包(package)** :一个 ** 文件夹 **,里面包含多个模块(`.py` 文件)。在 **Python 3.3 之前 **,如果要让一个目录被识别为 Python 包,必须在里面创建 `__init__.py` 文件。** 但从 Python 3.3 开始,即使没有 `__init__.py`,Python 也能识别它是一个包(称为 “命名空间包”)。** 不过,大部分实际项目 ** 依然建议添加 `__init__.py`**,因为它可以:✅ 明确这个文件夹是一个包,避免某些工具(如打包工具)识别错误。 ✅ 允许在包初始化时执行特定代码,比如自动导入子模块。 ✅ 让导入行为更加可控,避免意外的命名冲突。比如,咱们有个 `math_utils` 目录,里面放了几个数学相关的模块:math_utils/ # 这个文件夹就是一个包 │── __init__.py │── basic.py │── advanced.py 其中,`basic.py` 和 `advanced.py` 分别是两个模块,而 `__init__.py` 可以用来 ** 自定义包的导入行为 **。顺便吆喝一声,技术大厂,前后端、测试 [捞人] 捞人],待遇还不错~🎭 那么 `__init__.py` 到底是干嘛的?虽然 `__init__.py` 不再是创建包的 ** 必需 ** 条件,但它依然是 Python 项目里一个重要的组件。它的主要作用有 ** 两个 **: 1️⃣ 明确标记目录为 Python 包如果 `__init__.py` 存在,Python 解析器就会知道: **“这个目录是个 Python 包,而不是普通文件夹。”**即使 Python 3.3+ 之后不强制要求 `__init__.py`,但加上它可以:✅ 避免 Python 解释器在某些情况下误认为这是普通目录。 ✅ 兼容旧版本 Python,让代码能在不同环境中运行得更稳定。 ✅ 让某些工具(如 `pytest`、`mypy`)更好地识别项目结构。 2️⃣ 让包能像模块一样被导入如果 `__init__.py` 里什么都不写,那它的作用只是个 “标志”。但如果我们在 `__init__.py` 里加点代码,它就能 ** 自定义包的导入行为 **。🌟 ** 示例 1:让包直接暴露子模块 **# math_utils/__init__.py from .basic import add, subtract from .advanced import power 这样,我们就可以直接 import 整个 `math_utils`,而不需要写 `.basic` 或 `.advanced` 了:import math_utils print(math_utils.add(2, 3)) # 输出 5 print(math_utils.power(2, 3)) # 假设 advanced 里有个 power 函数 等于说,`__init__.py` 让 ** 包变得像一个大模块 ** 一样,外部不需要知道里面的模块结构,直接用就行。 🌟示例 2:包初始化操作`__init__.py` 还能在包被导入时执行一些初始化操作,比如加载配置、设置日志等:# math_utils/__init__.py print("数学工具包加载成功!") # 只要 import 这个包,就会执行这行代码 🔥 `__init__.py` 还能干点啥?大厂的 Python 项目里,`__init__.py` 还经常被用来做这些事:✅ 1. ** 动态导入子模块 **在大型 Python 项目中,随着模块越来越多,手动维护 `__init__.py` 将变得特别复杂还容易出错,这时候动态导入子模块就成了香饽饽了。 假设我们不知道 `math_utils` 里具体有哪些模块,可以让 `__init__.py` 在导入时动态扫描并加载:# math_utils/__init__.py import os import importlib # 获取当前包的路径 package_path = os.path.dirname(__file__) # 遍历当前目录下的所有 .py 文件(不包括 __init__.py 本身) for module in os.listdir(package_path): if module.endswith(".py") and module != "__init__.py": module_name = module[:-3] # 去掉 .py 后缀 importlib.import_module(f"{__name__}.{module_name}") # 动态导入模块 ✨ 效果:** 这样,当你在别的地方写 `import mypackage`,所有 `mypackage` 里的 `.py` 文件都会自动加载,不用再手动 `import` 了!🎉✨没加动态导入要这么写:**import math_utils.basic print(math_utils.basic.add(1,2)) #如果直接 import math_utils 会报错AttributeError: module 'math_utils' has no attribute 'basic' **✨加了动态导入可以这么写:**import math_utils print(math_utils.basic.add(1,2)) ✅ 2. ** 控制对外暴露的模块 **有时候,我们不想让 ** 所有 ** 子模块都被自动导入,而是只暴露一部分给外部用。这时候可以用 `__all__` 来 ** 手动控制 ** 允许被 `from mypackage import *` 访问的模块。# math_utils/__init__.py import os import importlib package_path = os.path.dirname(__file__) __all__ = [] for module in os.listdir(package_path): if module.endswith(".py") and module != "__init__.py": module_name = module[:-3] __all__.append(module_name) # 只暴露在 __all__ 里的模块 importlib.import_module(f"{__name__}.{module_name}") 🌟 ** 效果 **:from math_utils import * print(basic) # 只有在 __all__ 里的模块能被导入 ✅ **3. 懒加载(Lazy Import)如果某些模块比较大,加载它们会影响性能,那可以用 ** 懒加载 **(lazy import)技术,在需要时才导入,而不是在 `import mypackage` 时一次性全加载。# math_utils/__init__.py import importlib def lazy_import(name): return importlib.import_module(f"{__name__}.{name}") module1 = lazy_import("basic") 🌟 ** 效果 **: 这样,`basic` 只有在第一次被使用时才会真正导入,提高了性能!💡 ✅ 4. ** 做版本控制 **`__init__.py` 还能给包加上版本号,让外部代码可以访问: # math_utils/__init__.py __version__ = "1.0.0" 然后,在别的地方可以这样用:import math_utils print(math_utils.__version__) # 输出 "1.0.0" ✅ 5. ** 隐藏内部实现 **有些模块是 “内部用” 的,不想让外部访问,怎么办?可以在 `__init__.py` 里手动控制 ** 对外暴露的内容 **:# math_utils/__init__.py from .basic import add, subtract __all__ = ["add", "subtract"] # advanced.py 里的东西就不会被直接 import 这样,外部只能用 `math_utils.add ()`,但 `math_utils.advanced` 就不让直接访问了。🎉 结尾 关于 `__init__.py`,咱们就聊到这儿!希望这篇文章能帮你彻底搞懂它的作用,今后写 Python 项目时能更自信地使用它。—— 转载自:花小姐的春天
Jetbrains正式宣布免费,有点猛啊!
提到 Jetbrains 这家公司,相信搞开发的同学应该都不陌生。该公司盛产各种编程 IDE 和开发工具,虽然2000年才成立,到现在却已经发布了超 30 款世界顶级的编程软件,同时也收获了来自全球范围内开发者和用户的青睐。众所周知,在去年10月份的时候,Jetbrains 曾经搞过一个大动作,那就是:官宣 WebStorm 和 Rider 这两款强大的 IDE 对非商业用途全面免费![顺便吆喝一声,技术大厂跳板机会机会,前后端测试捞人,感兴趣可看,待遇还不错~]当时这个消息出来的时候,就曾在开发者圈子里引起了一阵轰动和讨论。而且我清楚地记得,在当时的评论区,还有小伙伴这样问道:“啥时候轮到 CLion 也免费呢?”这不,好消息再次来临了!!最近 Jetbrains 再度官宣:CLion 从现在开始,对非商业用途全面免费!众所周知,CLion 是由 JetBrains 设计开发的跨平台 C/C++ 集成开发环境,通过智能代码补全、深度代码分析和集成调试工具,为开发者提供高效、现代化的 C 语言和 C++ 开发体验。然而,CLion 一直以来的高昂授权费用也让不少初学者和开源爱好者为之望而却步。因此这回消息一出,又再次在开发者圈子里引起了一阵热烈的讨论,不少网友直呼 Jetbrains 这波格局打开了。看到这里,相信大家也挺好奇,那他们这里所说的 「非商业用途免费」具体指的是哪些情形呢?对此,Jetbrains 官方也给出了对应的说明,目前的非商业用途情形包括像:学习、自我教育、开源项目开发、内容创作、业余爱好开发等场景就可以免费使用这个 IDE 。所以现在无论是学生、Arduino 开发者,还是无惧 C 语言和 C++ 重重挑战的忠实爱好者,只要使用场景不涉及商业活动,都可以来免费使用 CLion 进行开发。说到这里,那具体的非商业用途免费版 CLion 怎么申请和使用呢?操作其实也非常简单。1、首先,去官网下载 CLion 安装包并安装。不过这里要注意的是,用户需要确保下载的 IDE 版本是支持非商业许可证的最新版本即可。2、启动运行 IDE 后,将会看到一个许可证对话框。在该对话框中,用户可以在其中选择 Non-commercial use(非商业用途)选项。3、登录自己的 JetBrains Account 或创建一个新的帐户。4、登录完成后,用户需要接受 Toolbox 非商业用途订阅协议。5、尽情享受在 IDE 中的开发。包括如果用户已经开始了试用期或使用付费许可证激活了自己的 IDE,也仍然可以改用非商业订阅,只需要转到帮助|注册里,并在打开的窗口中点击 Remove License(移除许可证)按钮,然后再选择 Non-commercial use(非商业用途)就行了。不过这里依然还有两个点需要格外注意。第一点,也是官方公告里明确所说的。如果用户选择使用 Non-commercial use 非商业用途的免费版,那软件是有可能会向 JetBrains 发送 IDE 遥测信息的,包括像:框架、产品中使用的文件模板、调用的操作,以及与产品功能的其他交互,但是官方提到不会包含个人数据。另外还有一点需要注意的是,虽说免费版本的 IDE 在功能上与付费版本并无二致,但在某些特定功能上可能存在一定的限制。例如,免费版本的 Code With Me 功能将仅限于 Community 版本。不过对于大多数非商业用途的开发者们来说,这些限制并不会对日常开发工作造成太大的影响。所以总而言之,JetBrains 推出的这些非商业用途免费使用政策,虽说有一些要求,但是总体来说还是极大地降低了 JetBrains IDE 的使用门槛。同时也会让更广泛的用户群体更容易获取并使用,从而鼓励更多的人投身于编程学习,参与到开源项目的建设,共同推动技术的进步与发展。文章的最后,我们也不妨再次大胆憧憬一下:既然目前的 WebStorm、Rider 以及 CLion 都已经开放了非商业用途的免费使用,那么接下来像: GoLand、IntelliJ IDEA 等的免费开放还会不会远呢?再次期待 Jetbrains 的下一步操作。——转载自:CodeSheep
什么鬼?两行代码就能适应任何屏幕?
你可能想不到,只用两行 CSS,就能让你的卡片、图片、内容块自动适应各种屏幕宽度,彻底摆脱复杂的媒体查询! 秘诀就是 CSS Grid 的 auto-fill 和 auto-fit。马上教你用!✨🧩 基础概念假设你有这样一个需求:一排展示很多卡片每个卡片最小宽度 200px,剩余空间平均分配屏幕变窄时自动换行只需在父元素加两行 CSS 就能实现: /* 父元素 */ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } /* 子元素 */ .item { height: 200px; background-color: rgb(141, 141, 255); border-radius: 10px; } 下面详细解释这行代码的意思: grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 这是 CSS Grid 布局里定义列宽的常用写法,逐个拆解如下:1. grid-template-columns作用:定义网格容器里有多少列,以及每列的宽度。2. repeat(auto-fit, ...)repeat 是个重复函数,表示后面的模式会被重复多次。auto-fit 是一个特殊值,意思是:自动根据容器宽度,能放下几个就放几个,每列都用后面的规则。容器宽度足够时,能多放就多放,放不下就自动换行。3. minmax(200px, 1fr)minmax 也是一个函数,意思是:每列最小200px,最大可以占1fr(剩余空间的平分)具体来说:当屏幕宽度很窄时,每列最小宽度是200px,再窄就会换行。当屏幕宽度变宽,卡片会自动拉伸,每列最大可以占据剩余空间的等分(1fr),让内容填满整行。4. 综合起来这行代码的意思就是:网格会自动生成多列,每列最小200px,最大可以平分一行的剩余空间。屏幕宽了就多显示几列,屏幕窄了就少显示几列,自动换行,自适应各种屏幕!不需要媒体查询,布局就能灵活响应。总结一句话:grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 让你的网格卡片最小200px,最大自动填满一行,自动适应任何屏幕,布局永远美观!这里还能填 auto-fill,和 auto-fit 有啥区别?顺便吆喝一声,民族企业机会,前、后端/测试缺人,待遇给的还可以哦~🥇 auto-fill 和 auto-fit 有啥区别?1. auto-fill🧱 尽可能多地填充列,即使没有内容也会“占位”会自动创建尽可能多的列轨道(包括空轨道),让网格尽量填满容器。适合需要“列对齐”或“固定网格数”的场景。2. auto-fit🧱 自动适应内容,能合并多余空列,不占位会自动“折叠”没有内容的轨道,让现有的内容尽量拉伸占满空间。适合希望内容自适应填满整行的场景。👀 直观对比假设容器宽度能容纳 10 个 200px 的卡片,但你只放了 5 个卡片:auto-fill 会保留 10 列宽度,5 个卡片在前五列,后面五列是“空轨道”。auto-fit 会折叠掉后面五列,让这 5 个卡片拉伸填满整行。👇 Demo 代码: auto-fill item1 item2 item3 item4 item5 auto-fit item1 item2 item3 item4 item5 .grid-fill { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 40px; } .grid-fit { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; } .grid-fill div { background: #08f700; } .grid-fit div { background: #f7b500; } .grid-fill div, .grid-fit div { padding: 24px; font-size: 18px; border-radius: 8px; text-align: center; } 兼容性caniuse.com/?search=aut…🎯 什么时候用 auto-fill,什么时候用 auto-fit?希望每行“有多少内容就撑多宽”,用 auto-fit 适合卡片式布局、相册、响应式按钮等。希望“固定列数/有占位”,用 auto-fill 比如表格、日历,或者你希望网格始终对齐,即使内容不满。📝 总结属性空轨道内容拉伸适用场景auto-fill保留否固定列数、占位网格auto-fit折叠是流式布局、拉伸填充 🌟 小结auto-fill 更像“占位”,auto-fit 更像“自适应”推荐大部分响应式卡片用 auto-fit善用 minmax 配合,让列宽自适应得更自然只需两行代码,你的页面就能优雅适配各种屏幕! 觉得有用就点赞收藏吧,更多前端干货持续更新中!🚀✨——转载自:前端九哥
MyBatis中的 10 个宝藏技巧!
前言说到 MyBatis,很多小伙伴都会用,但未必用得“惊艳”。实际上,这个轻量级的持久层框架还有很多隐藏的“宝藏技巧”。如果你能掌握这些技巧,不但能让开发更高效,还能避免掉入一些常见的“坑”。今天就从浅入深,分享 10 个让人眼前一亮的 MyBatis 开发技巧,每一个都配上具体的场景和代码示例,务求通俗易懂,希望对你会有所帮助。1. 灵活使用动态 SQL很多小伙伴在写 SQL 的时候,喜欢直接用拼接字符串的方式,比如: String sql = "SELECT * FROM user WHERE 1=1"; if (name != null) { sql += " AND name = '" + name + "'"; } 这种写法不仅麻烦,而且安全性很差(容易引发 SQL 注入)。MyBatis 的动态 SQL 是专门为解决这种问题设计的,你可以用 if、choose、foreach 等标签来动态构造 SQL。示例:动态条件查询 SELECT * FROM user WHERE 1=1 AND name = #{name}ANDage=#{age} 这个代码的好处是,SQL 逻辑清晰,不会因为某个参数为空就导致整个 SQL 报错。顺便吆喝一声,民族企业机会,前、后端/测试缺人,待遇给的还可以哦~2. 善用 resultMap 自定义结果映射有些小伙伴会遇到这样的问题:数据库表字段是下划线命名,但 Java 对象是驼峰命名。比如 user_name 对应 userName。如果直接用默认的 resultType,MyBatis 是无法自动映射的。这个时候,用 resultMap 就能完美解决。示例:自定义结果映射 SELECT id, user_name, age FROM user WHERE id = #{id} 有了 resultMap,再复杂的字段映射都可以轻松搞定。3. 利用 foreach 实现批量操作有些小伙伴可能会遇到这种需求:传入一个 ID 列表,查询所有匹配的用户信息。如果用拼接字符串的方式生成 IN 条件,不但代码丑,还容易踩坑。MyBatis 提供了 foreach 标签,可以优雅地处理这种场景。示例:批量查询 SELECT * FROM user WHERE id IN #{id} 传入的 idList 是一个 List 或数组,MyBatis 会自动帮你展开为 IN (1, 2, 3) 这样的格式,完全不用担心语法问题。4. MyBatis-Plus 的分页功能很多小伙伴在做分页的时候,习惯自己写 LIMIT 的 SQL,这样不仅麻烦,还容易出错。其实,用 MyBatis-Plus 的分页插件能省不少事。示例:MyBatis-Plus 分页功能 Page page = new Page[removed](1, 10); // 第 1 页,每页 10 条 IPage userPage = userMapper.selectPage(page, null); System.out.println("总记录数:" + userPage.getTotal()); System.out.println("当前页数据:" + userPage.getRecords()); 只需引入分页插件,就能轻松完成分页操作,简直不要太爽。5. 使用 @Mapper的接口代理有些小伙伴觉得 XML 文件太多太麻烦,其实 MyBatis 支持纯注解的开发模式,尤其是对于简单的 SQL,非常方便。示例:注解方式查询 @Mapper public interface UserMapper { @Select("SELECT * FROM user WHERE id = #{id}") User getUserById(int id); @Insert("INSERT INTO user(name, age) VALUES(#{name},#{age})") void addUser(User user); } 用这种方式,可以完全省掉 XML 配置,代码更加简洁。6. 二级缓存MyBatis 内置了一级缓存(SqlSession 范围内),但对于多次查询的场景,可以开启二级缓存来提升性能。示例:开启二级缓存 SELECT * FROM user WHERE id = #{id} 开启二级缓存后,同一个 Mapper 下的查询会自动命中缓存,大幅提高性能。总结MyBatis 的魅力在于简单、高效,但很多时候我们用得太“基础”,没有发挥它的全部潜力。希望这 些技巧能帮你更高效地使用 MyBatis,也让你的代码看起来更“惊艳”。如果觉得有帮助,记得收藏分享!最后说一句如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。求一键三连:点赞、转发、在看。——转载自:苏三说技术
技术 battle,我的就是错的吗?
问题这个周日一直被昨天与同事争论的一个问题所困扰(周六加了班),背景是上周的一个项目,TL 让我新建了一个 JAR 类型的工程,设计一个提供灰度功能的模块,经过清明的奋战赶工,也是如期的赶上了项目进度。我提供的灰度模块功能是这样的,最外层是业务所属的灰度层,提供每个业务定制的灰度功能,比如各个灰度间的串联,前置数据的查询等。内层为一个通用的单个灰度服务,灰度服务串联了 白名单、阻断、版本判断、AB 实验、梵高人群 五个环节,接口类提供了一个判断单个用户灰度的方法,主体流程都写在 abstract 抽象类里面,子类只需要实现一个 getConfig () 方法,提供本灰度需要的配置,例如 白名单配置、版本配置、AB 实验配置 等。当单个灰度接入时,只需要实现抽象类,返回灰度配置,定义好本灰度场景值即可。组内的一位同事,看了我设计的灰度流程后,反应比较激烈。同事的观点同事的观点是,我提供的判断单个灰度的流程,过于复杂冗余,不符合单一职责原则,abstract 类逻辑过多,其它同事接入时的学习成本太高。同事认为,不需要提供内层的单个灰度流程,而是提供自定义的灰度服务和几个工具类即可,即最外层的业务灰度,以及 白名单、阻断、版本判断、AB 实验、梵高判断 这 5 种工具,当使用者接入灰度时,新建一个业务灰度类,在业务灰度类内部调用工具串联起整个流程。同事举了一个例子,如果业务诉求是判断 1 个版本以及 3 个实验时,如果按照我的方式,需要写 3 个单个的灰度实现类,然后新建一个业务灰度类,再去调用这 3 个灰度实现类进行判断。如果按照他的方式,只需要新建一个业务灰度类,然后调用一个版本工具类判断,再调用 3 次实验判断,就可以完成业务方诉求。** 顺便推个机会,[民族企业]大厂,前、后端 / 测试缺人,待遇给的还可以哦~**我的理由我的观点是,所提供的通用单个灰度服务,并非不符合单一职责原则,单一职责原则虽然要求提供粒度小、功能单一的类,但是单一职责的目的是 可复用性、可维护性、可扩展性、可读性,虽然我所提供的单个灰度判断流程的可读性稍差,但是做到了很好的可复用、可维护、可扩展,因此与单一职责并没有冲突。而且使用者接入,在绝大多数灰度场景下,都不需要感知整体逻辑,只需要进行简单的抽象类实现,定义好版本、实验等配置即可,接入成本是非常低的。而且,同事所列举的例子,是属于比较少数的场景,绝大多数场景是提供【单个版本】+【单个实验】判断即可,如果按照我的流程,只需要提供单个灰度实现类,再新建一个业务灰度类,直接进行简单调用,这比在业务类中调用工具进行流程串联成本是更低的。而即使是同事所举的例子,接入成本也不高,虽然新建 3 个实现类,有类膨胀的趋势,但是这 3 个实现,本身就是 3 个单一的灰度,应该进行隔离。再者,因为通用的单一灰度框架进行了统一的监控打点、灰度降级等能力,因此从可观测可降级角度考虑,也是优于同事的方式的。结论这个问题目前并没有结论,同事准备下周一把问题抛给 TL,让 TL 进行决断,如果结论是同事的方式更好的话,他准备另写一套。不知大家怎么认为,是我的方式更好呢,还是同事的观点更为正确。 —— 转载自:起风了布布
2025年了请使用更加优雅的Bean注入(@Resource过时了)
日常开发发现思考:大家看到如下代码,有发现什么问题呢?是不是很多@Resource,造成不仅是代码的整洁度,还是代码观感,其实都不是很好,我们常常说尽量消除冗余代码,增强复用,那么这里的注解我们是不是可以消除掉呢?[顺便推个机会]大厂摇人,前、后端/测试机会,偶尔有加班,加班有加班费,、薪酬待遇还不错。Spring官方更推荐我们使用构造器注入原先使用 @Autowired 注解官方会提示我们:删除此字段注入并使用构造函数注入 Remove this field injection and use constructor injection instead. 并且会以一个波浪线警告的形式出现,有代码洁癖的人会极度不舒适@Resource 这个注解不是Spring官方提供的,而是J2EE(Java 250 规范提案)提供的,它不会有波浪线警告两个注解最大区别就是:@Autowired根据 By Type 查找Bean,如果存在多个Bean,再根据 By Name 查找Bean@Resource根据By Name查找Bean,如果ByName找不到Bean,再根据By Type查找Bean其实,两种方式都是通过setter方式进行注入,终究不是Spring官方推荐的方式我们能不能使用构造器注入,么稳态啦!我们能不能优雅的构造器注入,么稳态啦!代码整洁优化前提引入Lombok依赖,这个依赖出现很多纷争,有些人推荐用,有些人不推荐用,可根据各公司实际情况使用使用 @RequiredArgsConstructor + final @NonNull field 实现构造器注入Bean是不是上面的代码不管是整洁度,还是代码观感都十分优雅了,而且我们不用在属性上加上@Resource 或者@Autowired,只需要像书写最终属性一样即可,而且我们还可以通过点击左边的小点,找到实现类。原理解读@RequiredArgsConstructor 这个属性是Lombok依赖提供的,作用域在类上,作用是生成所需要参数的构造函数,但是注意:字段必须是final修饰和具有@NonNull等约束的字段(这里的 final 和 @NonNull 满足其一即可)之前如果我们需要使用构造器注入,则需要手动书写构造器,而是对于后续更多Bean的注入,我们又需要重新在构造器中添加需要注入的Bean参数,略显繁琐,但是如今通过这种方法就可以省去了这个步骤,并且是Spring官方推荐的Bean注入方式。特别注意对于Lombok比较熟悉的人来说,肯定知道@AllArgsConstructor,这个注解可以生成该类下的全部属性的构造方法,那他们的区别是什么呢?@AllArgsConstructor:生成该类下全部属性的构造方法。@RequiredArgsConstructor:生成该类下被final修饰或者带有@NonNull的构造方法。使用@AllArgsConstructor之后,@Value就会不起作用可以使用@RequiredArgsConstructor代替——转载自:本当迷
为什么停止在小型项目中使用 TypeScript
我曾经是那种把 TypeScript 推到公司里每个项目中的前端开发者。感觉这真是个正确的操作——毕竟,静态类型让一切都变得更好了,不是吗?嗯,并非总是如此。多年来,我一直强迫自己在每个项目中都使用 TypeScript,现在我终于承认了一件事:对于小型项目来说,TypeScript 带来的麻烦远大于帮助。 如果我要快速构建一个 MVP、个人项目或一个简单的 API,我不再默认使用 TypeScript。原因如下。1. 前期准备工作不值得让我们面对现实吧——TypeScript 需要设置。配置tsconfig.json确保依赖项与 TypeScript 兼容安装和配置类型定义(@types/whatever)调整构建过程是的,我知道像 Vite、Next.js 或 Nuxt 这样的现代框架可以通过零配置模板简化设置。但是,当你从头开始或不使用完整框架时,这些配置仍然存在——对于快速 hack 或脚本来说,我宁愿避免这种摩擦。对于大型项目来说,这种设置确实值得。但对于一些小项目——比如一个快速 API 或一个周末的业余项目——我为什么要花 20 分钟来处理配置,而不是真正地编写代码呢?一个简单的 JavaScript 文件就可以工作:// index.js console.log("Hello, world!"); 有了 TypeScript,即使是这么基础的事情也需要额外的仪式:const message: string = "Hello, world!"; console.log(message); 让我们解决这个问题:不,您不需要string在这里明确注释 - TypeScript 可以很好地推断类型。这个例子对我来说有点象征意义。它表明,即使是最简单的脚本,在使用了 TypeScript 之后,也会变得愈发正式和冗长。在一个我只想打印一条消息或调用一个 API 的快速项目中,这层额外的代码层往往感觉像是阻力,而不是帮助。这是在设置构建过程 之前。[顺便推个机会]大厂摇人,前、后端/测试机会,可以吃零食,加班有加班费,稳定性较高,薪酬待遇还不错。2. TypeScript 会减缓开发速度JavaScript 最大的优势之一就是它的灵活性。想要快速完成一个概念验证?没问题。有了 TypeScript,这种灵活性就消失了。假设我正在尝试一个新的 API。在 JavaScript 中,我只需获取一些数据并继续:fetch("https://api.example.com/data") .then(res => res.json()) .then(data => console.log(data)) .catch(err => console.error(err)); 在 TypeScript 中?现在我需要定义类型:interface ApiResponse { id: number; name: string; email: string; } fetch("https://api.example.com/data") .then(res => res.json()) .then((data: ApiResponse) => console.log(data)) .catch(err => console.error(err)); 当然,TypeScript 允许你使用any或逐步引入类型。但这有点违背了使用 TypeScript 的初衷,对吧?我的意思是——当我处于开发模式时,我根本不想考虑类型。我想要快速的反馈,并且没有摩擦。当然,它更安全——但如果我只是随便玩玩,为什么我在知道这个 API 是否有用之前就编写了额外的代码?3. TypeScript 的优势在小型项目中并不那么有用我明白 TypeScript 有助于防止 bug。但是在小项目中,这真的很重要吗?大多数时候,TypeScript 在小项目中阻止的“错误”都是我会立即发现的。不好的例子:const age = "30"; console.log(age * 2); // NaN 好的,TypeScript 可以捕获这个问题。但这种 bug 会让我彻夜难眠吗?不会。如果我的整个应用程序只有 500 行代码,我不需要编译器来保护我——我可以直接阅读代码。4. 额外的构建步骤感觉没有必要使用 JavaScript,我可以立即运行我的脚本:node script.js 使用 TypeScript,我必须先编译它:tsc script.ts && node script.js 对于大型项目来说?没问题。但如果我写的是一个快速实用的脚本,这个额外的步骤会扼杀我的动力。是的,我知道可以使用它ts-node来避免手动编译,但它仍然会带来不必要的复杂性。5. 并非所有依赖项都能与 TypeScript 兼容是否曾经安装第三方包并立即遇到 TypeScript 错误?Property 'xyz' does not exist on type 'SomeModule'. 然后你检查了该包的 GitHub 仓库,发现它不支持 TypeScript。现在你有三个选择:查找 DefinitelyTyped 包(@types/xyz) (如果存在)。编写自己的类型定义(呃)。使用any并假装 TypeScript 不存在。如果我正在做一个大项目,我会花时间解决这个问题。但对于一个小应用程序来说,这只是另一个令人头疼的问题。当我仍然使用 TypeScript 时我并不是说 TypeScript 不好——我仍然将它用于正确的项目。✅大型应用(尤其是多人协作的应用)。✅需要长期维护的项目。✅代码库严重依赖模块间的严格契约。但对于:❌副项目❌快速脚本❌ MVP 和原型我坚持用 JavaScript。它更快、更简单,而且不用和编译器较劲, 也更有趣。TypeScript 是一种工具,而不是信仰有些开发者把 TypeScript 视为2025 年编写 JavaScript 的唯一方法。但事实并非如此。TypeScript 在合理的地方使用效果很好——但强制每个项目都使用它?这只会造成不必要的摩擦。如果你喜欢 TypeScript,那很好——在它对你有利的地方使用它。但是,如果你正在做一些小事,觉得 TypeScript 带来的麻烦比它的价值更大……也许确实如此。你的看法是什么?你现在还在用 TypeScript 做所有事情吗?还是已经开始选择自己的阵营了?欢迎在评论区留言讨论!——转载自作者:CF14年老兵
websocket和socket有什么区别?
WebSocket 和 Socket 的区别WebSocket 和 Socket 是两种不同的网络通信技术,它们在使用场景、协议、功能等方面有显著的差异。以下是它们之间的主要区别:1. 定义Socket:Socket 是一种网络通信的工具,可以实现不同计算机之间的数据交换。它是操作系统提供的 API,广泛应用于 TCP/IP 网络编程中。Socket 可以是流式(TCP)或数据报(UDP)类型的,用于低层次的网络通信。WebSocket:WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务器和客户端之间实时地交换数据。WebSocket 是建立在 HTTP 协议之上的,主要用于 Web 应用程序,以实现实时数据传输。2. 协议层次Socket:Socket 是一种底层通信机制,通常与 TCP/IP 协议一起使用。它允许开发者通过编程语言直接访问网络接口。WebSocket:WebSocket 是一种应用层协议,建立在 HTTP 之上。在初始握手时,使用 HTTP 协议进行连接,之后切换到 WebSocket 协议进行数据传输。3. 连接方式Socket:Socket 通常需要手动管理连接的建立和关闭。通过调用相关的 API,开发者需要处理连接的状态,确保数据的可靠传输。WebSocket:WebSocket 的连接管理相对简单。建立连接后,不需要频繁地进行握手,可以保持持久连接,随时进行数据交换。[顺便推个机会]大厂摇人,前/后端、测试,可以吃零食,加班有加班费,稳定性加高,薪酬待遇还不错,全国多城市有机会哈。4. 数据传输模式Socket:Socket 可以实现单向或双向的数据传输,但通常需要在发送和接收之间进行明确的控制。WebSocket:WebSocket 支持全双工通信,客户端和服务器之间可以随时互相发送数据,无需等待响应。这使得实时通信变得更加高效。5. 适用场景Socket:Socket 常用于需要高性能、低延迟的场景,如游戏开发、文件传输、P2P 网络等。由于其底层特性,Socket 适合对网络性能有严格要求的应用。WebSocket:WebSocket 主要用于 Web 应用程序,如即时聊天、实时通知、在线游戏等。由于其易用性和高效性,WebSocket 特别适合需要实时更新和交互的前端应用。6. 数据格式Socket:Socket 发送的数据通常是二进制流或文本流,需要开发者自行定义数据格式和解析方式。WebSocket:WebSocket 支持多种数据格式,包括文本(如 JSON)和二进制(如 Blob、ArrayBuffer)。WebSocket 的数据传输格式非常灵活,易于与 JavaScript 进行交互。7. 性能Socket:Socket 对于大量并发连接的处理性能较高,但需要开发者进行优化和管理。WebSocket:WebSocket 在建立连接后可以保持长连接,减少了握手带来的延迟,适合高频率的数据交换场景。8. 安全性Socket:Socket 的安全性取决于使用的协议(如 TCP、UDP)和应用层的实现。开发者需要自行处理安全问题,如加密和身份验证。WebSocket:WebSocket 支持通过 WSS(WebSocket Secure)进行加密,提供更高层次的安全保障。它可以很好地与 HTTPS 集成,确保数据在传输过程中的安全性。9. 浏览器支持Socket:Socket 是底层的网络通信技术,通常不直接在浏览器中使用。Web 开发者需要通过后端语言(如 Node.js、Java、Python)来实现 Socket 通信。WebSocket:WebSocket 是专为 Web 应用设计的,所有现代浏览器均支持 WebSocket 协议,开发者可以直接在客户端使用 JavaScript API 进行通信。10. 工具和库Socket:使用 Socket 进行开发时,开发者通常需要使用底层网络编程库,如 BSD Sockets、Java Sockets、Python's socket 模块等。WebSocket:WebSocket 提供了简单的 API,开发者可以使用原生 JavaScript 或第三方库(如 Socket.IO)轻松实现 WebSocket 通信。结论总结来说,WebSocket 是一种为现代 Web 应用量身定制的协议,具有实时、双向通信的优势,而 Socket 是一种底层的网络通信机制,提供更灵活的使用方式。选择使用哪种技术取决于具体的应用场景和需求。对于需要实时交互的 Web 应用,WebSocket 是更合适的选择;而对于底层或高性能要求的网络通信,Socket 提供了更多的控制和灵活性。——转载自作者:Riesenzahn
优秀的后端应该知道的易错点
编程社区给出了 2024 年编程语言流行度的指标, Java 排第三~1. 数据类型1.1 static修饰的变量大家在玩Java时有没发现,下面这样一个对象,我们即使没有给变量赋值,在创建它后这个变量依旧会有默认值。 class A { int a; } System.out.println(new A().a); 程序执行结果: 0 有时前端同学要求后端给个默认值0,我们甚至不用动手,Java编译器就把活给干完。这实际上是Java语言的一个特性,对于实例变量即成员变量,如果是基本数据类型都会有一个默认值。不同的基本类型默认值不同,我们看看以下各种基本类型的默认值。 int a; //0 short b; //0 long c; //0 float d; //0.0 double e; //0.0 boolean f; //false byte g; //0 char h; //空字符 1.2 自动类型提升(1)Java中的byte、short、char进行数学计算时都会提升为int类型,很容易忽略的基础知识,南哥慢慢道来。以下代码的运行正常吗? byte b1 = 1, b2 = 2, b3; b3 = b1 + b2; 答案在你意料之中,就是编译报错。 # 报错内容 java: 不兼容的类型: 从int转换到byte可能会有损失 既然byte、short、char进行数学计算时都会提升为int类型,那我们就需要在运行过程中把结果转换成byte类型。正确的做法如下。 b3 = (byte)(b1 + b2); (2)但假如byte变量是这样的写法,我们给b1和b2都加个final,很神奇,编译不会报错。 final byte b1 = 1, b2 = 2, b3; b3 = b1 + b2; 这种情况是一个特殊情况,Java编译器会为其进行特殊处理,我们称它为编译时常量表达式的求值。b1、b2、b3都是常量值,b3在编译阶段就会被编译器进行赋值,不会涉及到上面我们提到的数学计算提升为int类型,也就不会编译错误。(3)但如果是这种情况呢? final byte b1 = 1; byte b2 = 2, b3; b3 = b1 + b2; 以上两个byte变量,只有一个final修饰,也就是说对b3赋值运算不能在编译时进行,那这段代码依旧会报错,我们还是需要把结果转换为byte类型。正确做法如下。 java 代码解读 复制代码 b3 = (byte)(b1 + b2); 1.3 byte溢出byte类型的数据范围在-128 ~ 127,当这个值超过127会转变成 - 128。为什么呢? byte i = 127; System.out.println(++i); shell 程序执行结果: -128 byte类型的最大值127在二进制中表示为01111111,当我们对127的值增加1时,每位加1后都会产生进位,导致的结果就是所有的位都会翻转(从01111111变成10000000),而10000000十进制的表示就是-128。1.4 Bollean赋值业务开发编写最多就是条件语句了,特别在迭代年代比较旧的老项目,一套接一套的if语句。既然见识了那么多条件语句,那以下代码的执行结果是什么? Boolean flag = false; if (flag = true) { System.out.println("true"); } else { System.out.println("false"); } 在Java里,条件判断是有赋值的功能,try语句同样也有。此时falg在条件判断里被赋值了。 程序执行结果: true [需要看新机会的]顺便吆喝一句,技术大厂,待遇给的还可以,就是偶尔有加班(放心,加班有加班费) 前、后端/测试,多地缺人,感兴趣的可以来试试~2. 程序运算2.1 三元运算符三元运算符的坑,相信不少南友遇到过。。。我们来看看三元运算符是什么?Java中的三元运算符是一种简洁的条件表达式工具,其语法格式为:条件 ? 表达式1 : 表达式2。如果条件为真(true),则表达式的结果是表达式1;如果为假(false),则结果是表达式2。假如是这种情况呢,南哥问:o1最终的数据类型是什么? Object o1 = true ? new Integer(1) : new Double(2.0); 上面的代码行其实等同于这一行。 Object o1 = true ? new Double(1.0) : new Double(2.0); 三元运算符的一个非常关键的细节就是类型的统一化。Double类型的数据范围更大于Interger类型,所以Java编译器会对值类型进行类型提升,最终把Integer类型提升为Double类型。2.2 自增问题下面是南哥编写的两个i++自增的易错问题,面试考核经常出现在笔试题。(1)南哥第一问:以下代码执行的结果是什么? int i = 0; i = i++ + i; shell 程序执行结果: 1 (2)南哥第二问:以下代码执行的结果是什么? int i = 0; i = i++; System.out.println(i); shell 程序执行结果: 0 2.3 String对象我们创建一个String对象,JVM在背后实际上做了很多功夫,String对象在常量池、堆内存都有可能存在。我们具体问题来具体分析下。(1)以下代码段不包含引用类型,只是单纯的字面量拼接,所以只会创建一个对象存在于常量池中。 String s = "JavaProGuide" + "南哥" + 666; (2)以下代码段包含了引用类型,一共创建了3个对象,猜对了吗? String s = "Hello"; s = s + " world!" "Hello"、" world!"都属于字面量,所以它们都会被加入到Java字符串常量池中。而s + " world!"这么一个代码段涉及了引用类型,所以它在内存里创建了一个新的String对象,并不存在于常量池,而是存在于堆内存里。(3)以下代码段一共创建了两个对象,分别存在于常量池、堆内存。首先new对象会把该String对象放到堆内存里,而过程中会先检查常量池是否存在JavaProGuide String str = new String("JavaProGuide"); …… ——转载自作者:JavaSouth南哥
后端开发和你聊聊 JVM 如何优化
作者:京东零售京麦研发 马万全首先应该明确的是JVM调优不是常规手段,JVM的存在本身就是为了减轻开发对于内存管理的负担,当出现性能问题的时候第一时间考虑的是代码逻辑与设计方案,以及是否达到依赖中间件的瓶颈,最后才是针对JVM进行优化。1.JVM内存模型针对JAVA8的模型进行讨论,JVM的内存模型主要分为几个关键区域:堆、方法区、程序计数器、虚拟机栈和本地方法栈。堆内存进一步细分为年轻代、老年代,年轻代按其特性又分为E区,S1和S2区。关于内存模型的一些细节就不在这里讨论了接下来从内存模型简单流转来看一个对象的生命周期,对JVM的回收有一个概念,其中弱化堆栈和程序计数器1.首先我们写的.java文件通过java编译器javac编译成.class文件2.类被编译成.class文件后,通过类加载器(双亲委派模型)加载到JVM的元空间中3.当创建对象时,JVM在堆内存中为对象分配空间,通常首先在年轻代的E区(这里只讨论在堆上分配的情况)4.对象经历YGC后,如果存活移动到S区,多次存活后晋升到老年代5.当对象不再被引用下一次GC,垃圾收集器会回收对象并释放其占用的内存。[需要看新机会的]1、顺便吆喝一句,技术大厂,待遇给的还可以,就是偶尔有加班(放心,加班有加班费)前、后端/测试,多地缺人原理对象创建会在年轻代的E区分配内存,当失去引用后,变成垃圾存在E区中,随着程序运行E区不断创建对象,就会逐步塞满,这时候E区中绝大部分都是失去引用的垃圾对象,和一小部分正在运行中的线程产生的存活对象。这时候会触发YGC(Young Gc)回收年轻代。然后把存活对象都放入第一个S区域中,也就是S0区域,接着垃圾回收器就会直接回收掉E区里全部垃圾对象,在整个这个垃圾回收的过程中全程会进入Stop the Wold状态,系统代码全部停止运行,不允许创建新的对象。YGC结束后,系统继续运行,下一次如果E区满了,就会再次触发YGC,把E区和S0区里的存活对象转移到S1区里去,然后直接清空掉E区和S0区中的垃圾对象1.2 、那么对象什么时候去老年代呢?1.2.1、对象的年龄躲过15次YGC之后的对象晋升到老年代,默认是15,这个值可以通过-XX:MaxTenuringThreshold设置这个值设置的随意调整会有什么问题?现在java项目普遍采用Spring框架管理对象的生命周期。Spring默认管理的对象都是单例的,这些对象是长期存活的应该直接放到老年代中,应该避免它们在年轻代中来回复制。调大晋升阀值会导致本该晋升的对象停留在年轻代中,造成频繁YGC。但是如果设置的过小会导致程序中稍微存在耗时的任务,就会导致大量对象晋升到老年代,导致老年代内存持续增长,不要盲目的调整晋升的阀值。1.2.2、动态对象年龄判断JVM都会检查S区中的对象,并记录下每个年龄段的对象总大小。如果某个年龄段及其之前所有年龄段的对象总大小超过了S区的一半,则从该年龄段开始的所有对象在下一次GC时都会被晋升到老年代。假设S区可以容纳100MB的数据。在进行一次YGC后,JVM统计出如下数据:•年龄1的对象总共占用了10MB。•年龄2的对象总共占用了20MB。•年龄3的对象总共占用了30MB。此时,年龄1至3的对象总共占用了60MB,超过了S区一半的容量(50MB)。根据动态对象年龄判断规则,所有年龄为3及以上的对象在下一次GC时都将被晋升到老年代,而不需要等到它们的年龄达到15。(注意:这里S区指的是S0或者S1的空间,而不是总的S,总的在这里是200MB)这个机制使得JVM能够根据实际情况动态调整对象的晋升策略,从而优化垃圾收集的性能。通过这种方式,JVM尽量保持S区空间的有效利用,同时减少因年轻代对象过多而导致的频繁GC。1.2.3.大对象直接进入老年代如果对象的大小超过了预设的阈值(可以通过-XX:PretenureSizeThreshold参数设置),这个对象会直接在老年代分配,因为大对象在年轻代中经常会导致空间分配不连续,从而提早触发GC,避免在E区及两个S区之间来回复制,减少垃圾收集时的开销。1.2.4.临时晋升在某些情况下,如果S区不足以容纳一次YGC后的存活对象,这些对象也会被直接晋升到老年代,即使它们的年龄没有达到晋升的年龄阈值。这是一种应对空间不足的临时措施。1.3老年代的GC触发时机一旦老年代对象过多,就可能会触发FGC(Full GC),FGC必然会带着Old GC,也就是针对老年代的GC 而且一般会跟着一次YGC,也会触发永久代的GC,但具体触发条件和行为还取决于使用的垃圾收集器,文章的最后会简单的介绍下垃圾收集器。•Serial Old/Parallel Old当老年代空间不足以分配新的对象时,会触发FGC,这包括清理整个堆空间,即年轻代和老年代。•CMS当老年代的使用达到某个阈值(默认情况下是68%)时,开始执行CMS收集过程,尝试清理老年代空间。如果在CMS运行期间老年代空间不足以分配新的对象,可能会触发一次Full GC。 启动CMS的阈值参数:-XX:CMSInitiatingOccupancyFraction=75,-XX:+UseCMSInitiatingOccupancyOnly• G1G1收集器将堆内存划分为多个区域(Region),包括年轻代和老年代区域。当老年代区域中的空间使用率达到一定比例(基于启发式方法或者显式配置的阈值)默认45%时,G1会计划并执行Mixed GC,这种GC包括选定的一些老年代区域和所有年轻代区域的垃圾收集。Mixed GC的阈值参数-XX:InitiatingHeapOccupancyPercent=40,-XX:MaxGCPauseMillis=2002.JVM优化调优目标:2.1JVM调优指标•低延迟(Low Latency) :GC停顿时间短。•高吞吐量(High Throughput) :单位时间内能处理更多的工作量。更多的是CPU资源来执行应用代码,而非垃圾回收或其他系统任务。•大内存(Large Heap) :支持更大的内存分配,可以存储更多的数据和对象。在处理大数据集或复杂应用时尤为重要,但大内存堆带来的挑战是GC会更加复杂和耗时。但是不同目标在实现是本身时有冲突的,为什么难以同时满足?•低延迟 vs. 高吞吐量:要想减少GC的停顿时间,就需要频繁地进行垃圾回收,或者采用更复杂的并发GC算法,这将消耗更多的CPU资源,从而降低应用的吞吐量。•低延迟 vs. 大内存:大内存堆意味着GC需要管理和回收的对象更多,这使得实现低延迟的GC变得更加困难,因为GC算法需要更多时间来标记和清理不再使用的对象。•高吞吐量 vs. 大内存:虽然大内存可以让应用存储更多数据,减少内存管理的开销,但是当进行全堆GC时,大内存堆的回收过程会占用大量CPU资源,从而降低了应用的吞吐量。2.2如何权衡在实际应用中,根据应用的需求和特性,开发者和运维工程师需要在这三个目标之间做出权衡:2.2.1Web应用和微服务 - 低延迟优先场景描述:对于用户交互密集的Web应用和微服务,快速响应是提供良好用户体验的关键。在这些场景中,低延迟比高吞吐量更为重要。推荐收集器:大内存应用推荐G1,内存偏小可以使用CMS,CMS曾经是低延迟应用的首选,因其并发回收特性而被广泛使用。不过由于CMS在JDK 9中被标记为废弃,并在后续版本中被移除可以使用极低延迟ZGC或Shenandoah。这两种收集器都设计为低延迟收集器,能够在大内存堆上提供几乎无停顿的垃圾回收,从而保证应用的响应速度,但是支持这两个回收器的JDK版本较高,在JDK8版本还是CMS和G1的天下。2.2.2 大数据处理和科学计算 - 高吞吐量优先场景描述:大数据处理和科学计算应用通常需要处理大量数据,对CPU资源的利用率要求极高。这类应用更注重于高吞吐量,以完成更多的数据处理任务,而不是每个任务的响应时间。推荐收集器:Parallel GC。这是一种以高吞吐量为目标设计的收集器,通过多线程并行回收垃圾,以最大化应用吞吐量,非常适合CPU资源充足的环境。2.2.3. 大型内存应用 - 大内存管理优先场景描述:对于需要管理大量内存的应用,例如内存数据库和某些缓存系统,有效地管理大内存成为首要考虑的因素。这类应用需要垃圾回收器能够高效地处理大量的堆内存,同时保持合理的响应时间和吞吐量。推荐收集器:G1 GC或ZGC。G1 GC通过将堆内存分割成多个区域来提高回收效率,适合大内存应用且提供了平衡的延迟和吞吐量。ZGC也适合大内存应用,提供极低的延迟,但可能需要对应用进行调优以实现最佳性能。总结:JVM优化没有拿过来直接用的方案,所有好的JVM优化方案都是在当前应用背景下的,还是开头那句话 JVM调优不是常规手段,如果没有发现问题尽量不主动优化JVM,但是一定要了解应用的JVM运行情况,这时候好的监控就显得格外重要。那么好的JVM应该是什么样的呢?简单的说就是尽量让每次YGC后的存活对象小于S区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少FGC的频率,避免频繁FGC对JVM性能的影响。了解了JVM优化的基本原理之后,实战就需要在日常中积累了,墨菲定律我觉得在这个场景很适用,不要相信线上的机器是稳定的,如果观察到监控有异常,过一会可能恢复了就不了了之,要敢于去排查问题,未知的总是令人恐惧的,在排查的过程中会加深自己对JVM的理解的同时,也会对应用更有信心。
当一个前端学了很久的神经网络...
前言最近在学习神经网络相关的知识,并做了一个简单的猫狗识别的神经网络,结果如图。虽然有点绷不住,但这其实是少数情况,整体的猫狗分类正确率已经来到 90% 了。本篇文章是给大家介绍一下我是如何利用前端如何做神经网络-猫狗训练的。步骤概览还是掏出之前那个步骤流程,我们只需要按照这个步骤就可以训练出自己的神经网络处理数据集定义模型神经网络层数每层节点数每层的激活函数编译模型训练模型使用模型最终的页面是这样的[removed]顺便吆喝一句,技术大厂,待遇给的还可以,就是偶尔有加班(放心,加班有加班费)前、后端/测试,多地有位置找到数据集,本次使用的是这个 www.kaggle.com/datasets/li… 2000 个猫图,2000 个狗图,足够我们使用(其实我只用了其中 500 个,电脑跑太慢了)由于这些图片大小不一致,首先我们需要将其处理为大小一致。这一步可以使用 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 作为参数将其处理为 tfjs 可用的对象 // 加载单个图片并处理为 tfjs 对应格式 const loadImage = async (category: "cat" | "dog", index: number): Promise => { 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 [removed] { 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; }; 这里实际上只需要额外注意两点:卷积层的激活函数 activation: "relu",这里理论上是个非线性激活函数就行。但是我个人更喜欢 relu,函数好记,速度和效果又不错。输出层的激活函数 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
async/await 必须使用 try/catch 吗?
前言在 JavaScript 开发者的日常中,这样的对话时常发生:- 👨💻 新人:"为什么页面突然白屏了?"- 👨🔧 老人:"异步请求没做错误处理吧?"async/await 看似优雅的语法糖背后,隐藏着一个关键问题:错误处理策略的抉择。在 JavaScript 中使用 `async/await` 时,很多人会问: “必须使用 try/catch 吗?”其实答案并非绝对,而是取决于你如何设计错误处理策略和代码风格。接下来,我们将探讨 async/await 的错误处理机制、使用 try/catch 的优势,以及其他可选的错误处理方法。 async/await 的基本原理异步代码的进化史// 回调地狱时代 fetchData(url1, (data1) => { process(data1, (result1) => { fetchData(url2, (data2) => { // 更多嵌套... }) }) }) // Promise 时代 fetchData(url1) .then(process) .then(() => fetchData(url2)) .catch(handleError) // async/await 时代 async function workflow() { const data1 = await fetchData(url1) const result = await process(data1) return await fetchData(url2) } async/await 是基于 **Promise 的语法糖**,它使异步代码看起来**更像同步代码**,从而更易读、易写。一个 async 函数总是返回一个 Promise,你可以在该函数内部使用 await 来等待异步操作完成。如果在异步操作中出现错误(例如网络请求失败),该错误会使 Promise 进入 rejected 状态。async function fetchData() { const response = await fetch("https://api.example.com/data"); const data = await response.json(); return data; } [removed]顺便吆喝一句,技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费) 前、后端/测试,多地有空位,感兴趣的可以[试试] 个比喻,就好比**铁路信号系统**想象 async 函数是一列高速行驶的列车:- await 是轨道切换器:控制代码执行流向- 未捕获的错误如同脱轨事故:会沿着铁路网(调用栈)逆向传播- try/catch 是智能防护系统: - 自动触发紧急制动(错误捕获) - 启动备用轨道(错误恢复逻辑) - 向调度中心发送警报(错误日志)为了优雅地捕获 async/await 中出现的错误,通常我们会使用 try/catch 语句。这种方式**可以在同一个代码块中捕获抛出的错误**,使得错误处理逻辑更集中、直观。- 代码逻辑集中,错误处理与业务逻辑紧密结合。- 可以捕获多个 await 操作中抛出的错误。- 适合需要在出错时进行统一处理或恢复操作的场景。async function fetchData() { try { const response = await fetch("https://api.example.com/data"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error("Error fetching data:", error); // 根据需要,可以在此处处理错误,或者重新抛出以便上层捕获 throw error; } } 不使用 try/catch 的替代方案虽然 try/catch 是最直观的错误处理方式,但你也可以不在 async 函数内部使用它,而是**在调用该 async 函数时捕获错误**。在 Promise 链末尾添加 `.catch()`async function fetchData() { const response = await fetch("https://api.example.com/data"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 调用处使用 Promise.catch 捕获错误 fetchData() .then(data => { console.log("Data:", data); }) .catch(error => { console.error("Error fetching data:", error); }); 这种方式将错误处理逻辑移至函数调用方,适用于以下场景:- 当多个调用者希望以不同方式处理错误时。- 希望让 async 函数保持简洁,将错误处理交给全局统一的错误处理器(例如在 React 应用中可以使用 Error Boundary)。将 `await` 与 `catch` 结合async function fetchData() { const response = await fetch('https://api.example.com/data').catch(error => { console.error('Request failed:', error); return null; // 返回兜底值 }); if (!response) return; // 继续处理 response... } 全局错误监听(慎用,适合兜底)// 浏览器端全局监听 window.addEventListener('unhandledrejection', event => { event.preventDefault(); sendErrorLog({ type: 'UNHANDLED_REJECTION', error: event.reason, stack: event.reason.stack }); showErrorToast('系统异常,请联系管理员'); }); // Node.js 进程管理 process.on('unhandledRejection', (reason, promise) => { logger.fatal('未处理的 Promise 拒绝:', reason); process.exitCode = 1; }); 错误处理策略矩阵 决策树分析错误处理体系1. 基础层:80% 的异步操作使用 try/catch + 类型检查1. 中间层:15% 的通用错误使用全局拦截 + 日志上报1. 战略层:5% 的关键操作实现自动恢复机制 小结我的观点是:不强制要求,但强烈推荐- 不强制:如果不需要处理错误,可以不使用 `try/catch`,但未捕获的 Promise 拒绝(unhandled rejection)会导致程序崩溃(在 Node.js 或现代浏览器中)。- 推荐:90% 的场景下需要捕获错误,因此 `try/catch` 是最直接的错误处理方式。所有我个人观点:使用 async/await 尽量使用 try/catch。好的错误处理不是消灭错误,而是让系统具备优雅降级的能力。你的代码应该像优秀的飞行员——在遇到气流时,仍能保持平稳飞行。大家如有不同意见,还请评论区讨论,说出自己的见解。 ———转载自作者:雨夜寻晴天
Java 泛型中的通配符 T,E,K,V,?有去搞清楚吗?
前言不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。泛型有什么用?在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。 Java泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型,就是在编译时对输入的参数指定一个数据类型。类型安全:编译是检查类型是否匹配,避免了ClassCastexception的发生。 // 非泛型写法(存在类型转换风险) List list1 = new ArrayList(); list1.add("a"); Integer num = (Long) list1.get(0); // 运行时抛出 ClassCastException // 泛型写法(编译时检查类型) List list2 = new ArrayList[removed](); // list.add(1); // 编译报错 list2.add("a"); String str = list2.get(0); // 无需强制转换 消除代码强制类型转换:减少了一些类型转换操作。 // 非泛型写法 Map map1 = new HashMap(); map1.put("user", new User()); User user1 = (User) map1.get("user"); // 泛型写法 Map map2 = new HashMap[removed](); map2.put("user", new User()); // 自动转换 User user2 = map2.get("user"); 3.代码复用:可以支持多种数据类型,不要重复编写代码,例如:我们常用的统一响应结果类。 @Data @NoArgsConstructor @AllArgsConstructor public class Result { /** * 响应状态码 */ private int code; /** * 响应信息 */ private String message; /** * 响应数据 */ private T data; /** * 时间戳 */ private long timestamp; 其他代码省略... 增强可读性:通过类型参数就直接能看出要填入什么类型。 List list = new ArrayList[removed](); [removed]顺便吆喝一句,技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费)前、后端/测试,多地有空位,感兴趣的可以戳试试~~泛型里的通配符我们在使用泛型的时候,经常会使用或者看见多种不同的通配符,常见的 T,E,K,V,?这几种,相信大家一定不陌生,但是真的问你他们有什么作用?有什么区别时,很多人应该是不能很好的介绍它们的,接下来我就来给大家介绍介绍。T,E,K,VT(Type) T表示任意类型参数,我们举个例子 pubile class A{ prvate T t; //其他省略... } //创建一个不带泛型参数的A A a = new A(); a.set(new B()); B b = (B) a.get();//需要进行强制类型转换 //创建一个带泛型参数的A A a = new A(); a.set(new B()); B b = a.get(); E(Element) E表示集合中的元素类型 List list = new ArrayList[removed](); K(Key) K表示映射的键的数据类型 Map map = new HashMap[removed](); V(Value) V表示映射的值的数据类型 Map map = new HashMap[removed](); 通配符 ?无界通配符 表示未知类型,接收任意类型 // 使用无界通配符处理任意类型的查询结果 public void logQueryResult(List resultList) { resultList.forEach(obj -> log.info("Result: {}", obj)); } 上界通配符 表示类型是T或者是子类 // 使用上界通配符读取缓存 public T getCache(String key, Class clazz) { Object value = redisTemplate.opsForValue().get(key); return clazz.cast(value); } 下界通配符 表示类型是T或者是父类 // 使用下界通配符写入缓存 public void setCache(String key, value) { redisTemplate.opsForValue().set(key, value); } 总结我们在很多时候只是单纯的会使用某些技术,但是对它们里面许许多多常见的都是一知半解的,只是会使用确实很重要,但是如果有时间,我们不妨好好的在对这些技术进行深入学习,不仅知其然,而且知其所以然,这样我们的技术才会不断提升进步。——转载自作者:镜花水月linyi
Java利用Deepseek进行项目代码审查
一、为什么需要AI代码审查?写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。 二、环境准备(5分钟搞定)1. 安装Deepseek插件(以VSCode为例):- 插件市场搜索"Deepseek Code Review"- 点击安装(就像安装手机APP一样简单)1. Java项目配置: com.deepseek code-analyzer 1.3.0 [removed]顺便吆喝一句,技术大厂,待遇之类的给的还可以!前、后端/测试,多地有空位,偶尔有加班(放心,加班有加班费),感兴趣的可以[试试][试试]~~三、真实案例:用户管理系统漏洞检测 原始问题代码:public class UserService { // 漏洞1:未处理空指针 public String getUserRole(String userId) { return UserDB.query(userId).getRole(); } // 漏洞2:资源未关闭 public void exportUsers() { FileOutputStream fos = new FileOutputStream("users.csv"); fos.write(getAllUsers().getBytes()); } // 漏洞3:SQL注入风险 public void deleteUser(String input) { Statement stmt = conn.createStatement(); stmt.execute("DELETE FROM users WHERE id = " + input); } } 使用Deepseek审查后:智能修复建议:1. 空指针防护 → 建议添加Optional处理1. 流资源 → 推荐try-with-resources语法1. SQL注入 → 提示改用PreparedStatement 修正后的代码:public class UserService { // 修复1:Optional处理空指针 public String getUserRole(String userId) { return Optional.ofNullable(UserDB.query(userId)) .map(User::getRole) .orElse("guest"); } // 修复2:自动资源管理 public void exportUsers() { try (FileOutputStream fos = new FileOutputStream("users.csv")) { fos.write(getAllUsers().getBytes()); } } // 修复3:预编译防注入 public void deleteUser(String input) { PreparedStatement pstmt = conn.prepareStatement( "DELETE FROM users WHERE id = ?"); pstmt.setString(1, input); pstmt.executeUpdate(); } } 四、实现原理揭秘Deepseek的代码审查就像"X光扫描仪",通过以下三步工作:1. **模式识别**:比对数千万个代码样本- 就像老师批改作业时发现常见错误1. **上下文理解**:分析代码的"人际关系"- 数据库连接有没有"成对出现"(打开/关闭)- 敏感操作有没有"保镖"(权限校验)1. **智能推理**:预测代码的"未来"- 这个变量走到这里会不会变成null?- 这个循环会不会变成"无限列车"?五、进阶使用技巧1. 自定义审查规则(配置文件示例):rules: security: sql_injection: error performance: loop_complexity: warning style: var_naming: info 2. 与CI/CD集成(GitHub Action示例):- name: Deepseek Code Review uses: deepseek-ai/code-review-action@v2 with: severity_level: warning fail_on: error 六、开发者常见疑问Q:AI会不会误判我的代码?A:就像导航偶尔会绕路,Deepseek给出的是"建议"而非"判决",最终决策权在你手中Q:处理历史遗留项目要多久?A:10万行代码项目约需3-5分钟,支持增量扫描七、效果对比数据指标人工审查Deepseek+人工平均耗时4小时30分钟漏洞发现率78%95%误报率5%12%知识库更新速度季度实时 ——转载自作者:Java技术小馆
URL地址末尾加不加”/“有什么区别
URL 结尾是否带 / 主要影响的是 服务器如何解析请求 以及 相对路径的解析方式,具体区别如下:1. 基础概念URL(统一资源定位符) :用于唯一标识互联网资源,如网页、图片、API等。目录 vs. 资源:以 / 结尾的 URL 通常表示目录,例如: https://example.com/folder/ 不以 / 结尾的 URL 通常指向具体的资源(如文件),例如: https://example.com/file 2. 带 / 和不带 / 的具体区别(1)目录 vs. 资源https://example.com/folder/服务器通常会将其解析为 目录,并尝试返回该目录下的默认文件(如 index.html)。https://example.com/folder服务器可能会将其视为 文件,如果 folder 不是文件,而是目录,服务器可能会返回 301 重定向到 folder/。📌 示例:访问 https://example.com/blog/服务器可能返回 https://example.com/blog/index.html。访问 https://example.com/blog(如果 blog 是个目录)服务器可能重定向到 https://example.com/blog/,再返回 index.html。(2)相对路径解析URL 末尾是否有 / 会影响相对路径的解析。假设 HTML 页面包含以下 标签: 📌 示例:访问 https://example.com/folder/图片路径解析为 https://example.com/folder/image.png访问 https://example.com/folder图片路径解析为 https://example.com/image.png可能导致 404 错误,因为 image.png 在 folder/ 里,而浏览器错误地去 example.com/ 下查找。原因:以 / 结尾的 URL,浏览器会认为它是一个目录,相对路径会基于 folder/ 解析。不带 /,浏览器可能认为 folder 是文件,相对路径解析可能会出现错误。(3)SEO 影响搜索引擎对 https://example.com/folder/ 和 https://example.com/folder 可能会视为两个不同的页面,导致 重复内容问题,影响 SEO 排名。因此:网站通常会选择 一种形式 并用 301 重定向 规范化 URL。例如:https://example.com/folder 自动跳转 到 https://example.com/folder/。反之亦然。(4)API 请求对于 RESTful API,带 / 和不带 / 可能导致不同的行为:https://api.example.com/users可能返回所有用户数据。https://api.example.com/users/可能返回 404 或者产生不同的结果(取决于服务器实现)。一些 API 服务器对 / 非常敏感,因此最好遵循 API 文档的规范。[removed]顺便吆喝一句,技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费)前、后端/测试,多地有空位,感兴趣的可以戳试试~~3. 总结URL形式 作用 影响https://example.com/folder/ 目录 通常返回 folder/ 下的默认文件,如 index.html,相对路径解析基于 folder/https://example.com/folder资源(或重定向) 可能被解析为文件,或者服务器重定向到 folder/,相对路径解析可能错误https://api.example.com/data/ API 路径 可能与 https://api.example.com/data 表现不同,具体由 API 设计决定如果你在开发网站,建议:统一 URL 规则,例如所有目录都加 / 或者所有请求都不加 /,然后用 301 重定向 确保一致性。测试 API 的行为,确认带 / 和不带 / 是否影响请求结果。——转载自作者:Chiyamin