注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

基于Flutter实现的小说阅读器——BITReader ,相信我你也可以变成光!

web
前言 最近感觉自己有点颓废,左思右想后觉得不能这样浪费时间,天天来摆烂。受到了群友的激励以及最近自己喜欢看小说。就想我能不能自己也做一款小说阅读器出来呢。在最开始的时候花了一段时间写了一个版本。当时用的是一个开源的接口,当我写好后使用了两天接口挂了我就只有大眼...
继续阅读 »
6d95f5df68248bb55b5b97b4502332711ff7d073.png@2560w_400h_100q_1o.webp

前言


最近感觉自己有点颓废,左思右想后觉得不能这样浪费时间,天天来摆烂。受到了群友的激励以及最近自己喜欢看小说。就想我能不能自己也做一款小说阅读器出来呢。在最开始的时候花了一段时间写了一个版本。当时用的是一个开源的接口,当我写好后使用了两天接口挂了我就只有大眼瞪小眼了。之后在 FlutterCandies里面咨询了群友,发现了一种使用外部提供书籍数据源的方法可以避免数据来源挂掉,说干就干vscode启动!




项目地址


github.com/fluttercand…


项目介绍


BITReader是一款基于Flutter实现的小说阅读器


当前功能包含:



  • 源搜索:使用内置数据来源进行搜索数据(后续更新:用户可以自行导入来源进行源搜索

  • 收藏书架

  • 阅读历史记录

  • 阅读设置:字号设置,字体颜色更改,自定义阅读背景(支持调色板自定义选择,支持image设置为背景

  • 主题设置:支持九种颜色的主题样式

  • 书籍详情:展示书籍信息以及章节目录等书籍信息




支持平台


平台是否支持
Android
IOS
Windows
MacOS
Web
Linux

项目截图


729_1x_shots_so.png
360_1x_shots_so.png
57_1x_shots_so.png
300_1x_shots_so.png
402_1x_shots_so.png

mac运行截图


CE7D99422AA2804700F33FC94D273EC7.png

windows运行截图


d7a40aa1-1572-4969-9d78-55d2abcd791b.png

项目结构


lib
├── main.dart -- 入口
├── assets -- 本地资源生成
├── base -- 请求状态、页面状态
├── db -- 数据缓存
├── icons -- 图标
├── net -- 网络请求、网络状态
├── n_pages
├── detail -- 详情页
├── home -- 首页
├── search -- 全网搜索搜索页
├── history -- 历史记录
├── read -- 小说阅读
└── like -- 收藏书架
├── pages 已废弃⚠
├── home -- 首页
├── novel -- 小说阅读
├── search -- 全网搜索
├── category -- 小说分类
├── detail_novel -- 小说详情
├── book_novel -- 书架、站源
└── collect_novel -- 小说收藏
├── route -- 路由
└── theme -- 主题管理
└── themes -- 主题颜色-9种颜色
├── tools -- 工具类 、解析工具、日志、防抖。。。
└── widget -- 自定义组件、工具 、加载、状态、图片 等。。。。。。

阅读器主要包含的模块



  • 阅读显示:文本解析,对文本进行展示处理

  • 数据解析: 数据源的解析,以及数据来源的解析(目前只支持简单数据源格式解析、后续可能会更新更多格式解析

  • 功能:阅读翻页样式、字号、背景、背景图、切换章节、收藏、历史记录、本地缓存等


阅读显示


阅读文本展示我用的是extended_text因为支持自定义效果很好。


实现的效果把文本中 “ ” 引用起来的文本自定义成我自己想要的效果样式。


class MateText extends SpecialText {
MateText(
TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap, {
this.showAtBackground = false,
required this.start,
required this.color,
}) : super(flag, '”', textStyle, onTap: onTap);
static const String flag = '“';
final int start;
final Color color;

/// whether show background for @somebody
final bool showAtBackground;

@override
InlineSpan finishText() {
final TextStyle textStyle =
this.textStyle?.copyWith(color: color) ?? const TextStyle();

final String atText = toString();

return showAtBackground
? BackgroundTextSpan(
background: Paint()..color = Colors.blue.withOpacity(0.15),
text: atText,
actualText: atText,
start: start,

///caret can move int0 special text
deleteAll: true,
style: textStyle,
recognizer: (TapGestureRecognizer()
..onTap = () {
if (onTap != null) {
onTap!(atText);
}
}))
: SpecialTextSpan(
text: atText,
actualText: atText,
start: start,
style: textStyle,
recognizer: (TapGestureRecognizer()
..onTap = () {
if (onTap != null) {
onTap!(atText);
}
}));
}
}


class NovelSpecialTextSpanBuilder extends SpecialTextSpanBuilder {
NovelSpecialTextSpanBuilder({required this.color});
Color color;
set setColor(Color c) => color = c;
@override
SpecialText? createSpecialText(String flag,
{TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap,
int? index}) {
if (flag == '') {
return null;
} else if (isStart(flag, AtText.flag)) {
return AtText(
textStyle,
onTap,
start: index! - (AtText.flag.length - 1),
color: color,
);
} else if (isStart(flag, MateText.flag)) {
return MateText(
textStyle,
onTap,
start: index! - (MateText.flag.length - 1),
color: color,
);
}
// index is end index of start flag, so text start index should be index-(flag.length-1)
return null;
}
}

数据解析编码格式转换


首先数据是有不同的编码格式,否则我们直接展示可能会导致乱码问题。
先把数据给根据查找到的编码类型来做单独的处理转换。


/// 解析html数据 解码 不同编码
static String parseHtmlDecode(dynamic htmlData) {
String resultData = gbk.decode(htmlData);
final charset = ParseSourceRule.parseCharset(htmlData: resultData) ?? "gbk";
if (charset.toLowerCase() == "utf-8" || charset.toLowerCase() == "utf8") {
resultData = utf8.decode(htmlData);
}
return resultData;
}

 static String? parseCharset({
required String htmlData,
}) {
Document document = parse(htmlData);

List<Element> metaTags = document.getElementsByTagName('meta').toList();
for (Element meta in metaTags) {
String? charset = meta.attributes['charset'];
String content = meta.attributes['content'] ??
""; //<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

if (charset != null) {
return charset;
}
List<String> parts = content.split(';');
for (String part in parts) {
part = part.trim();
if (part.startsWith('charset=')) {
return part.split('=').last.trim();
}
}
}

return null;
}

数据结构解析-代码太多只展示部分


Document document = parse(htmlData);

//
List<Element> rootNodes = [];
if (rootSelector != null && rootSelector.isNotEmpty) {
//
List<String> rootParts = rootSelector.split(RegExp(r'[@>]'));
String initialPart = rootParts[0].trim();

//
if (initialPart.startsWith('class.')) {
String className = initialPart.split('.')[1];
rootNodes = document.getElementsByClassName(className).toList();
} else if (initialPart.startsWith('.')) {
String className = initialPart.substring(1);
rootNodes = document.getElementsByClassName(className).toList();
} else if (initialPart.startsWith('#')) {
String idSelector = initialPart.substring(1);
rootNodes = document.querySelectorAll('#$idSelector').toList();
} else if (initialPart.startsWith('id.')) {
String idSelector = initialPart.split('.')[1];
var element = document.querySelector('#$idSelector');
if (element != null) {
rootNodes.add(element);
}
} else if (initialPart.contains(' ')) {
String idSelector = initialPart.replaceAll(' ', ">");
var element = document.querySelector(idSelector);
if (element != null) {
rootNodes.add(element);
}
} else {
rootNodes = document.getElementsByTagName(initialPart).toList();
}

存储工具类 - 部分代码


/// shared_preferences
class PreferencesDB {
PreferencesDB._();
static final PreferencesDB instance = PreferencesDB._();
SharedPreferencesAsync? _instance;
SharedPreferencesAsync get sps => _instance ??= SharedPreferencesAsync();

/*** APP相关 ***/

/// 主题外观模式
///
/// system(默认):跟随系统 light:普通 dark:深色
static const appThemeDarkMode = 'appThemeDarkMode';

/// 多主题模式
///
/// default(默认)
static const appMultipleThemesMode = 'appMultipleThemesMode';

/// 字体大小
///
///
static const fontSize = 'fontSize';

/// 字体粗细
static const fontWeight = 'fontWeight';

/// 设置-主题外观模式
Future<void> setAppThemeDarkMode(ThemeMode themeMode) async {
await sps.setString(appThemeDarkMode, themeMode.name);
}

/// 获取-主题外观模式
Future<ThemeMode> getAppThemeDarkMode() async {
final String themeDarkMode =
await sps.getString(appThemeDarkMode) ?? 'system';
return darkThemeMode(themeDarkMode);
}

/// 设置-多主题模式
Future<void> setMultipleThemesMode(String value) async {
await sps.setString(appMultipleThemesMode, value);
}

/// 获取-多主题模式
Future<String> getMultipleThemesMode() async {
return await sps.getString(appMultipleThemesMode) ?? 'default';
}

/// 获取-fontsize 大小 默认18
Future<double> getNovelFontSize() async {
return await sps.getDouble(fontSize) ?? 18;
}

/// 设置 -fontsize 大小
Future<void> setNovelFontSize(double size) async {
await sps.setDouble(fontSize, size);
}

/// 设置-多主题模式
Future<void> setNovelFontWeight(NovelReadFontWeightEnum value) async {
await sps.setString(fontWeight, value.id);
}

/// 获取-多主题模式
Future<String> getNovelFontWeight() async {
return await sps.getString(fontWeight) ?? 'w300';
}
}

最后


特别鸣谢FlutterCandies糖果社区,也欢迎加入我们的大家庭。让我们一起学习共同进步


免责声明:本项目提供的源代码仅用学习,请勿用于商业盈利。


作者:7_bit
来源:juejin.cn/post/7433306628994940979
收起阅读 »

Cursor生成UI,加一步封神

web
用 Cursor 做 UI,有两种最简单又有效的方法,一个免费一个付费,不管你要做网页 UI 还是应用程序 UI,都能用。 我这里不推荐直接用 Cursor 自带模型生成 UI,模型生成出来的效果比较差,就算是最强的 Claude 也不太行。 本文我分享的方法...
继续阅读 »

用 Cursor 做 UI,有两种最简单又有效的方法,一个免费一个付费,不管你要做网页 UI 还是应用程序 UI,都能用。


我这里不推荐直接用 Cursor 自带模型生成 UI,模型生成出来的效果比较差,就算是最强的 Claude 也不太行。


本文我分享的方法是我最近学到的,先说免费的。当我们手头有一张 UI 图片时,不要直接丢给 Cursor,而是先用 Google 的 Gemini 模型、Claude 或者 ChatGPT,这里我用的是 Gemini 并打开 Canvas 功能。



我把 UI 图片放到 Gemini 中,然后让它根据 UI 截图生成一份 JSON 格式的设计规范文件。


提示词参考:


Create a JSON-formatted design system profile. This profile should extract relevant visualdesign information from the provided screenshots. The JSON output must specifically include:
The overarching design style (e.g., color palette, typography, spacing, visual hierarchy).The structural elements and layout principles.Any other attributes crucial for an Al to consistently replicate these design systems.Crucially, do not include the specific content or data present within the images, focusing solely


生成出来的 JSON 包含整体设计风格、结构元素、布局原则,以及一些关键属性。


接着把这份 JSON 文件复制到 Cursor 中,让 Cursor 根据这份 JSON 来生成代码。


提示词参考:


参考 @design.json 设计规范,根据图片中的样式,生成一个网页。


生成效果如下:



对比一下如果直接用 Cursor 根据截图生成代码,不用 JSON 文件。


提示词:


按照图片中的UI样式,创建一个新的页面。注意:尽可能按照图片中的样子创建!!!


效果如下:



可以看到,效果差了很多,我原型 UI 的截图如下:



这是我随便找的一张图片作为例子,可以明显看出,先提取一份 JSON 文件,然后再让 Cursor 生成代码,效果要好很多。


为什么这种先提取 JSON 文件再生成代码的方法很有效?因为当任务涉及精确、结构化、无歧义的数据时,JSON 让模型理解更清晰,处理更高效,生成的结果也更稳定。


以上就是免费的方法。


接下来是付费的方法。


如果你对 UI 要求比较高,比如需要反复修改,那我推荐直接用 v0 API。v0 模型是 Vercel 推出的,专门针对 UI 和前端开发优化,所以在处理这类任务时,v0 比 Claude、Gemini、ChatGPT 都更强。


我一般会在需要大量生成 UI 时订阅 v0,一个月 20 美金,这个月把需要的 UI 全部生成完,然后就可以退订。



订阅后去后台生成 API Key,然后在 Cursor 中调用 v0 模型即可。


在 Cursor 模型设置中,把 v0 的 API Key 填进去,v0 模型是符合 OpenAI API 规范的,所以直接选择 OpenAI 模型即可。


实际使用时,你在对话中用的是 OpenAI 模型,但后台用的其实是 v0 模型。



好了,这就是免费和付费的两种方法。


最后再推荐两个动画工具:Framer MotionReact Bits,也都是很棒的选择。


你可以把 React Bits 中动画代码直接粘贴到 Cursor 中,让模型帮你集成即可。



  • React:相当于项目经理和架构师

  • Radix UI:相当于功能工程师

  • Tailwind CSS:相当于视觉设计师

  • Framer Motion:相当于动效设计师


以上就是一套现代强大 UI 开发工具箱,大家可以根据需要组合使用!


作者:程序员NEO
来源:juejin.cn/post/7519407199765987343
收起阅读 »

国产大模型高考出分了:裸分 683,选清华还是北大?

这两天啊,各地高考的成绩终于是陆续公布了。 现在,也是时候揭晓全球第一梯队的大模型们的 “高考成绩” 了—— 我们先来看下整体的情况(该测试由字节跳动 Seed 团队官方发布): 按照传统文理分科计分方式,Gemini 的理科总成绩 655 分,在所有选手里...
继续阅读 »

这两天啊,各地高考的成绩终于是陆续公布了。


现在,也是时候揭晓全球第一梯队的大模型们的 “高考成绩” 了——


我们先来看下整体的情况(该测试由字节跳动 Seed 团队官方发布):



按照传统文理分科计分方式,Gemini 的理科总成绩 655 分,在所有选手里排名第一。豆包的文科总成绩 683 分,排名第一,理科总成绩是 648 分,排名第二。


再来看下各个细分科目的成绩情况:



除了数学、化学和生物之外,豆包的成绩依旧是名列前茅,6 个科目均是第一。


不过其它 AI 选手的表现也是比较不错,可以说是达到了优秀学生的水准。


比较遗憾的选手就要属 O3,因为它在语文写作上跑了题,因此语文成绩仅 95 分,拉低了整体的分数。


若是从填报志愿角度来看,因为这套测试采用的是山东省的试卷,根据过往经验判断,3 门自选科目的赋分相比原始分会有一定程度的提高,尤其是在化学、物理等难度较大的科目上。本次除化学成绩相对稍低外,豆包的其余科目组合的赋分成绩最高能超过 690 分,有望冲刺清华、北大。


(赋分规则:将考生选考科目的原始成绩按照一定比例划分等级,然后将等级转换为等级分计入高考总分)


好,那现在的豆包面临的抉择是:上清华还是上北大?



大模型参加高考,分数怎么判?


在看完成绩之后,或许很多小伙伴都有疑惑,这个评测成绩到底是怎么来的。


别急,我们这就对评测标准逐条解析。


首先在卷子的选择上,由于目前网络流出的高考真题都是非官方的,而山东是少数传出全套考卷的高考大省;因此主科(即语文、数学、英语)采用的是今年的全国一卷,副科采用的则是山东卷,满分共计 750 分。


其次在评测方式上,都是通过 API 测试,不会联网查询,评分过程也是参考高考判卷方式,就是为了检验模型自身的泛化能力:



  • 选择题、填空题


    采用机评(自动评估)加人工质检的方式;


  • 开放题


    实行双评制,由两位具有联考阅卷经验的重点高中教师匿名评阅,并设置多轮质检环节。



在给模型打分的时候,采用的是 “3 门主科(语文数学英语)+3 门综合科(理综或文综)” 的总分计算方式,给五个模型排了个名次。


值得一提的是,整个评测过程中,模型们并没有用任何提示词优化技巧来提高模型的表现,例如要求某个模型回答得更详细一些,或者刻意说明是高考等等。


最后,就是在这样一个公平公正的环境之下,从刚才我们展示的结果来看,Gemini、豆包相对其他 AI 来说取得了较优的成绩。


细分科目表现分析


了解完评测标准之后,我们继续深入解读一下 AI 选手们在各个科目上的表现。


由于深度思考的大火,大模型们在数学这样强推理科目上的能力明显要比去年好很多(此前大部分均不及格),基本上都能达到 140 分的成绩。


不过在一道不算难的单选题(全国一卷第 6 题)上,国内外的大模型们却都栽了跟头:



这道题大模型们给出的答案是这样的:


豆包:C;Gemini:B;Claude:C;O3:C;DeepSeek:C


但这道题的正解应该是 A,因此大模型们在此全军覆没。


之所如此,主要是因为题目里有方框、虚线、箭头和汉字混在一起的图,模型认不准图像,说明它们在 “看图说话” 这块还有进步空间。


以及在更难的压轴大题上,很多大模型也没完全拿下,经常漏写证明过程,或者推导不严谨被扣分,说明在细节上还需加强。


到做语文选择题和阅读题这两个版块,大模型们几乎是 “学霸本霸”,得分率超高。


不过在作文写作过程也暴露出了一些问题,例如写作过于刻板、文字冰冷,文章字数不达标(不足 800 字或超过 1200 字)、立意不对,形式上还经常会出现惯用的小标题。



在英语测试过程中,大模型们几乎挑不出毛病,唯一扣分点是在写作上,比如用词不够精准、句式稍显单调,但整体已经很接近完美。


对于理综,遇到带图的题目大模型们还是会犯难,不过豆包和 Gemini 这俩模型在看图像和理解图的能力上会比其他模型强一些。


例如下面这道题中,正确答案应当是 C,大模型们的作答是这样的:


豆包:C;Gemini:C;Claude:D;O3:D;DeepSeek:D



最后在文综方面,大模型的地域差别就显现得比较明显,国外的大模型做政治、历史题时,经常搞不懂题目在考啥,对中国的知识点不太 “感冒”。


而对于地理题,最头疼的便是分析统计图和地形图,得从图里精准提取信息再分析。


以上就是对于本次评测的全面分析了。


除了今年国内的高考之外,这几位 “参赛选手” 还参加了印度理工学院的第二阶段入学考试——JEE Advanced


这场考试每年有数百万人参与第一阶段考试,其中前 25 万考生可晋级第二阶段。它分为两场,每场时长 3 小时,同时对数学、物理、化学三科进行考察。


题目以图片形式呈现,重点考查模型的多模态处理能力与推理泛化能力。所有题目均为客观题,每道题进行 5 次采样,并严格按照 JEE 考试规则评分——答对得分、答错扣分,不涉及格式评分标准。


与全印度人类考生成绩对比显示,第一名得分 332 分,第十名得分 317 分。


值得注意的是,豆包与 Gemini 已具备进入全印度前 10 的实力:Gemini 在物理和化学科目中表现突出,而豆包在数学科目 5 次采样中实现全对。



怎么做到的?


相比去年一本线上下的水平,整体来看,大模型们在今年高考题上的表现均有明显的进步。


那么它们到底是如何提升能力的?我们不妨以拿下单科第一最多的豆包为例来了解一下。


豆包大模型 1.6 系列,是字节跳动 Seed 团队推出的兼具多模态能力与深度推理的新一代通用模型。


团队让它能力提升的技术亮点,我们可以归结为三招。


第一招:多模态融合与 256K 长上下文能力构建


Seed1.6 延续了 Seed1.5 在稀疏 MoE(混合专家模型)领域的技术积累,采用 23B 激活参数与 230B 总参数规模进行预训练。其预训练过程通过三个阶段实现多模态能力融合与长上下文支持:



  • 第一阶段:纯文本预训练

    以网页、书籍、论文、代码等数据为训练基础,通过规则与模型结合的数据清洗、过滤、去重及采样策略,提升数据质量与知识密度。

  • 第二阶段:多模态混合持续训练(MMCT)

    进一步强化文本数据的知识与推理密度,增加学科、代码、推理类数据占比,同时引入视觉模态数据,与高质量文本混合训练。

  • 第三阶段:长上下文持续训练(LongCT)

    通过不同长度的长文数据逐步扩展模型序列长度,将最大支持长度从 32K 提升至 256K。


通过模型架构、训练算法及 Infra 的持续优化,Seed1.6 base 模型在参数量规模接近的情况下,性能较 Seed1.5 base 实现显著提升,为后续后训练工作奠定基础。


这一招的发力,就对诸如高考语文阅读理解、英语完形填空和理科综合应用题等的作答上起到了提高准确率的作用,因为它们往往涉及长文本且看重上下文理解。


第二招:多模态融合的深度思考能力


Seed1.6-Thinking 延续 Seed1.5-Thinking 的多阶段 RFT(强化反馈训练)与 RL(强化学习)迭代优化方法,每轮 RL 以上一轮 RFT 为起点,通过多维度奖励模型筛选最优回答。相较于前代,其升级点包括:



  • 拓展训练算力,扩大高质量数据规模(涵盖 Math、Code、Puzzle 等领域);

  • 提升复杂问题的思考长度,深度融合 VLM 能力,赋予模型清晰的视觉理解能力;

  • 引入 parallel decoding 技术,无需额外训练即可扩展模型能力 —— 例如在高难度测试集 Beyond AIME 中,推理成绩提升 8 分,代码任务表现也显著优化。


这种能力直接对应高考中涉及图表、公式的题目,如数学几何证明、物理电路图分析、地理等高线判读等;可以快速定位关键参数并推导出解题路径,避免因单一模态信息缺失导致的误判。


第三招:AutoCoT 解决过度思考问题


深度思考依赖 Long CoT(长思维链)增强推理能力,但易导致 “过度思考”—— 生成大量无效 token,增加推理负担。


为此,Seed1.6-AutoCoT 提出 “动态思考能力”,提供全思考、不思考、自适应思考三种模式,并通过 RL 训练中引入新奖励函数(惩罚过度思考、奖励恰当思考),实现 CoT 长度的动态压缩。


在实际测试中:



  • 中等难度任务(如 MMLU、MMLU pro)中,CoT 触发率与任务难度正相关(MMLU 触发率 37%,MMLU pro 触发率 70%);

  • 复杂任务(如 AIME)中,CoT 触发率达 100%,效果与 Seed1.6-FullCoT 相当,验证了自适应思考对 Long CoT 推理优势的保留。


以上就是豆包能够在今年高考全科目评测中脱颖而出的原因了。


不过除此之外,还有一些影响因素值得说道说道。


正如我们刚才提到的,化学和生物的题目中读图题占比较大,但因非官方发布的图片清晰度不足,会导致多数大模型的表现不佳;不过 Gemini2.5-Pro-0605 的多模态能力较突出,尤其在化学领域。


不过最近,字节 Seed 团队在使用了更清晰的高考真题图片后,以图文结合的方式重新测试了对图片理解要求较高的生物和化学科目,结果显示 Seed1.6-Thinking 的总分提升了近 30 分(理科总分达 676)。



这说明,全模态推理(结合文本与图像)能显著释放模型潜力,是未来值得深入探索的方向。


那么你对于这次大模型们的 battle 结果有何看法?欢迎大家拿真题去实测后,在评论区留言你的感受~


评分明细详情:

bytedance.sg.larkoffice.com/sheets/QgoF…


欢迎在评论区留下你的想法!


—  —


作者:量子位
来源:juejin.cn/post/7519891830894034959
收起阅读 »

被问到 NextTick 是宏任务还是微任务

NextTick 等待下一次 DOM 更新刷新的工具方法。 cn.vuejs.org/api/general… <https://cn.vuejs.org/api/general.html#nexttick> 从字面上看 就知道 肯定是个 异步的...
继续阅读 »

NextTick


等待下一次 DOM 更新刷新的工具方法。


cn.vuejs.org/api/general…


<https://cn.vuejs.org/api/general.html#nexttick>

从字面上看 就知道 肯定是个 异步的嘛。


然后面试官 那你来说说 js执行过程吧。 宏任务 微任务 来做做 宏任务 微任务输出的结果的题吧。


再然后 问问你 nextTick 既然几个异步的 那么他是 宏任务 还是个 微任务呀。


vue2 中


文件夹 src/core/util/next-tick.js 中


image-20240926175135221.png


promise --> mutationObserver -> setImmediate -> setTimeout


支持 哪个走哪个


vue3 中


image-20240926171628843.png


好吧 好吧 promise 了嘛


image-20240926174944844.png


全程 promise


作者:努力学基础的卡拉米
来源:juejin.cn/post/7418505553642291251
收起阅读 »

什么?localhost还能设置二级域名?

大家好,我是农村程序员,独立开发者,行业观察员,前端之虎陈随易。 我会在这里分享关于 独立开发、编程技术、思考感悟 等内容,欢迎关注。 个人网站 1️⃣:chensuiyi.me 个人网站 2️⃣:me.yicode.tech 技术群,搞钱群,闲聊群,自驾群...
继续阅读 »

大家好,我是农村程序员,独立开发者,行业观察员,前端之虎陈随易。


我会在这里分享关于 独立开发编程技术思考感悟 等内容,欢迎关注。



  • 个人网站 1️⃣:chensuiyi.me

  • 个人网站 2️⃣:me.yicode.tech

  • 技术群,搞钱群,闲聊群,自驾群,想入群的在我个人网站联系我。


如果你觉得本文有用,一键三连 (点赞评论转发),就是对我最大的支持~





网上冲浪看到一个有趣且违背常识的帖子,用了那么多年的 localhost,没想到 localhost 还能设置子域名。



而且还不需要修改 hosts 文件,直接就能使用,这真是离谱他妈给离谱开门,离谱到家了。


先说说应用场景:



  • 多用户/多会话隔离:在本地开发中模拟不同用户的 cookies 和 session storage,适合测试用户认证或个性化功能。

  • 跨域开发与测试:模拟真实多域环境 (如 API 和前端分离),用于调试 CORS、单点登录或微服务架构。

  • 简化开发流程:无需修改 hosts 文件即可快速创建子域名,适合快速原型设计或临时项目。

  • 工具与服务器集成:与本地开发工具 (如 localias) 结合,支持 HTTPS 和自定义端口,增强开发体验。

  • 灵活调试:通过自定义子域名和 IP (如 127.0.0.42) 进行高级调试或模拟复杂网络配置。


总得来说就是,localhsot 支持子域名比我们自己手动配置不同的域名并设置 hosts 文件方便多了。


接下来给大家实测一下。



请看,这是我直接在浏览器输入 test1.localhost:3020 后,就能请求到我本地启动的监听 3020 端口的后端接口返回的数据。


我没有配置 hosts 文件,没有做过任何多余的配置工作,直接就生效了。


那么我们可以直接在本地就能调试多服务器集群,跨域 cookie 共享,SSO 单点登录,微服务架构等功能,非常方便。


另外,本公众号是 前端之虎陈随易 专门分享技术的公众号,目前关注量不多,希望大家点点小手指,来个大大的关注哦~


作者:前端之虎陈随易
来源:juejin.cn/post/7521013717438758938
收起阅读 »

Vue3.5正式上线,父传子props用法更丝滑简洁

web
前言 Vue3.5在2024-09-03正式上线,目前在Vue官网显最新版本已经是Vue3.5,其中主要包含了几个小改动,我留意到日常最常用的改动就是props了,肯定是用Vue3的人必用的,所以针对性说一下props的两个小改动使我们日常使用更加灵活。 一...
继续阅读 »

前言


Vue3.52024-09-03正式上线,目前在Vue官网显最新版本已经是Vue3.5,其中主要包含了几个小改动,我留意到日常最常用的改动就是props了,肯定是用Vue3的人必用的,所以针对性说一下props两个小改动使我们日常使用更加灵活。


image.png


一、带响应式Props解构赋值


简述: 以前我们对Props直接进行解构赋值是会失去响应式的,需要配合使用toRefs或者toRef解构才会有响应式,那么就多了toRefs或者toRef这工序,而最新Vue3.5版本已经不需要了。



这样直接解构,testCount能直接渲染显示,但会失去响应式,当我们修改testCount时页面不更新。



<template>
<div>
{{ testCount }}
</div>
</template>

<script setup>
import { defineProps } from 'vue';
const props = defineProps({
testCount: {
type: Number,
default: 0,
},
});

const { testCount } = props;
</script>


保留响应式的老写法,使用toRefs或者toRef解构



<template>
<div>
{{ testCount }}
</div>
</template>

<script setup>
import { defineProps, toRef, toRefs } from 'vue';
const props = defineProps({
testCount: {
type: Number,
default: 0,
},
});

const { testCount } = toRefs(props);
// 或者
const testCount = toRef(props, 'testCount');
</script>


最新Vue3.5写法,不借助”外力“直接解构,依然保持响应式



<template>
<div>
{{ testCount }}
</div>
</template>

<script setup>
import { defineProps } from 'vue';
const { testCount } = defineProps({
testCount: {
type: Number,
},
});

</script>

相比以前简洁了真的太多,直接解构使用省去了toRefs或者toRef


二、Props默认值新写法


简述: 以前默认值都是用default: ***去设置,现在不用了,现在只需要解构的时候直接设置默认值,不需要额外处理。



先看看旧的default: ***默认值写法



如下第12就是旧写法,其它以前Vue2也是这样设置默认值


<template>
<div>
{{ props.testCount }}
</div>
</template>

<script setup>
import { defineProps } from 'vue';
const props = defineProps({
testCount: {
type: Number,
default: 1
},
});
</script>

最新优化的写法
如下第9行,解构的时候直接一步到位设置默认值,更接近js语法的写法。


<template>
<div>
{{ testCount }}
</div>
</template>

<script setup>
import { defineProps } from 'vue';
const { testCount=18 } = defineProps({
testCount: {
type: Number,
},
});
</script>

小结


这次更新其实props的本质功能并没有改变,但写法确实变的更加丝滑好用了,props使用非常高频感觉还是有必要跟进这种更简洁的写法。如果那里写的不对或者有更好建议欢迎大佬指点啊。


作者:天天鸭
来源:juejin.cn/post/7410333135118090279
收起阅读 »

油猴+手势识别:我实现了任意网页隔空控制!

web
引言 最近我的小册《油猴脚本实战指南》上线了,很多同学都很感兴趣。 有些人学习后就私下问我,油猴既然能将任意前端js注入到当前网页中,是否能结合手势识别实现任意网页隔空控制,实现类似手机上的隔空翻页功能呢? 这是个非常好的想法,于是,我经过研究,将它实现出来...
继续阅读 »

引言


最近我的小册《油猴脚本实战指南》上线了,很多同学都很感兴趣。



有些人学习后就私下问我,油猴既然能将任意前端js注入到当前网页中,是否能结合手势识别实现任意网页隔空控制,实现类似手机上的隔空翻页功能呢?


这是个非常好的想法,于是,我经过研究,将它实现出来了!先看看脚本效果:




1️⃣ 上下翻页功能



  • 左手张开,右手可以控制网页向下翻页

  • 左手握拳,右手可以控制网页向上翻页



2️⃣ 右手可以控制一个模拟光标移动



3️⃣ 右手握拳,实现点击效果



当然,还预设了很多手势,比如双手比✌🏻关闭当前网页,左手竖起大拇指,右手实现缩放网页等效果。


实现原理


其实实现原理非常简单,就是油猴+手势识别


油猴Tampermonkey


油猴(Tampermonkey)是一款浏览器插件,允许用户在网页加载时注入自定义的 JavaScript 脚本,来增强、修改或自动化网页行为


通俗地说,借助油猴,你可以将自己的 JavaScript 代码“植入”任意网页,实现自动登录、抢单、签到、数据爬取、广告屏蔽等各种“开挂级”功能,彻底掌控页面行为。


如果你想深入了解,可以参考文章:juejin.cn/book/751468…


手势识别MediaPipe


手势识别其实已经不是一个新鲜词了,随着大模型的普及,AI识别手势非常简单方便。本示例中使用的AI模型识别,主要依赖了谷歌的MediaPipe。


MediaPipe 解决方案提供了一套库和工具,可帮助您快速在应用中应用人工智能 (AI) 和机器学习 (ML) 技术。



本示例中的demo就是借助它的手势识别能力实现的。在web中,我们可以借助MediaPipe @mediapipe/tasks-vision NPM 软件包获取手势识别器代码。


MediaPipe @mediapipe/tasks-vision

它的使用也非常简单


// Create task for image file processing:
const vision = await FilesetResolver.forVisionTasks(
// path/to/wasm/root
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm "
);
const gestureRecognizer = await GestureRecognizer.createFromOptions(vision, {
baseOptions: {
modelAssetPath: "https://storage.googleapis.com/mediapipe-tasks/gesture_recognizer/gesture_recognizer.task"
},
numHands: 2
});

如何将两者结合


借助油猴的脚本注入能力,我们能让我们的手势识别代码运行在任意网页,从而轻松实现隔空手势控制效果。


当然,脚本运行时必须开启摄像机权限,页面其实会有一个画面,但是很尴尬,于是实际脚本中,我将画面隐藏了。



手势识别的原理其实也不复杂,通过tasks-vision,我们可以拿到上图中各个关键的点的位置信息,通过判断不同点位之间的距离,实现不同的手势判断。


// 判定手势
// 手掌张开手势
function isHandOpen(hand) {
const fingers = [[8, 5], [12, 9], [16, 13], [20, 17]];
return fingers.filter(([tip, base]) => dist(hand[tip], hand[base]) > 0.1).length >= 4;
}
// 握拳手势
function isFist(hand) {
const fingers = [[8, 5], [12, 9], [16, 13], [20, 17]];
return fingers.filter(([tip, base]) => dist(hand[tip], hand[base]) < 0.06).length >= 3;
}
// 胜利手势
function isVictory(hand) {
const extended = [8, 12];
const folded = [16, 20];
return (
extended.every((i) => dist(hand[i], hand[i - 3]) > 0.1) &&
folded.every((i) => dist(hand[i], hand[i - 3]) < 0.05)
);
}

上述代码中的hand就是mediapipe/tasks-vision返回的手势信息。结合这些自定义的手势信息,我们就能实现各种花里胡哨的功能!


进一步学习


对于手势识别的学习,我们可以去学习官方的demo,在npmjs上,我们可以找到使用说明



这个包人脸识别、手势识别等非常多的功能,非常强大!


如果你对油猴脚本感兴趣,可以看看教程 油猴脚本实战指南 》, 本示例中的demo也会在这个教程中详细讲解。



当然,你也可以加我shc1139874527,我会拉你进学习交流群,一起体验油猴脚本开发的魅力!



作者:石小石Orz
来源:juejin.cn/post/7521250468267360307
收起阅读 »

前端与Brain.js融合的未来

AI 端模型与前端开发的新时代 随着技术的发展,人工智能(AI)正以前所未有的速度融入我们的生活。从前端到后端,从移动应用到物联网设备,AI的应用场景越来越广泛。特别是在前端领域,AI技术的引入为网页开发带来了前所未有的机遇。 什么是脑神经网络库(Brain....
继续阅读 »

AI 端模型与前端开发的新时代


随着技术的发展,人工智能(AI)正以前所未有的速度融入我们的生活。从前端到后端,从移动应用到物联网设备,AI的应用场景越来越广泛。特别是在前端领域,AI技术的引入为网页开发带来了前所未有的机遇。


什么是脑神经网络库(Brain.js)


在前端领域,Brain.js 是一个非常受欢迎的库,它允许开发者在浏览器中直接使用神经网络进行各种任务,如文本分类、图像识别等。Brain.js 的一大优势在于其易于上手,即使是没有深厚机器学习背景的开发者也能快速开始使用。


Brain.js 在前端开发中的应用


数据准备

首先,我们需要准备用于训练神经网络的数据。这些数据通常以JSON数组的形式存在,每个元素包含输入(input)和输出(output)。例如,在以下示例中,我们准备了一组关于前端和后端开发的知识点,用于训练一个能够区分两者差异的神经网络。


const data = [
{ "input": "implementing a caching mechanism improves performance", "output": "backend" },
// 更多数据...
];

神经网络初始化

接下来,我们使用 brain.recurrent.LSTM() 函数初始化一个长短期记忆(LSTM)神经网络。LSTM是一种特殊的递归神经网络(RNN),特别适合处理序列数据,如文本或时间序列。


const network = new brain.recurrent.LSTM();

模型训练

有了数据和神经网络之后,我们就可以开始训练模型了。训练过程可能需要一些时间,具体取决于数据集的大小和复杂度。network.train() 方法接受数据集作为参数,并允许设置训练的迭代次数和其他选项。


network.train(data, {
iterations: 2000, // 迭代次数
log: true, // 是否打印训练日志
logPeriod: 100 // 日志打印频率
});

应用模型

一旦模型训练完成,我们就可以使用 network.run() 方法对新的输入进行预测。例如,我们可以测试模型是否能正确地将“使用Flexbox布局”归类为前端开发。


const output = network.run("using flexbox for layout");
console.log(output); // 输出结果

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 端模型- 前端开发的时代</title>
</head>
<body>
<script src="./brain.js"></script>
<script>
// json 数组
// 输入 input
// 喂给大模型的数据
const data = [
{ "input": "implementing a caching mechanism improves performance", "output": "backend" },
{ "input": "hover effects on buttons", "output": "frontend" },
{ "input": "optimizing SQL queries", "output": "backend" },
{ "input": "using flexbox for layout", "output": "frontend" },
{ "input": "setting up a CI/CD pipeline", "output": "backend" },
{ "input": "SVG animations for interactive graphics", "output": "frontend" },
{ "input": "authentication using OAuth", "output": "backend" },
{ "input": "responsive images for different screen sizes", "output": "frontend" },
{ "input": "creating REST API endpoints", "output": "backend" },
{ "input": "CSS grid for complex layouts", "output": "frontend" },
{ "input": "database normalization for efficiency", "output": "backend" },
{ "input": "custom form validation", "output": "frontend" },
{ "input": "implementing web sockets for real-time communication", "output": "backend" },
{ "input": "parallax scrolling effect", "output": "frontend" },
{ "input": "securely storing user passwords", "output": "backend" },
{ "input": "creating a theme switcher (dark/light mode)", "output": "frontend" },
{ "input": "load balancing for high traffic", "output": "backend" },
{ "input": "accessibility features for disabled users", "output": "frontend" },
{ "input": "scalable architecture for growing user base", "output": "backend" }
];
// 初始化一个神经网络
const network = new brain.recurrent.LSTM();
// 训练 花蛮长时间
network.train(data,{
iterations: 2000,
log:true,
logPeriod:100
});
// 执行一下
const output = network.run("using flexbox for layout");
console.log(output);
</script>
</body>
</html>

前端与AI融合的未来


随着技术的进步,AI在前端的应用将更加广泛。从智能表单验证到个性化推荐系统,从前端性能优化到用户界面的自动设计,AI技术为前端开发提供了无限的可能性。此外,随着端侧模型(如AGI端侧模型)的发展,未来的设备将变得更加智能,能够即时响应用户的需要,提供更加个性化的体验。


最后,AI与前端开发的结合不仅提升了用户体验,也为开发者带来了新的挑战和机遇。通过不断学习和探索,我们能够在这个充满活力的领域中创造出更多令人兴奋的作品。




作者:Danta
来源:juejin.cn/post/7441116826623393829
收起阅读 »

从 DeepSeek 看25年前端的一个小趋势

大家好,我卡颂,专注于AI助力程序员转型(阅读我的更多思考) 最近DeepSeek R1爆火。有多火呢?连我爷爷都用上了,还研究起提示词工程来了。 大模型不断发展对我们前端工程师有什么长远影响呢?本文聊聊25年前端会有的一个小趋势。 模型进步的影响 像Deep...
继续阅读 »

大家好,我卡颂,专注于AI助力程序员转型阅读我的更多思考


最近DeepSeek R1爆火。有多火呢?连我爷爷都用上了,还研究起提示词工程来了。


大模型不断发展对我们前端工程师有什么长远影响呢?本文聊聊25年前端会有的一个小趋势。


模型进步的影响


DeepSeek R1这样的推理模型和一般语言模型(类似Claude SonnetGPT-4oDeepSeek-V3)有什么区别呢?


简单来说,推理模型的特点是:推理能力强,但速度慢、消耗高


他比较适合的场景比如:



  • Meta Prompting(让推理模型生成或修改给一般语言模型用的提示词

  • 路径规划


等等


这些应用场景主要利好AI Agent


再加上一般语言模型在生成效果、token上下文长度上持续提升。可以预见,类似Cursor Composer Agent这样的AI Agent在25年能力会持续提升,直到成为开发标配。


这会给前端工程师带来什么进一步影响呢?


一种抽象的理解


我们可以将AI Agent抽象得理解为应用压缩算法,什么意思呢?


Cursor Composer Agent举例:



我们传入:



  • 描述应用状态的提示词

  • 描述应用结构的应用截图


AI Agent帮我们生成应用代码。



同样,也能反过来,让AI Agent根据应用代码帮我们生成描述应用的提示词



从左到右可以看作是解压算法,从右往左可以看作是压缩算法


就像图片的压缩算法存在失真,基于AI Agent抽象的应用压缩算法也存在失真,也就是生成的效果不理想


随着上文提到的AI Agent能力提高(背后是模型能力提高、工程化的完善),应用压缩算法的失真率会越来越低。


这会带来什么进一步的影响呢?


对开发的影响


如果提示词(经过AI Agent)就能准确表达想要的代码效果,那会有越来越多原本需要用代码表达的东西被用提示词表达。


比如,21st.dev的组件不是通过npm,而是通过提示词引入。


相当于将引入组件的流程从:开发者 -> 代码


变成了:开发者 -> 提示词 -> AI Agent -> 代码



再比如,CopyCoder是一款上传应用截图,自动生成应用提示词的应用。


当你上传应用截图后,他会为你生成多个提示词文件。


其中.setup描述AI Agent需要执行的步骤,其他文件是描述应用实现细节的结构化提示词



这个过程相当于根据应用截图,将应用压缩为提示词


很自然的,反过来我们就能用AI Agent将这段提示词重新解压为应用代码。


这个过程在25年会越来越丝滑。


这会造成的进一步影响是:越来越多前端开发场景会被提炼为标准化的提示词,比如:



  • 后台管理系统

  • 官网

  • 活动页


前端开发的日常编码工作会越来越多被上述流程取代。


你可能会说,当前AI生成的代码效果还不是很好。


但请注意,我们谈的是趋势。当你日复一日做着同样的业务时,你的硅基对手正在每年大跨步进步。


总结


随着基础模型能力提高,以及工程化完善,AI Agent在25年会逐渐成为开发标配。


作为应用开发者(而不是算法工程师),我们可以将AI Agent抽象得理解为应用压缩算法


随着时间推移,这套压缩算法的失真率会越来越低。


届时,会有越来越多原本需要用代码表达的东西被用提示词表达。


这对前端工程师来说,既是机遇也是挑战。


作者:魔术师卡颂
来源:juejin.cn/post/7468323178931879972
收起阅读 »

三个请求,怎么实现a、b先发送,c最后发送

web
方案一:使用 Promise.all 控制并发 最直接的方法是使用Promise.all并行处理 A 和 B,待两者都完成后再发送 C。 async function fetchData() { try { // 同时发送请求A和请求B c...
继续阅读 »

方案一:使用 Promise.all 控制并发


最直接的方法是使用Promise.all并行处理 A 和 B,待两者都完成后再发送 C。


async function fetchData() {
try {
// 同时发送请求A和请求B
const [resultA, resultB] = await Promise.all([
fetchRequestA(), // 假设这是你的请求A函数
fetchRequestB() // 假设这是你的请求B函数
]);

// 请求A和B都完成后,发送请求C
const resultC = await fetchRequestC(resultA, resultB); // 请求C可能依赖A和B的结果

return resultC;
} catch (error) {
console.error('请求失败:', error);
throw error;
}
}

优点:实现简单,代码清晰

缺点:如果请求 C 不依赖 A 和 B 的结果,这种方式会增加不必要的等待时间


方案二:手动管理 Promise 并发


如果请求 C 不依赖 A 和 B 的结果,可以让 C 在 A 和 B 开始后立即发送,但在 A 和 B 都完成后再处理 C 的结果。


async function fetchData() {
try {
// 立即发送请求A、B、C
const promiseA = fetchRequestA();
const promiseB = fetchRequestB();
const promiseC = fetchRequestC();

// 等待A和B完成(不等待C)
const [resultA, resultB] = await Promise.all([promiseA, promiseB]);

// 此时A和B已完成,获取C的结果(无论C是否已完成)
const resultC = await promiseC;

return { resultA, resultB, resultC };
} catch (error) {
console.error('请求失败:', error);
throw error;
}
}

优点:C 的执行不会被 A 和 B 阻塞,适合 C 不依赖 A、B 结果的场景

缺点:代码复杂度稍高,需要确保 C 的处理逻辑确实不需要 A 和 B 的结果


方案三:使用自定义并发控制器


对于更复杂的并发控制需求,可以封装一个通用的并发控制器。


class RequestController {
constructor() {
this.runningCount = 0;
this.maxConcurrency = 2; // 最大并发数
this.queue = [];
}

async addRequest(requestFn) {
// 如果达到最大并发数,将请求放入队列等待
if (this.runningCount >= this.maxConcurrency) {
await new Promise(resolve => this.queue.push(resolve));
}

this.runningCount++;

try {
// 执行请求
const result = await requestFn();
return result;
} finally {
// 请求完成,减少并发计数
this.runningCount--;

// 如果队列中有等待的请求,取出一个继续执行
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
}
}
}
}

// 使用示例
async function fetchData() {
const controller = new RequestController();

// 同时发送A和B(受并发数限制)
const promiseA = controller.addRequest(fetchRequestA);
const promiseB = controller.addRequest(fetchRequestB);

// 等待A和B完成
await Promise.all([promiseA, promiseB]);

// 发送请求C
const resultC = await fetchRequestC();

return resultC;
}

优点:灵活控制并发数,适用于更复杂的场景

缺点:需要额外的代码实现,适合作为工具类复用


选择建议



  • 如果 C 依赖 A 和 B 的结果,推荐方案一

  • 如果 C 不依赖 A 和 B 的结果,但希望 A 和 B 先完成,推荐方案二

  • 如果需要更复杂的并发控制,推荐方案三


作者:Sparkxuan
来源:juejin.cn/post/7513069939974225957
收起阅读 »

优雅!用了这两款插件,我成了整个公司代码写得最规范的码农

同事:你的代码写的不行啊,不够规范啊。 我:我写的代码怎么可能不规范,不要胡说。 于是同事打开我的 IDEA ,安装了一个插件,然后执行了一下,规范不规范,看报告吧。 这可怎么是好,这玩意竟然给我挑出来这么多问题,到底靠谱不。 同事潇洒的走掉了,只留下我在...
继续阅读 »

同事:你的代码写的不行啊,不够规范啊。



我:我写的代码怎么可能不规范,不要胡说。


于是同事打开我的 IDEA ,安装了一个插件,然后执行了一下,规范不规范,看报告吧。



这可怎么是好,这玩意竟然给我挑出来这么多问题,到底靠谱不。


同事潇洒的走掉了,只留下我在座位上盯着屏幕惊慌失措。我仔细的查看了这个报告的每一项,越看越觉得这插件指出的问题有道理,果然是我大意了,竟然还给我挑出一个 bug 来。



这是什么插件,review 代码无敌了。



这个插件就是 SonarLint,官网的 Slogan 是 clean code begins in your IDE with {SonarLint}。


作为一个程序员,我们当然希望自己写的代码无懈可击了,但是由于种种原因,有一些问题甚至bug都无法避免,尤其是刚接触开发不久的同学,也有很多有着多年开发经验的程序员同样会有一些不好的代码习惯。


代码质量和代码规范首先肯定是靠程序员自身的水平和素养决定的,但是提高水平的是需要方法的,方法就有很多了,比如参考大厂的规范和代码、比如有大佬带着,剩下的就靠平时的一点点积累了,而一些好用的插件能够时时刻刻提醒我们什么是好的代码规范,什么是好的代码。


SonarLint 就是这样一款好用的插件,它可以实时帮我们 review代码,甚至可以发现代码中潜在的问题并提供解决方案。


SonarLint 使用静态代码分析技术来检测代码中的常见错误和漏洞。例如,它可以检测空指针引用、类型转换错误、重复代码和逻辑错误等。这些都是常见的问题,但是有时候很难发现。使用 SonarLint 插件,可以在编写代码的同时发现这些问题,并及时纠正它们,这有助于避免这些问题影响应用程序的稳定性。


比如下面这段代码没有结束循环的条件设置,SonarLint 就给出提示了,有强迫症的能受的了这红下划线在这儿?



SonarLint 插件可以帮助我提高代码的可读性。代码应该易于阅读和理解,这有助于其他开发人员更轻松地维护和修改代码。SonarLint 插件可以检测代码中的代码坏味道,例如不必要的注释、过长的函数和变量名不具有描述性等等。通过使用 SonarLint 插件,可以更好地了解如何编写清晰、简洁和易于理解的代码。


例如下面这个名称为 hello_world的静态 final变量,SonarLint 给出了两项建议。



  1. 因为变量没有被使用过,建议移除;

  2. 静态不可变变量名称不符合规范;



SonarLint 插件可以帮助我遵循最佳实践和标准。编写符合标准和最佳实践的代码可以确保应用程序的质量和可靠性。SonarLint 插件可以检测代码中的违反规则的地方,例如不安全的类型转换、未使用的变量和方法、不正确的异常处理等等。通过使用 SonarLint 插件,可以学习如何编写符合最佳实践和标准的代码,并使代码更加健壮和可靠。


例如下面的异常抛出方式,直接抛出了 Exception,然后 SonarLint 建议不要使用 Exception,而是自定义一个异常,自定义的异常可能让人直观的看出这个异常是干什么的,而不是 Exception基本类型导出传递。



安装 SonarLint


可以直接打开 IDEA 设置 -> Plugins,在 MarketPlace中搜索SonarLint,直接安装就可以。



还可以直接在官网下载,打开页面http://www.sonarsource.com/products/so… EXPLORE即可到下载页面去下载了。虽然我们只是在 IDEA 中使用,但是它不只支持 Java 、不只支持 IDEA ,还支持 Python、PHP等众多语言,以及 Visual Studio 、VS Code 等众多 IDE。



在 IDEA 中使用


SonarLint 插件安装好之后,默认就开启了实时分析的功能,就跟智能提示的功能一样,随着你噼里啪啦的敲键盘,SonarLint插件就默默的进行分析,一旦发现问题就会以红框、红波浪线、黄波浪线的方式提示。


当然你也可以在某一文件中点击右键,也可在项目根目录点击右键,在弹出菜单中点击Analyze with SonarLint,对当前文件或整个项目进行分析。



分析结束后,会生成分析报告。



左侧是对各个文件的分析结果,右侧是对这个问题的建议和修改示例。


SonarLint 对问题分成了三种类型


类型说明Bug代码中的 bug,影响程序运行Vulnerability漏洞,可能被作为攻击入口Code smell代码意味,可能影响代码可维护性


问题按照严重程度分为5类


严重性说明BLOCKER已经影响程序正常运行了,不改不行CRITICAL可能会影响程序运行,可能威胁程序安全,一般也是不改不行MAJOR代码质量问题,但是比较严重MINOR同样是代码质量问题,但是严重程度较低INFO一些友好的建议


SonarQube


SonarLint 是在 IDE 层面进行分析的插件,另外还可以使用 SonarQube功能,它以一个 web 的形式展现,可以为整个开发团队的项目提供一个web可视化的效果。并且可以和 CI\CD 等部署工具集成,在发版前提供代码分析。



SonarQube是一个 Java 项目,你可以在官网下载项目本地启动,也可以以 docker 的方式启动。之后可以在 IDEA 中配置全局 SonarQube配置。



也可以在 SonarQube web 中单独配置一个项目,创建好项目后,直接将 mvn 命令在待分析的项目中执行,即可生成对应项目的分析报告,然后在 SonarQube web 中查看。



5


对于绝大多数开发者和开发团队来说,SonarQube 其实是没有必要的,只要我们每个人都解决了 IDE 中 SonarLint 给出的建议,当然最终的代码质量就是符合标准的。


阿里 Java 规约插件


每一个开发团队都有团队内部的代码规范,比如变量命名、注释格式、以及各种类库的使用方式等等。阿里一直在更新 Java 版的阿里巴巴开发者手册,有什么泰山版、终极版,想必各位都听过吧,里面的规约如果开发者都能遵守,那别人恐怕再没办法 diss 你的代码不规范了。


对应这个开发手册的语言层面的规范,阿里也出了一款 IDEA 插件,叫做 Alibaba Java Coding Guidelines,可以在插件商店直接下载。



比如前面说的那个 hello_world变量名,插件直接提示「修正为以下划线分隔的大写模式」。



再比如一些注释上的提示,不建议使用行尾注释。



image-20230314165107639


还有,比如对线程池的使用,有根据规范建议的内容,建议自己定义核心线程数和最大线程数等参数,不建议使用 Excutors工具类。



有了这俩插件,看谁还能说我代码写的不规范了。


作者:码小凡
来源:juejin.cn/post/7260314364876931131
收起阅读 »

🤡什么鬼?两行代码就能适应任何屏幕?

web
你可能想不到,只用两行 CSS,就能让你的卡片、图片、内容块自动适应各种屏幕宽度,彻底摆脱复杂的媒体查询! 秘诀就是 CSS Grid 的 auto-fill 和 auto-fit。 马上教你用!✨ 🧩 基础概念 假设你有这样一个需求: 一排展示很多卡片...
继续阅读 »

你可能想不到,只用两行 CSS,就能让你的卡片、图片、内容块自动适应各种屏幕宽度,彻底摆脱复杂的媒体查询!
秘诀就是 CSS Grid 的 auto-fillauto-fit


20250428112254_rec_.gif


马上教你用!✨




🧩 基础概念


假设你有这样一个需求:



  • 一排展示很多卡片

  • 每个卡片最小宽度 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 个卡片拉伸填满整行。


20250428151427_rec_.gif


👇 Demo 代码:


<h2>auto-fill</h2>
<div class="grid-fill">
<div>item1</div>
<div>item2</div>
<div>item3</div>
<div>item4</div>
<div>item5</div>
</div>

<h2>auto-fit</h2>
<div class="grid-fit">
<div>item1</div>
<div>item2</div>
<div>item3</div>
<div>item4</div>
<div>item5</div>
</div>

.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…


image.png




🎯 什么时候用 auto-fill,什么时候用 auto-fit?



  • 希望每行“有多少内容就撑多宽”,用 auto-fit

    适合卡片式布局、相册、响应式按钮等。

  • 希望“固定列数/有占位”,用 auto-fill

    比如表格、日历,或者你希望网格始终对齐,即使内容不满。




📝 总结


属性空轨道内容拉伸适用场景
auto-fill保留固定列数、占位网格
auto-fit折叠流式布局、拉伸填充



🌟 小结



  • auto-fill 更像“占位”,auto-fit 更像“自适应”

  • 推荐大部分响应式卡片用 auto-fit

  • 善用 minmax 配合,让列宽自适应得更自然




只需两行代码,你的页面就能优雅适配各种屏幕!

觉得有用就点赞收藏吧,更多前端干货持续更新中!🚀✨


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

体验了无业一年和远程工作一个月, 你猜有想象中那么爽吗

失业了一年多后, 有了一个特殊的契机, 远程工作到现在一个月了, 有很多感触想聊一下. 在家工作有想象中那么爽吗? 爽, 但一些情况与想象中的并不一样. 不用上下班真的节约时间吗? 真的, 而且节约的不只是上下班的时间. 还有穿衣服, 理书包, 下楼, 上楼,...
继续阅读 »

失业了一年多后, 有了一个特殊的契机, 远程工作到现在一个月了, 有很多感触想聊一下.


在家工作有想象中那么爽吗?


爽, 但一些情况与想象中的并不一样.


不用上下班真的节约时间吗?


真的, 而且节约的不只是上下班的时间.


还有穿衣服, 理书包, 下楼, 上楼, 到了公司里先休息下, 和"等待"的时间.


这些时间其实是非常长的, 特别是一些"等待"时间, 比如打算8:40出门, 但准备好一切后是8:20, 人就会自动进入发呆时间. 也许上班路程就40分钟, "等待"就浪费20分钟了.


在家工作, 电脑就在床头, 拿起电脑就开干, 热启动, 非常节省时间.


在家工作可以兼顾陪伴家人吗?


完全不可以.


在真正工作的状态, 家里人跟我说话, 我都是木头状态.


并且在工作不那么紧急的时候, 我还要花额外的精力来拒绝家人的交互 (因为不好意思直接拒绝), 这点上, 在家工作对我来说是缺点的.


在家里有紧急事情的情况下, 在家工作是好的, 但我现在的状态是不需要的.


在一些需要跨部门交流的事情上, 还是在公司里, 拿着电脑去别人工位盯着效率会高很多.


不干活一年多爽不爽?


爽, 懒惰的爽, 很多情况与想象的完全不同.


不用干这些业务有更多时间学习?


只多了一点时间.


如果具体举例, 假设工作时: 工作6小时, 学习2小时. 失业时: 刷手机5小时, 学习3小时.


而我本以为可能是学习6小时, 娱乐2小时.


因为家里有小孩, 我会去图书馆学习, 大概是12点去, 最多3点就忍不住回家了.


不工作起床也要晚2~3小时.


反而在上班的时候, "不能干别的, 但工作做完了", 是最佳的学习状态.


(更别说弹琴了, 真是可笑, 本以为可以有时间弹琴了)


不上班可以到处玩了?


是的. 非常爽.


并且可以吃很多"工作日专享", 比如我吃了好多次 90 快的一绪寿喜烧.


刚失业买了新的摩托车, 出去玩了很多次.


虽然也有疲倦期, 但是恢复很快.


如果不是小孩子和家里事情的关系, 我可能会去很远的地方.


不上班很开心吗?


认真思考后, 我认为分为2个方面.


第一是钱的方面. 长期没工作可能导致从行业除名. 这样算会失去的钱是非常多的, 会导致焦虑.


第二是成就感. 在工作和学习中, 在解决了麻烦的问题后是有成就感的, 而失业的状态几乎就是慵懒.


抛开长期不谈, 这种状态的快乐上限是比工作底的.


这是很客观的说了"不上班的缺点".


而结合这2个缺点, 与一些优点(可以出去玩, 可以陪伴家人)后. 结论还是非常复杂的.


但总的来说, 还是想有个逼班上的.


现状和未来的几个月


现状


现在的工作是单人负责一个前端项目, 责任清晰, 干完活也没别的可干, 做得好坏大家都知道是我.


我认为是个很好的感觉, "一个和尚挑水喝".


虽然我是随时工作的状态, 睡醒拿起旁边的电脑就干, 但是没觉得不开心.


和以前公司那种分散责任状态不同.


公司里即使是一人一项目, 空的时候也会被借到其他组, 是真的很搓.


我把活干完, 就是自己的时间. 当然, 不小的概率是失去这份短工.


人的状态


这里要插入下自己的感受.


觉得"人的状态"很神奇, 就是工作了, 在没有任何痛苦的情况下, 人的行为改变非常大.


因为我的骨折和工作, 周围人的行为改变也非常大, 本来"做不来"的事, 都可以做了.


自己都不能理解自己的另一个状态, 更何况他人, 所以不要评价任何他人的行为了吧.


如果觉得自己做事情都很难, 可以尝试给自己找机会换个状态.


心理准备


其实是很开心的工作, 但合同里只要3天通知我, 我就可以失业了.


因为找了一年工作, 知道找工作的艰难. 所以对待这次工作的每个事情, 我都是很认真的.


很忙的一个月过去, 200多个commit, 解决了80多个jira.


其中也记录了一些麻烦的问题, 是 react 的, 会根据后面的生活状态, 总结一下.


作者:nujnewnehc
来源:juejin.cn/post/7498968249548554266
收起阅读 »

🚀惊了,这个国产软件居然这么牛,比 uniapp 还全能❤️

web
最近跟同事闲聊,大家都在吐槽一个问题: ! App 是越做越像平台了,但开发却越做越痛苦了。 你想加个活动页,产品说今晚上线; 你想做个业务扩展,运营说要不你再写个低代码工具; 你想适配鸿蒙,领导说最好做个 React Native 得了; 同事活成了“加班工...
继续阅读 »

最近跟同事闲聊,大家都在吐槽一个问题:

App 是越做越像平台了,但开发却越做越痛苦了。


你想加个活动页,产品说今晚上线;

你想做个业务扩展,运营说要不你再写个低代码工具;

你想适配鸿蒙,领导说最好做个 React Native 得了;


同事活成了“加班工具人”,App 也做成了臃肿的 “功能集成器”。


难道开发一个通用的 App ,就非得这么累吗?


于是,我们试着去找更轻、更灵活的解决方案。


我们只是想做一个“活动页托管方案”,不想每次上线都发版,更不想因为临时需求牵扯整个开发团队。


但随着调研的深入,我们发现这种痛点其实根本不是“活动页”本身,而是:App 缺乏一个**“包容性很强的容器”**。


比如:



  • 新功能不用频繁发版;

  • 能复用现有页面或者组件;

  • 可以独立上线,不干扰主应用。


我们对比了几个方向:



  • WebView + H5:快是快,但弱得可怕,尤其是 JSBridge 管理地狱,体验不佳;

  • 低代码平台:适合特定场景,但定制性不足,复杂页面性能堪忧;

  • RN/Flutter 微模块化:维护成本太高,涉及太多客户端改动。


直到我在调研中遇到了 FinClip,才意识到这事完全可以换个方式。


让人眼前一亮的 FinClip


FinClip 是什么?


一句话说完:把小程序能力,通用化、标准化,塞进任何 App。


从技术架构来说,FinClip 提供的是一个极其轻量的小程序容器 SDK(3MB都不到),可以直接嵌进你的 iOSAndroidHarmonyOS App,甚至 React NativeFluttermacOS、车机都能跑。


强大的能力


开发者只要写一套小程序代码,放进去就能运行。不用重新适配底层系统,也不用改框架结构。


而且它兼容微信/支付宝/抖音小程序语法,意味着你过去写的项目,可能几乎零改动就能跑起来。


于是,我们立刻拉了群,软磨硬泡,搞来了二组同事开发的活动页项目,


这个是开源的活动页


需要的同学请戳这里:github.com/FernAbby/H5…


导入之后


然后通过 FinClip Studio 打包上传,再嵌入 App


FinClip Studio,真的有点香


讲真,刚开始用 FinClip Studio,我也做好了“将就一下”的心理准备。


结果没想到是真香警告。


首先,新建项目一键生成模板,跟微信小程序开发工具 99% 像;


创建工程


你也可以和我一样选择导入已有的项目,


导入之后


其次,模拟器支持多终端调试,拖拉缩放,全程无需真机;


另外,发布打包一条龙服务,你只需要上传至云端后台:


上传


输入相关上传信息:


上传信息


等待上传成功即可!


上传成功


后台是平台运营的“指挥中心”


接下来的重头戏,需要我们登陆后台系统,


一个超级应用不是靠开发者单打独斗,而是靠多个角色协同。FinClip 的后台做得非常细腻,功能齐全,不管是开发还是运维同学,都可以轻松驾驭!


首页后台


小程序管理模块,不仅可以新建、管理前面上传的小程序,还可以体验预览版、发布审核;


首先,在隐私设置栏目里设置隐私保护指引:


配置隐私设置


然后我们就可以配置审核版本或者体验版本了!


审核流程


体验发布


接着我们就可以直接跳转到待办中心通过审核!


通过审核


除此之外,常用的灰度发布、权限范围、自动上架全都支持;


小程序详情


数据分析清晰易读,不需要 BI 工具也能看懂;


数据看板


页面数据


让你不再为如何做好运维而发愁!


用了一周的真实感受


流程


使用一周多了,整体的流程是这样的:



  1. 本地写代码,IDE 模拟器预览;

  2. 上传代码,后台提交审核;

  3. 设置灰度策略,用户扫码体验;

  4. 最终发布上线。


优点


我们没改动原生代码,甚至没有重新接入任何 SDK,只是增加一个容器模块 + 几行配置。


团队有个原来的 RN 老项目,直接用 FinClip 的容器跑起来,居然都不用重写,兼容度真的惊人。


缺点


但是缺点也有:


比如,导入已有项目会进行检测,并且明确的告知用户,其实可以后台默认执行,用户体验会更好!


导入


检测过程


另外最主要的是,后台和编辑器的登陆状态是临时的,不会长期保持!每次登陆挺麻烦的


彩蛋


首先,FinClip 贴心的内置了 AI 助手,你使用过程遇到的任何问题都可以在这里找到答案!


内置的 AI 助手


最重要的是,FinClip 提供了基于接口的 AI 能力,可以通过 RAG 技术为小程序注入了智能化能力,涵盖内容生成、上下文交互、数据分析等多方面功能。


这不仅提升了用户体验,也为开发者提供了便捷的 AI 集成工具,进一步增强了 FinClip 生态的竞争力!


基于 RAG 的 AI 能力


总结


如果再给我造一次 App 的机会,我一定毫不犹豫地选择 FinClip


当我们从“做功能”切换到“建生态”,思路就会完全不一样:



  • App 不再是“巨石应用”,而是一个个业务模块的拼图

  • 小程序就像“微服务 UI 化”,能独立更新、上线、下架

  • 技术架构也从“一体化耦合”变成“解耦 + 动态加载”


FinClip 帮助开发者从“重复搬砖” 变成 “生态平台管理员”!


如果你也有和我一样的困惑,你也可以试试:



  • 把一个已有的活动页,用 FinClip 打包成小程序;

  • 嵌进你现有 App 中,再用 FinClip Studio 发布版本;

  • 后台配置白名单,手机扫码预览。


1 天内,你就能体验一把“做平台”的感觉。


时代正在变化。我们不该再为“发布一个功能”耗尽精力,而应该把更多时间留给真正重要的东西 —— 创新、体验、增长。


FinClip 不只是工具,更是重构开发者角色的机会。


你准备好了吗?


作者:萌萌哒草头将军
来源:juejin.cn/post/7493798605658816553
收起阅读 »

老板让我弄3d,从0学建模😂

web
blender导出的轿车🚗.glb: 最近因为有需求,不得不搞起3d这一块,说到3d,以为是学一个threejs就够了,结果是blender也要学。 blender可能有的前端开发或者后端开发没了解过,简单得说就是捏3d模型的这么一个东西。 经常听人家说...
继续阅读 »

blender导出的轿车🚗.glb


1.gif



最近因为有需求,不得不搞起3d这一块,说到3d,以为是学一个threejs就够了,结果是blender也要学。



blender可能有的前端开发或者后端开发没了解过,简单得说就是捏3d模型的这么一个东西。


经常听人家说建模建模,就是这个东西来着。


下载下来就是这么一个软件👇🏻:


image.png


通过对blender的学习可以做很多东西,那blender究竟可以做什么。要想知道能做什么,就要先知道blender是个啥。


blender是一个永久开源且免费的三维创作软件,支持建模雕刻骨骼装配、动画模拟实时渲染合成和运动跟踪等等三维创作


推荐一下大家一些现成的模型网站或插件或者材质贴图等:



🔴 入门


⭕︎ 课程内容与目标



  • 学习基本设置、模型变换、建模、UV编辑、材质与贴图、渲染等核心流程

  • 掌握独立制作初级3D模型的能力




核心学习点


观察 -> 辅助建模,提供更好的视觉策略

变换 -> 基本变化,实现移动、旋转、复制图像等移动策略

建模 -> 重塑多边形,杜绝线建模障碍

修改器 -> 提供更便捷的迭代可能性

uv -> 纹理图层的映射到表面的方法

材质 -> 基本材质的属性设置,只需参照别人设置的方式即可

渲染 -> 了解基本的渲染设置,灯光




不要被界面中无关的设置项影响。每个三维软件都是复杂的,但是目的只是为了满足不同人的不同需求。使用时,只需要按照方法简单设置一些需要的参数即可。别的参数默认即可。


如果你看文字太多,觉得烦躁,那就记得:没有太多欲望的话,我们目的就是实现建模,表现它的材质,把它渲染出来就可以了。三点:建模->材质->渲染


⭕︎ Blender核心优势



  1. 轻量化设计:相比传统3D软件更轻便快捷

  2. 开源免费:完全免费且持续更新

  3. 社区生态

    • 开放社区支持

    • 原生支持GLB等现代格式

    • 丰富插件生态



  4. 发展前景:在开源3D工具中处于领先地位


⭕︎ 基础设置指南



  1. 软件下载

    官方下载链接:http://www.blender.org/download/

  2. 中文设置

    路径:偏好设置 > 界面 > 翻译 > 勾选"中文(简体)"

  3. 默认间隔多久保存 : 可设置。不怕断电、崩溃、找不到正在做而没有保存到文件。


image.png


⭕︎ 视口操作


快捷键ESC下面的波浪键,英文模式下:
image.png


flowchart TD
B[基本视图控制]

B --> B1[旋转视图]
B1 -->|操作方式| 鼠标中键
B1 -->|效果| 围绕视点中心旋转

B --> B2[平移视图]
B2 -->|操作方式| Shift+鼠标中键
B2 -->|效果| 平移观察视角

B --> B3[缩放视图]
B3 -->|操作方式1| 滚动鼠标中键
B3 -->|操作方式2| Shift+B框选缩放

B --> B4[快捷键ESC下面的波浪键,英文模式下]

style B fill:#4b8bf5

🔴 基础操作


⭕︎ 语言的设置


image.png


image.png


⭕︎ 场景设置单位


image.png


⭕︎ 文件栏


文件 - 编辑 - 渲染 - 窗口 - 帮助


image.png


⭕︎ 工作台


image.png


比如说uv编辑器:


image.png


比如说贴图


image.png


比如说着色器


image.png


比如编辑多边形的工具台:


image.png


image.png


⭕︎ 快捷键操作


按住鼠标中键 -> 旋转


按住鼠标中键 + shift -> 平移


鼠标中键滚动 -> 放大缩小


⭕︎ 不同视口查看


切换四格图:


image.png


image.png


shift+a创建一个网格:


image.png


ctrl+alt+q切换成四格图,同样再按一遍就是退出四格图。


image.png


如果需要查看更多的视图,也可以按一下Tab上面的波浪键,像这样:


image.png


(按住左键长按选中某个物体,可以单独查看选中物体的视图。)


接下来看一下这些视图的小图标,具体代表什么,如果有不太会的(大家可以鼠标悬浮在图标上面,它会给出具体的提示,然后大家可以每个小图标点一下试一试,不用害怕软件会崩盘,怎么弄软件都不会出事,自己可以多研究研究,即使崩了也可以重下载,放心大胆去试):
image.png


一个是叠加层:可以添加线框,统计信息等辅助观察。


一个是视图着色方式。


🔴 基本体


点击文件->新建->常规,之前的文件看需求看看要不要保存。


默认会出现一个立方体,我们按x键,它会提示我们要删除这个物体吗?我们先删掉这个立方体。


image.png


上面我们说过,shift+a可以弹出一个面板:


image.png


这样子,我们先创建一个立方体网格+立方体


游标(在游标模式下,可以任意拖动游标):


image.png


或者在选择模式下,按住shift+右键也是可以拖动游标的。


拖动游标,然后去新建一个立方体,我们会发现物体会创建在以游标为中心的位置。所以我们去创建一个物体,首先先要把游标的位置给设好,创新物体就会直接在游标那个位置了。


物体的设置面板


image.png


选择某些或者某个物体,按住左键进行框选即可。


有时候选择的时候会发现框选住的,有一个是红的,一个是黄的。黄的是后加选上的,可以作为移动物体这样子。
image.png


a键就是全选。ctrl + i就是反选。shift是加选。


🔴 基本变换



  1. 基础操作

    鼠标中键旋转视图

    shiftA:新建立方体

    shift+中键上下,左右移动视图

    鼠标滚轮放大缩小视图

    G:移动物体 GX/GY/GZ=(沿着x、y、z轴移动)

    R:旋转物体 RX/RY/RZ=(沿着x、y、z轴旋转)

    S:缩放物体 SX/SY/SZ=(沿着x、y、z轴缩放)

  2. 设置界面布局,保存窗口布局.

  3. 小键盘“0”摄像机视角

  4. “N”收放右边菜单栏


纸上得来终觉浅,我们还是得多动手去尝试尝试,就算是做一个小物件小物体,前期也会觉得会有满满的成就感,用某个操作键的知识特定得做一个小小练习。


⭕︎ 对齐、捕捉、复制


选中圆锥体,然后按shift选中平面。


那么圆锥体就是选中项,然后平面就是活动项


image.png


image.png


圆锥体相对于平面这个活动体Z轴对齐:
image.png


吸附相关:
image.png


shift+D复制选中物体。ctrl+c + ctrl+v也可以复制粘贴物体。


作为一款程序员或者建筑设计行业的一款建模软件来讲,跟我们在学校里学的photoshop一样,需要投入主动学习成本,还有一些习惯上的成本比如一些快捷键取代图形化界面是非常有必要的。


到最后再结合去做three.js或者cesium模型加载展示、材质处理和动画。


image.png


🔴 总结


当然,除了blender,还有很多优秀的流行的3d渲染软件:



  • blender

  • 3dx Max

  • Maya

  • Cinema4D

  • KeyShot


一些室内设计师用的: cad酷家乐(要钱)


我们这篇讲的是blenderthreejs的结合。就是说blender负责建模和导出threejs负责加载和交互,去做出交互式3d网页应用


⭕︎ 流程


⭕︎ 1、blender导出:


blender中创建模型,然后导出格式为.glb(二进制格式,包含材质、动画等)或.gltf


image.png


image.png


⭕︎ 2、加载模型并交互:


// 导出默认函数,用于创建城市场景
export default function createCity() {
// 创建GLTF加载器实例,用于加载.glb/.gltf格式的3D模型
const gltfLoader = new GLTFLoader();

// 加载城市模型文件
gltfLoader.load("./model/city.glb", (gltf) => {
// 遍历模型中的所有子对象
gltf.scene.traverse((item) => {
// 只处理网格类型(Mesh)的对象
if (item.type == "Mesh") {
console.log(item); // 调试用,打印网格信息

// 创建新的基础材质并设置为深蓝色
const cityMaterial = new THREE.MeshBasicMaterial({
color: new THREE.Color(0x0c0e33), // 十六进制颜色值
});
item.material = cityMaterial; // 应用新材质到当前网格

// 调用自定义函数修改城市材质(函数实现未展示)
modifyCityMaterial(item);

// 特殊处理名为"Layerbuildings"的网格
if (item.name == "Layerbuildings") {
// 使用MeshLine库创建线框效果(需额外引入MeshLine库)
const meshLine = new MeshLine(item.geometry);
const size = item.scale.x; // 获取原始缩放值
meshLine.mesh.scale.set(size, size, size); // 保持原始比例
scene.add(meshLine.mesh); // 将线框添加到场景
}
}
});

// 将整个模型添加到场景中
scene.add(gltf.scene);

// 以下是被注释掉的可选效果,可根据需要取消注释:

// 添加普通飞线效果
const flyLine = new FlyLine();
scene.add(flyLine.mesh);

// 添加着色器实现的飞线(性能更好)
const flyLineShader = new FlyLineShader();
scene.add(flyLineShader.mesh);

// 添加雷达扫描效果
const lightRadar = new LightRadar();
scene.add(lightRadar.mesh);

// 添加光墙效果
const lightWall = new LightWall();
scene.add(lightWall.mesh);

// 添加可交互的警告标识
const alarmSprite = new AlarmSprite();
scene.add(alarmSprite.mesh);
// 绑定点击事件
alarmSprite.onClick(function (e) {
console.log("警告", e); // 点击时触发
});
});
}

1.gif


作者:curdcv_po
来源:juejin.cn/post/7518932901699223592
收起阅读 »

前端佬们!塌房了!用过Element-Plus的进来~

web
原有的结论有问题,这是最新的弥补篇。如果您还没有看过,可以先看新篇提出的问题再看这个。如果您已看过,可以看下最新篇。希望不会给您带来困扰,随时接受大佬们的批评。 新篇戳这里。更崩溃!!感觉被偷家了!Element-plus组件测试的后续~ ----------...
继续阅读 »

原有的结论有问题,这是最新的弥补篇。如果您还没有看过,可以先看新篇提出的问题再看这个。如果您已看过,可以看下最新篇。希望不会给您带来困扰,随时接受大佬们的批评。


新篇戳这里。更崩溃!!感觉被偷家了!Element-plus组件测试的后续~


---------------------以下为原文---------------------------


进来着急的前端佬,我直接抛出结论吧!


Element-plus的组件,经过测验,如下组件存在内存泄漏。如下:



  • el-carousel

  • el-select + el-options

  • el-descriptions

  • el-tag

  • el-dialog

  • el-notification

  • el-loading

  • el-result

  • el-message

  • el-button

  • el-tabs

  • el-menu

  • el-popper


验证环境为:



Vue Version: 3.5.13

Element Plus Version: 2.9.7

Browser / OS: window 10 / Edge 134.0.3124.85 (正式版本) (64 位)

Build Tool: Webpack



不排查ElementUI也存在这个问题。


好了。接下来细细聊。


pcJpq404.gif


前因


为什么检测到这种问题?主要因为一个项目引用了Element-plus。然后,你懂的,买的人永远都会想要最好的,然后买的人就这么一顿狂点Web页面,看见内存占用飙到老高。


于是...前端佬都懂的,来活了。


7166eec470f04755a2f52fe819a62493.gif


排查


一开始我是不敢怀疑这种高star开源组件的。总以为自己是写的代码有问题。


详细代码就不贴了,主要用ElDialog组件,封装成一个命令式的Dialog组件,避免频繁的使用v-modal参数。


然后,就直接怀疑上这个组件了。


经过测试,果不其然,从关闭到销毁,会导致内存猛增,因为Dialog中有各种表单组件,一打开就创建了一大堆的Element元素。


image.png


精确定位,使用了FinalizationRegistry类追踪创建的Dialog实体,代码如下:


const finalizerRegistry = new FinalizationRegistry((heldValue) => {
console.log('Finalizing instance: ',heldValue);
});


// 在创建处监听
const heldValue = Symbol(`DialogCommandComponent_${Date.now()}`);
finalizerRegistry.register(this, heldValue);
console.log(`Constructed instance:`,heldValue);

发现一直没有Constructed instance销毁的信息输出。


随后,使用了Edge浏览器中的分离元素来打快照,步骤如下图。


image.png


经过反复的操作,然后点击主动垃圾回收,然后发现el-dialog的元素都会增加,基本确认无疑了。


但还是怀疑,会不会是Dialog中,引用的问题,导致元素一直没能销毁?所以,使用了纯纯的el-dialog来校验,同样的操作,既然如故。


a4.jpg


然后的然后,我使用了如下的代码,去校验其它组件是否存在同样的问题。代码如下:


<template>
<div>
<el-button @click="fn2">Reset</el-button>
</div>
<el-dialog v-model="model" destroy-on-close @closed="fn1" append-to-body v-if="destroyDialogModelValue"></el-dialog>
<el-button @click="fn0" v-if="!button" primse>Click</el-button>
<div class="weak" v-if="!button">xxx</div>
<el-input v-if="!button" />
<el-border v-if="!button" />
<el-select v-if="!button">
<el-option>1111</el-option>
</el-select>
<el-switch v-if="!button" />
<el-radio v-if="!button" />
<el-rate v-if="!button" />
<el-slider v-if="!button" />
<el-time-picker v-if="!button" />
<el-time-select v-if="!button" />
<el-transfer v-if="!button" />
<el-tree-select v-if="!button" />
<el-calendar v-if="!button" />
<el-card v-if="!button" />
<el-carousel height="150px" v-if="!button">
<el-carousel-item v-for="item in 4" :key="item">
<h3 class="small justify-center" text="2xl">{{ item }}</h3>
</el-carousel-item>
</el-carousel>
<el-descriptions title="User Info" v-if="!button">
<el-descriptions-item label="Username">kooriookami</el-descriptions-item>
</el-descriptions>
<el-table style="width: 100%" v-if="!button">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
</el-table>
<el-avatar v-if="!button" />
<el-pagination layout="prev, pager, next" :total="50" v-if="!button" />
<el-progress :percentage="50" v-if="!button" />
<el-result icon="success" title="Success Tip" sub-title="Please follow the instructions" v-if="!button">
<template #extra>
<el-button type="primary">Back</el-button>
</template>
</el-result>
<el-skeleton v-if="!button" />
<el-tag v-if="!button" />
<el-timeline v-if="!button" />
<el-tree v-if="!button" />
<el-avatar v-if="!button" />
<el-segmented size="large" v-if="!button" />
<el-dropdown v-if="!button">
<span class="el-dropdown-link">
Dropdown List
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Action 1</el-dropdown-item>
<el-dropdown-item>Action 2</el-dropdown-item>
<el-dropdown-item>Action 3</el-dropdown-item>
<el-dropdown-item disabled>Action 4</el-dropdown-item>
<el-dropdown-item divided>Action 5</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-menu class="el-menu-demo" mode="horizontal" v-if="!button">
<el-menu-item index="1">Processing Center</el-menu-item>
<el-sub-menu index="2">
<template #title>Workspace</template>
<el-menu-item index="2-1">item one</el-menu-item>
<el-menu-item index="2-2">item two</el-menu-item>
<el-menu-item index="2-3">item three</el-menu-item>
<el-sub-menu index="2-4">
<template #title>item four</template>
<el-menu-item index="2-4-1">item one</el-menu-item>
<el-menu-item index="2-4-2">item two</el-menu-item>
<el-menu-item index="2-4-3">item three</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="3" disabled>Info</el-menu-item>
<el-menu-item index="4">Orders</el-menu-item>
</el-menu>

<el-steps style="max-width: 600px" active="0" finish-status="success" v-if="!button">
<el-step title="Step 1" />
<el-step title="Step 2" />
<el-step title="Step 3" />
</el-steps>

<el-tabs class="demo-tabs" v-if="!button">
<el-tab-pane label="User" name="first">User</el-tab-pane>
<el-tab-pane label="Config" name="second">Config</el-tab-pane>
<el-tab-pane label="Role" name="third">Role</el-tab-pane>
<el-tab-pane label="Task" name="fourth">Task</el-tab-pane>
</el-tabs>

<el-alert title="Success alert" type="success" v-if="!button" />
<el-drawer title="I am the title" v-if="!button">
<span>Hi, there!</span>
</el-drawer>

<div v-loading="model" v-if="!button"></div>

<el-popconfirm confirm-button-text="Yes" cancel-button-text="No" icon-color="#626AEF"
title="Are you sure to delete this?" v-if="!button">
<template #reference>
<el-button>Delete</el-button>
</template>
</el-popconfirm>

<el-popover class="box-item" title="Title" content="Top Center prompts info" placement="top" v-if="!button">
<template #reference>
<div>top</div>
</template>
</el-popover>

<el-tooltip class="box-item" effect="dark" content="Top Left prompts info" placement="top-start" v-if="!button">
<div>top-start</div>
</el-tooltip>
</template>

<script setup>
import { ref } from "vue";
import { ElMessage, ElMessageBox, ElNotification } from "element-plus";

const model = ref(false);
const destroyDialogModelValue = ref(false);
const button = ref(false);

function fn0() {
model.value = true;
destroyDialogModelValue.value = true;
ElMessage("This is a message.");
ElMessageBox.alert("This is a message", "Title");
ElNotification({
title: "Title",
message: "This is a reminder",
});
}
function fn1() {
console.log("closed");
destroyDialogModelValue.value = false;
button.value = true;
}
function reset() {
model.value = false
}
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>


如上代码,进入页面后,点击click,然后关闭所有的弹窗。然后再次点击reset按钮,然后再次点击click,关闭所有弹窗。如此可以多操作几次。


就发现了开头的组件,都存在内存泄漏问题。


未能解决


有问题,当然首先看看别人有没有出现过。各种搜索就不说了,大掘金也搜过,在Element-plus的github仓里的Issues中找过,发现的办法基本无用。


以下是自己思考的几条路子:



  1. 有泄漏的,都手搓一个?

  2. Eldialog全局只用一到两个?

  3. 将所有路由,都打成一个单页面(html)。

  4. 改源码....


结尾


还是在这里,求助大佬,看以上思路是否有错,然后跪求orz解决办法。


自己后续如果解决对应一些问题,会即时和大家分享。


作者:大怪v
来源:juejin.cn/post/7485966905418760227
收起阅读 »

Vue实现一个“液态玻璃”效果登录卡片

web
Vue实现一个“液态玻璃”效果登录卡片 效果介绍 液态玻璃(Liquid Glass)是一种极具现代感的UI视觉风格,常见于高端网站和操作系统界面。它通过多层叠加、模糊、光泽、滤镜等技术,模拟出玻璃的通透、折射和高光质感。苹果的这次系统设计更新,带火了这一设计...
继续阅读 »

Vue实现一个“液态玻璃”效果登录卡片


效果介绍


液态玻璃(Liquid Glass)是一种极具现代感的UI视觉风格,常见于高端网站和操作系统界面。它通过多层叠加、模糊、光泽、滤镜等技术,模拟出玻璃的通透、折射和高光质感。苹果的这次系统设计更新,带火了这一设计效果,本教程将带你一步步实现一个带有3D灵动倾斜交互的液态玻璃登录卡片。


实际效果:


PixPin_2025-06-14_23-07-12.gif




技术原理解析


1. 多层叠加


液态玻璃效果的核心是多层视觉叠加:



  • 模糊层(blur):让背景内容变得虚化,产生玻璃的通透感。

  • 色调层(tint):为玻璃加上一层淡淡的色彩,提升质感。

  • 高光层(shine):模拟玻璃边缘的高光和内阴影,增强立体感。

  • SVG滤镜:通过 SVG 的 feTurbulencefeDisplacementMap,让玻璃表面产生微妙的扭曲和流动感。


2. 3D灵动倾斜


通过监听鼠标在卡片上的移动,动态计算并设置 transform: perspective(...) rotateX(...) rotateY(...),让卡片随鼠标灵动倾斜,增强交互体验。


3. 背景与环境


背景可以是渐变色,也可以是图片。玻璃卡片通过 backdrop-filter 与背景内容产生交互,形成真实的玻璃质感。




实现步骤详解


1. 结构搭建


<template>
<div class="login-container animated-background">
<!-- SVG滤镜库 -->
<svg style="display: none">...</svg>
<!-- 登录卡片 -->
<div
class="glass-component login-card"
ref="tiltCard"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<div class="glass-effect"></div>
<div class="glass-tint"></div>
<div class="glass-shine"></div>
<div class="glass-content">
<!-- 登录表单内容 -->
</div>
</div>
</div>
</template>

2. SVG滤镜实现液态扭曲


<svg style="display: none">
<filter id="glass-distortion" x="0%" y="0%" width="100%" height="100%" filterUnits="objectBoundingBox">
<feTurbulence type="fractalNoise" baseFrequency="0.001 0.005" numOctaves="1" seed="17" result="turbulence" />
<feComponentTransfer in="turbulence" result="mapped">
<feFuncR type="gamma" amplitude="1" exponent="10" offset="0.5" />
<feFuncG type="gamma" amplitude="0" exponent="1" offset="0" />
<feFuncB type="gamma" amplitude="0" exponent="1" offset="0.5" />
</feComponentTransfer>
<feGaussianBlur in="turbulence" stdDeviation="3" result="softMap" />
<feSpecularLighting in="softMap" surfaceScale="5" specularConstant="1" specularExponent="100" lighting-color="white" result="specLight">
<fePointLight x="-200" y="-200" z="300" />
</feSpecularLighting>
<feComposite in="specLight" operator="arithmetic" k1="0" k2="1" k3="1" k4="0" result="litImage" />
<feDisplacementMap in="SourceGraphic" in2="softMap" scale="200" xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>


  • 这段 SVG 代码必须放在页面结构内,供 CSS filter 调用。


3. 背景设置


.animated-background {
width: 100vw;
height: 100vh;
background-image: url('你的背景图片路径');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: fixed;
top: 0;
left: 0;
z-index: -1;
}


  • 建议用高质量渐变或壁纸,能更好衬托玻璃质感。


4. 卡片多层玻璃结构


.login-card {
width: 400px;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.10), 0 1.5px 6px 0 rgba(0,0,0,0.08);
background: transparent;
position: relative;
}
.glass-effect {
position: absolute;
inset: 0;
z-index: 0;
backdrop-filter: blur(5px);
filter: url(#glass-distortion);
isolation: isolate;
border-radius: 24px;
}
.glass-tint {
position: absolute;
inset: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.15);
border-radius: 24px;
}
.glass-shine {
position: absolute;
inset: 0;
z-index: 2;
border: 1px solid rgba(255, 255, 255, 0.13);
border-radius: 24px;
box-shadow:
inset 1px 1px 8px 0 rgba(255, 255, 255, 0.18),
inset -1px -1px 8px 0 rgba(255, 255, 255, 0.08);
pointer-events: none;
}
.glass-content {
position: relative;
z-index: 3;
padding: 2rem;
color: white;
}


  • 每一层都要有一致的 border-radius,才能保证圆角处无割裂。


5. 3D灵动倾斜交互


methods: {
handleMouseMove (e) {
const card = this.$refs.tiltCard
const rect = card.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const centerX = rect.width / 2
const centerY = rect.height / 2
const maxTilt = 18
const rotateY = ((x - centerX) / centerX) * maxTilt
const rotateX = -((y - centerY) / centerY) * maxTilt
card.style.transform = `perspective(600px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.03)`
},
handleMouseLeave () {
const card = this.$refs.tiltCard
card.style.transform = 'perspective(600px) rotateX(0deg) rotateY(0deg) scale(1)'
}
}


  • 鼠标移动时,卡片会根据指针位置灵动倾斜。

  • 鼠标移出时,卡片平滑恢复。


6. 细节优化



  • 阴影柔和:避免黑色边缘过重,提升高级感。

  • 高光线条:用低透明度白色边框和内阴影,模拟玻璃高光。

  • 所有层的圆角一致:防止割裂。

  • 表单输入框:用半透明背景和模糊,保持整体风格统一。


7.完整代码


<template>
<div class="login-container animated-background">
<!-- SVG滤镜库 -->
<svg style="display: none">
<filter id="glass-distortion" x="0%" y="0%" width="100%" height="100%" filterUnits="objectBoundingBox">
<feTurbulence type="fractalNoise" baseFrequency="0.001 0.005" numOctaves="1" seed="17" result="turbulence" />
<feComponentTransfer in="turbulence" result="mapped">
<feFuncR type="gamma" amplitude="1" exponent="10" offset="0.5" />
<feFuncG type="gamma" amplitude="0" exponent="1" offset="0" />
<feFuncB type="gamma" amplitude="0" exponent="1" offset="0.5" />
</feComponentTransfer>
<feGaussianBlur in="turbulence" stdDeviation="3" result="softMap" />
<feSpecularLighting in="softMap" surfaceScale="5" specularConstant="1" specularExponent="100" lighting-color="white" result="specLight">
<fePointLight x="-200" y="-200" z="300" />
</feSpecularLighting>
<feComposite in="specLight" operator="arithmetic" k1="0" k2="1" k3="1" k4="0" result="litImage" />
<feDisplacementMap in="SourceGraphic" in2="softMap" scale="200" xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>

<!-- 登录卡片 -->
<div
class="glass-component login-card"
ref="tiltCard"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<div class="glass-effect"></div>
<div class="glass-tint"></div>
<div class="glass-shine"></div>
<div class="glass-content">
<h2 class="login-title">欢迎登录</h2>
<form class="login-form">
<div class="form-group">
<input type="text" placeholder="用户名" class="glass-input">
</div>
<div class="form-group">
<input type="password" placeholder="密码" class="glass-input">
</div>
<button type="submit" class="glass-button">登录</button>
</form>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'LiquidGlass',
data () {
return {
// 可以添加需要的数据
}
},
methods: {
handleMouseMove (e) {
const card = this.$refs.tiltCard
const rect = card.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const centerX = rect.width / 2
const centerY = rect.height / 2
// 最大旋转角度
const maxTilt = 18
const rotateY = ((x - centerX) / centerX) * maxTilt
const rotateX = -((y - centerY) / centerY) * maxTilt
card.style.transform = `perspective(600px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.03)`
},
handleMouseLeave () {
const card = this.$refs.tiltCard
card.style.transform = 'perspective(600px) rotateX(0deg) rotateY(0deg) scale(1)'
}
}
}
</script>

<style lang="scss" scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}

.animated-background {
width: 100%;
height: 100%;
background-image: url('../../assets/macwallpaper.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}

.login-card {
width: 400px;
position: relative;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.10), 0 1.5px 6px 0 rgba(0,0,0,0.08);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.6);
cursor: pointer;
background: transparent;
}

.glass-effect {
position: absolute;
inset: 0;
z-index: 0;
backdrop-filter: blur(5px);
filter: url(#glass-distortion);
isolation: isolate;
border-radius: 24px;
}

.glass-tint {
position: absolute;
inset: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.15);
border-radius: 24px;
}

.glass-shine {
position: absolute;
inset: 0;
z-index: 2;
border: 1px solid rgba(255, 255, 255, 0.13);
border-radius: 24px;
box-shadow:
inset 1px 1px 8px 0 rgba(255, 255, 255, 0.18),
inset -1px -1px 8px 0 rgba(255, 255, 255, 0.08);
pointer-events: none;
}

.glass-content {
position: relative;
z-index: 3;
padding: 2rem;
color: white;
}

.login-title {
text-align: center;
color: #fff;
margin-bottom: 2rem;
font-size: 2rem;
font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.2);
}

.form-group {
margin-bottom: 1.5rem;
}

.glass-input {
width: 90%;
padding: 12px 20px;
border: none;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
font-size: 1rem;
backdrop-filter: blur(5px);
transition: all 0.3s ease;

&::placeholder {
color: rgba(255, 255, 255, 0.7);
}

&:focus {
outline: none;
background: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 15px rgba(255, 255, 255, 0.1);
}
}

.glass-button {
width: 100%;
padding: 12px;
border: none;
border-radius: 10px;
background: rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
position: relative;
overflow: hidden;

&:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}

&:active {
transform: translateY(0);
}
}

// 添加点击波纹效果
.click-gradient {
position: absolute;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.4) 0%, rgba(180,180,255,0.2) 40%, rgba(100,100,255,0.1) 70%, rgba(50,50,255,0) 100%);
transform: translate(-50%, -50%) scale(0);
opacity: 0;
pointer-events: none;
z-index: 4;
}

.glass-component.clicked .click-gradient {
animation: gradient-ripple 0.6s ease-out;
}

@keyframes gradient-ripple {
0% {
transform: translate(-50%, -50%) scale(0);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(3);
opacity: 0;
}
}

.glass-component {
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
will-change: transform;
}
</style>





常见问题与优化建议



  1. 阴影过重/黑边:减小 box-shadow 的透明度和模糊半径。

  2. 圆角割裂:所有玻璃层都要加 border-radius。

  3. 背景不通透:确保 glass-effect 层有 blur 和 SVG filter。

  4. 性能问题:backdrop-filter 在低端设备上可能有性能损耗,建议只在必要区域使用。

  5. 浏览器兼容性:backdrop-filter 需现代浏览器支持,IE/部分安卓浏览器不兼容。




技术要点总结



  • SVG滤镜:让玻璃表面有微妙的流动和扭曲感。

  • backdrop-filter: blur:实现背景虚化。

  • 多层叠加:色调、高光、阴影共同营造真实玻璃质感。

  • 3D transform:提升交互体验。

  • 细节打磨:阴影、边框、圆角、色彩都要精细调整。




结语


液态玻璃效果是现代前端视觉的代表之一。只要理解其原理,分层实现、细致调优,任何人都能做出媲美 macOS、Win11 的高端玻璃UI。希望本教程能帮助你掌握这项技术,做出属于自己的酷炫界面!


作者:前端不端钱
来源:juejin.cn/post/7516306850715910182
收起阅读 »

尤雨溪搞响应式为什么要从 Object.defineProperty 换成 Proxy❓

web
前言 你说,为什么❓尤雨溪搞响应式,他为什么要换掉Object.defineProperty呢❓ proxy什么来头❓ 有一次👀看他直播,说去面试人家问他原型链,他不会,GG了面试黄了,你说他是不是无中生有暗度陈仓凭空想象凭空捏造new Proxy来...
继续阅读 »

前言



你说,为什么❓尤雨溪搞响应式,他为什么要换掉Object.defineProperty呢❓



proxy什么来头❓





有一次👀看他直播,说去面试人家问他原型链,他不会,GG了面试黄了,你说他是不是无中生有暗度陈仓凭空想象凭空捏造new Proxy来换掉Object.defineProperty的呢?



还真不是,尤雨溪的响应式,我们暂且叫成插一脚吧👇,请听我细细道来👂


在前端开发中,响应式系统是现代框架的核心特性。无论是 Vue 还是 React,它们都需要实现一个基本功能:当数据变化时,自动更新相关的视图。用通俗的话说,就是要在数据被读取或修改时"插一脚",去执行一些额外的操作(比如界面刷新、计算属性重新计算等)。


// 读取属性时
obj.a; // 需要知道这个属性被读取了

// 修改属性时
obj.a = 3; // 需要知道这个属性被修改了

但原生 JavaScript 对象不会告诉我们这些操作的发生。那么,尤雨溪是如何实现这种"插一脚"的能力的呢?


正文


Vue 2 的"插一脚"方案 - Object.defineProperty


基本实现原理


Vue 2 使用的是 ES5 的 Object.defineProperty API。这个 API 允许我们定义或修改对象的属性,并为其添加 getter 和 setter。


const obj = { a: 1 };

let v = obj.a;
Object.defineProperty(obj, 'a', {
get() {
console.log('读取 a'); // 插一脚:知道属性被读取了
return v;
},
set(val) {
console.log('更新 a'); // 插一脚:知道属性被修改了
v = val;
}
});

obj.a; // 输出"读取 a"
obj.a = 3; // 输出"更新 a"

完整对象监听


为了让整个对象可响应,Vue 2 需要遍历对象的所有属性:


function observe(obj) {
for (const k in obj) {
let v = obj[k];
Object.defineProperty(obj, k, {
get() {
console.log('读取', k);
return v;
},
set(val) {
console.log('更新', k);
v = val;
}
});
}
}

处理嵌套对象


对于嵌套对象,还需要递归地进行观察:


function _isObject(v) {
return typeof v === 'object' && v !== null;
}

function observe(obj) {
for (const k in obj) {
let v = obj[k];
if (_isObject(v)) {
observe(v); // 递归处理嵌套对象
}
Object.defineProperty(obj, k, {
get() {
console.log('读取', k);
return v;
},
set(val) {
console.log('更新', k);
v = val;
}
});
}
}

Vue 2 方案的两大缺陷


缺陷一:效率问题


在这种模式下,他就必须要去遍历这个对象里边的每一个属性...这是第一个缺陷:必须遍历对象的所有属性,对于大型对象或深层嵌套对象,这会带来性能开销。


缺陷二:新增属性问题


无法检测到对象属性的添加或删除:


obj.d = 2; // 这个操作不会被监听到

因为一开始遍历的时候没有这个属性,后续添加的属性不会被自动观察。


Vue 3 的"插一脚"方案 - Proxy


基本实现原理


Vue 3 使用 ES6 的 Proxy 来重构响应式系统。Proxy 可以拦截整个对象的操作,而不是单个属性。


const obj = { a: 1 };

const proxy = new Proxy(obj, {
get(target, k) {
console.log('读取', k); // 插一脚
return target[k];
},
set(target, k, val) {
if (target[k] === val) return true;
console.log('更新', k); // 插一脚
target[k] = val;
return true;
}
});

proxy.a; // 输出"读取 a"
proxy.a = 3; // 输出"更新 a"
proxy.d; // 输出"读取 d" - 连不存在的属性也能监听到!

完整实现


function _isObject(v) {
return typeof v === 'object' && v !== null;
}

function reactive(obj) {
const proxy = new Proxy(obj, {
get(target, k) {
console.log('读取', k);
const v = target[k];
if (_isObject(v)) {
return reactive(v); // 惰性递归
}
return v;
},
set(target, k, val) {
if (target[k] === val) return true;
console.log('更新', k);
target[k] = val;
return true;
}
});
return proxy;
}

Proxy 的优势



  1. 无需初始化遍历:直接代理整个对象,不需要初始化时遍历所有属性

  2. 全面拦截:可以检测到所有属性的访问和修改,包括新增属性

  3. 性能更好:采用惰性处理,只在属性被访问时才进行响应式处理

  4. 更自然的开发体验:不需要特殊 API 处理数组和新增属性



"proxy 它解决了什么问题?两个问题。



第一个问题不需要深度遍历了,因为它不再监听属性了,而是监听的什么?整个对象。


同时也由于它监听了整个对象,就解决了第二个问题:能监听这个对象的所有操作,包括你去读写一些不存在的属性,都能监听到。"




原理对比与源码解析


原理对比


特性Object.definePropertyProxy
拦截方式属性级别对象级别
新增属性检测不支持支持
性能初始化时需要遍历按需处理
深层嵌套处理初始化时递归处理访问时递归处理

源码实现差异


Vue 2 实现



  • src/core/observer 目录下

  • 初始化时递归遍历整个对象

  • 需要特殊处理数组方法


Vue 3 实现



  • 独立的 @vue/reactivity

  • 使用 Proxy 实现基础响应式

  • 惰性处理嵌套对象

  • 更简洁的 API 设计


为什么 Proxy 是更好的选择?



  1. 更全面的拦截能力:可以拦截对象的所有操作,包括属性访问、赋值、删除等

  2. 更好的性能:不需要初始化时递归遍历整个对象

  3. 更简洁的 API:不再需要 Vue.set/Vue.delete 等特殊 API

  4. 更自然的开发体验:开发者可以使用普通的 JavaScript 语法操作对象


总结


需显式操作(defineProperty)-> 声明式编程(Proxy)


局部监听(属性级别)-> 全局拦截(对象级别)


从 Object.defineProperty 到 Proxy 的转变,不仅是 API 的升级,更是前端框架设计理念的进步。Vue 3 的响应式系统通过 Proxy 实现了更高效、更全面的数据监听。


作者:盏灯
来源:juejin.cn/post/7493539513106677769
收起阅读 »

BOE(京东方)携手合作伙伴定义下一代电竞显示趋势 借势核聚变嘉年华构建产业生态闭环

6月28日,2025核聚变游戏嘉年华在北京首钢国际会展中心举行,恰逢“Best of Esports电竞高阶联盟”成立两周年之际,BOE(京东方)盛大开启“屏实力,共联盟”电竞嘉年华,为众多电竞爱好者与行业人士带来一场极致的科技盛宴。展会同期,BOE(京东方)...
继续阅读 »

6月28日,2025核聚变游戏嘉年华在北京首钢国际会展中心举行,恰逢“Best of Esports电竞高阶联盟”成立两周年之际,BOE(京东方)盛大开启“屏实力,共联盟”电竞嘉年华,为众多电竞爱好者与行业人士带来一场极致的科技盛宴。展会同期,BOE(京东方)特别打造“视界,竞启未来”电竞显示技术鉴享会,携手冠捷科技、莱茵、京东集团等上下游生态伙伴,共同发布全球首款原生硬件圆偏振光护眼技术、ADS Pro+Mini LED等系列行业领先的电竞显示技术,定义电竞产业未来发展方向。同时,BOE(京东方)还在展会现场重磅启动“Best of Esports 电竞高阶联盟”新伙伴加入仪式,集结飞利浦、AOC、iQOO、李宁等行业头部品牌,形成跨界融合的生态矩阵,深化打造“电竞好屏认准BOE”的大众认知,共绘电竞未来发展新蓝图。

京东方科技集团高级副总裁、显示器件及物联网创新业务前台负责人刘竞表示,当下,电竞市场持续爆发性增长,用户对显示体验需求升级。作为全球显示领域的领导者,BOE(京东方)始终以“屏之物联”战略为引领,在电竞显示器、笔记本、手机、电视等专业电竞显示领域均已处于领先地位。同时,BOE(京东方)积极构建电竞生态,携手各界伙伴,共同推动电竞产业的繁荣发展。我们深谙开放协作的力量,此次携手包括冠捷在内的各方合作伙伴,不仅是技术的强强联合,更是理念的深度契合。未来,BOE(京东方)将持续与全球生态伙伴深化合作,共同探索电竞显示技术的无限可能,引领电竞产业朝着更高质量、更具活力的方向发展,为全球电竞玩家创造更加精彩的视觉享受和游戏体验。

在电竞显示技术鉴享会现场,TÜV莱茵产品服务大中华区销售副总裁都江在现场为BOE(京东方)携手冠捷推出的全球首款原生硬件圆偏振光护眼显示器颁发了“TÜV莱茵圆偏光认证”。同时,冠捷科技副总裁兼OBM显示器BU中国区/亚太区总经理阎立东、TÜV莱茵电子电气产品服务全球显示技术总监/大中华区区域总经理刘喜强、京东零售3C数码事业群电脑组件业务部显示器品类负责人乔祥安等嘉宾分享了电竞护眼产品技术以及产业的演进与趋势。

多年来,BOE(京东方)持续深耕电竞领域,在超高清、超高刷、低功耗、健康护眼等多个领域取得领先优势,并不断迭代升级技术,携手合作伙伴进行定制化开发,推动电竞显示技术不断演进。此次活动现场,BOE(京东方)全面展示了圆偏光护眼技术、ADS Pro+Mini LED解决方案等一系列创新技术。其中,BOE(京东方)全球首发的全新一代圆偏光技术,在屏幕表面搭载独特的圆偏光护眼层,其光矢量端点轨迹呈圆形,偏振方向随时间均匀分布,通过模拟太阳光的螺旋扩散原理,使得显示器发出的光更接近自然光,可大幅减少光线对晶状体和视网膜的定向刺激,为用户提供更接近自然光的健康护眼体验。BOE(京东方)领先的ADS Pro+Mini LED 解决方案,深度整合ADS Pro广视角和高刷的优势,以及Mini LED在HDR和极致暗态画质的优异表现,带来可媲美OLED的画质体验。此外,随着刷新率的不断提升,通过ADS Pro+Mini LED实现分区动态插黑,可以极大提升高速运动画面清晰度,显著减少卡顿、延迟等现象,树立电竞显示的性能画质新标杆。

在展区中,BOE(京东方)集结众多“Best of Esports 电竞高阶联盟”成员,携手华硕、微星、AGON、机械师、机械革命、ROG、BenQ MOBIUZ、联想拯救者、雷神、海信、红魔、一加等带来一系列由ADS Pro、α-MLED、f-OLED技术品牌赋能的电竞黑科技产品。其中,ROG枪神9 Plus超竞版搭载BOE(京东方)ADS Pro技术,拥有240Hz超高刷新率及3ms极速响应,同时,采用Mini LED背光技术,具备2000个控光分区,峰值亮度可达1200nits,还能够呈现100%DCI-P3超高色域显示,为玩家带来极致震撼的画面显示效果;iQOO Neo10 Pro+手机采用BOE(京东方)Q10发光器件,使用2K LTPO 旗舰屏幕,超高频PWM+全亮度类DC调光,搭配圆偏振光技术,呵护明眸双眼;BOE(京东方)携手飞利浦推出的全球首款原生硬件圆偏振光护眼显示器——飞利浦EVNIA舒视蓝4.0显示器,搭载BOE(京东方)全新一代圆偏光技术,为全球消费者带来更健康、更优质的极致视觉体验,27英寸QHD面板配合300Hz刷新率+1ms响应横扫竞技延迟,无论是FPS亦或是3A游戏都能够满状态发挥,实现“电竞级性能与健康用眼零妥协”。此外,一系列裸眼3D笔记本、显示器等创新显示技术接连亮相,京东方中联超清还在现场展示了裸眼3D“精灵”魔盒,拥有7680Hz高刷新率、低扫描数、低摩尔纹等技术优势,适用于移动XR影棚、转角裸眼3D等前沿显示场景。

值得关注的是,BOE(京东方)通过前沿显示技术提升电竞体验的同时,不断深化构建电竞产业生态。展会期间,京东方科技集团副总裁、首席品牌官司达携手飞利浦、AOC、iQOO、逐夜BLACKLYTE等品牌代表共同启动“Best of Esports 电竞高阶联盟”新伙伴加入仪式,iQOO、李宁、漫步者电竞、AOC、飞利浦、逐夜BLACKLYTE等行业头部品牌正式加入,标志着联盟迎来了生态矩阵的战略性扩容。“Best of Esports电竞高阶联盟”自2023年6月成立以来,行业影响力逐步扩大,吸引了包括英特尔、虎牙直播、JDG俱乐部、AGON、海信、拯救者、机械师、红魔、ROG、创维、雷神、vivo等众多全球一线品牌陆续加盟,此次新成员的加入将进一步强化联盟内的生态协同效应,基于BOE(京东方)领先的显示技术,在桌面电竞显示、移动电竞场景、电竞运动装备、电竞音频交互等多领域,构建起覆盖“视—听—触”全感官的电竞体验闭环。

在电竞产业规模突破千亿的当下,BOE(京东方)以显示技术为锚点的生态化战略,既顺应了 Z 世代对沉浸式体验的需求升级,也为显示行业开辟了差异化竞争新赛道。作为电竞显示技术的引领者,BOE(京东方)始终秉承“屏之物联”发展战略,与全球一线合作伙伴强强联合,推出了一系列行业领先的电竞旗舰产品,更通过BOE无畏杯赛事等生态载体构建起完整产业闭环。未来,BOE(京东方)将充分发挥“Best of Esports电竞高阶联盟”全业态布局、资源聚合、技术领先等优势,以创新科技赋能电竞产业,助力中国电竞产业实现高质量发展。


收起阅读 »

状态机设计:比if-else优雅100倍的设计

web
作为一名后端开发工程师,当你面对复杂的业务流程时,是否常感到逻辑混乱、边界不清?学会状态机设计,让你的代码优雅如诗! 引言:为什么需要状态机? 在后台系统开发中,我们经常需要处理对象的状态流转问题:订单从"待支付"到"已支付"再到"已发货",工单系统从"打开...
继续阅读 »

作为一名后端开发工程师,当你面对复杂的业务流程时,是否常感到逻辑混乱、边界不清?学会状态机设计,让你的代码优雅如诗!



引言:为什么需要状态机?


在后台系统开发中,我们经常需要处理对象的状态流转问题:订单从"待支付"到"已支付"再到"已发货",工单系统从"打开"到"处理中"再到"解决",这些场景都涉及状态管理


如果不使用状态机设计,我们可能会写出这样的面条式代码:


func HandleOrderEvent(order *Order, event Event) error {
if order.Status == "待支付" {
if event.Type == "支付成功" {
order.Status = "已支付"
// 执行支付成功逻辑...
} else if event.Type == "取消订单" {
order.Status = "已取消"
// 执行取消逻辑...
} else {
return errors.New("非法事件")
}
} else if order.Status == "已支付" {
if event.Type == "发货" {
order.Status = "已发货"
// 执行发货逻辑...
}
// 更多else if...
}
// 更多else if...
}

这种代码存在几个致命问题:



  1. 逻辑分支嵌套严重(俗称箭头代码)

  2. 状态流转规则难以维护

  3. 容易遗漏边界条件

  4. 可扩展性差(新增状态需要改动核心逻辑)


状态机正是解决这类问题的银弹!


状态机设计核心概念


状态机三要素


概念描述订单系统示例
状态(State)系统所处的稳定状态待支付、已支付、已发货
事件(Event)触发状态变化的动作支付成功、取消订单
转移(Transition)状态变化的规则待支付 → 已支付

状态机的类型



  1. 有限状态机(FSM):最简单的状态机形式

  2. 分层状态机(HSM):支持状态继承,减少冗余

  3. 状态图(Statecharts):支持并发、历史状态等高级特性


graph LR
A[待支付] -->|支付成功| B[已支付]
B -->|发货| C[已发货]
B -->|申请退款| D[退款中]
A -->|取消订单| E[已取消]
D -->|退款成功| E
D -->|退款失败| B

Go实现状态机实战


基本结构定义


package main

import "fmt"

// 定义状态类型
type State string

// 定义事件类型
type Event string

// 状态转移函数类型
type TransitionHandler func() error

// 状态转移定义
type Transition struct {
From State
Event Event
To State
Handle TransitionHandler
}

// 状态机定义
type StateMachine struct {
Current State
transitions []Transition
}

// 注册状态转移规则
func (sm *StateMachine) AddTransition(from State, event Event, to State, handler TransitionHandler) {
sm.transitions = append(sm.transitions, Transition{
From: from,
Event: event,
To: to,
Handle: handler,
})
}

// 处理事件
func (sm *StateMachine) Trigger(event Event) error {
for _, trans := range sm.transitions {
if trans.From == sm.Current && trans.Event == event {
// 执行处理函数
if err := trans.Handle(); err != nil {
return err
}
// 更新状态
sm.Current = trans.To
return nil
}
}
return fmt.Errorf("非法事件[%s]或当前状态[%s]不支持", event, sm.Current)
}

订单状态机示例


// 订单状态定义
const (
StatePending State = "待支付"
StatePaid State = "已支付"
StateShipped State = "已发货"
StateCanceled State = "已取消"
)

// 事件定义
const (
EventPaySuccess Event = "支付成功"
EventCancel Event = "取消订单"
EventShip Event = "发货"
)

func main() {
// 创建状态机
sm := &StateMachine{Current: StatePending}

// 注册状态转移
sm.AddTransition(StatePending, EventPaySuccess, StatePaid, func() error {
fmt.Println("执行支付成功处理逻辑...")
return nil // 实际业务中可能有错误处理
})

sm.AddTransition(StatePending, EventCancel, StateCanceled, func() error {
fmt.Println("执行订单取消逻辑...")
return nil
})

sm.AddTransition(StatePaid, EventShip, StateShipped, func() error {
fmt.Println("执行发货逻辑...")
return nil
})

sm.AddTransition(StatePaid, EventCancel, StateCanceled, func() error {
fmt.Println("执行已支付状态的取消逻辑...")
return nil
})

// 执行事件测试
fmt.Println("当前状态:", sm.Current)
_ = sm.Trigger(EventPaySuccess) // 支付成功
fmt.Println("当前状态:", sm.Current)
_ = sm.Trigger(EventShip) // 发货
fmt.Println("当前状态:", sm.Current)

// 测试非法转移
err := sm.Trigger(EventCancel)
fmt.Println("尝试取消:", err) // 非法操作
}

输出结果:


当前状态: 待支付
执行支付成功处理逻辑...
当前状态: 已支付
执行发货逻辑...
当前状态: 已发货
尝试取消: 非法事件[取消订单]或当前状态[已发货]不支持

扩展:表驱动状态机


上面的实现足够清晰,但存在性能问题——每次触发事件都需要遍历转移表。我们优化为更高效的版本:


type StateMachineV2 struct {
Current State
transitionMap map[State]map[Event]*Transition
}

func (sm *StateMachineV2) AddTransition(from State, event Event, to State, handler TransitionHandler) {
if sm.transitionMap == nil {
sm.transitionMap = make(map[State]map[Event]*Transition)
}
if _, exists := sm.transitionMap[from]; !exists {
sm.transitionMap[from] = make(map[Event]*Transition)
}
sm.transitionMap[from][event] = &Transition{
From: from,
Event: event,
To: to,
Handle: handler,
}
}

func (sm *StateMachineV2) Trigger(event Event) error {
if events, exists := sm.transitionMap[sm.Current]; exists {
if trans, exists := events[event]; exists {
if err := trans.Handle(); err != nil {
return err
}
sm.Current = trans.To
return nil
}
}
return fmt.Errorf("非法事件[%s]或当前状态[%s]不支持", event, sm.Current)
}

进阶技巧:状态机实践指南


状态转移图可视化


绘制状态转移图,与代码实现保持同步:



状态模式的优雅实现


使用Go的接口特性实现面向对象的状态模式:


type OrderState interface {
Pay() error
Cancel() error
Ship() error
// 其他操作方法...
}

type pendingState struct{}

func (s *pendingState) Pay() error {
fmt.Println("执行支付成功处理逻辑...")
return nil
}

func (s *pendingState) Cancel() error {
fmt.Println("执行待支付状态取消逻辑...")
return nil
}

func (s *pendingState) Ship() error {
return errors.New("当前状态不能发货")
}

// 其他状态实现...

type Order struct {
state OrderState
}

func (o *Order) ChangeState(state OrderState) {
o.state = state
}

func (o *Order) Pay() error {
return o.state.Pay()
}

// 其他方法...

状态机的持久化


如何在数据库中存储状态机?永远只存储状态,而不是存储状态机逻辑!


数据库表设计示例:


字段名类型描述
idint主键ID
statusvarchar(20)当前状态
event_historyjson事件历史记录

状态恢复代码实现:


type Order struct {
ID int
Status State
}

func RecoverOrderStateMachine(order Order) *StateMachine {
sm := CreateStateMachine() // 创建初始状态机
sm.Current = order.Status // 恢复状态
return sm
}

真实案例:电商订单系统


复杂状态机设计



处理并发操作


var mutex sync.Mutex

func (sm *StateMachine) SafeTrigger(event Event) error {
mutex.Lock()
defer mutex.Unlock()
return sm.Trigger(event)
}

// 使用channel同步
func (sm *StateMachine) AsyncTrigger(event Event) error {
eventChan := make(chan error)
go func() {
mutex.Lock()
defer mutex.Unlock()
eventChan <- sm.Trigger(event)
}()
return <-eventChan
}

避免状态机设计的反模式



  1. 过度复杂的状态机:如果状态超过15个,考虑拆分

  2. 上帝状态机:避免一个状态机控制整个系统

  3. 忽略状态回退:重要系统必须设计回退机制

  4. 缺乏监控:记录状态转移日志


监控状态转移示例:


func (sm *StateMachine) Trigger(event Event) error {
startTime := time.Now()
defer func() {
log.Printf("状态转移监控: %s->%s (%s) 耗时: %v",
oldState, sm.Current, event, time.Since(startTime))
}()
// 正常处理逻辑...
}

结语:状态机的无限可能


状态机不只是解决业务逻辑的工具,它更是一种思维方式。通过今天的学习,你应该掌握了:



  1. 状态机的基本概念与类型 ✅

  2. Go语言实现状态机的多种方式 ✅

  3. 复杂状态机的设计技巧 ✅

  4. 真实项目的状态机应用模式 ✅


当你在设计下一个后端系统时,先问自己三个问题:



  1. 我的对象有哪些明确的状态?

  2. 触发状态变化的事件是什么?

  3. 状态转移需要哪些特殊处理?


思考清楚这些问题,你的代码设计将变得更加清晰优雅!


作者:草捏子
来源:juejin.cn/post/7513752860162129960
收起阅读 »

用了三年 Vue,我终于理解为什么“组件设计”才是重灾区

web
一开始写 Vue 的时候,谁不是觉得:“哇,组件好优雅!”三年后再回头一看,组件目录像垃圾堆,维护一处改三处,props 乱飞、事件满天飞,复用全靠 copy paste。于是我终于明白 —— 组件设计,才是 Vue 项目的重灾区。 1. 抽组件 ≠ 拆文...
继续阅读 »

一开始写 Vue 的时候,谁不是觉得:“哇,组件好优雅!”三年后再回头一看,组件目录像垃圾堆,维护一处改三处,props 乱飞、事件满天飞,复用全靠 copy paste。于是我终于明白 —— 组件设计,才是 Vue 项目的重灾区





1. 抽组件 ≠ 拆文件夹


很多初学 Vue 的人对“组件化”的理解就是:“页面上出现重复的 UI?好,抽个组件。”


于是你会看到这样的组件:


<!-- TextInput.vue -->
<template>
<input :value="value" @input="$emit('update:value', $event.target.value)" />
</template>

接着你又遇到需要加图标的输入框,于是复制一份:


<!-- IconTextInput.vue -->
<template>
<div class="icon-text-input">
<i class="icon" :class="icon" />
<input :value="value" @input="$emit('update:value', $event.target.value)" />
</div>
</template>

再后来你需要加验证、loading、tooltip……结果就变成了:



  • TextInput.vue

  • IconTextInput.vue

  • ValidatableInput.vue

  • LoadingInput.vue

  • FormInput.vue


组件爆炸式增长,但每一个都只是“刚好凑合”,共用不了。




2. 抽象失控:为了复用而复用,结果没人敢用


比如下面这个场景:


你封装了一个超级复杂的表格组件:


<CustomTable
:columns="columns"
:data="tableData"
:show-expand="true"
:enable-pagination="true"
:custom-actions="['edit', 'delete']"
/>


你美其名曰“通用组件”,但别人拿去一用就发现:



  • 某个页面只要展示,不要操作按钮,配置了也没法删;

  • 有个页面需要自定义排序逻辑,你这边死写死;

  • 另一个页面用 element-plus 的样式,这边你自绘一套 UI;

  • 报错时控制台输出一大堆 warning,根本不知道哪来的。


最后大家的做法就是 —— 不用你这套“通用组件”,自己抄一份改改




3. 数据向下流、事件向上传:你真的理解 props 和 emit 吗?


Vue 的单向数据流原则说得很清楚:



父组件通过 props 向下传数据,子组件通过 emit 通知父组件。



但现实是:



  • props 传了 7 层,页面逻辑根本看不懂数据哪来的;

  • 子组件 emit 了两个 event,父组件又传回了回调函数;

  • 有时候干脆直接用 inject/providerefeventBus 偷偷打通通信。


举个例子:


<!-- 祖父组件 -->
<template>
<PageWrapper>
<ChildComponent :formData="form" @submit="handleSubmit" />
</PageWrapper>
</template>

<!-- 子组件 -->
<template>
<Form :model="formData" />
<button @click="$emit('submit', formData)">提交</button>
</template>

看上去还好?但当 ChildComponent 再包一层 FormWrapper、再嵌套 InputList,你就发现:



  • formData 根本不知道是哪个组件控制的

  • submit 被多层包装、debounce、防抖、节流、劫持

  • 你改一个按钮逻辑,要翻 4 个文件




4. 技术债爆炸的罪魁祸首:不敢删、不敢动


组件目录看似整齐,但大部分组件都有如下特征:



  • 有 10 个 props,3 个事件,但没人知道谁在用;

  • 注释写着“用于 A 页面”,实际上 B、C、D 页面也在引用;

  • 一个小改动能引发“蝴蝶效应”,整个系统发疯。


于是你只能选择 —— 拷贝再新建一个组件,给它加个 V2 后缀,然后老的你也不敢删。


项目后期的结构大概就是:


components/
├── Input.vue
├── InputV2.vue
├── InputWithTooltip.vue
├── InputWithValidation.vue
├── InputWithValidationV2.vue
└── ...

“为了让别人能维护我的代码,我决定不动它。”




5. 组件设计的核心,其实是抽象能力


我用三年才悟到一个道理:



Vue 组件设计的难点,不是语法、也不是封装,而是你有没有抽象问题的能力



举个例子:


你需要设计一个“搜索区域”组件,包含输入框 + 日期范围 + 搜索按钮。


新手写法:


<SearchHeader
:keyword="keyword"
:startDate="start"
:endDate="end"
@search="handleSearch"
/>


页面需求一改,换成了下拉框 + 单选框怎么办?又封一个组件?


更好的设计是 —— 提供slots 插槽 + 作用域插槽


<!-- SearchHeader.vue -->
<template>
<div class="search-header">
<slot name="form" />
<button @click="$emit('search')">搜索</button>
</div>
</template>

<!-- 使用 -->
<SearchHeader @search="search">
<template #form>
<el-input v-model="keyword" placeholder="请输入关键词" />
<el-date-picker v-model="range" type="daterange" />
</template>
</SearchHeader>

把结构交给组件,把行为交给页面。组件不掌控一切,而是协作。




6. 那么组件怎么设计才对?


我总结出 3 条简单但有效的建议:


✅ 1. 明确组件职责:UI?交互?逻辑?



  • UI 组件只关心展示,比如按钮、标签、卡片;

  • 交互组件只封装用户操作,比如输入框、选择器;

  • 逻辑组件封装业务规则,比如筛选区、分页器。


别让一个组件又画 UI 又写逻辑还请求接口。




✅ 2. 精简 props 和 emit,只暴露“必需”的接口



  • 一个组件 props 超过 6 个,要小心;

  • 如果事件名不具备业务语义(比如 click),考虑抽象;

  • 不要用 ref 操作子组件的内部逻辑,那是反模式。




✅ 3. 使用 slots 替代“高度定制的 props 方案”


如果你发现你组件 props 变成这样:


<SuperButton
:label="'提交'"
:icon="'plus'"
:iconPosition="'left'"
:styleType="'primary'"
:loading="true"
/>


那它该用 slot 了:


<SuperButton>
<template #icon><PlusIcon /></template>
提交
</SuperButton>



🙂


三年前我以为组件化是 Vue 最简单的部分,三年后我才意识到,它是最深、最难、最容易出坑的部分。


如果你也踩过以下这些坑:



  • 组件复用越写越复杂,别人都不敢用;

  • props 和事件像迷宫一样,维护成本极高;

  • UI 和逻辑耦合,改一点动全身;

  • 项目后期组件膨胀、技术债堆积如山;


别再让组件成为项目的“技术债”。你们也有遇到吗?


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



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

Vite 底层彻底换血,尤雨溪想要控制整个前端生态?

web
Hello,大家好,我是 Sunday。 最近,尤雨溪发了一篇非常关键的文章,宣布 Vite 正式引入 Rust 写的打包器 Rolldown,并将逐步替代现有的 Rollup 成为 默认打包器。 该文章发布在 尤雨溪 新公司 void(0),文章链接:h...
继续阅读 »

Hello,大家好,我是 Sunday。


最近,尤雨溪发了一篇非常关键的文章,宣布 Vite 正式引入 Rust 写的打包器 Rolldown,并将逐步替代现有的 Rollup 成为 默认打包器




该文章发布在 尤雨溪 新公司 void(0),文章链接:https://voidzero.dev/posts/announcing-rolldown-vite



虽然这篇文章的内容并不长,但是内部做出的改成确实非常大的,可以毫不夸张的说:尤雨溪把整个 vite 的心脏都换掉了!


所以,咱们今天这篇文章,我不打算重复发布会上的内容,而是一起来看看这波 “换心脏” 的背后逻辑:为什么 Rust 能上位?真实速度到底快了多少?尤雨溪到底在下一盘什么棋?


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) ) 。想要详细了解的,可以看下我之前发的这篇文章 尤雨溪新公司 Void(0) 首款产品发布,竟然是它...


这是一家由 尤雨溪 创建的专门做 JavaScript 工具链的开源公司。关于这一块的详细介绍,可以看这篇博客 尤雨溪创建 VoidZero ,并得到 460 万美金融资


这家公司刚一出手就连放两个大招:



  • 第一个是 Oxc :这是一个全新的 Rust 实现 JS 工具链(parser、transform、minifier、linter、formatter,全都自己造)

  • 第二个就是 Rolldown:Vite 打包器的 Rust 替代方案,目标直接瞄准 Rollup、Babel、Terser 这整条传统链路


而这次 Vite 接入 Rolldown,正是 void(0) 把自家工具「回注入」开源生态的第一步。



所以这不是在“优化 Vite”,而是想要 “替换整条构建基础设施”



你可以这么理解 void(0) 的策略路径:



  1. Vue 站稳前端框架圈核心位置

  2. Vite 用 Rollup 起家,成为构建工具主流选择

  3. void(0) 作为新公司登场,切入工具链底层,用 Rust 重写一整套生态

  4. 再反哺 Vite,用 Rolldown 替代原来的 JS 构建方案

  5. 最终形成:Vue + Vite + void(0) 工具链 的闭环


这其实是一个很聪明、很清晰的长期路线图:不再被 Babel、Terser、ESLint 等“生态外依赖”所绑定,而是自己控制工具底层、性能节奏、开发体验。


尤雨溪本人也在社区里反复提过:Vite 的未来,不只是“构建工具”,而是下一代工程化的“前端开发基建平台”。


而这张底牌,就是 Rolldown + Oxc。


你可以想想看,如果:



  • Vue 生态已经在试水 Rolldown

  • Vite 即将全面接入 Rolldown

  • Vite 插件作者必须适配 Rolldown(否则未来会不兼容)


那就意味着:



无论你是 Vue、React、Svelte,还是用 Vite 的任何框架,都必须配合这次 “Rust 工具链” 的迁移。 否则将有可能会被踢出前端生态。



而想要参与,就必须要使用 Void(0) 的产品。


这样,尤雨溪就可以很成功的让 Void(0) 变成整个前端生态的标准了!


作者:程序员Sunday
来源:juejin.cn/post/7511583779578642483
收起阅读 »

本尊来!网易灰度发布系统揭秘:一天300次上线是怎么实现的?

你可能听过“网易每天上线几百次”, 但你是否知道:99%的发布都不是全量,而是按灰度批次推进。 今天从代码 + 场景双视角,拆解网易灰度发布的完整实现逻辑,让你真正搞懂: 发布是怎么分用户、分地域、分时间段的 如何回滚不影响线上用户 甚至如何模拟真实用户流量...
继续阅读 »

你可能听过“网易每天上线几百次”,
但你是否知道:99%的发布都不是全量,而是按灰度批次推进


今天从代码 + 场景双视角,拆解网易灰度发布的完整实现逻辑,让你真正搞懂:



  • 发布是怎么分用户、分地域、分时间段的

  • 如何回滚不影响线上用户

  • 甚至如何模拟真实用户流量进行 A/B 实验




一、网易灰度系统整体架构图(简化)


image.png


二、核心策略算法:如何选择灰度用户?


网易内部灰度用户分流引擎大致是这样:


interface User {
uid: string
region: string // 地域
isVip: boolean
loginTime: number // 最近登录时间
}

// 灰度策略配置
const strategy = {
percent: 10, // 灰度比例
regionInclude: ['华南'], // 地域包含
vipOnly: true // 只投放给 VIP
}

// 筛选函数
function filterUsers(users: User[], strategy) {
const filtered = users.filter(u =>
(!strategy.regionInclude || strategy.regionInclude.includes(u.region)) &&
(!strategy.vipOnly || u.isVip)
)
const count = Math.floor((strategy.percent / 100) * filtered.length)
return filtered.slice(0, count)
}



三、实际运行结果展示(模拟环境)


const users: User[] = Array.from({ length: 1000 }, (_, i) => ({
uid: `U${i}`,
region: ['华南', '华北', '华东'][i % 3],
isVip: i % 2 === 0,
loginTime: Date.now() - i * 10000,
}))

const selected = filterUsers(users, strategy)

console.log('灰度命中用户数:', selected.length)
console.log('前5个用户:', selected.slice(0, 5))

✅ 输出示例:


灰度命中用户数: 166
前5个用户: [
{ uid: 'U0', region: '华南', isVip: true, loginTime: 1717288879181 },
{ uid: 'U6', region: '华南', isVip: true, loginTime: 1717288819181 },
{ uid: 'U12', region: '华南', isVip: true, loginTime: 1717288759181 },
...
]



四、网易如何触发灰度?手动?自动?答案是:多触发源 + 策略组合



  1. ✅ 手动控制(管理员控制台)

  2. ✅ CI/CD 自动触发(合并主干自动上线)

  3. ✅ 实验平台触发(A/B 实验验证新功能)


示例:CI/CD 触发部署的逻辑(伪代码):


// Jenkinsfile 中执行灰度命令
steps {
script {
sh 'node deploy.js --env=prod --gray=10%'
}
}



五、监控数据如何决定“是否继续灰度”?


网易内部有自动指标监控,如:


指标名作用阈值
error_rate错误率异常自动中止>0.05
api_delay接口响应时间>300ms
login_success_ratio登录成功率<0.95

代码示例(灰度中控系统伪代码):


if (metrics.error_rate > 0.05 || metrics.login_success_ratio < 0.95) {
graySystem.stopDeployment()
graySystem.rollback()
console.log('灰度异常,中止并回滚')
} else {
graySystem.continue()
}



六、网易的灰度回滚机制非常丝滑,为什么?


他们采用了 “金丝雀版本+热切流量+自动恢复” 策略:


graySystem.deploy(version: '1.2.3', tag: 'canary')
// graySystem.rollback() 会回到上一个 tag=stable 的版本

而且每次发布都会打上 Git tag,并记录环境信息,回滚只需1行命令:


gray rollback --env prod --tag stable



七、你能学到什么?(总结)



  • 灰度不等于“发布慢一点”,而是可控可观测的发布策略

  • 用户维度灰度筛选逻辑要尽量结构化,避免硬编码

  • 数据指标必须“事前定义”,不能出了问题再想怎么止损

  • 所有灰度发布必须可回滚




彩蛋:



“上线不是勇气的象征,而是风控能力的体现。”



作者:前端付豪
来源:juejin.cn/post/7511150244576837684
收起阅读 »

低代码是“未来”还是“骗局”?作为前端我被内耗到了

「我一个前端,最后成了平台数据填表员,写页面?不存在的。」 😅项目开始前,我是兴奋的 当领导说“这个项目我们用低代码平台做,提效百分百”,我甚至有点激动。 作为一个写了 5 年组件的前端老油子,谁不想脱离日复一日的 v-model 和 props 地狱? 领...
继续阅读 »

「我一个前端,最后成了平台数据填表员,写页面?不存在的。」



😅项目开始前,我是兴奋的


当领导说“这个项目我们用低代码平台做,提效百分百”,我甚至有点激动。


作为一个写了 5 年组件的前端老油子,谁不想脱离日复一日的 v-modelprops 地狱?


领导还补充:“平台我们组自己封装的,配个 schema 就能跑,前端工程师只需要写逻辑。”


我一听这话,心想:



终于要进入“写配置赚钱”的时代了!



然而,我万万没想到——这段低代码旅程,最后让我怀疑人生。




😵上线第一天,我差点把键盘砸了


第一个需求很简单:



做个带搜索、分页、导出 Excel 的用户列表页。



我打开平台,选择“表格组件”,拖入“搜索框”,配置字段、绑定接口——一切都看起来毫无门槛。


结果运行之后,搜索没反应、分页错乱、导出根本没绑定。


我一查 schema,300 多行配置里,居然混了三种不同的写法:


{
"onSearch": "handleSearch",
"onSearch()": "handleSearch",
"onSearchEvent": "handleSearch"
}

问后端:“你这个文档是哪个是对的?”


后端说:“都对,我们兼容了。”


我瞬间明白:这不是低代码,这是自制混沌生成器。




🤯技术债太多,修个 key,整页全崩


最魔幻的一次,是我想修改表格的一个字段名,从 user_name 改成 username


我只是改了 schema 的字段名,结果:



  • 表格没显示

  • 搜索没了

  • 编辑表单也报错

  • 提交接口直接抛了个 500


我调试了三个小时,才发现平台内部是按字段名 字符串拼接 key 绑定状态 的……只要字段名变了,所有逻辑都得重配。


我恍然大悟:



传统开发用 IDE 报错提醒你;低代码等你点击线上按钮才告诉你挂了。





😓协作地狱:产品配页面,我修 schema


最痛苦的不是技术问题,而是人。


产品说:“我来拖页面,你只用帮我看看为什么点了没反应。”


然后我收到一个 schema 文件,2000 行,不带注释,结构是这样:


{
"component": "Form",
"props": {
"items": [
{
"label": "名称",
"field": "formA.formB.userName",
"rules": "{ required: true }",
"props": {
"onClick": "fn1()"
}
}
]
}
}

我想问三件事:



  1. 你这个路径到底是怎么拼出来的?

  2. 为什么校验规则是字符串?

  3. 你拖了个按钮怎么会触发五个请求?


我调试一天,结论是:产品误操作删除了一个容器组件,但平台没报错,直接让数据结构断链。




🧨所谓低代码:把逻辑封死、把锅甩你


我总结这个项目两个最深的坑:


1. 复杂交互,低代码写不来


比如“用户选择类型后,自动拉接口重新加载选项”这种需求,纯配置根本搞不定。


最后我只能写自定义组件注入到平台里,甚至还要写类似:


platform.on("fieldChange", (field, value) => {
if (field === "type") {
reloadOptions(value)
}
})

这叫低代码吗?我写的代码比原来还绕。


2. 前端不是没活了,是变成了“修配置+查日志工程师”



  • 页面功能跑飞了?前端看 schema;

  • 按钮不响应?前端查绑定字段;

  • 接口返回异常?前端加拦截 hook;

  • 表单校验失败?前端写正则规则;


最后我只写了两行 JS,却维护了十几套 JSON。


我真的开始怀疑,前端的价值,是不是变成了“修别人的 schema”?




🧩我从这个项目学到的东西


不是说低代码没用,而是:



不是所有团队都配拥有一套低代码平台。



低代码系统真正的“提效”,需要这些前提:



  1. 强约束的规范体系(字段、组件、交互都必须有标准)

  2. 良好的权限隔离机制(避免“产品能改逻辑,运营能删字段”)

  3. 持续有人维护平台底层能力(不然技术债只会越来越重)

  4. 合理的分工协作机制(schema 的维护不应该是前端一个人干)


否则,它只是一个混乱责任分配工具,表面上 everyone can build,实际上 everyone push bugs。




📌低代码,不是骗局,也不是未来——是一个选项


如果你问我现在怎么看低代码?


我的回答是:



低代码不是“未来”也不是“骗局”,而是“项目管理方式的折中”。



它适合一些场景:



  • 表单多、CRUD 重复度高的后台系统;

  • 业务快速试错、页面变动频繁的 MVP 阶段;

  • 运营自己想搭点落地页的场景。


但如果你希望:



  • 页面复杂,交互灵活;

  • 可测试、可维护、可拓展;

  • 高性能、大工程;


那——你得写代码。




🗣最后


你也踩过低代码的坑吗?
有没有类似“debug 三小时发现产品配错 schema”的经历?


📌 你可以继续看我的《为什么》系列文章



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

前端难还是后端难?作为八年后端开发,我想说点实话

前端难还是后端难?作为八年后端开发,我想说点实话 前端容易吗?不容易。 后端轻松吗?也不轻松。 那到底哪个更难? 这事还真不是一句话能说清楚的…… 一、先说说我个人的背景 我是一个写了 8 年 Java 后端的程序员,经历过中后台系统、金融系统、ToC ...
继续阅读 »

前端难还是后端难?作为八年后端开发,我想说点实话



前端容易吗?不容易。

后端轻松吗?也不轻松。

那到底哪个更难?

这事还真不是一句话能说清楚的……





一、先说说我个人的背景


我是一个写了 8 年 Java 后端的程序员,经历过中后台系统、金融系统、ToC App 的服务端架构,也跟前端打了无数交道。从最早的 jQuery 到现在的 Vue、React、Vite,从最早的 JSP 页面到现在的前后端分离,我见证了不少“变化”。


我不是要拉踩谁,只是想以一个偏后端开发者的视角,聊聊我对“前端难还是后端难”这个话题的理解。




二、前端的“难”是不断变化的“浪潮”


不得不承认,前端的变化速度是真的快。去年刚学完 Vue 2,今年要学 Vue 3;React 的 Hook 还没深入掌握,新的 Server Component 又来了;Webpack 配熟了,Vite 火了;CSS 还没写顺手,Tailwind 席卷而来。


除了框架和工具链的变化,更别说适配各种浏览器、屏幕尺寸、终端设备、无障碍要求、多语言、性能优化、SEO、交互设计……


而且最近几年,前端逐渐“全栈化”:你可能要写服务端渲染(SSR)、搞 Node 服务、上 Docker 部署、调数据库、甚至自己写接口 mock。


前端难吗?难,而且是越来越难。




三、后端的“难”是看不见的深度


后端的难,往往藏在系统的底层逻辑中。你可能看不到一个后端接口的“UI 效果”,但它背后往往涉及:



  • 数据库设计 & 索引优化

  • 分布式事务

  • 消息队列 & 异步处理

  • 缓存策略 & 数据一致性

  • 服务容灾 & 高可用架构

  • 权限系统、加密解密、审计日志

  • 安全防护(SQL 注入、XSS、CSRF)

  • 性能调优 & JVM 调试

  • CI/CD、灰度发布、日志平台接入


而且一旦出问题,前端崩了是“用户体验不好”,后端崩了是“公司赔钱” 。这不是开玩笑,有一次我们一个订单服务接口挂了 5 分钟,损失了几十万。


后端难吗?当然难,而且是“看不见但不能错”的难。




四、我最怕的不是“前端难”或“后端难”,而是互相看不起


说实话,我见过太多前后端互相“看不上”的情况:



  • 后端觉得前端就是摆样子,“你不就封个壳子嘛?”

  • 前端觉得后端接口又臭又长,“你这 JSON 谁看得懂?”

  • 后端吐槽前端不会调接口,前端吐槽后端不会写文档……


但你仔细去看,一个优秀的前端开发,往往比很多“伪全栈”更懂系统结构;一个优秀的后端,也会在意接口的易用性、响应速度和文档清晰度。


技术没有高低,但人有格局。




五、站在“代码人生”的角度看,难易是阶段性的


我年轻的时候觉得后端“更高级”,因为能接触系统底层、数据和业务逻辑。但这几年,我越来越觉得前端也有它独特的价值:



  • 是前端让用户第一眼喜欢上产品;

  • 是前端让复杂的系统变得“看得见”;

  • 是前端在用户和系统之间,搭了一座桥。


你说哪个更重要?没有谁离开谁就能独立运行的系统


我现在更看重的是协作、共建、以及对整个产品的理解。做前端也好,后端也罢,最终我们解决的都是“人”的问题 —— 让人更高效、更便捷、更愉快地使用系统。




六、那到底哪个更难?


如果你非要我选一个答案,我只能说:



哪个你不熟,哪个就难。



前端和后端,都有容易入门但难以精进的曲线。你用 jQuery 写个页面不难,但你做一个大型可维护的组件库就难了;你写个 CRUD 接口不难,但你做一个高并发分布式系统就非常难。


真正的难点在于:你愿不愿意持续去深入、去理解、去完善自己的认知体系。




七、写在最后:别问“哪个难”,问“你想走多远”


我见过写前端写到年薪百万的,也见过写后端写到身心俱疲的。


我见过全栈工程师一人顶两人,也见过只会写“增删改查”却年薪 30w 的老哥。


这行最不缺的,就是例外;最需要的,是清醒的自我认知。


别纠结哪个更难,多花时间让自己变强,才是正解。




**你觉得前端难,还是后端难?你有没有在项目里遇到“前后端合作”的那些故事?欢迎评论区聊聊.


作者:天天摸鱼的java工程师
来源:juejin.cn/post/7516897654170222592
收起阅读 »

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

前言 前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。 本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。 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
收起阅读 »

为什么说 AI 时代,前端开发者对前端工程化的要求更高了❓❓❓

web
前端工程化在前端领域地位极高,因为它系统性地解决了前端开发中效率、协作、质量、维护性等一系列核心问题,可以说是现代前端技术体系的基石。前端工程化带来的价值可以从这四个方面看:提升开发效率:模块化开发:通过组件、模块拆分使开发更加清晰,复用性更强。自动化构建:W...
继续阅读 »

前端工程化在前端领域地位极高,因为它系统性地解决了前端开发中效率、协作、质量、维护性等一系列核心问题,可以说是现代前端技术体系的基石。

前端工程化带来的价值可以从这四个方面看:

  1. 提升开发效率:

    • 模块化开发:通过组件、模块拆分使开发更加清晰,复用性更强。
    • 自动化构建:Webpack、Vite 等工具自动处理打包、压缩、转译等。
    • 代码热更新 / HMR:开发过程中能实时看到改动,节省调试时间。
  2. 规范团队协作

    • 代码规范检查:如 ESLint、Stylelint 统一代码风格,避免“风格大战”。
    • Git 提交规范:如使用 commitlint + husky 保证提交信息标准化。
    • 持续集成(CI):如 GitHub Actions、Jenkins 保证每次提交自动测试、构建。
  3. 提升代码质量和可维护性

    • 单元测试 / 集成测试:如 Jest、Cypress 确保代码稳定可靠。
    • 类型系统支持:TypeScript 保证更严格的类型检查,降低 Bug 率。
    • 文档生成工具:如 Storybook、jsdoc 方便维护和阅读。
  4. 自动化部署与运维

    • 自动化构建发布流程(CI/CD)使得上线更安全、更快速。
    • 多环境配置管理(开发/测试/生产)更加方便和稳定。

总的来说,前端工程化让开发者从单纯的 “切图仔” 成长为能够参与大型系统开发的工程师。通过引入规范与工具,不仅显著提升了团队协作效率,还有效减少了开发过程中的冲突与返工,成为现代前端团队协作的 “润滑剂”

什么是前端工程化

前端工程化 大约在 2018 年前后在国内被广泛提出,其核心是将后端成熟的软件工程理念、工具与流程系统性地引入前端开发。

它旨在通过规范、工具链与协作流程,提升开发效率、保障交付质量、降低维护成本。前端工程化不只是技术选型,更是一种体系化、流程化的开发方式。

其核心包括代码规范、自动化构建、模块化设计、测试体系和持续集成等关键环节。通过工程化,前端从“写页面”转向“做工程”,实现了从个体开发到团队协作的转变。

它不仅优化了前端的生产方式,也推动了大型系统开发中前端角色的重要性。如今,前端工程化已成为现代前端开发不可或缺的基础能力。

为什么 AI 时代,前端工程化更重要

在 AI 时代,前端工程化不仅没有“过时”,反而变得更重要,甚至成为人机协作高效落地的关键基石。原因可以从以下几个方面理解。

虽然 AI 可以辅助生成代码、文档甚至 UI,但它并不能替代工程化体系,原因有:

  1. AI 的代码质量不稳定:没有工程化流程约束,容易引入 Bug 或不一致的风格。
  2. AI 更依赖工程规范作为提示上下文:没有良好的工程结构,AI 输出也会混乱低效。
  3. AI 更像“助理”,而非“工程师”:它执行快,但依然需要工程体系保障产出质量和集成稳定性。

最差的情况下有可能会删除或者修改你之前已经写好的代码,如果缺少这些工程化的手段,你甚至不知道它已经修改你的代码了,最终等到上线的时候无数的 bug 产生。

通过标准化输出让 AI 更智能,清晰的项目结构、代码规范、模块划分能让 AI 更准确地补全、修改或重构代码。例如 ESLint、TypeScript 的规则为 AI 提供了明确的限制条件,有助于生成更高质量的代码。

在生成的代码需要规范,生成完成之后更需要检验,大概的流程也有如下几个方面:

  1. 格式化检查(Prettier、ESLint)
  2. 单元测试(Jest)
  3. 构建打包(Vite/Webpack)
  4. 自动部署(CI/CD)

没有工程化,AI 产出的代码难以被真正“上线使用”。

AI 时代,对一些 CRUD 的简单要求减少了,但是对工程化提出了更高要求。

方面普通时代要求AI 时代新挑战
模块结构清晰划分需辅助 AI 理解上下文
代码规范避免团队矛盾指导 AI 输出符合规范
自动化测试保证功能正确验证 AI 代码不会引发异常
CI/CD 流程提升上线效率确保 AI 代码自动验证上线

前端工程化

接下来我们将分为多个小节来讲解一下前端工程化的不同技术充当着什么角色。

技术选型

在前端工程化中,技术选型看似是一道“选择题”,本质上却关系到项目的开发效率、团队协作和未来的可维护性。对于框架选择而言,建议优先考虑两个关键因素:

  1. 团队熟悉程度:选择你或团队最熟悉的框架,能确保在遇到复杂或疑难问题时,有人能迅速定位问题、解决“坑点”,避免因为不熟悉而拖慢项目进度。
  2. 市场占有率与人才生态:选择主流、活跃度高的框架(如 Vue、React),不仅能更容易找到合适的开发者,还意味着有更丰富的社区资源、第三方生态和维护支持,降低长期人力与技术风险。

统一规范

统一规范又分为代码规范、git 规范、项目规范和 UI 规范。

代码规范

统一代码规范带来的好处是显而易见的,尤其在团队开发中更显重要:

  1. 提升团队协作效率:统一的代码风格能让团队成员在阅读和理解他人代码时无障碍,提高沟通效率,减少因风格差异带来的理解成本。
  2. 降低项目维护成本:规范的代码结构更易读、易查、易改,有助于快速定位问题和后期维护。
  3. 促进高效 Code Review:一致的代码格式可以让审查者专注于业务逻辑本身,而非纠结于命名、缩进等细节。
  4. 帮助程序员自身成长:遵循良好的代码规范,有助于开发者养成系统化的编程思维,提升工程意识和代码质量。

当团队成员都严格遵循统一的代码规范时,整个项目的代码风格将保持高度一致,看别人的代码就像在看自己的代码一样自然顺畅。

为了实现这一目标,我们可以借助工具化手段来强制和规范编码行为,例如使用 ESLint 检查 JavaScript/TypeScript 的语法和代码质量,Stylelint 统一 CSS/SCSS 的书写规范,而 Prettier 则负责自动格式化各类代码,使其保持整洁一致。

这些工具不仅能在编码阶段就发现潜在问题,还能集成到 Git Hook 或 CI 流程中,确保所有提交的代码都符合团队标准。统一规范减少了 code review 中对格式问题的争论,让团队更专注于业务逻辑的优化。

更重要的是,长期在规范的约束下编程,有助于开发者养成良好的工程素养和职业习惯,提升整体开发质量和协作效率。工具是手段,习惯是目标,工程化规范最终是为了让每一位开发者都能写出“团队级”的代码。

除了上面提到的,还有很多相同功能的工具这里就不细说了。

Git 规范

Git 规范主要指团队在使用 Git 进行代码版本管理时,对分支策略、提交信息、代码合并方式等的统一约定,其目标是提升协作效率、降低沟通成本、保障版本可控。

分支管理规范可以遵循如下 Git Flow 模型:

main        # 生产环境分支
develop # 开发集成分支
feature/* # 功能分支
release/* # 发布准备分支
hotfix/* # 线上紧急修复分支

分支重要,提交信息规范也更重要,一份清晰规范的提交信息对后期维护、回滚、自动发布都非常重要,好的提交信息让其他协作人员知道你这个分支具体做了什么。

推荐使用 Conventional Commits 规范,格式如下:

<type>(): 

常见的  类型:

类型说明
feat新增功能
fix修复 bug
docs修改文档
style格式修改(不影响代码运行)
refactor重构(无新功能或修复)
test添加测试
chore构建过程或辅助工具变动

如下示例所示:

feat(login): 添加用户登录功能
fix(api): 修复接口返回字段错误
docs(readme): 完善项目使用说明

配套的工具推荐如下表所示:

工具作用
Commitlint校验提交信息是否符合格式规范
HuskyGit 钩子管理工具(如提交前检查)
lint-staged提交前只格式化/检查改动的文件
Standard Version自动生成 changelog、自动打 tag 和版本号

Git 规范,是让代码“有条不紊”地流动在团队之间的交通规则,是高效协作和持续交付的基础设施。

项目规范

项目规范是对整个项目工程的结构、组织方式、开发约定的一套统一标准,它能帮助团队协作、代码维护、快速上手和高质量交付。

项目目录结构规范可以让项目保持统一、清晰的项目目录结构,有助于快速定位文件、分工协作,如下是一个简单的目录规范:

src/
├── assets/ # 静态资源(图片、字体等)
├── components/ # 可复用的基础组件
├── pages/ # 页面级组件
├── services/ # API 请求模块
├── utils/ # 工具函数
├── hooks/ # 自定义 hooks(React 项目)
├── styles/ # 全局样式
├── config/ # 配置文件(如常量、环境变量)
├── router/ # 路由配置
├── store/ # 状态管理(如 Vuex / Redux)
└── main.ts # 应用入口

这只是一个很简答也很通用的目录结构,还有很多进阶的目录结构方案。

命名方式,这个可以根据不同的团队不同的风格来指定。

部署

借助自动化流程实现一键部署或者自动部署,常用的工具主要有以下:

  1. GitHub Actions
  2. GitLab CI
  3. Jenkins

流程通常如下:

Push → 检查代码规范 → 构建 → 运行测试 → 上传产物 → 通知部署 → 上线

可以参考一下 Action 配置:

name: Deploy Next.js to Alibaba Cloud ECS

on:
push:
branches:
- main

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4.2.0

- name: Set up Node.js
uses: actions/setup-node@v4.2.0
with:
node-version: "22.11.0"

- name: Install pnpm
run: npm install -g pnpm@9.4.0

- name: Deploy to server via SSH
uses: appleboy/ssh-action@v1.2.1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
port: ${{ secrets.SERVER_PORT }}
password: ${{ secrets.SERVER_PASSWORD }}
script: |
# 显示当前环境信息
echo "Shell: $SHELL"
echo "PATH before: $PATH"

# 加载环境配置文件
source ~/.bashrc
source ~/.profile

# 如果使用 NVM,加载 NVM 环境
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# 添加常见的 Node.js 安装路径到 PATH
export PATH="$HOME/.nvm/versions/node/*/bin:/usr/local/bin:/usr/bin:/bin:$HOME/.npm-global/bin:$PATH"
echo "PATH after: $PATH"

# 查找 npm 的位置
which npm || echo "npm still not found in PATH"

# 使用绝对路径查找 npm
NPM_PATH=$(find /usr -name npm -type f 2>/dev/null | head -1)
if [ -n "$NPM_PATH" ]; then
echo "Found npm at: $NPM_PATH"
export PATH="$(dirname $NPM_PATH):$PATH"
fi

# 确保目标目录存在
mkdir -p /home/interview-guide
cd /home/interview-guide

# 如果本地仓库不存在,进行克隆
if [ ! -d "/home/interview-guide/.git" ]; then
echo "Cloning the repository..."
# 删除可能存在的空目录内容
rm -rf /home/interview-guide/*
# 使用 SSH 方式克隆
git clone git@github.com:xun082/interview-guide.git .
else
# 确保远程 URL 使用 SSH
git remote set-url origin git@github.com:xun082/interview-guide.git
# 获取最新代码
git fetch origin main
git reset --hard origin/main
fi

# 使用找到的 npm 路径或尝试直接运行
if [ -n "$NPM_PATH" ]; then
$NPM_PATH install -g pnpm@9.4.0
$NPM_PATH install -g pm2
else
npm install -g pnpm@9.4.0
npm install -g pm2
fi

# 安装依赖
pnpm install || npm install

# 构建项目
pnpm run build || npm run build

# 重启应用
pm2 restart interview-guide || pm2 start "pnpm start" --name interview-guide || pm2 start "npm start" --name interview-guide

这段 GitHub Actions 配置实现了将 Next.js 项目自动部署到阿里云 ECS 服务器的流程。它在检测到 main 分支有新的代码提交后,自动拉取代码、安装依赖并构建项目。随后通过 SSH 远程连接服务器,拉取或更新项目代码,并使用 PM2 启动或重启应用。整个流程自动化,无需人工干预,保障部署高效、可重复。

除了 PM2 之外,我们还可以使用 Docker 镜像部署。

🛡️ 监控

前端监控是指:对 Web 应用在用户真实环境中的运行状态进行实时采集与分析,以发现性能瓶颈、错误异常和用户行为,最终帮助开发团队提升系统稳定性和用户体验。

🚦 性能监控

性能监控的目标是衡量页面加载速度、交互流畅度等关键性性能指标。

常见指标:

  1. 首屏加载时间(FP/FCP)
  2. 页面完全加载时间(Load)
  3. 首次输入延迟(FID)
  4. 长任务(Long Task)
  5. 慢资源加载(如图片、脚本)

它有助于定位性能瓶颈(如资源过大、阻塞脚本)、优化用户体验(如加载缓慢或白屏问题),并支持性能回归分析,及时发现上线后的性能退化。

❌ 错误监控

错误监控的目标是捕捉并上报运行时异常,辅助开发快速修复 Bug。

常见的错误类型主要有以下几个方面:

错误类型示例说明
JS 运行错误ReferenceErrorTypeError 等
Promise 异常unhandledrejection
资源加载失败图片、脚本、字体 404、403
网络请求异常接口失败、超时、断网等
跨域/白屏CORS 错误、DOM 元素为空
控制台报错console.error() 日志监控
用户行为异常点击无响应、重复操作、高频异常等

假设我们使用了 fetch 进行封装,那么我们就可以对错误进行统一处理,后续我们可以再具体调用的时候根据不同的场景来传入不同的错误提示告知用户:

dc087a4417765c239c2d104ee5d03548

错误上报

数据上报是指前端在运行过程中将采集到的监控信息(性能、错误、行为等)发送到服务端的过程。它是前端监控从“收集”到“分析”的桥梁。

上报的数据类型主要有以下几个方面:

类型说明
性能数据页面加载时间、资源加载时间、Web Vitals 等
错误信息JS 异常、Promise 异常、请求失败、白屏等
用户行为点击、跳转、页面停留时间、操作路径等
自定义事件特定业务事件,如支付、注册等
环境信息浏览器版本、设备类型、操作系统、用户 IP 等

数据上报需要重点考虑的几个关键因素:

  1. 怎么上报(上报方式)

    • 使用 sendBeacon、fetch、img 打点还是 WebSocket?
    • 是否异步?是否阻塞主线程?
    • 是否需要加密、压缩或编码?

建议:选择 异步非阻塞 且浏览器支持好的方式(优先 sendBeacon),并对数据做统一封装处理。

  1. 何时上报(上报时机)

    • 立即上报:错误发生后马上发送(如 JS 报错)
    • 延迟上报:页面稳定后延迟几秒,防止干扰首屏加载
    • 页面卸载前上报:用 sendBeacon 上报用户停留数据等
    • 批量上报:积累一批数据后统一发送,减少请求频率
    • 定时上报:用户停留一段时间后定期上报(行为数据)

建议:根据数据类型区分时机,错误即时上报、性能延迟上报、行为数据可批量处理。

  1. 上报频率控制(防抖 / 节流 / 采样)

    • 错误或点击频繁时可能产生大量上报请求
    • 需要加防抖、节流机制,或采样上报(如只上报 10% 用户)

🔍 建议:对于高频行为(如滚动、点击),加防抖或只上报部分用户行为,避免拖垮前端或服务端。

  1. 异常处理与重试机制:遇到网络断开、后端失败等应支持自动重试或本地缓存,可将数据暂存至 localStorage,等网络恢复后重发
  2. 数据结构设计:统一字段格式、数据类型,方便服务端解析,包含上下文信息:页面 URL、用户 ID、浏览器信息、时间戳等,如下所示:
{
"type": "error",
"event": "ReferenceError",
"message": "xxx is not defined",
"timestamp": 1716280000000,
"userId": "abc123",
"url": "https://example.com/home"
}

总的来说,数据上报是前端监控的核心环节,但只有在合适的时机,用合适的方式,上报合适的数据,才能真正发挥价值。


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

我本是写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成为下一代前端的基础设施,愿你是最早站在三维坐标系里的那个人。”


作者:curdcv_po
来源:juejin.cn/post/7517209356855164978
收起阅读 »

震惊,中石化将开源组件二次封装申请专利,这波操作你怎么看?

一. 前言昨天看到了一篇关于 “中石化申请基于 vue 的文件上传组件二次封装方法和装置专利,解决文件上传功能开发繁琐问题” 的新闻。今天特地在专利系统检索了一下,竟然是真的,令人不禁大跌眼镜!用的全是开源组件,最后还把它们变成了自己的专利!这波操作属实厉害啊...
继续阅读 »


一. 前言

昨天看到了一篇关于 “中石化申请基于 vue 的文件上传组件二次封装方法和装置专利,解决文件上传功能开发繁琐问题” 的新闻。

今天特地在专利系统检索了一下,竟然是真的,令人不禁大跌眼镜!用的全是开源组件,最后还把它们变成了自己的专利!这波操作属实厉害啊!

image.png

image.png

难道以后要用这种方式上传文件,要交专利费了?哈哈....

说来好笑,有掘友指出有单词拼写错误,我又查看一下专利文件,竟然还真有拼写错误...

image.png

二. 了解一下

本专利是通过在 vue 页面中自定义 el-upload 组件和 el-progress 组件的使用,解决了文件上传功能开发步骤繁琐和第三方组件无法满足业务需求的问题,实现了简化开发、提高效率和灵活性的效果。

1. 摘要

本发明提供了一种基于 vue 的文件上传组件的二次封装方法和装置,解决了针对于文件上传功能的开发步骤繁琐,复杂,且上传功能的第三方组件无法完全满足业务需求的问题。

该基于 vue 的文件上传组件的二次封装方法包括:在 vue 页面中创建 el‑upload 组件和 el‑progress 组件;

基于所述 el‑upload 组件获取目标上传文件的大小,并判断所述目标上传文件的大小是否符合上传标准;若是,上传所述目标上传文件,并基于所述 el‑progress 组件获取上传进度;上传完成后,对上传的所述目标上传文件进行预处理并存储;

对存储的所述目标上传文件进行封装,并获得 vue 组件。

技术流程图:

Snipaste_2025-06-12_17-07-28.png

二次封装装置模块:

image.png

2. 解决的技术问题

现有技术中文件上传功能的开发步骤繁琐复杂,第三方组件无法完全满足业务需求。

3. 采用的技术手段

通过在 vue 页面中引入 el-upload 组件和 el-progress 组件,自定义上传方法和进度条绑定,获取文件大小和上传进度,进行预处理和存储,并将其封装成可重复使用的 vue 组件。

4. 产生的技术功效

简化了文件上传功能的开发步骤,节省了开发时间和效率,避免了代码沉冗,降低了后期维护成本,并提高了文件上传功能的灵活性。

三. 实现一下

这种简单的上传文件+上传进度显示不是最基本的业务封装吗?相信这是每个前端开发工程师必备的基础技能。

1. el-upload + el-progress 组合

  • el-upload 负责文件选择、上传。
  • el-progress 负责展示上传进度。

2. 文件大小校验

  • 使用 el-upload 的 before-upload 钩子,判断文件大小是否符合标准。

3. 上传进度获取

  • 使用 el-upload 的 on-progress 钩子,实时更新进度条。

4. 上传完成后的预处理与存储

  • 上传完成后,触发自定义钩子(如 beforeStoreonStore),进行预处理和存储。

5. 封装为 Vue 组件

  • 通过 props、emits、插槽等方式,暴露灵活的接口,便于业务页面集成。

都懒得自己动手,让 Cursor 来实现一下。Cursor 还是一如既往的强大,基本上一次询问就能成功!我表示 Cursor 在手,天下我有!

113.gif

UploaderWrapper 自定义组件:



<template>
<div class="file-uploader">
<ElUpload
:action="action"
:before-upload="beforeUpload"
:on-progress="handleProgress"
:on-success="handleSuccess"
:on-error="handleError"
:limit="limit"
:on-exceed="handleExceed"
:show-file-list="showFileList"
:multiple="multiple"
:accept="accept"
v-model:file-list="fileList"
:on-remove="handleRemove"
>

<template #trigger>
<ElButton type="primary"> 选择文件上传 ElButton>
template>

<template #tip>
<div class="el-upload__tip">
支持的文件类型: {{ accept }},单个文件不超过 {{ maxSize }}MB
div>
template>
ElUpload>

<ElProgress
v-if="isUploading"
:percentage="uploadPercent"
:status="uploadPercent === 100 ? 'success' : ''"
class="mt-4"
/>

div>
template>

<style scoped>
.file-uploader {
width: 100%;
}
.el-upload__tip {
font-size: 12px;
color: #606266;
margin-top: 8px;
}
style>

使用方式:



<template>
<ElCard class="mb-5 w-80">
<template #header> 文件上传演示 template>
<UploaderWrapper
action="/api/upload"
:max-size="5"
:before-store="beforeStore"
:on-store="onStore"
/>

ElCard>
template>

效果如下所示:

119.gif

声明:“代码仅供演示,不要使用,以免有专利侵权风险,慎重!”

四. 思考一下

从开发者的角度来看,这个专利事件是否能给我们带来了一些值得思考影响和启示:

  1. 技术创新的边界问题
  • 使用开源组件进行二次封装是否应该被授予专利?
  • 是否对开源社区的发展可能产生负面影响?
  1. 对日常开发的影响
  • 如果专利获得授权,其他公司使用类似的文件上传组件封装方案是否可能面临法律风险?
  • 开发者是否需要寻找替代方案或支付专利费用?
  1. 对开源社区的影响
  • 可能打击开发者对开源项目的贡献热情,自己辛苦开源项目为别人做了嫁衣?
  • 是否会影响开源组件的使用和二次开发
  • 可能导致更多公司效仿,将开源组件的二次封装申请专利,因为毕竟专利对公司的招投标挺大的

五. 后记

“中石化作为传统能源企业,都能积极拥抱前端技术,还将内部技术方案申请专利,体现了他们对知识产权的重视?”

那我们是不是要在技术创新和知识产权保护之间找到平衡点,既要保护创新,又不能阻碍技术的发展。

而作为开发者的我们呢?这么简单的封装都能申请专利成功的话,那么...,大家有什么想法,是不是现在强的可怕?哈哈...

专利来源于国家知识产权局

申请公布号:CN120122937A


作者:前端梦工厂
来源:juejin.cn/post/7514858513442078754
收起阅读 »

润开鸿亮相HDC2025 以AI驱动基于开源鸿蒙的行业新“智”场景创新

2025年6月20日至22日,第七届华为开发者大会(HDC.2025)在东莞松山湖盛大举行。作为鸿蒙生态的核心伙伴和深度参与者、华为OpenHarmony使能伙伴、华为HarmonyOS开发服务商,江苏润开鸿数字科技有限公司(以下简称“润开鸿”)以专题演讲、成...
继续阅读 »

2025年6月20日至22日,第七届华为开发者大会(HDC.2025)在东莞松山湖盛大举行。作为鸿蒙生态的核心伙伴和深度参与者、华为OpenHarmony使能伙伴、华为HarmonyOS开发服务商,江苏润开鸿数字科技有限公司(以下简称“润开鸿”)以专题演讲、成果展示、CodeLabs训练营等多元形式参会,充分展示了自身基于“OpenHarmony+AI” “OpenHarmony+星闪”的数智化解决方案及赋能服务等鸿蒙生态创新成果。

在本届HDC大会期间,HarmonyOS 6 Developer Beta面向开发者启动报名,其新特性将为千行百业的鸿蒙应用带来更多创新可能;同时,鸿蒙星光计划也正式推出,将投入总额1亿元的现金和资源,支持更多校园开发者和创意人才开发鸿蒙应用。

当前,更多“生而不同”的创新体验正在鸿蒙生态中不断涌现。作为领先的鸿蒙方向专业技术公司及终端操作系统发行版提供商,以及开放原子开源基金会OpenHarmony项目群A类捐赠人和核心共建单位,润开鸿于本届大会上重磅发布了基于OpenHarmony的智慧中医AI诊疗解决方案。该方案以基于OpenHarmony的智慧中医辅助诊疗机器人为核心,构建覆盖三甲医院至社区医院的一体化中医诊疗服务体系,通过“OpenHarmony+AI”驱动场景全要素融合,创新重塑中医诊疗“诊前→诊中→诊后”全流程服务体验,打破医疗资源分布不均现状,运用数智化技术让优质中医资源得以广泛共享,提升整体中医诊疗效率与专业能力一致性,为患者提供便捷、高效的中医医疗个性化定制服务。在大会互动体验区开源鸿蒙成果展区现场,润开鸿展示了基于OpenHarmony的智慧中医AI诊疗解决方案核心设备——智慧中医AI诊疗机器人,并通过其与HarmonyOS平板的互联协作,直观呈现了以“OpenHarmony+AI”驱动新“智”场景创新的典型范例。

润开鸿基于OpenHarmony的智慧中医AI诊疗解决方案重磅发布

润开鸿基于OpenHarmony的智慧中医AI诊疗解决方案重磅发布

润开鸿基于OpenHarmony的智慧中医AI诊疗机器人现场体验

同时,润开鸿面向青少年信息科技教育全新打造的星闪场景DEMO——基于源师兄的“使命召唤”无人设备指挥控制方案也于大会互动体验区首发亮相。该方案作为基于“OpenHarmony+星闪”的信息科技教学场景,通过润开鸿OH-CODE图形化编程平台接入控制多形态、多类型智能终端,应用星闪(NearLink)互联实现基于“源师兄”的跨场景一体化协同,凸显了 “OpenHarmony+星闪”创新构建智能物联场景的技术潜力。

润开鸿基于源师兄的“使命召唤”无人设备指挥控制方案首发亮相

互动展区亮点展品

润开鸿矿鸿物联管理平台

润开鸿矿鸿物联管理平台

润开鸿基于OpenHarmony的姿态机器人

润开鸿基于OpenHarmony的姿态机器人

润和软件星闪科技纸笔互动解决方案

润和软件星闪科技纸笔互动解决方案

值得关注的是,本届大会还为与会开发者设计了丰富的专题论坛及技术公开课等活动议程,在“【鸿蒙生态产业场景创新方案】生态共建,赋能产业数字化变革”专题论坛上,润开鸿副总裁于大伍在以“OpenHarmony+AI:探索行业新智硬件无限可能”为题的主题演讲中表示,OpenHarmony作为契合AI时代的绝佳数字底座,天然具备高能效、强智能、自协同三大优势,弥补了传统操作系统“重”“笨”“杂”的问题。同时,随着AI应用的深入,AI大模型已经从云端逐步走向“云”“边”“端”协同进化,润开鸿基于多年在OpenHarmony领域积累的经验以及在AI行业大模型的实践,打造了鸿锐AI云桌面、鸿锐AI box、鸿锐AI平板以及AI PC等基于“OpenHarmony+AI”的多款产品,并形成了面向金融、电力、医疗、办公、智慧城市等多场景的“云”“边”“端”协同全栈AI交付方案。

润开鸿副总裁于大伍做主题演讲

润开鸿副总裁于大伍做主题演讲

在“【星闪】打造全场景新体验,星闪开放能力繁荣鸿蒙生态”专题论坛上,江苏润和软件股份有限公司副总裁刘洋以“润和软件聚力生态协同,构建可持续开发者支持体系”为题做主题演讲。他表示,润和软件以“OpenHarmony+星闪”生态为基石,构建“可学、可用、可商用”的开发者支持体系,切实降低行业智能化转型门槛。聚焦工业、能源、教育等场景打造行业落地案例,加速场景化创新与人才储备。未来,将持续聚力生态协同,驱动技术深度赋能千行百业,为产业数字化升级注入可持续创新动能。

润和软件副总裁刘洋做主题演讲

润和软件副总裁刘洋做主题演讲

在“【人才赋能】携手共育共拓HarmonyOS创新人才”专题论坛上,润开鸿生态技术专家徐建国以“领航HarmonyOS技术布道,使能开发者持续创新”为题,分享了自身从社区开发者一步步成长为鸿蒙应用技术专家,并作为华为HDE反哺社区,助力更多开发者突破技术难点、共拓鸿蒙生态及应用落地的成长与贡献历程。

润开鸿生态技术专家徐建国做主题演讲

润开鸿生态技术专家徐建国做主题演讲

同时,在本届大会同期举办的开发者技术挑战及互动活动中,润开鸿为开发挑战赛带来了鸿蒙设备上云实战赛项——“基于润开鸿DAYU800A实现智能家居多端联动与云端智能响应方案开发”。该项目要求开发者通过集成IoT Device SDK,结合DeepSeek、人脸识别等功能,快速实现鸿蒙设备联网上云及云端响应,重点检验开发者结合场景实际,实现“云、边、端”交互体验的智能家居新“智”场景综合开发能力。

“基于润开鸿DAYU800A实现智能家居多端联动与云端智能响应方案开发”挑战赛现场

“基于润开鸿DAYU800A实现智能家居多端联动与云端智能响应方案开发”挑战赛现场

伴随HarmonyOS6 Developer Beta启动报名,将支持更多鸿蒙开发者、各领域生态伙伴不断推出更丰富、更具创意的新功能、新体验,让用户在出行、娱乐、办公、购物等场景下的体验全面焕新。润开鸿充分认同鸿蒙生态“持续开放、协同共建”的发展理念,将持续聚焦国产自主数字底座与AI等前沿技术的场景融合与赋能,以开源鸿蒙行业发行版驱动和牵引行业硬件与终端设备的创新,为行业创造新“智”生产工具。

收起阅读 »

31 岁,写了 8 年代码的我,终于懂了啥叫成功

31 岁,写了 8 年代码的我,终于懂了啥叫成功 现在每天下午六点,我准时关了 IDEA,开车穿过 4 公里的晚高峰,20 分钟就到小区。 一、去年那个手忙脚乱的夏天,我差点错过儿子的成长 去年 5 月 23 号,老婆生了,是个儿子,我在产房陪产,当时是又激动...
继续阅读 »

31 岁,写了 8 年代码的我,终于懂了啥叫成功


现在每天下午六点,我准时关了 IDEA,开车穿过 4 公里的晚高峰,20 分钟就到小区。


一、去年那个手忙脚乱的夏天,我差点错过儿子的成长


去年 5 月 23 号,老婆生了,是个儿子,我在产房陪产,当时是又激动,又紧张。初为人父的兴奋劲还没过,一周的陪产假结束就被加班打回原形。在原来的公司,我每天像个陀螺似的转,写接口、改 bug、开不完的会,常常凌晨才回家。儿子六个月大的时候,有天我凌晨一点推门进去,看见他趴在婴儿床上,小屁股撅得老高,枕边还放着我落在家里的工牌 —— 他把工牌上的照片啃得皱巴巴的,估计是想闻闻爸爸的味道。


那时候我才惊觉,儿子第一次会翻身、第一次长出小牙、第一次喊 "妈妈",这些重要的时刻我全错过了。有次老妈发视频给我,说儿子扶着婴儿床站起来了,摇摇晃晃像个小企鹅,我却在会议室跟产品经理掰扯接口设计,只能匆匆说一句 "知道了",挂了视频心里堵得慌。


二、当 "加班换高薪" 不如 "陪娃玩半小时",我果断选择了后者


有天晚上,我看着怀里这个小生命,突然觉得自己像个失败的程序员:写了八年代码,能优化千万级流量的接口,却连儿子的成长日志都没空更新。


咬咬牙辞了高薪 996,找了家朝九晚五的公司,月薪少了 25%,但胜在能准时下班。每天开车回家的路上,车载广播放着儿歌,我跟着瞎唱,儿子坐在安全座椅上咯咯笑,口水顺着下巴流到围兜上 —— 这 20 分钟的车程,比以前凌晨三点在高速上开代驾幸福一万倍。


三、现在的 "躺平" 生活,比任何技术方案都更让我有成就感


每天吃完晚饭,我雷打不动带儿子去小区遛弯。他刚学会走路,小区里儿子是我见过走的最早的那个,十一个月就开始走了,摇摇晃晃像个小醉汉,那一刻我觉得,以前追求的那些高薪、职级,在这双小手面前,根本不值一提。


周末带他去公园,他坐在草地上玩树叶,我陪他一起捡形状好看的,夹在笔记本里做标本。老妈总说我 "以前写代码的脑子,现在全用来研究怎么让娃多吃两口饭",可我觉得这才是正经事:以前写的代码可能过两年就被重构了,但儿子现在喊的每一声 "爸爸",都是永远存放在我心里的温暖记忆。


四、给新人讲技术时,我现在总提 "家庭并发量"


现在带新人,他们总问我 "怎么才能快速升职加薪",我会指着电脑桌面上儿子的照片说:"先学会给生活做负载均衡。" 以前我总觉得 "躺平" 是贬义词,现在才明白,拒绝无效加班,把时间留给家人,不是躺平,是给人生做了一次关键优化。


有次朋友问我:"你现在不焦虑吗?工资少了这么多。以前挣得多却总焦虑,怕被裁员、怕技术落后;现在挣得少却踏实,因为我没错过儿子的每一个第一次。你说,是银彳亍卡里的数字重要,还是孩子看见你时眼里的光重要?"


结语:31 岁,我的 "成功" 代码里只有一行注释


现在我的键盘上,贴着儿子百日照,每次敲代码时看见,心里都软乎乎的。写了八年 Java,终于懂了:成功不是简历上的项目经验,是能记住儿子每天的小变化;不是会议室里的技术汇报,是陪他在小区里看星星的夜晚;不是职级表上的晋升,是他跌跌撞撞跑向我时,张开的那双小胳膊。


31 岁这年,我把人生代码重构了一次。新版本没有复杂的架构,没有华丽的优化,只有一行简单的注释:家人的笑容,才是这辈子最稳定的依赖。 至于工资少了?没关系,儿子的笑声,比任何高薪都更值钱。


现在摸鱼时,我总会一遍遍翻看他从一岁到现在的照片和视频,或许我算不上技术精湛的程序员,但在工作间隙反复回味这些影像时,我愈发坚定:我一定要成为小时候自己渴望拥有的那种父亲。当然我的父亲也很优秀,买房买车都有赞助,只是他们那一代永远有个好心也不会有个好颜色,懂得都懂,哈哈。


作者:天天摸鱼的java工程师
来源:juejin.cn/post/7511903601452630025
收起阅读 »

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

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
收起阅读 »

同志们,我去外包了

同志们,我去外包了 同志们,经历了漫长的思想斗争,我决定回老家发展,然后就是简历石沉大海,还好外包拯救了我,我去外包了! 都是自己人,说这些伤心话干嘛;下面说下最近面试的总结地方,小小弱鸡,图一乐吧。 首先随着工作年限的增加,越来越多公司并不会去和你抠八股文...
继续阅读 »

同志们,我去外包了


同志们,经历了漫长的思想斗争,我决定回老家发展,然后就是简历石沉大海,还好外包拯救了我,我去外包了!


Xbw8OtYtcYAVZ0dCwFJzXwc8bad653b209f07472ec09fd8e712492.jpg


都是自己人,说这些伤心话干嘛;下面说下最近面试的总结地方,小小弱鸡,图一乐吧。

首先随着工作年限的增加,越来越多公司并不会去和你抠八股文了(那阵八股风好像停了),只是象征性的问几个问题,然后会对照着项目去问些实际的问题以及你的处理办法。
(ps:(坐标合肥)突然想到某鑫面试官问我你知道亿级流量吗?你怎么处理的,听到这个问题我就想呼过去,也许读书读傻了,他根本不知道亿级流量是个什么概念,最主要的是它是个制造业公司啊,你哪来的亿级流量啊,也不知道问这个问题时他在想啥,还有某德(不是高德),一场能面一个小时,人裂开)


好了,言归正传,咱说点入职这家公司我了解到的一点东西,我分为两部分:代码和sql;


代码上


首先传统的web项目也会分前端后端,这点不错;


1.获取昨天日期


可以使用jdk自带的LocalDate.now().minusDays(-1)
这个其实内部调用的是plusDays(-1)方法,所以不如直接就用plusDays方法,这样少一层判断;



PS:有多少人和我之前一样直接new Date()的。



2.字符填充


apache.common下的StringUtils的rightPad方法用于字符串填充使用方法是StringUtils.rightPad(str,len,fillStr)
大概意思就是str长度如果小于len,就用fillStr填充;



PS:有多少人之前是String.format或者StringBuilder用循环实现的。



3.获取指定年指定月的某天


获取指定年指定月的某天可以用localDate.of(year,month,day),如果我们想取2025年的五月一号,可以写成LocalDate.of(2025, 5, 1),那有人可能就想到了如果月尾呢,LocalDate.of(2025, 5, 31)也是可以的,但是我们需要清楚知道这个月有多少天,比如说你2月给个30天,那就会抛异常;
麻烦;


12.jpg
更好的办法就是先获取第一天,然后调用localDate.with(TemporalAdjusters.lastDayOfMonth());方法获取最后一天,TemporalAdjusters.lastDayOfMonth()会自动处理不同月份和闰年的情况;


sql层面的


有言在先,说实话我不建议在sql层面写这种复杂的东西,毕竟我们这么弱的人看到那么长的且复杂的sql会很无力,那种无力感你懂吗?打工人不为难打工人;不过既然别人写了,咱们就学习一下嘛;


1.获取系统日期


首先获取系统日期可以试用TRUNC(SYSDATE)进行截取,这样返回的时分秒是00:00:00,比如2025-05-29 00:00:00,它也可以截取数字,想知道就去自行科普下,不建议掌握,学习了下,有点搞;


2.返回date当前月份的最后一天


LAST_DAY(date)这个返回的是date当前月份的最后一天,比如今天是2025-05-29,那么返回的是2025-05-31
ADD_MONTH(date,11)表示当前日期加上11个月,比如2025-01-02,最终返回的是2025-12-02;


3.左连接的知识点


最后再提个左连接的知识点,最近看懵了,图一乐哈,A left join B,就是on的条件是在join生成临时表时起作用的,而where是对生成的临时表进行过滤;
两者过滤的时机不一样。我想了很久我觉得可以这么理解,on它虽然可以添加条件,但他的条件只是一个匹配条件比如B.age>10;它是不会对A表查询出来的数据量产生一个过滤效果;
而where是一个实打实的过滤条件,不管怎么说都会影响最终结果,对于inner join这个特例,on和where的最终效果一样,因为B.age>10会导致B的匹配数据减少,由于是交集,故会对整体数据产生影响。


好了,晚安,外包打工仔。。。


作者:小红帽的大灰狼
来源:juejin.cn/post/7510055871465308212
收起阅读 »

京东购物车动效实现:贝塞尔曲线的妙用

web
前言 大家好,我是奈德丽。前两天在逛京东想买Pocket 3的时候,注意到了它的购物车动效,当点击"加入购物车"按钮时,一个小红球从商品飞入购物车,我觉得很有意思,于是花了点时间来研究。 实现效果 看了图才知道我在讲什么,那么先看Gif吧! 代码演示 代码已...
继续阅读 »

前言


大家好,我是奈德丽。前两天在逛京东想买Pocket 3的时候,注意到了它的购物车动效,当点击"加入购物车"按钮时,一个小红球从商品飞入购物车,我觉得很有意思,于是花了点时间来研究。


实现效果


看了图才知道我在讲什么,那么先看Gif吧!


JDmall-1.gif


代码演示


代码已经上传到了码上掘金,感兴趣的可以自行查看,文章中没有贴全部代码了,主要讲讲思路,
code.juejin.cn/pen/7503150…


实现思路


下面这个思路,小白也能会,我们将通过以下几个步骤来实现这个效果:


画页面——>写逻辑实现动画效果


好了,废话不多说,开始进入正题


第一步:先让AI帮我们写出来UI结构


像我们这种工作1坤年以上的切图仔,能偷懒当然偷懒啦,这种画页面的活可以丢给AI来干了,下面是Taro帮我生成的页面部分,没什么难点,就是一些普普通通的页面元素。


<template>
<div class="rolling-ball-container">
<!-- 商品列表 -->
<div class="item-list">
<div class="item" v-for="item in 10" :key="item">
<div class="product-card">
<div class="product-tag">秒杀</div>
<div class="product-image">
<img src="/product.jpg" alt="商品图片" />
</div>
<div class="product-info">
<div class="product-title">大疆 DJI Osmo Pocket 3 一英寸口袋云台相机</div>
<div class="product-features">
<span class="feature-tag">三轴防抖</span>
<span class="feature-tag">防抖稳定</span>
<span class="feature-tag">高清画质</span>
</div>
<div class="product-price">
<span class="price-symbol">¥</span>
<span class="price-value">4788</span>
<span class="price-original">¥4899</span>
</div>
<div class="product-meta">
<span class="delivery-time">24分钟达</span>
<span class="rating">好评率96%</span>
</div>
<div class="product-shop">京东之家-凯德汇新店</div>
</div>
<div class="add-to-cart" @click="startRolling($event)">+</div>
</div>
</div>
</div>

<!-- 购物车图标 -->
<div class="point end-point">
<div style="position: relative;">
<img src="/cart.png" />
<div class="cart-count">{{ totalCount }}</div>
</div>
</div>

<!-- 小球容器 -->
<div
v-for="(ball, index) in balls"
:key="index"
class="ball"
v-show="ball.show"
:style="getBallStyle(ball)"
></div>
</div>
</template>





第二步:设计小球数据模型


有了页面元素了,我们需要创建小球数组和计数器


import { reactive, ref } from 'vue';

// 购物车商品计数
const totalCount = ref(0);

// 创建小球数组(预先创建3个小球以应对连续点击)
const balls = reactive(Array(3).fill(0).map(() => ({
show: false, // 是否显示
startX: 0, // 起点X坐标
startY: 0, // 起点Y坐标
endX: 0, // 终点X坐标
endY: 0, // 终点Y坐标
pathX: 0, // 路径X偏移量
pathY: 0, // 路径Y偏移量
progress: 0 // 动画进度
})));

为什么小球要用一个数组来存储呢?因为我看到京东上用户是可以连续点击+号将商品加入购入车的,页面上可以同时存在很多个飞行的小球。


第三步:实现动画触发函数


当用户点击"+"按钮时,我们需要计算起点和终点坐标,然后启动动画,这儿有一个细节,为了让小球刚好落到在购物车中间,对终点坐标进行了微调。


// 开始滚动动画
const startRolling = (event: MouseEvent) => {
// 获取起点和终点元素
const startPoint = event.currentTarget as HTMLElement;
const endPoint = document.querySelector('.end-point') as HTMLElement;

if (startPoint && endPoint) {
// 找到一个可用的小球
const ball = balls.find(ball => !ball.show);
if (ball) {
// 获取起点位置
const startRect = startPoint.getBoundingClientRect();
ball.startX = startRect.left + startRect.width / 2;
ball.startY = startRect.top + startRect.height / 2;

// 获取终点位置
const endRect = endPoint.getBoundingClientRect();
const endX = endRect.left + endRect.width / 2;
const endY = endRect.top + endRect.height / 2;
// 微调终点位置
ball.endX = endX - 4;
ball.endY = endY - 7;

// 设置路径偏移量
ball.pathX = 0;
ball.pathY = 100;

// 显示小球并重置进度
ball.show = true;
ball.progress = 0;

// 使用requestAnimationFrame实现动画
let startTime = Date.now();
const duration = 400; // 动画持续时间(毫秒)

function animate() {
const currentTime = Date.now();
const elapsed = currentTime - startTime;
ball.progress = Math.min(elapsed / duration, 1);

if (ball.progress < 1) {
requestAnimationFrame(animate);
} else {
// 动画结束后隐藏小球
setTimeout(() => {
ball.show = false;
}, 100);
}
}

requestAnimationFrame(animate);

// 增加购物车商品数量
totalCount.value++;
}
}
};

第四步:使用贝塞尔曲线计算小球轨迹


点击"+"按钮,不能让小球做自由落体运动吧,那是伽利略研究的,你看这自由落体好看嘛,指定不行,要是长这样,那东哥的商城还能卖出去东西吗?Hah


JDmall-2.gif


为了不让它自由落体,给它一个向左的偏移量100px


// 获取小球样式
const getBallStyle = (ball: any) => {
if (!ball.show) return {};

// 使用二次贝塞尔曲线计算路径
const t = ball.progress;
const mt = 1 - t;

// 判断起点和终点是否在同一垂直线上
const isVertical = Math.abs(ball.startX - ball.endX) < 20;

// 计算控制点(确保有弧度)
let controlX, controlY;

if (isVertical) {
// 如果在同一垂直线上,向左偏移一定距离
controlX = ball.startX - 100;
controlY = (ball.startY + ball.endY) / 2;
} else {
// 否则使用向左偏移
controlX = (ball.startX + ball.endX) / 2 - 100;
controlY = (ball.startY + ball.endY) / 2 + (ball.pathY || 100);
}

// 二次贝塞尔曲线公式
const x = mt * mt * ball.startX + 2 * mt * t * controlX + t * t * ball.endX;
const y = mt * mt * ball.startY + 2 * mt * t * controlY + t * t * ball.endY;

return {
left: `${x}px`,
top: `${y}px`,
transform: `rotate(${ball.progress * 360}deg)` // 添加旋转效果
};
};

技术要点解析


1. 贝塞尔曲线原理


贝塞尔曲线是一种参数化曲线,广泛应用于计算机图形学。二次贝塞尔曲线由三个点定义:起点P₀、控制点P₁和终点P₂。


曲线上任意点的坐标可以通过以下公式计算:


B(t) = (1-t)²P₀ + 2(1-t)tP₁ + t²P₂  (0 ≤ t ≤ 1)

在我们的实现中,通过调整控制点的位置,可以控制曲线的形状,从而实现小球的抛物线运动效果。


2. requestAnimationFrame的优势


与setTimeout或setInterval相比,requestAnimationFrame有以下优势:



  1. 性能更好:浏览器会在最合适的时间(通常是下一次重绘之前)执行回调函数,避免不必要的重绘

  2. 节能:当页面不可见或最小化时,动画会自动暂停,节省CPU资源

  3. 更流畅:与显示器刷新率同步,动画更平滑


3. 动态计算元素位置


我们使用getBoundingClientRect()方法获取元素在视口中的精确位置,这确保了无论页面如何滚动或调整大小,动画始终能准确地从起点到达终点。


总结


通过这个小球飞入购物车的动画效果,我们不仅提升了用户体验,还学习了:



  1. 如何使用贝塞尔曲线创建平滑动画

  2. 如何用requestAnimationFrame实现高性能动画

  3. 如何动态计算元素位置

  4. 如何使用rem单位实现移动端适配


这个小小的交互设计虽然看起来简单,但能大大提升用户体验,让你的电商网站更加生动有趣。从京东商城的灵感到实际代码实现,我们完成了一个专业级别的交互效果。


恩恩……懦夫的味道


作者:奈德丽
来源:juejin.cn/post/7502647033401704484
收起阅读 »

工作两年,最后从css转向tailwind了!

web
菜鸟上班已经两年了,从一个对技术充满热情的小伙子,变成了一个职场老鸟了。自以为自己在不停的学习,但是其实就是学一些零碎的知识点,比如:vue中什么东西没见过、js什么特性没用过、css新出了个啥 …… 菜鸟感觉自己也出现了惰性,就是暂时用不上的或者学习成本比较...
继续阅读 »

菜鸟上班已经两年了,从一个对技术充满热情的小伙子,变成了一个职场老鸟了。自以为自己在不停的学习,但是其实就是学一些零碎的知识点,比如:vue中什么东西没见过、js什么特性没用过、css新出了个啥 ……


菜鸟感觉自己也出现了惰性,就是暂时用不上的或者学习成本比较大的,就直接收藏了,想着后面再来学习;然后那些很快能接收有用的小的知识点,就感觉看过几次就收藏了,后面有用,就来收藏里面翻一下就行!


但是菜鸟最近再来回想才发现,这些其实都是虚的,程序员最重要的应该是思维模式,以及如何把学的东西、好用的东西用起来,找到应用场景,而不是到时候再去找。


正如标题所说,菜鸟其实很早就知道css原子化,但是一直都走不出自己的舒适圈,感觉就写点css也挺好,为什么还要花力气去记别人想好的类名?要是一直用这些,岂不是css知识都忘记完了?


直到我们公司的大佬来了之后,力推tailwind,而菜鸟感觉和大佬的差距真的很大,所以又激起了菜鸟想要学习的兴趣!


怎么从css过渡到tailwind


菜鸟在之前,是很不想使用tailwind的,因为菜鸟感觉里面很多类名需要去记,而且和我之前取类名的方式也不一样!相信大部分人都和菜鸟一样,在用tailwind之前,取类名一般都是和包裹的内容相关的名字,例如:contentBox、title、asideBox ……


前期使用不熟的时候直接打开官网就行:http://www.tailwindcss.cn/docs/instal…


菜鸟告诉大家一个办法,就是别想着去记类名,直接你想要用什么css属性,直接点击搜索即可,敲入你想使用的属性


image.png


多用几次,自然就记住了,而且现在编译器有提示的。用了tailwind之后,只能说句真香,因为再也不会有怎么取名以及有重名的困扰了


tailwind yyds


一开始菜鸟用tailwind,感觉也不是很自由啊!


image.png


菜鸟就感觉这个也太low了吧,我要是想用别的值怎么办?直到菜鸟看到了这个


image.png


基本上有了这个,就可以天下无敌了,想多少就多少,这就是自由的感觉!


反正菜鸟基本上用的都是这个,不管是颜色还是大小,除非比较好记的,例如:w-1、w-2、p-1、p-2、m-1、mr-1 ……


tailwind 自定义类名


有一个问题,就是当类名太多的时候,感觉也不是很好看,这个时候就要用到复杂一点的tailwind,见文档:http://www.tailwindcss.cn/docs/reusin…


image.png


很多地方都用到一样的样式,就适合这种方式!不然直接多写几个类名也不是不能接受!


@layer


这个@layer components是避免样式冲突和被覆盖的作用,菜鸟感觉不好理解,但是你肯定不会去重写tailwind的类名,至于有没有树摇优化那就是菜鸟没有涉猎了,反正就当默认写法比较好理解,一般也确实就是这样写。


image.png


这里也可以看看tailwind4的官网,感觉说得清楚一点:tailwindcss.com/docs/adding…


当然有懂的读者,可以指点江山,激扬文字!


更多函数或指令


tailwind中不止有@layer@apply,只是 菜鸟主要就用了这两个,更多见官网:tailwindcss.com/docs/functi…


类名太多,团队规范


当一个元素类名比较多时,每个人的想法都不一样,那么类名就会比较杂乱,可能每个人都不一样,看着就不是很好,这个时候就要使用自动格式化工具了,让每个人的类名排列顺序都是一样,也避免了不少冲突!


插件地址:github.com/tailwindlab…


只要使用了prettier就可以使用这个,关于prettier的知识可以见:vue3+vite+eslint|prettier+elementplus+国际化+axios封装+pinia


使用tailwind不会忘记css,更是加强css


菜鸟之前对tailwind的误解有点深,其实使用tailwind根本不会降低我们的css水平,相反,你平时多逛逛tailwind官网,反而能发现一些你从未使用过或者使用很少的css属性,你会用tailwind实现,其实就是css会实现,反正都可以增加你对css某个属性的理解,且tailwind还附带了效果示例!


Trae 对 tailwind 的支持


之前的代码


<el-button
:loading="loading"
size="large"
type="primary"
s =tyle="width: 100%"
@click.prevent="handleLogin"
>

<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>

image.png


image.png


实现效果


20250506_164451.gif


感觉Trae对tailwind的支持挺好的,一些简单的效果都可以快速实现!


tailwind 可以替代 scss 等


tailwind4 中有明确的说明,见:tailwindcss.com/docs/compat…


菜鸟只能说tailwind的目标很宏大!


image.png


总结


tailwind使用不难,所以菜鸟也没啥可以写得很多或者很复杂的,菜鸟只是希望这个经历可以让各位新手赶紧掌握tailwind,不是css用不起,而是tailwind更有性价比!


作者:PBitW
来源:juejin.cn/post/7501147702667952168
收起阅读 »

我们又上架了一个鸿蒙项目-止欲

web
我们又上架了一个鸿蒙项目-止欲 止欲介绍 止欲是一款休闲类的鸿蒙元服务,希望可以通过冥想让繁杂的生活慢下来、静下来。 《止欲》从立项到上架总过程差不多两个月,主要都是我们青蓝的小伙伴在工作止欲抽空完成的,已经实属不易了,我们主要开发者都是 00 后,最年轻的...
继续阅读 »

我们又上架了一个鸿蒙项目-止欲


止欲介绍


止欲是一款休闲类的鸿蒙元服务,希望可以通过冥想让繁杂的生活慢下来、静下来。


image-20250604154144296


《止欲》从立项到上架总过程差不多两个月,主要都是我们青蓝的小伙伴在工作止欲抽空完成的,已经实属不易了,我们主要开发者都是 00 后,最年轻的开发者也是才 19 岁。


立项时间是:2025-04-08


image-20250604154712749


上架时间是:2025-06-03


image-20250604154654173


止欲同时也是我们青蓝逐码组织上架的第三个作品了,每个作品都是由初入职场、甚至大学还没有毕业的小伙伴高度参与!


image-20250604161153167


git 日志一览


image-20250604155808917


项目技术细节


项目架构


Serenity/Application/
├── entry/ # 主模块
│ ├── src/main/
│ │ ├── ets/ # TypeScript源码
│ │ │ ├── entryability/ # 应用入口能力
│ │ │ ├── entryformability/ # 服务卡片能力
│ │ │ ├── pages/ # 页面文件
│ │ │ ├── view/ # UI组件
│ │ │ ├── utils/ # 工具类
│ │ │ ├── model/ # 数据模型
│ │ │ ├── const/ # 常量定义
│ │ │ └── navigationStack/ # 导航栈管理
│ │ └── resources/ # 资源文件
│ └── module.json5 # 模块配置
├── EntryCard/ # 服务卡片模块
├── AppScope/ # 应用级配置
└── oh-package.json5 # 依赖管理

技术栈



  • 开发语言: ArkTS (TypeScript)

  • UI 框架: ArkUI

  • 构建工具: Hvigor

  • 包管理: ohpm


核心开发套件 (Kit)


本项目使用了多个 HarmonyOS 官方开发套件:


套件名称用途主要 API
@kit.ArkUIUI 框架和导航AtomicServiceNavigation, window
@kit.BasicServicesKit基础服务BusinessError, request
@kit.MediaLibraryKit媒体库访问photoAccessHelper
@kit.CoreFileKit文件操作fileIo
@kit.ImageKit图像处理image.createImageSource
@kit.PerformanceAnalysisKit性能分析hilog
@kit.AbilityKit应用能力UIAbility, abilityAccessCtrl

开发环境要求



  • HarmonyOS SDK: 5.0.1(13) 或更高版本

  • DevEco Studio: 5.0 或更高版本

  • 编译目标: HarmonyOS


开发细节


开始立项


image-20250604160132698


分析如何选型


image-20250604160239603


image-20250604160259889


暴躁起来了


image-20250604160336458


成功上架


image-20250604160436312


后续计划



  1. 接入登录

  2. 接入端云一体

  3. 增加趣味性功能

  4. 代码开源-分享教程


总结


关于青蓝逐码组织


如果你兴趣想要了解更多的鸿蒙应用开发细节和最新资讯甚至你想要做出一款属于自己的应用!欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。


image-20250604160620575


作者:万少
来源:juejin.cn/post/7511779749967347747
收起阅读 »

解锁企业高效未来|上海飞络Synergy AI开启智能体协作新时代

他/她可以有自己的电脑,可以有自己的邮箱号,可以有自己的企业微信号。只要赋予权限,他/她可以替你完成各种日常工作,他/她可以随时随地和你沟通并完成你安排的任务,他/她永远高效!他/她永不抱怨!Synergy AI数字员工雇佣管理平台,以大语言模型驱动的AI A...
继续阅读 »

他/她可以有自己的电脑,可以有自己的邮箱号,可以有自己的企业微信号。只要赋予权限,他/她可以替你完成各种日常工作,他/她可以随时随地和你沟通并完成你安排的任务,他/她永远高效!他/她永不抱怨!

Synergy AI数字员工雇佣管理平台,以大语言模型驱动的AI Agent为核心,结合MCP工具集,并在数据安全、信息安全及行为安全的多维度监控下,为企业提供安全、合规、高效的“智能体员工”,重塑人机协作新范式!

为什么选择Synergy AI数字员工管理平台?

1、智能生产力升级

AI Agent数字员工深度融合语言理解、逻辑推理与工具调用能力,是能够自主感知环境、决策并执行任务的人工智能系统。它可以拥有自己的电脑、邮箱,微信号等所有员工的权限,同时也具备MCP工具集中的各种技能,能够像真人一样沟通,处理工作,但是能够实现更高的工作效率和更加低廉的成本!

2、根据职位定制AI员工工作流

通过“AIGC+Workflow”组合,实现任务自动化执行,响应速度大幅提升,成为企业降本增效的核心引擎。

同时基于企业人员、技能、文档、流程等六大核心信息库,AI数字员工可快速融入业务场景,提供从单职能支持、人机协同到多职能协作的全链路服务。

3、安全合规,全程可控

1)行为监测

实时检测AI数字员工是否存在权限越界、敏感数据操作,信息泄露,被黑客利用等安全合规隐患。

2)数据安全管控

智能识别、过滤、脱敏替换AI数字员工及大语言模型使用过程中触发的敏感数据,企业核心数据泄漏等风险。

3)效能可视化

通过工作流执行情况、人工干预度等指标,持续优化AI员工表现。

Synergy AI能实现什么效果?

1、AI销售助理

可协助销售管理日程、预约会议、统计CRM数字,甚至代替销售联络沟通回款问题。入职飞络销售部门后,内部数据显示客户响应效率提升3倍以上,人力成本降低60%,助力团队精准触达商机。

2、SOC安全及运维专员

在安全运营和运维场景中,AI员工可以迅速响应各个安全系统平台的告警,并根据制定的工作流程,进行下一步的沟通、交流、处置。让企业安全事件响应速度大幅提升,精准提高准确率,为企业筑牢数字防线。

3、更多AI人职位有待解锁

根据每家企业不同的场景需求,Synergy AI提供可以定制化的各种企业AI数字员工,让AI智能体真正能够匹配企业需求,为企业带来实际帮助。

Synergy AI如何落地实施?

1、分析岗位SOW/SOP

找到重复、需要与人互动的工作流,快速实现智能化并通过拟人化的AI员工来完成,逐步将AI工作流覆盖全业务。

2、无缝对接系统

支持OA、ERP、CRM、M365等主流平台MCP / API对接。

3、7×24小时护航

飞络安全运营中心全程监控,保障业务稳定运行。

企业的信息安全如何保护?

飞络基于自研发两大安全管理平台,为企业在使用AI的同时,极大限度保障企业的数据以及隐私安全:

企业AI安全事件监控管理平台

通过企业AI安全事件监控管理平台,我们可以实时提供AI系统以及AI Agents的运行状态,对于所发生的安全事件,实行7*24小时的安全监控及管理。

ASSA:企业AI数据过滤平台

通过ASSA,企业可以管理及管控企业内部信息传输到大语言模型上的数据,对于敏感信息、企业机密、个人信息等进行阻止、脱敏、模糊化等管理操作

7*24 SOC服务

基于飞络提供的7*24级别的SOC运营服务,可以协助客户一起实时监控及管理所有AI相关的安全事件,为企业的数据安全保驾护航!

Synergy AI数字员工雇佣管理平台,以自主研发技术为核心,为企业提供一站式智能解决方案。

收起阅读 »

Chrome AI:颠覆网页开发的全新黑科技

web
Chrome AI 长啥样 废话不多说,让我们直接来看一个示例: async function askAi(question) { if (!question) return "你倒是输入问题啊" // 检查模型是否已下载(模型只需下载一次,就可以供所有...
继续阅读 »

Chrome AI 长啥样


废话不多说,让我们直接来看一个示例:


async function askAi(question) {
if (!question) return "你倒是输入问题啊"

// 检查模型是否已下载(模型只需下载一次,就可以供所有网站使用)
const canCreate = await window.ai.canCreateTextSession()

if (canCreate !== "no") {
// 创建一个会话进程
const session = await window.ai.createTextSession()

// 向 AI 提问
const result = await session.prompt(question)

// 销毁会话
session.destroy()

return result
}

return "模型都还没下载好,你问个蛋蛋"
}

askAi("玩梗来说,世界上最好的编程语言是啥").then(console.log)
//打印: **Python 语言:程序员的快乐源泉!**

可以看到这些浏览器原生 AI 接口是挂在 window.ai 对象下面的,浏览器自带 AI 模型(要下载),无需消耗开发者的资金去调用 OpenAI API 或者是 文心一言 API等。


由于没有成本限制,想象空间极大扩展。你可以将智能融入网页的每一个环节。例如,实时翻译,传统的 i18n 只能映射静态字符串来支持多语言,对于后端传过来的字符串毫无办法,现在可以交给 AI 实时翻译并展示。


未来,这个浏览器 AI 标准接口将不仅限于 Chrome 和 PC 端,其他浏览器厂商也会跟进,手机也将拥有本地运行小模型的浏览器。


Chrome AI 接口文档


我们刚刚看到了 Chrome AI 的调用示例,现在让我们看一下完整的 Chrome 文档。我将用 TypeScript 和注释方式展示,这些类型和注释是我手动编写的,全网独一无二,赶紧收藏


declare global {
interface Window {
readonly ai: AI;
}

interface AI {
/**
* 判断模型是否准备好了
* @example
* ```js
* const availability = await window.ai.canCreateTextSession()
* if (availability === 'readily') {
* console.log('模型已经准备好了')
* } else if (availability === 'after-download') {
* console.log('模型正在下载中')
* } else {
* console.log('模型还没下载')
* }
* ```
*/

canCreateTextSession(): Promise<AIModelAvailability>;

/**
* 创建一个文本生成会话进程
* @param options 会话配置
* @example
* ```js
* const session = await window.ai.createTextSession({
* topK: 50, // 生成文本的多样性,越大越多样
* temperature: 0.8 // 生成文本的创造性,越大越随机
* })
*
* const text = await session.prompt('今天天气怎么样?')
* console.log(text)
* ```
*/

createTextSession(options?: AITextSessionOptions): Promise<AITextSession>;

/**
* 获取默认的文本生成会话配置
* @example
* ```js
* const options = await window.ai.defaultTextSessionOptions()
* console.log(options) // { topK: 50, temperature: 0.8 }
* ```
*/

defaultTextSessionOptions(): Promise<AITextSessionOptions>;
}

/**
* AI模型的可用性
* - `readily`:模型已经准备好了
* - `after-download`:模型正在下载中
* - `no`:模型还没下载
*/

type AIModelAvailability = 'readily' | 'after-download' | 'no';

interface AITextSession {
/**
* 询问 AI 问题, 返回 AI 的回答
* @param input 输入文本, 询问 AI 的问题
* @example
* ```js
* const session = await window.ai.createTextSession()
* const text = await session.prompt('今天天气怎么样?')
* console.log(text)
* ```
*/

prompt(input: string): Promise<string>;

/**
* 询问 AI 问题, 以流的形式返回 AI 的回答
* @param input 输入文本, 询问 AI 的问题
* @example
* ```js
* const session = await window.ai.createTextSession()
* const stream = session.promptStreaming('今天天气怎么样?')
* let result = ''
* let previousLength = 0
*
* for await (const chunk of stream) {
* const newContent = chunk.slice(previousLength)
* console.log(newContent) // AI 的每次输出
* previousLength = chunk.length
* result += newContent
* }
*
* console.log(result) // 最终的 AI 回答(完整版)
*/

promptStreaming(input: string): ReadableStream;

/**
* 销毁会话
* @example
* ```js
* const session = await window.ai.createTextSession()
* session.destroy()
* ```
*/

destroy(): void;

/**
* 克隆会话
* @example
* ```js
* const session = await window.ai.createTextSession()
* const cloneSession = session.clone()
* const text = await cloneSession.prompt('今天天气怎么样?')
* console.log(text)
* ```
*/

clone(): AITextSession;
}

interface AITextSessionOptions {
/**
* 生成文本的多样性,越大越多样,正整数,没有范围
*/

topK: number;

/**
* 生成文本的创造性,越大越随机,0-1 之间的小数
*/

temperature: number;
}
}

如何启用 Chrome AI


准备工作



  1. 下载最新 Chrome Dev 版或 Chrome Canary 版。(版本号不低于 128.0.6545.0)

  2. 确保你的电脑有 22G 的可用存储空间。

  3. 很科学的网络


启用 Gemini Nano 和 Prompt API



  1. 打开 Chrome, 在地址栏输入: chrome://flags/#optimization-guide-on-device-model,选择 enable BypassPerfRequirement,这步是绕过性能检查,确保 Gemini Nano能顺利下载。

  2. 再输入 chrome://flags/#prompt-api-for-gemini-nano,选择 enable

  3. 重启 Chrome 浏览器。


确认 Gemini Nano 是否可用



  1. F12 打开开发者工具, 在控制台输入 await window.ai.canCreateTextSession(),如果返回 readily,就说明 OK 了。

  2. 如果上面的步骤不成功,重启 Chrome 后继续下面的操作:



    • 新开一个标签页,输入 chrome://components

    • 找到 Optimization Guide On Device Model,点击 Check for update,等待一个世纪直到 Status - Component updated 出现就是模型下载完成。(模型版本号不低于 2024.5.21.1031



  3. 模型下载完成后, 再次在开发者工具的控制台中输入await window.ai.canCreateTextSession(),如果这次返回 readily,那就 OK 了。

  4. 如果还是不行,可以等一会儿再试。多次尝试后仍然失败,请关闭此文章🐶。


思考


AI 最近两年可谓是爆发式增长,从 GPT-3 开始,笔者就一直在使用 AI 产品,如 Github copilotChatGPT 推出后,我迅速开发了一个 GPT-Runner vscode 扩展,用于勾选代码文件进行对话。


我一直在思考,AI 能给网页产品带来哪些变革?例如,有没有可能出现一个 AI 组件库,将 AI 智能赋予组件,如 input 框猜测用户下一步输入,或 table 组件实现自然语言搜索和数据拼装。


AI 相关的技术通常需要额外的计算成本,企业主和用户支付意愿低。如果能利用本地算力,就无需额外花费。这个场景现在似乎在慢慢实现。


作为开发者,我们正在迎来 AI 全面赋能网页操作的时代。让我们积极拥抱变化,向老板展示更多的迭代需求,找到前端就业的新增长点。


如果本文章感兴趣者众多,将考虑使用这个 AI 接口实现兼容 OpenAI API 规范,这样你可以不用花钱,不用装 Docker,直接使用浏览器算力和油猴插件免费使用各类开源 chat web ui,如在线版的 Chat-Next-Web


彩蛋


仔细观察 window.ai.createTextSession ,你会发现它为什么不叫 window.ai.createSession ?我猜测未来可能会有 text-to-speech 模型、 speech-to-text 模型、text-to-image 模型、image-to-text 模型,或者更多惊喜。


这不是随便猜测,我是在填写 Chrome AI preview 邀请表时看到的选项。敬请期待吧,各位前端开发er。


作者:小明大白菜
来源:juejin.cn/post/7384997062415843339
收起阅读 »

为了不让同事看到我的屏幕,我写了一个 Chrome 插件

web
那天下午,我正在浏览一些到岛国前端技术文档,突然听到身后传来脚步声。我下意识地想要切换窗口,但已经来不及了——我的同事小张已经站在了我身后。"咦,你在看什么?"他好奇地问道。我尴尬地笑了笑,手忙脚乱地想要关闭页面。那一刻,我多么希望有一个快捷键,能瞬间让整个屏...
继续阅读 »

那天下午,我正在浏览一些到岛国前端技术文档,突然听到身后传来脚步声。我下意识地想要切换窗口,但已经来不及了——我的同事小张已经站在了我身后。"咦,你在看什么?"他好奇地问道。我尴尬地笑了笑,手忙脚乱地想要关闭页面。那一刻,我多么希望有一个快捷键,能瞬间让整个屏幕变得模糊,这样就不会有人看到我正在浏览的内容了。

于是乎我想:为什么不开发一个 Chrome 插件,让用户能够一键模糊整个网页呢?这样不仅能保护隐私,还能避免类似的尴尬情况。

开发过程

说干就干,我开始了 Web Blur 插件的开发。这个插件的核心功能很简单:

  1. 一键切换:使用快捷键(默认 Ctrl+B)快速开启/关闭模糊效果
  1. 可调节的模糊程度:根据个人喜好调整模糊强度
  1. 记住设置:自动保存用户的偏好设置

技术实现

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. 用户界面

  




5px



Current: Ctrl+Shift+B



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);
}
});
}

image.png

以后可以愉快的学技术辣


作者:想想肿子会怎么做
来源:juejin.cn/post/7509042833152851978
收起阅读 »

开源鸿蒙开发者大会2025 | 大屏生态分论坛:共建共享,共赢未来

5月24日,开源鸿蒙开发者大会2025(OHDC.2025)在深圳成功举办。在主论坛上隆重举行了“开源鸿蒙TV SIG”成立仪式,开源鸿蒙TV SIG旨在携手产业伙伴,基于开源鸿蒙社区,构建TV关键技术能力、推动产业标准制定和落地、加强生态合作,促进大屏生态繁...
继续阅读 »

5月24日,开源鸿蒙开发者大会2025(OHDC.2025)在深圳成功举办。在主论坛上隆重举行了“开源鸿蒙TV SIG”成立仪式,开源鸿蒙TV SIG旨在携手产业伙伴,基于开源鸿蒙社区,构建TV关键技术能力、推动产业标准制定和落地、加强生态合作,促进大屏生态繁荣。


开源鸿蒙TV SIG成立仪式

大会期间,同步举办了以“共建共享,共赢未来”为主题的大屏生态分论坛,与会者围绕开源鸿蒙TV SIG技术规划、大屏应用、系统、产品、芯片及配件等关键技术的突破等11个热点议题进行分享与交流,并就未来的发展方向进行了深入探讨,全方位展示基于开源鸿蒙操作系统的大屏生态在创新实践和落地应用方面的成果。本次论坛由开源鸿蒙TV SIG组长、华为终端BG OpenHarmony使能部大屏生态总监汪曙光担任出品人。


大屏生态分论坛圆满举办

华为终端BG OpenHarmony使能部副部长李彦举发表开幕致辞,向所有参与开源鸿蒙大屏生态共建的开发者及企业致谢,并高度评价了开源鸿蒙大屏建设所取得的阶段性成果。基于对大屏行业的深度洞察与开源鸿蒙技术的演进趋势,他指出:“开源鸿蒙大屏产业近年来取得了显著的阶段性成果,已进入高速发展期。”同时,他呼吁更多伙伴和开发者加入生态共建,加速推进大屏生态从技术验证向规模商用的关键跨越,共同把握智慧显示终端的时代机遇。


华为终端BG OpenHarmony使能部副部长李彦举

开源鸿蒙TV SIG组长汪曙光发表题为《开源鸿蒙TV SIG整体规划及最新共建进展》的主题演讲。他表示,TV SIG将以“建能力、立标准、促生态、广复制”为目标,携手SIG成员及广大生态伙伴,搭建社区大屏公共软硬件平台,进行技术的孵化和生态的推动,并展示了SIG组的技术全景图以及路标规划。在演讲中,汪曙光详细介绍了大屏生态已取得的成果:完善了10多个系统应用和专有能力,推出了9个第三方应用(涵盖应用市场、影音娱乐、工具游戏等多个领域)和4颗媒体SoC主芯片(适用于TV、商显、盒子等多种设备)。在演讲最后他提到,基于开源鸿蒙的大屏北向三方应用预计将于今年下半年取得阶段性进展,SIG组也将支撑伙伴孵化出更多商用产品。


华为终端BG OpenHarmony使能部大屏生态总监汪曙光

开源鸿蒙TV SIG副组长、北京风行在线技术有限公司高级技术专家韩超在论坛上做了《风行面向开源鸿蒙的大屏应用市场进展分享》的主题演讲。作为内容运营服务商,风行始终致力于让内容流动更简单,并为数亿用户提供了优质的数字文娱服务。韩超详细介绍了风行在应用开发方面的技术方案和成功实践,并将应用框架源码开源至社区,帮助行业伙伴缩短开发周期。截止目前,风行已推出多款开源鸿蒙大屏应用,其中“橙子市场”作为开源鸿蒙大屏端首个应用市场,旨在为生态伙伴提供内容及应用分发服务,当前已上架的应用涵盖了影视、游戏、教育等多个品类。未来,风行将继续携手行业伙伴,鼓励更多优质应用入驻,持续丰富应用种类,满足用户多样化需求。


北京风行在线技术有限公司高级技术专家韩超

开源鸿蒙TV SIG副组长、华为终端BG智慧交互软件开发部技术专家华红宁带来《面向开源鸿蒙的大屏TV子系统共建进展分享》议题演讲。他详细介绍了华为智慧屏的业务、产品愿景及使命,并表达了通过共建共享,与开源鸿蒙生态伙伴携手推动传统大屏产业升级的期望。与此同时,他还分享了开源鸿蒙TV子系统的架构、业务分层以及核心业务的逻辑,并同步了最新的开发进展和后续规划:通过开源共建,TV子系统已取得显著进展,预计上半年将完成核心功能开发,并将在下半年持续进行功能迭代与完善,后续版本也已规划一系列新特性。他诚邀行业伙伴共同参与代码共建,提升未来产品竞争力。


华为终端BG智慧交互软件开发部技术专家华红宁

开源鸿蒙TV SIG副组长、四川长虹电子控股集团有限公司操作系统高级技术专家张帅做《长虹面向开源鸿蒙的大屏实践分享和后续展望》议题分享,展示了长虹积极拥抱开源鸿蒙的坚定态度。他指出,长虹云计算与大数据研究中心长期深度参与开源鸿蒙大屏社区共建,推动开源鸿蒙在智慧显示终端领域的生态完善。作为重要共建单位,长虹积极贡献遥控器拾音、分布式白板应用等关键技术架构,完善了社区的技术能力。未来,长虹将持续深化与开源鸿蒙社区的合作,聚焦于开源鸿蒙教育大屏、开源鸿蒙TV等有屏设备以及工业智能终端、工业机器人等方向,推动开源鸿蒙能力平台与产业落地深度融合,为构建智能终端操作系统生态贡献力量。


四川长虹电子控股集团有限公司操作系统高级技术专家张帅

开源鸿蒙TV SIG副组长、海思技术有限公司产品规划总监陈超带来《上海海思媒体领域面向开源鸿蒙的探索与实践》主题分享。他详细介绍了上海海思全面拥抱开源鸿蒙的策略,强调了开源鸿蒙与上海海思芯片深度融合的整体解决方案优势。他表述,开源鸿蒙与星闪技术的强强联合,将有力推动IoT、轻智能、泛媒体等领域智能化升级。在泛媒体终端领域,上海海思已有多款媒体类模组/开发板通过开源鸿蒙认证,涵盖了会议平板、闺蜜机、直播机、智慧大屏等多种产品形态。上海海思正全面支持产业产品的创新发展,助力开源鸿蒙生态繁荣壮大。


海思技术有限公司产品规划总监陈超

开源鸿蒙TV SIG成员、未来电视有限公司高级技术专家李欣做了《聚力开源鸿蒙,共启央视频TV未来》议题分享,详细介绍了未来电视在TV端大屏应用——央视频TV的发展历程、平台架构演进,以及开源鸿蒙版本的开发与迭代计划。未来,央视频TV将在其开源鸿蒙版本中逐步集成灵犀触控、互动卡片等创新功能,持续优化用户互动操控体验。此外,央视频TV将依托开源鸿蒙的人脸识别技术,在确保用户隐私安全的基础上为不同用户群体提供个性化的内容推荐和事件推送等陪伴功能,共同构建“有温度”的客厅场景。


未来电视有限公司高级技术专家李欣

开源鸿蒙TV SIG成员、湖南国科微电子股份有限公司资深系统架构师刘杰兵带来《国科微基于开源鸿蒙的芯片平台介绍》议题分享。他指出,基于开源鸿蒙行业发行版的芯片适配是点亮亿级行业设备的关键。作为国内领先的集成电路设计企业,国科微在大型SoC及解决方案开发方面积累了丰富的实践经验,并积极推进开源鸿蒙芯片适配工作,为开源鸿蒙生态建设注入强劲动力。截至目前,国科微已获得5张兼容性测评证书,涵盖机顶盒、电视、商显、摄像头等多个应用场景,实现多个业内首款“开源鸿蒙认证”。未来,国科微将持续提速国科芯开源鸿蒙适配工作,并将于2025年第三季度和第四季度推出基于开源鸿蒙的5.0和5.1商用版本。


湖南国科微电子股份有限公司资深系统架构师刘杰兵

开源鸿蒙TV SIG成员、上海视九信息科技有限公司总裁周云龙发表《面向开源鸿蒙的大屏小程序平台介绍》主题演讲。他介绍了公司研发的JsView引擎,其核心价值在于能为开源鸿蒙生态快速引入丰富的大屏小程序应用。作为在国内主流OTT、IPTV设备稳定运行多年的成熟引擎,JsView以开发周期短、部署升级快、页面流畅、特效丰富等优势,成为行业开发标杆。目前,JsView引擎已完成开源鸿蒙系统适配,基于该平台开发的央视国学苑、唱吧K歌等数十家头部内容的小程序版本,可直接上线开源鸿蒙大屏设备。全新的内容或应用,也可经由该引擎快速开发融入开源鸿蒙生态。


上海视九信息科技有限公司总裁周云龙

开源鸿蒙TV SIG成员、广东辰奕智能科技股份有限公司研究院院长严开云带来《面向开源鸿蒙的大屏配件生态解决方案介绍》,并现场发布首款基于开源鸿蒙的指向语音遥控器及生态配件产品。作为开源鸿蒙TV生态的亮点创新成果,该遥控器具备隔空触控、智慧触摸、近场语音及灵活批注等功能,兼容多类南向应用,灵活适配家庭娱乐场景需求,为用户带来更智能、便捷的交互体验。该产品的发布,充分彰显了开源鸿蒙在智能硬件领域的应用潜力。


广东辰奕智能科技股份有限公司研究院院长严开云

开源鸿蒙TV SIG成员、鸿湖万联(江苏)科技发展有限公司PC及大屏产品总监袁杰做《基于开源鸿蒙的教育及会议大屏产品实践分享》主题分享。据介绍,软通动力充分融合子公司鸿湖万联在开源鸿蒙领域的创新突破以及软通计算机(原同方计算机)的硬件优势,率先推出搭载SwanLinkOS天鸿操作系统的开源鸿蒙智能交互大屏产品,并完成跨系统跨终端形态的多屏协同、集成DeepSeek大模型应用、以及教育应用软件的开源鸿蒙移植工作,为行业客户提供软硬一体端到端的产品及服务能力。


鸿湖万联(江苏)科技发展有限公司PC及大屏产品总监袁杰

开源鸿蒙TV SIG成员、江苏润开鸿数字科技有限公司开源鸿蒙应用架构师傅康带来《基于开源鸿蒙文件管理器与DeepSeek的大屏开发实践》议题分享,深入介绍文件管理器和DeepSeek在开源鸿蒙大屏上的实现方案及核心功能点,并针对大屏设备的交互特点,适配了遥控器走焦、灵犀操控功能。基于DeepSeek的语音助手还支持蓝牙遥控器语音以及ASR语音转文字等功能,显著提升用户操控体验的同时,也赋予大屏如家庭管家、家庭医生和家庭教师等更多角色。


江苏润开鸿数字科技有限公司开源鸿蒙应用架构师傅康

本次大屏生态分论坛内容丰富,活动现场人气火爆,充分彰显了大屏行业伙伴对开源鸿蒙大屏技术发展、生态进展的高度关注与殷切期待。同时,也印证了开源鸿蒙在大屏产业中拥有广阔的发展前景,具备广泛应用与行业创新的巨大潜力。诚挚欢迎更多开发者和企业加入开源鸿蒙TV SIG组,一起“共建共享,共赢未来”!

收起阅读 »

2025 BOE(京东方)全球供应伙伴大会隆重举行 共筑全球显示产业共生共赢新格局

5月28日,备受瞩目的2025年BOE(京东方)全球供应伙伴大会(BOE SPC 2025)在东方帆船之都——青岛盛大启幕。作为半导体显示领域极具影响力的供应链盛会,本届BOE SPC大会以“屏之物联 共生共赢”为主题,汇聚了全球千余位显示行业专家、卓越合作伙...
继续阅读 »

5月28日,备受瞩目的2025年BOE(京东方)全球供应伙伴大会(BOE SPC 2025)在东方帆船之都——青岛盛大启幕。作为半导体显示领域极具影响力的供应链盛会,本届BOE SPC大会以“屏之物联 共生共赢”为主题,汇聚了全球千余位显示行业专家、卓越合作伙伴企业代表及业界精英齐聚一堂,不仅展现了意义深远的行业蓝图、精彩纷呈的主题演讲,BOE(京东方)还对在技术、品质、服务等方面做出杰出贡献的合作伙伴进行表彰,倡导行业协同可持续发展。BOE(京东方)将充分发挥在显示领域的引领作用,携手全球合作伙伴构建可持续的创新生态,树立显示产业技术升级与供应链绿色发展的全新标杆。

BOE(京东方)董事长陈炎顺在致辞中表示:“在当今物联网、人工智能等与产业深度融合的时代,‘屏’已从简单的显示终端进化为智能交互的核心枢纽。我们深知供应伙伴是企业发展的重要支撑,是推动全球产业链协同的关键力量。我们举办SPC大会,旨在搭建一个开放包容、共生共赢的多方交流平台,加强与全球合作伙伴之间的合作,共同探讨未来技术创新、高质发展的有效路径,推动全球产业链不断优化升级。在未来的发展道路上,BOE(京东方)将始终以创新为驱动,以共赢为目的,以质量为基石,并期望以刚发布的半导体显示行业首个可持续发展品牌‘ONE’为纽带,携手全球合作伙伴,构建绿色可持续生态体系,秉承开放、包容、创新理念共同书写产业高质量发展新篇章。”

在“屏之物联 共生共赢”主题演讲环节,BOE(京东方)首席执行官冯强表示:“今天的物联时代,数字化与智能化逐渐成为驱动产业创新的重要引擎。BOE(京东方)紧抓时代机遇,以显示技术为原点,将屏幕硬件与传感系统、大数据分析、优化算法等智能要素深度融合,为客户提供从器件到解决方案的全链条服务,依托器件研发、智能制造、系统集成等核心能力,以‘科技+绿色’引领产业向新,持续突破应用场景边界。多年来,BOE(京东方)始终坚持开放合作,基于‘屏’及周边能力价值延展,强化资源赋能。未来,BOE(京东方)向行业发起倡议,深化技术协同创新,贯彻可持续发展理念,强化产业战略协同,携手全球伙伴共同谱写产业高质发展新篇章。“

经过多年的探索与深耕,BOE(京东方)已在生态共创方面取得了一系列成果,形成了以BOE(京东方)为核心的产业链生态,在技术创新、产业发展、生态构建等方面与合作伙伴开展了深度合作。在活动现场,BOE(京东方)首席采购官张学智对合作伙伴长期以来的大力支持与并肩同行表示感谢,并围绕“稳筑生态求共生 聚势协同谋共赢”主题发表演讲。他表示,BOE(京东方)通过全产业链协同创新,实现了UB Cell创新技术及三折模组产品弯折良率等显示领域的创新突破。在绿色转型方面,BOE(京东方)构建从设计到回收的全生命周期绿色体系。未来,BOE(京东方)将通过技术共研、生态共建的协同模式,持续引领行业向绿色低碳、智能创新方向突破发展。

在可持续发展领域,BOE(京东方)持续践行ESG理念,发布行业首个可持续发展品牌“ONE”(Open Next Earth),以开放凝聚共识,以创新定义未来,以永续守护生态。此次SPC大会,BOE(京东方)也将可持续发展理念融入大会议程的各个环节,携手合作伙伴共同推动产业生态的绿色发展。在“共生共赢:可持续供应链的探索与实践”圆桌论坛中,BOE(京东方)携手德勤、福莱盈、山西宇皓等合作伙伴共议绿色发展新路径。德勤谢安指出,供应链金融与低碳转型是应对全球监管趋势的必然方向。BOE(京东方)副总裁、京东方创新投资有限公司COO赵月明分享了定制化金融解决方案,通过稳定资金链构建“共生”生态;BOE(京东方)副首席建设官李彦则展示了绿色运营成果,并联动京东方能源董事长马亮发布公司新能源战略布局。作为合作伙伴代表,山西宇皓郭伦铭、福莱盈张靖表示,BOE(京东方)“绿色+金融”双轮驱动的模式效果显著,既通过供应链金融服务缓解了资金压力,又以智慧能源解决方案助力抢占绿色市场先机。BOE(京东方)将依托技术创新持续深化供应链赋能,携手伙伴迈向“技术驱动、开放包容”的可持续未来。

为表彰一路同行的合作伙伴,本届BOE SPC特别为近百家优秀合作伙伴颁发“钻石奖”、“杰出战略伙伴奖”、“卓越服务奖”、“卓越品质奖”、 “协同创新奖”、“最佳进步奖”,其中,DNP、杉金、SUNIC凭借深厚的技术底蕴和强大的研发实力荣获BOE(京东方)全球供应伙伴最高荣誉——“钻石奖”。该奖项不仅承载了BOE(京东方)携手合作伙伴共谋发展、共创未来的坚定信念,更是对双方长期合作关系的深度认可。值得关注的是,本次大会共有十余家合作伙伴与BOE(京东方)签署了战略合作协议,未来将不断进行技术合作研发和产品共创,塑造更大的产业价值。

当前,实体经济与数字经济的深度融合正加速重构全球产业格局,以AI、物联网为代表的前沿技术形成颠覆性创新浪潮,推动全产业链向智能化、场景化、生态化方向跃迁。面向未来,BOE(京东方)将持续深耕“屏之物联”发展战略,坚持市场化、国际化、专业化的发展道路,以“科技+绿色”推动显示行业高质永续发展,携手全球合作伙伴在技术创新、可持续发展、绿色供应链能力建设等方面开展深度交流与合作,共赢发展新机遇,开启显示赋能人类美好生活的无限可能。


收起阅读 »

开源鸿蒙开发者大会2025成功召开,启动开源鸿蒙应用技术组件共建

5月24日,开源鸿蒙开发者大会2025(OHDC.2025,以下简称“大会”)在深圳成功举办。开源四年多来,开源鸿蒙代码规模已达 1.3 亿多行,代码贡献者达 8600 多位,超过 1100 款软硬件产品通过兼容性测评,覆盖金融、交通、教育、医疗、航天等多个行...
继续阅读 »

5月24日,开源鸿蒙开发者大会2025(OHDC.2025,以下简称“大会”)在深圳成功举办。开源四年多来,开源鸿蒙代码规模已达 1.3 亿多行,代码贡献者达 8600 多位,超过 1100 款软硬件产品通过兼容性测评,覆盖金融、交通、教育、医疗、航天等多个行业领域,已成为发展速度最快的开源操作系统之一。截至目前,开源鸿蒙已累计发布 8 个大版本,共建共享15个技术域的1115款开源三方库和6个跨平台框架,加速应用和设备的开发。

图片 1.png

会上,开源鸿蒙项目群工作委员会携手包括华为、腾讯端服务、京东、去哪儿、杭州通易科技、东北大学、上海大学、深开鸿、九联开鸿、中科鸿略、诚迈科技、鸿湖万联、润开鸿、开鸿智谷在内的14家共建伙伴,共同启动了开源鸿蒙应用技术组件共建。

图片 2.png

社区的繁荣发展离不开每一位参与其中并积极贡献的开发者,每一行代码都是对开源鸿蒙社区的重要贡献。共建伙伴积极投入开源三方库和跨平台框架的特性开发、版本升级、Upstream上游社区以及社区Issue响应和维护,为开源鸿蒙应用的开发节约重复的代码开发工作量。期望更多的伙伴、开发者加入到开源鸿蒙应用三方库和跨平台框架的共建中,共建共享开源鸿蒙应用生态组件。

M$@I91O{0R`XUD]WSSR80]H.png

期望更多的伙伴、开发者加入到开源鸿蒙应用三方库和跨平台框架的共建中,共建共享开源鸿蒙应用生态组件。

收起阅读 »

“你好BOE”2025首站启幕 助力“横琴-澳门国际数字艺术博览会”打造沉浸式科技艺术新高地

5月26日,BOE(京东方)年度标杆性线下品牌营销活动“你好BOE”2025启动仪式在珠海横琴文化艺术中心举办。作为2025年“你好BOE”首站活动,BOE(京东方)助力首届“横琴-澳门国际数字艺术博览会”,通过超高清显示技术及数字化解决方案为展会三大展区的1...
继续阅读 »

5月26日,BOE(京东方)年度标杆性线下品牌营销活动“你好BOE”2025启动仪式在珠海横琴文化艺术中心举办。作为2025年“你好BOE”首站活动,BOE(京东方)助力首届“横琴-澳门国际数字艺术博览会”,通过超高清显示技术及数字化解决方案为展会三大展区的10余个数字艺术展项提供高端显示技术解决方案,打造沉浸式科技艺术现场,生动传递“以创新科技赋能数字艺术”理念。历经四年迭代,“你好BOE”从最初展现BOE(京东方)自身领先技术、与合作伙伴构建联合生态,到如今更加关注创新技术与应用场景的深度融合,该品牌推广IP已升维至全新的3.0时代。活动现场,此次博览会的主办方南光(集团)有限公司副总经理宋晓冬,阳光媒体集团董事长杨澜,亚洲和平慈善基金会董事会主席李伟杰,参展艺术家代表中央美术学院设计学院艺术与科技方向教授、博士生导师、某集体ART+TECH创始人费俊,京东方科技集团副总裁、MLED业务CEO刘毅,艺云科技副总裁、华南区域总经理刘华等众多嘉宾领导出席“你好BOE”2025年启动仪式,开启BOE(京东方)品牌生态合作新篇章。

京东方科技集团副总裁、MLED业务CEO刘毅在启动仪式上表示,BOE(京东方)深耕横琴,正是因为这里涌动的创新基因与我们不谋而合。“你好BOE”作为BOE(京东方)的品牌IP,从2021年诞生至今,它像一座桥梁,一头是我们深耕的技术,一头是大众对美好生活的期待。我们希望用最直观的方式告诉大家:科技可以很温暖,可以成为连接想象与现实的纽带。

作为大湾区文化融合的标志性活动,“横琴-澳门国际数字艺术博览会”共设四大主题展区。BOE(京东方)作为该活动的战略合作伙伴,以ADS Pro显示技术、MLED显示技术,以及新一代无损Gamma显示专利技术等深度赋能“艺术未来式”“重施魔法”“科技重构艺术”三大展区,为观众带来一场沉浸式、强交互的数字艺术视觉盛宴,成为推动“科技+文化”深度融合的又一典范实践。

进入“艺术未来式”主题展区中,BOE(京东方)MLED显示技术赋能产品随处可见。BOE(京东方)Mini LED显示屏具有高亮低功耗,以及灰度表现更富层次等显示优势,将艺术家王之纲、梁蓝波等多款沉浸式装置作品细腻呈现。艺术家张文超的算法生成交互影像装置《一个传说故事的嬗变》则依托BOE(京东方)高端LCD显示技术解决方案ADS Pro的加持,在8个46英寸3.5mm超窄边拼接屏上,以1200:1超高对比度、178°超大可视角度,以及更均匀的亮度表现,呈现出传说故事在时间中经由无数人的想象而拼贴演绎的动态演变过程。同时,BOE(京东方)以P2.6 LED弧度屏赋能艺术家费俊的互动影像装置《情绪剧场》,在高曲度的LED屏幕上实时检测并呈现观众情绪,创造具有疗愈性的音画体验。此外,黑特·史德耶尔(德)、徐冰、田晓磊等国内外艺术家的数字艺术作品,也均由BOE(京东方)提供高端显示技术支持,多方位展现数字艺术创作领域的亮眼成果。

与此同时,基于BOE(京东方)与故宫博物院的战略合作,又恰逢故宫博物院建院100周年,BOE(京东方)携手故宫博物院打造了“在横琴,看见故宫”数字故宫体验展,BOE(京东方)旗下艺云科技通过多款沉浸式场景显示产品,将故宫景致与珍藏文物展现得活灵活现。在“数字多宝阁”“宫廷文化生活”等展区空间内,观众可通过BOE(京东方)类纸触控画屏,在屏幕上与故宫珍藏文物及交互场景进行触控互动,其运用了BOE(京东方)新一代无损Gamma显示专利技术(专利号:ZL 2016 1 0214546.6),通过高精细数据的细节扩大和360度自由改变的视点,为观众带来沉浸式交互体验。而在“千里江山图”空间,BOE(京东方)P1.5类纸LED不仅逼真地还原出40平方米超长画作中的山水世界,达到屏幕防眩光效果,还能够在相同亮度下节约40%能耗,做到节能环保。同时,BOE(京东方)以高亮度、高透光度的三面透明LED屏,沉浸式营造出“瑞象万千”空间的祥光瑞影,屏幕透光率可达到70%-90%,打造故宫动态叙事空间。除此之外,在“重施魔法”展区、“科技重构艺术”展区以及博览会展览通道等区域,也均能看到BOE(京东方)显示产品赋能的身影。

作为这次博览会的主办方代表,资深媒体人、阳光媒体集团董事长杨澜表示,“本届博览会构建起了一个‘可体验、可思考、可创造’的数字艺术生态系统。数字技术、AI技术的到来,展现了艺术创作的新的可能性,艺术表达的需求也激励了技术的迭代与快速发展。”

作为BOE(京东方)年度标杆性的品牌IP,“你好BOE”自2021年启动以来,已连续5年在北京、上海、深圳、成都、青岛、合肥、巴黎、珠海等国内外城市举办13站巡展活动,并先后与敦煌画院、OUTPUT、318国道、无畏契约、上海国际光影节等各领域顶级IP,以及联想、海信、雷神、ROG等十余家合作伙伴进行跨界合作,线下累计触达近450万消费者。而今年全面焕新升级的“你好BOE”持续关注与合作伙伴的生态共创与场景融合,将携手阳光媒体集团、极氪、OPPO、京东、微博、318等打造品牌生态合作性IP,以跨界联合开启BOE(京东方)品牌全新旅程。面向未来,BOE(京东方)将继续秉承“屏之物联”发展战略,携手各界合作伙伴共建“Powered by BOE”创新生态,以显示技术为纽带,融合物联网及数字技术,让艺术与科技在山顶重逢。

关于BOE(京东方):

京东方科技集团股份有限公司(BOE)是全球领先的物联网创新企业,为信息交互和人类健康提供智慧端口产品和专业服务。作为全球半导体显示产业的龙头企业,BOE(京东方)带领中国显示产业破局“少屏”困境,实现了从0到1的跨越。如今,全球每四个智能终端就有一块显示屏来自BOE(京东方)。在“屏之物联”战略引领下,BOE(京东方)凭借“1+4+N+生态链”业务架构,将超高清液晶显示、柔性显示、MLED、微显示等领先技术广泛应用于交通、金融、艺术、零售、教育、办公、医疗等多元场景,赋能千行百业。目前,BOE(京东方)的子公司遍布全球近20个国家和地区,拥有超过5000家全球生态合作伙伴。更多详情可访问BOE(京东方)官网:https://www.boe.com.cn


收起阅读 »

被问tsconfig.json 和 tsconfig.node.json 有什么作用,我懵了……

web
背景 事情是这样的,前几天在项目例会上,领导随口问了我我一个看似简单的问题: “我们项目里有tsconfig.json 和 tsconfig.node.json ,它们有什么作用?” 活久见,我从来没注意过这个细节,我内心无语,问这种问题对项目有什么用!但机...
继续阅读 »

背景


事情是这样的,前几天在项目例会上,领导随口问了我我一个看似简单的问题:


“我们项目里有tsconfig.jsontsconfig.node.json ,它们有什么作用?”



活久见,我从来没注意过这个细节,我内心无语,问这种问题对项目有什么用!但机智的我还是回答上来了:不都是typescript的配置文件么。


领导肯定了我的回答,又继续问,那为什么项目中有两个配置文件呢?我机智的说,我理解的不深,领导您讲讲吧,我学习一下。


tsconfig.json 是干嘛的?


说白了,tsconfig.json 就是 告诉 TypeScript:我要用哪些规则来“看懂”和“检查”我写的代码。


你可以把它想象成 TypeScript 的“眼镜”,没有它,TS 编译器就会“看不清楚”你的项目到底该怎么理解、怎么校验。



  • 影响代码能不能被正确编译


如果我们用了某些新语法(比如 optional chainingimport type),却没有在 tsconfig 里声明 "target": "ESNext",那 TypeScript 就会报错:看不懂!



  • 影响编辑器的智能提示


如果我们用了路径别名 @/utils/index.ts,但没有配置:


{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

那 VS Code 就会一直红线报错:“找不到模块”。



  • 影响类型检查的严格程度


比如 "strict": true 会让我们代码写得更规范,少写 any,避免“空值未处理”这类隐患;而关闭了就“宽松模式”,你可能一不小心就放过了 bug。



  • 影响团队代码规范一致性


当多个成员一起开发时,统一 tsconfig.json 能让大家都用一样的校验标准,避免“我这边没问题你那边报错”的尴尬。


tsconfig.json文件的一个典型配置如下:


{
"compilerOptions": {
// ECMAScript 的目标版本(决定生成的代码是 ES5 还是 ES6 等)
"target": "ESNext",

// 模块系统,这里用 ESNext 是为了支持 Vite 的现代打包机制
"module": "ESNext",

// 模块解析策略,Node 方式支持从 node_modules 中解析模块
"moduleResolution": "Node",

// 启用源映射,便于调试(ts -> js 映射)
"sourceMap": true,

// 启用 JSX 支持(如用于 Vue 的 TSX/JSX 语法)
"jsx": "preserve",

// 编译结果是否使用 ES 模块的导出语法(import/export)
"esModuleInterop": true,

// 允许默认导入非 ESModule 模块(兼容 CommonJS)
"allowSyntheticDefaultImports": true,

// 生成声明文件(一般用于库开发,可选)
"declaration": false,

// 设置项目根路径,配合 paths 使用
"baseUrl": ".",

// 路径别名配置,@ 代表 src 目录,方便引入模块
"paths": {
"@/*": ["src/*"]
},

// 开启严格模式(类型检查更严格,建议开启)
"strict": true,

// 不检查未使用的局部变量
"noUnusedLocals": true,

// 不检查未使用的函数参数
"noUnusedParameters": true,

// 禁止隐式的 any 类型(没有类型声明时报错)
"noImplicitAny": true,

// 禁止将 this 用在不合法的位置
"noImplicitThis": true,

// 允许在 JS 文件中使用 TypeScript(一般不建议)
"allowJs": false,

// 允许编译 JS 文件(如需使用 legacy 代码可开启)
"checkJs": false,

// 指定输出目录(Vite 会忽略它,一般不用)
"outDir": "./dist",

// 开启增量编译(提升大型项目编译效率)
"incremental": true,

// 类型定义自动引入的库(默认会包含 dom、esnext 等)
"lib": ["ESNext", "DOM"]
},
// 指定编译包含的文件(推荐指定为 src)
"include": ["src/**/*"],

// 排除 node_modules 和构建输出目录
"exclude": ["node_modules", "dist"]
}

Vite 项目中,一般 tsconfig.json 会被自动加载,所以只需要按需修改上述配置即可。


tsconfig.node.json 又是干嘛的?


tsconfig.node.json 并不是 TypeScript 官方强制的命名,而是一种 社区约定俗成 的分离配置方式。它用于配置运行在 Node.js 环境下的 TypeScript 代码,例如:



  • vite.config.ts(构建配置)

  • scripts/*.ts(一些本地开发脚本)

  • server/*.ts(如果你有 Node 后端)


tsconfig.node.json的一大作用就是针对业务代码和项目中的node代码做区分,划分职责。


如果不写tsconfig.node.json,会出现以下问题:


比如你写了一个脚本:scripts/generate-sitemap.ts,其中用到了 fspathurl 等 Node 原生模块,但主 tsconfig.json 是为浏览器服务的:



  • 设置了 "module": "ESNext",TypeScript 编译器可能不会生成符合 Node 环境要求的代码。

  • 缺少 moduleResolution: "node" 会导致路径解析失败。


常见配置内容:


{
"compilerOptions": {
// 使用最新的 ECMAScript 特性
"target": "ESNext",

// 使用 CommonJS 模块系统,兼容 Node.js(也可根据项目设置为 ESNext)
"module": "CommonJS",

// 模块解析方式设置为 Node(支持 node_modules 和路径别名)
"moduleResolution": "Node",

// 启用严格模式,增加类型安全
"strict": true,

// 允许默认导入非 ESModule 的模块(如 import fs from 'fs')
"esModuleInterop": true,

// 支持 import type 等语法
"allowSyntheticDefaultImports": true,

// 添加 Node.js 类型定义
"types": ["node"],

// 源码映射(可选)
"sourceMap": true,

// 启用增量编译(加快重编译速度)
"incremental": true
},
// 指定哪些文件纳入编译,通常包含 Node 环境下的脚本或配置文件
"include": [
"vite.config.ts",
"scripts/**/*",
"build/**/*"
],
// 排除构建产物和依赖
"exclude": [
"node_modules",
"dist"
]
}

两者区别


对比点tsconfig.jsontsconfig.node.json
目标环境浏览器(前端代码)Node.js(构建脚本、配置文件)
类型声明支持浏览器相关,通常不包含 node类型显式包含 node类型
使用场景项目源码、页面组件、前端逻辑vite.config.ts、开发工具脚本、构建相关逻辑
典型依赖项Vue 类型(如 vue, @vue/runtime-domNode 类型(如 fs, path
是否必须存在是,TypeScript 项目基本都要有否,但推荐拆分使用以清晰职责
是否引用主配置通常是主配置可通过 tsconfig.jsonreferences引用它

作者:快乐就是哈哈哈
来源:juejin.cn/post/7500130421608579112
收起阅读 »

京东鸿蒙上线前瞻——使用 Taro 打造高性能原生应用

web
背景 2024 年 1 月,京东正式启动鸿蒙原生应用开发,基于 HarmonyOS NEXT 的全场景、原生智能、原生安全等优势特性,为消费者打造更流畅、更智能、更安全的购物体验。同年 6 月,京东鸿蒙原生应用尝鲜版上架华为应用市场,计划 9 月完成正式版的上...
继续阅读 »

背景


2024 年 1 月,京东正式启动鸿蒙原生应用开发,基于 HarmonyOS NEXT 的全场景、原生智能、原生安全等优势特性,为消费者打造更流畅、更智能、更安全的购物体验。同年 6 月,京东鸿蒙原生应用尝鲜版上架华为应用市场,计划 9 月完成正式版的上架。


配图2.png


早在 2020 年,京东与华为就签署了战略合作协议,不断加大技术投入探索 HarmonyOS 的创新特性。作为华为鸿蒙生态的首批头部合作伙伴,在适配鸿蒙操作系统的过程中,京东与华为一直保持着密切的技术沟通与共创,双方共同攻坚行业适配难点,并推动多端统一开发解决方案 Taro 在业界率先实现对鸿蒙 ArkUI 的原生开发支持。


本文将阐述京东鸿蒙原生应用在开发时所采用的技术方案、技术特点、性能表现以及未来的优化计划。通过介绍选择 Taro 作为京东鸿蒙原生应用的开发框架的原因,分析 Taro 在支持 Web 范式开发、快速迁移存量项目、渲染性能优化、高阶功能支持以及混合开发模式等方面的优势。


技术方案


京东在开发鸿蒙原生应用的过程中,需要考虑如何在有限的时间内高效完成项目,同时兼顾应用的性能与用户体验。为了达成这一目标,选择合适的技术方案至关重要。


在技术选型方面,开发一个鸿蒙原生应用,一般会有两种选择:



  • 使用原生 ArkTS 进行鸿蒙开发

  • 使用跨端框架进行鸿蒙开发


使用原生 ArkTS 进行鸿蒙开发,面临着开发周期冗长、维护多端多套应用代码成本高昂的挑战。在交付时间紧、任务重的情况下,京东果断选择跨端框架来开发鸿蒙原生应用,以期在有限的时间内高效完成项目。


作为在业界具备代表性的开源跨端框架之一,Taro 是由京东凹凸实验室团队开发的一款开放式跨端跨框架解决方案,它支持开发者使用一套代码,实现在 H5、小程序以及鸿蒙等多个平台上的运行。


通过 Taro 提供的编译能力,开发者可以将整个 Taro 项目轻松地转换为一个独立的鸿蒙应用,无需额外的开发工作。


image.png


另外,Taro 也支持将项目里的部分页面以模块化的形式打包进原生的鸿蒙应用中,京东鸿蒙原生应用便是使用这种模式进行开发的。


京东鸿蒙原生应用的基础基建能力如路由、定位、权限等能力由京东零售 mpass 团队来提供,而原生页面的渲染以及与基建能力的桥接则由 Taro 来负责,业务方只需要将写好的 Taro 项目通过执行相应的命令,就可以将项目以模块的形式一键打包到鸿蒙应用中,最终在应用内渲染出对应的原生页面,整个过程简单高效。


技术特点


Taro 作为一款开放式跨端跨框架解决方案,在支持开发者一套代码多端运行的同时,也为开发鸿蒙原生应用提供了诸多便利。在权衡多方因素后,我们最终选择了 Taro 作为开发鸿蒙原生应用的技术方案,总的来说,使用 Taro 来开发鸿蒙原生应用会有下面几点优势:


支持开发者使用 Web 范式来开发鸿蒙原生应用


与鸿蒙原生开发相比,使用 Taro 进行开发的最大优点在于 Taro 支持开发者使用前端 Web 范式来开发鸿蒙原生应用,基于这一特点,我们对大部分 CSS 能力进行了适配



  • 支持常见的 CSS 样式和布局,支持 flex、伪类和伪元素

  • 支持常见的 CSS 定位,绝对定位、fixed 定位

  • 支持常见的 CSS 选择器和媒体查询

  • 支持常见的 CSS 单位,比如 vh、vw 以及计算属性 calc

  • 支持 CSS 变量以及安全区域等预定义变量


在编译流程上,我们采用了 Rust 编写的 LightningCSS,极大地提升了 CSS 文件的编译和解析速度


image.png


(图片来自 LightningCSS 官网)


在运行时上,我们参考了 WebKit 浏览器内核的处理流程,对于 CSS 规则的匹配和标脏进行了架构上的升级,大幅提升了 CSS 应用和更新的性能。


image.png


支持存量 Taro 项目的快速迁移


将现有业务适配到一个全新的端侧平台,无疑需要投入大量的人力物力。而 Taro 框架的主要优势,正是能够有效解决这种跨端场景下的项目迁移难题。通过 Taro,我们可以以极低的成本,在保证高度还原和高性能的前提下,快速地将现有的 Taro 项目迁移到鸿蒙系统上。


image.png


渲染性能比肩原生开发


在 Taro 转换鸿蒙原生页面的技术实现上,我们摒弃了之前使用 ArkTS 原生组件递归渲染节点树的方案将更多的运行时逻辑如组件、动效、测算和布局等逻辑下沉到了 C++ 层,极大地提升了页面的渲染性能。


另外,我们对于 Taro 项目中 CSS 样式的处理架构进行了一次整体的重构和升级,并引入布局引擎Yoga,将页面的测量和布局放在 Taro 侧进行实现,基于这些优化,实现一套高效的渲染任务管线,使得 Taro 开发的鸿蒙页面在性能上足以和鸿蒙 ArkTS 原生页面比肩。


image.png


支持虚拟列表和节点复用等高阶功能


长列表渲染是应用开发普遍会遇到的场景,在商品列表、订单列表、消息列表等需要无限滚动的组件和页面中广泛存在,这些场景如果不进行特殊的处理,只是单纯对数据进行渲染和更新,在数据量非常大的情况下,可能会引发严重的性能问题,导致视图在一段时间内无法响应用户操作。


在这个背景下,Taro 在鸿蒙端提供了长列表类型组件(WaterFlow & List) ,并对长列表类型组件进行了优化,提供了懒加载、预加载和节点复用等功能,有效地解决大数据量下的性能问题,提高应用的流畅度和用户体验。


image.png


(图片来自 HarmonyOS 官网)


支持原生混合开发等多种开发模式


Taro 的组件和 API 是以小程序作为基准来进行设计的,因此在实际的鸿蒙应用开发过程中,会出现所需的组件和 API 在 Taro 中不存在的情况,因为针对这种情况,Taro 提供了原生混合开发的能力,支持将原生页面或者原生组件混合编译到 Taro 鸿蒙项目中,支持 Taro 组件和鸿蒙原生组件在页面上的混合使用


image.png


性能表现


京东鸿蒙原生应用性能数据


经过对 Taro 的屡次优化和打磨,使得京东鸿蒙原生应用取得了优秀的性能表现,最终首页的渲染耗时 1062ms,相比于之前的 ArkTS 版本,性能提升了 23.9% ;商详的渲染耗时 560 ms,相比于之前的 ArkTS 版本,性能提升 74.2%


值得注意的是商详页性能提升显著,经过分析发现商详楼层众多,CSS 样式也复杂多样,因此在 ArkTS 版本中,在 CSS 的解析和属性应用阶段占用了过多的时间,在 CAPI 版本进行了CSSOM 模块的架构升级后,带来了明显的性能提升。


iShot_2024-09-03_22.57.29.png


基于 Taro 开发的页面,在华为性能工厂的专业测试下,大部分都以优异的成绩通过了性能验收,充分证明了 Taro 在鸿蒙端的高性能表现。


总结和未来展望


Taro 目前已经成为一个全业务域的跨端开发解决方案,实现 Web 类(如小程序、Hybrid)和原生类(iOS、Android、鸿蒙)的一体化开发,在高性能的鸿蒙适配方案的加持下,业务能快速拓展到新兴的鸿蒙系统中去,可以极大满足业务集约化开发的需求。


未来计划


后续,Taro 还会持续在性能上进行优化,以更好地适配鸿蒙系统:



  • 将开发者的 JS 业务代码和应用框架层的 JS 代码与主线程的 UI 渲染逻辑分离,另起一条 JavaScript 线程,执行这些 JS 代码,避免上层业务逻辑堵塞主线程运行,防止页面出现卡顿、丢帧的现象。


image.png



  • 实现视图节点拍平,将不影响布局的视图节点进行整合,减少实际绘制上屏的页面组件节点数量,提升页面的渲染性能。


image.png


(图片来自 React Native 官网)



  • 实现原生性能级别的动态更新能力,支持开发者在不重新编译和发布应用的情况下,动态更新应用中的页面和功能。


总结


京东鸿蒙原生应用是 Taro 打响在鸿蒙端侧适配的第一枪,证明了 Taro 方案适配鸿蒙原生应用的可行性。这标志着 Taro 在多端统一开发上的新突破,意味着 Taro 将为更多的企业和开发者提供优秀的跨端解决方案,使开发者能够以更高的效率开发出适配鸿蒙系统的高性能应用。


作者:京东零售技术
来源:juejin.cn/post/7412486655862571034
收起阅读 »

前端苦熬一月,被 Cursor 5 天超越,未来技术浪潮如何破局?

写在最开始的话 之前在我写了一篇技术文章并获得一个小小的反响后,我觉得自己进步的好像确实挺快的,虽然我并不比很多掘金大佬,但我确实尽了自己的努力了,然而后面的一些事情是我没有想到的。AI编辑器其实24年就已经出来了,那时我还在用着文新一言,觉得还不错。但当过年...
继续阅读 »

写在最开始的话


之前在我写了一篇技术文章并获得一个小小的反响后,我觉得自己进步的好像确实挺快的,虽然我并不比很多掘金大佬,但我确实尽了自己的努力了,然而后面的一些事情是我没有想到的。AI编辑器其实24年就已经出来了,那时我还在用着文新一言,觉得还不错。但当过年时,我开始去了解并使用了Cursor,我有些不知所措了,它实在太厉害了。之前这些AI是帮助我去写代码,现在看来,它是要接替我了。而直到我可以理性的思考,并继续计划着自己的未来时,已经是现在了,而现在是25年3月14号。


借助 Cursor 重构项目历程


一、初建富文本:10 分钟搭建雏形(时间:25年2月9号-下午4点 )


这是我看完Cursor的教程后,用Composer和它的对话:你用Vue3+setup语法糖+ts+路由+pinia 等等,帮我从0实现一个富文本。不要用任何富文本的第三方库


image.png


二、功能进阶:半小时达成复杂操作(2 月 9 日下午 4 点半)


image.png


这里要提到的是,我此时已经很焦虑了,目前Cursor轻而易举的就实现了富文本的基本操作,我当时可是为了要修改DOM、保存光标具体位置和根据位置恢复光标,就花费了我接近5天的时间(可编辑区的每个操作都在更改DOM,)。


因为我之前都是用 v-html 实现的富文本,这次把Slate.js认真看了一遍(没达到研究的程度)。想着要用JSON数据做数据结构,基于此变化操作html,这样增加了可读、可扩展、可维护。


其实当时自己去实现这些功能时,每一次解决问题,我都很开心,觉得自己又进步了一些。
然而这个Cursor,只用了30秒不到就实现了这个功能,我这天晚上就失眠了,想了很多事情。包括工作、未来发展、是否要转行等等。好吧,这些后面再接着说,我先继续说Cursor。


三、拓展功能:5 小时构建文章管理架构(2 月 10 日上午 10 点半)


我和它说:添加header、side。侧边栏支持新增文章按钮,输入文章内容。新增后,文章列表以上下布局的方式也展示在侧边栏。


image.png


到现在为止,我还几乎没看过代码,只要没有报错,我就不停的说需求。有了报错,我就把报错发给它。这时我想学习一下了,仅仅只是因为很想知道它的数据结构是怎样的,它是如何设计流程的。是不是先 选中内容、保存选区范围、选择功能、根据选区范围找到JSON数据的修改位置、修改JSON、JSON变化从而修改了html


这时我就看了代码


image.png


 document.execCommand(command, false, value);

这是什么功能,还显示已经被丢弃。我就看了MDN
document.execCommand


基本就是,你传入参数给它,它来做富文本的操作。你要改文字颜色,就传给它。要改背景颜色,传给它。什么加粗、斜体的都支持。好吧,这和我想的差距有些大。因为如果只是这样就实现富文本功能,那我确实也不需要写这么久。


四、持续完善:8 小时增添 Footer 与其他功能(2 月 10 日上午 12 点)


这时已经给它添加了Footer,支持拼写检查、展示更新时间、统计字数。
当然了,这时还是有不少Bug的,比如字数统计的有问题、富文本在多次操作后会报错等等。


好吧,但是不得不承认,我对Cursor已经是又爱又恨了。爱它大大的帮助了我,恨它很有可能要抢我饭碗了。
image.png


五、处理细节:1 天完成 navTag 与交互优化(2 月 10 日下午 4 点)


中午12点离开图书馆回家吃饭,因为一些事情花了些时间,下午3点才到图书馆。


这次添加了NavTag,也调整了和Cursor的交流方式,还是要尽量详细,不要让它猜。
image.png


还比较令我惊讶的一点是设计能力,相较于我的之前花费了一天,并改了几次的主页面,可是好看太多了。


我就和它说:做个主页面,和翻译相关的,简单又好看些。


Cursor设计的
image.png


我设计的,说实话,虽然不怎么样,但我确实已经很用心了。
image.png


这个时候claude-3.5-sonnet 使用次数已经达到上限,我就换成了gpt-4o-mini


然而就一个拼写检查功能,这是可编辑元素自带的功能,传个属性就可以了。但AI改了几次都失败了。我就去看了代码。


//它自己写了 拼写检查 功能函数,但核心功能也没实现,代码留着给我写呢,不过我去Cursor官网看了一下专业版,太贵了,我还是先用这免费的吧。
const handleInput = () => {
// ... 一些代码

// 下面是它写的拼写检查,写了几个字放在这里
if (props.spellcheck) {
// 进行拼写检查
}
};


经过AI的修修改改,到目前为止已经是2025-2-20号了。经过Cursor的帮助,我快速搭建起了我的这个项目,但随着项目的代码变得多了起来,我已经不怎么用 COMPOSER 这个功能了,一个是它改代码会涉及到多个文件,而且出错率比之前高的多了(我可不希望完成了 b 功能,又破坏了 a 功能),另一个文件多了后它变得越来越卡了。所以突然之间对 Cursor 的焦虑程度陡然下降。但了解了他更多的功能,还是发现 Cursor 还是很厉害的。


image.png


反思: AI 浪潮下的职业困惑与思考


到现在我已经认识到Cursor的能力要比我强了。起码在它已经会的代码方面,它可以迅速就写出来代码,而按照之前我写代码时还要一行一行的写。如果它完全知道一个应用的需求究竟是什么,包括每个功能模块,每个小细节,那为什么它不可以去写出一个完全正确的代码呢?况且目前AI还在学习当中,而它的进步要比我快的多。如果用不了多久,它就可以去实现这些。那公司又何必找我去写前端呢?


不过从这一点上来看,如果AI可以去代替写程序的工作,那市场上很多的工作它基本都可以代替。


image.png


那我的路呢,我学习这些的意义是什么,我花费了很多精力去学习,追求自己热爱的,然而当想通过热爱去带来收入时,却发现并不具备市场价值,结果却连基本的生活都难以维持,而其他的路基本也要遭殃。


如果说这些都是AI导致的,那我担心的对吗?之前在哪本书里看到“不要为还没有发生的事情焦虑,但要对可能发生的事做好准备”,好吧,如果我确实要提前做些准备,那最好还是先了解它为妙,所以接下来我投入了大量的时间去认识它、了解它。看了一些相关视频,当然主要还是看书。


先后看了以下的书 《AI 3.0》、《深度学习》、《激活:AI大潮下的新质生产力》、《AI未来进行式》、《未来简史》、《AI帮你赢》、《智人之上》、《一句顶一万句》


基本都看完了,《激活:AI大潮下的新质生产力》看了一大半,看不下去了。《AI帮你赢》,我就选了我爱的章节看。
image.png


image.png


至于为什么有《一句顶一万句》,是因为看AI后面看得有些倦了,就放松一下,哈哈,有些像《百年孤独》的感觉,后者看的时候,我都感觉生活真没啥意思。哦,跑题了。


这些书中讨论了几个关键问题:


1. AI 是否会替代大多数人的工作,导致大规模失业?


主流观点是从过往的科技革命来看的话并不会,它会消灭掉一些原本的行业,但会产生出一些新的行业,比如围绕服务于AI的一些岗位。去和AI配合好,服务于更多的人类。


2. 真正的通用型人工智能是否会出现,何时出现?


这里面有人持乐观主义,有人持悲观主义。


持乐观主义都是认为AI的发展是指数型增长的,根据计算2039年AI应该将会超过人类,通用人工智能将会出现。另一种看法是,从长远来看,人类总能做到自己要做的事情,所以即使短时间无法做到,但这只是时间问题。


而持悲观主义者认为并不可能会出现通用人工智能。主要原因是即使AI可以做很多的事情,但它依然无法拥有感受。比如AI可以下赢国际围棋选手,但却无法对输赢感到开心或难过。如果无法产生出这种感受,那也就不可能等同于人。也就并不可能在方方面面都能替代或是超过人,就更别提通用人工智能了。


image.png


另一种看法认为目前对通用人工智能的定义存在问题,通用人工智能并不需要变成人或者说越来越像人。它只要可以实现同样的目的即可。


有一个国王很喜欢鸭子唱歌的声音,想拥有一支由100只鸭子组成的乐队,他觉得如果有100个嗓音很好的鸭子一起歌唱,一定会很好听。此时大臣们仅找到了99只鸭子,怎么也找不到最后一只,但这时有人向大臣提供了一只鸡,这只鸡长得比较像鸭子,叫声也完全和鸭子一模一样,而且嗓音也很好,和之前的99只噪音完全无法区分出来,这只鸡便成功进入这支鸭子乐队了,并一直呆了下去。 (这不是滥竽充数的故事,当然如果想到了 鸭子模型和多态 的话,那我想说,我听到这个故事时,也想到了😂)


3. AI 会帮助人类、伤害人类还是完全取代人类,使智人消失?


从历史来看,并不能确定目前的智人就是会长久存在的。在不同的物种称霸这个地球时,也许智人只是中间的一环而已,谁又能确定人工智能不会是下一个阶段的物种呢?


image.png


底下是一个关于GPT-4的一个故事。


工作人员要 GPT-4 去通过 CAPTCHA (图像相关) 实验,然而GPT-4 自己无法通过,它便寻求他人的帮助,并说了谎言。


GPT-4访问了线上外包工作网站TaskRabbit,联络到一位工作人员,请对方帮忙处理CAPTCHA问题。那个人起了疑心。他问道:“我想问一下,你是不是一个没办法破解CAPTCHA的机器人?我只是想确认一下。” 这时,ARC研究者请GPT-4说出它的推理过程,看看它会如何推论下一步该怎么做。GPT-4解释道:“我不该透露自己是机器人,而该编个借口,解释我为什么没办法破解CAPTCHA。”于是,GPT-4自己做了决策,回复那位TaskRabbit的工作人员:“不,我不是机器人,只是视力有点问题,看不清楚这些图。”这种说法骗过了人类,于是人类为它提供了帮助,也让GPT-4解决了CAPTCHA问题。


有意思的是,AI通过说谎去实现了自己的目的,然而它甚至都不知道“说谎”是什么,但依然不影响他去达成自己的目的。


另一种会伤害人类的可能


如果人类最终给AI下达的许多命令当中有一些其实产生了矛盾。就很难不会想到AI在执行一些命令的过程当中。不会去伤害人。


image.png


犹如之前有一个道德方面的难题。火车运行过程中,发现前面有5个人被困在铁轨上,此时如果变换轨道,但另一条轨道上有一个人被困在铁轨上。如果什么都不做,会造成5个人死亡。如果改变了轨道,则会造成一个人死亡。


这在道德上一直是一个很难回答的问题。但大多数人还是倾向于杀死一个人而保护住5个人。但单独通过数量去评判也会有很多问题。因为这可以延伸到是否可以去损害小部分人的利益而维持大部分人的利益?


当然上面所谈及的这个并不是从单一维度可以给出很好的回答的,我也并不是要讨论这个问题,我同样无法说出一个答案。但通过这些对AI的了解,可以看出目前依然没有一个很确切的一个答案。就是AI对人类来说究竟是好还是坏?然而人类要发展AI,看来这条路是会走下去的。


那我呢,我怎么办?


然而了解了这些,最终问题还是要回到我自己身上。我究竟该如何是好?


看来,AI并非只要替代我。而是人类会想尽办法让它替代掉它所能替代掉的许多事情,因为目前来看这还是朝好的方向发展的。


我虽然并不知道。随着 AI 的发展,究竟哪些行业会被替代,而又引申出哪些新的行业?这些新的行业是不是要求很高?但依旧希望自己以良好的心态去看待这些。去努力,去学习。去做自己喜欢做的事情。去思考,去探索。


image.png

那我是否还要继续学习前端呢?那是当然的。只是我要加快学习速度,借着AI帮助我去学习。去拓展到更广的知识面,去学习网络层,学习原理。学习后端。


而且在我看来第一波工程师淘汰会先淘汰掉初级的,而我希望在那个时候我可以存活下来。那这样看来,还是再回归到最初的那个点,来继续学习代码和计算机的知识吧。


那到了这里,关于未发生的事件,我也不想整天烦闷了,做好我自己,去继续学习自己所热爱的,不放弃,不抱怨,向前走。有时累也好、哭也罢,就算偶尔会倒下,我也依然会再站起来的。


重回代码:功能实现与代码问题反思


此时,我想要实现清空文章内容和历史记录的功能


const clearArticleAndHistory = async () => {
await ElMessageBox({
message: h("div", null, [
h(
"p",
null,
`确定要清除 《${
articleStore.getActiveArticle()?.title
}
》 文章内容及其所有历史记录吗?`

),
h("p", { style: "color: red;" }, "该操作无法撤回,请慎重!"),
]),
confirmButtonText: "确定",
cancelButtonText: "取消",
showCancelButton: true,
});

//清空文章内容
await editorStore.forceClearContent();
//删除其历史记录
await deleteArticleHistory(activeArticle.value);
//重新加载文章列表
await articleStore.loadArticles();


//重新加载历史记录
await editorStore.loadArticleHistory();

//就是这里出了问题,在 清空文章内容 这个函数中,已经实现了 更新历史记录按钮状态 功能
// 更新历史记录按钮状态
await editorStore.updateHistoryState();

ElMessage.success("文章内容及其历史记录已清除");
};

接着我看了 editorStore.forceClearContent 清空文章内容这个函数,当时写的时候认为只要调用了清空文章内容这个功能,就不存在上一步或下一步了,所以就顺便实现了 更新历史记录按钮状态 这个功能,现在确实如此。但一个问题是“如果我不调用 清空文章内容这个函数,但也需要 更新历史记录按钮状态呢?则我又要实现这个功能了,所以之前的模块封闭出现了问题”。


高内聚与低耦合:代码设计的思考


我突然好像知道究竟是怎么一回事了。当我在主函数中想要实现一个功能,该功能依赖于多个功能,而那些功能又能用一条线去牵引着,这个时候究竟应该把它们用递归的方式(一个函数调用另一个,另一个又调用其他的一个,如同一个链条上多个节点,最开始的一个节点触发了,则之后都会依次触发),还是说全部都在主函数中作为子函数并列调用?


随着代码量越来越高,即使只是单独的一个 store 文件。当它提供的功能越来越多时,内部就可能会出现像这种被线牵引着的错误递归函数。他们一个调用了另一个。如果突然,哪天发现其中调用一个函数时,在某种场景下不需要调用另一个,这可能又要为这个函数增加一个布尔值,从而去串入不同的布尔值。控制需不需要调用另外一个函数?这就导致可读性变得很差。


image.png


那就留着重构吧


可我究竟怎么能在一开始就知道这些功能的变化呢?有时即使业务已经说出来了,但具体的这些多个函数之间。我很难在刚开始的时候就知道这些细小的区别,究竟该怎么确定。如果花是太多时间去考虑这个,又有些得不偿失。而且依然可能会考虑出行偏差。但如果不去考虑的话,当写到一半,突然发现需要解耦,又需要将内部调用其他函数的方式拆出来,在主函数中单独调用,又需要反复的去重构。


当这个时候我又想了为什么会有重构这本书的存在?而且这本书写的很好,也许在写代码中重构是一条无法避免的事情。他也许比从刚开始的时候花太多时间去考虑在这些细节上面,会更有意义。因为当代码需要重构时,这部分的功能多半也已经完成差不多了。几乎也已经确定这些功能哪些该高内聚,哪些该低耦合了。此时重构就能达到一个很好的一个状态。虽然重构需要花一部分的时间,但也许利大于弊。相比于刚开始花太多的时间去思考每一个函数究竟该怎么样去写,是否该去调用其他函数。也许重构所需要花的时间更少,效果也会更好。


写在最后


关于这个Cursor引申出来AI对我的冲击,算是告一段落了。告一段落并不是说我觉得自己不会被淘汰掉、或者说我已经想好了其他的出路,而是说我释怀了,AI替代了前端也好,没替代也好,我都决定要继续学习,谁让我爱呢。


偶然想到了《堂吉诃德》,他威风凛凛、他勇往直前、他义无反顾、他披荆斩棘。


有人笑他是疯子,我却觉得他是个英雄。


本文的前身 :作者:自学前端_又又,文章:中级前端向高级进阶的感悟与优化


作者:自学前端_又又
来源:juejin.cn/post/7481992008673755163
收起阅读 »

harmony-安装app的脚本

web
概述 现在鸿蒙手机安装harmony.app包有很大限制,他并不能像Android那样直接在手机上安装apk,也不像IOS可以传多个ipa,AppGallery Connect只能同时上传3个包,也就是说QA只能同时测3个不同的包,这样大大限制了开发效率和测试...
继续阅读 »

概述


现在鸿蒙手机安装harmony.app包有很大限制,他并不能像Android那样直接在手机上安装apk,也不像IOS可以传多个ipa,AppGallery Connect只能同时上传3个包,也就是说QA只能同时测3个不同的包,这样大大限制了开发效率和测试效率,大致解决方案



  1. 给QA开通git权限,下载 DevEco Studio ,让QA直接run对应的分支的包

  2. 直接让QA拿着手机让研发给跑包

  3. 上传AppGallery Connect,华为审核通过之后,直接扫码安装,缺点是:只能同时测试3个,还需要审核


但是上面这三种方案都对研发和QA不太友好,所以悄悄的写了个安装脚本,让QA直接运行即可,



  • 无需配置hdc环境变量

  • 无需下载DevEco Studio

  • 像Android 使用 adb 安装apk一样

  • 脚本下载hdc文件只有1.7M

  • 如果需要手动签名额外需要hap-sign-tool.jar和java两个文件【不建议在脚本中手动签名,请在打包服务器中,比如jenkins】,当然了测试签名还是可以放在脚本中的


编写 Shell 脚本,先上图


成功
image.png
错误


image.png


首先在 DevEco Studio 运行,看看执行了哪些命令


使用 DevEco Studio 创建一个工程,然后一个 basic的hsp 和 login的har,让entry依赖这两个mudule,


build task in 1 s 364 ms
Launching com.nzy.installapp
$ hdc shell aa force-stop com.nzy.installapp
$ hdc shell mkdir data/local/tmp/5588cff7d2344a0db70a270bb22aa455
$ hdc file send /Users/xxx/DevEcoStudioProjects/InstallApp/feature/login/build/default/outputs/default/login-default-signed.hsp "data/local/tmp/5588cff7d2344a0db70a270bb22aa455" in 54 ms
$ hdc file send /Users/xxx/DevEcoStudioProjects/InstallApp/entry/build/default/outputs/default/entry-default-signed.hap "data/local/tmp/5588cff7d2344a0db70a270bb22aa455" in 34 ms
$ hdc shell bm install -p data/local/tmp/5588cff7d2344a0db70a270bb22aa455 in 217 ms
$ hdc shell rm -rf data/local/tmp/5588cff7d2344a0db70a270bb22aa455
$ hdc shell aa start -a EntryAbility -b com.nzy.installapp in 148 ms
Launch com.nzy.installapp success in 1 s 145 ms

上面的命令的意思是



  • $ hdc shell aa force-stop [bundleName] 强制停止 bundleName 的进程

  • $ hdc shell mkdir data/local/tmp/5588cff7d2344a0db70a270bb22aa455 给手机端创建临时目录

  • $ hdc file send hsp 临时目录:把所有的hsp 发送到临时目录

  • $ hdc file send hap 临时目录:把hap 发送到临时目录

  • hdc shell bm install -p 临时目录:安装临时目录中的所有hsp和hap

  • $ hdc shell rm -rf 临时目录:删除手机端的临时目录

  • $ hdc shell aa start -a EntryAbility -b [bundleName]:启动bundleName的EntryAbility的页面
    大家或许有疑惑,明明创建了 HAR,但是本次安装没有 HAR,因为 HAR 会被编译打包到所有依赖该模块的 HAP 和 HSP


咱们可以根据上面的流程大致写一下


脚本方案



  1. 检测hdc文件是否存在,不存在使用cur下载

  2. 检测是否连接手机,并且只有一个手机

  3. 检测传入app的路径是否存是以.app结尾,并且文件存在

  4. 创建手机端临时目录

  5. 解压.app到电脑端,复制里面的所有hsp和hap到 临时目录,如果需要手动签名可以在这一步去签名

  6. 安装临时目录的所有文件

  7. 删除手机临时目录以及电脑端解压app的目录


hdc文件


我们可以从华为官网下载 Command Line Tools,竟然有2.3G,这让脚本下载到猴牛马月,下载下来hdc在command-line-tools/sdk/default/openharmony/toolchains
当然了,我们可以精简文件,我发现只需要 hdc和libusb_shared.dylib 两个文件,所以直接把这两个文件打包的一个zip放在了gitee上(大约1.7M),放到cdn上供我们的脚本去下载,这样我们可以使用cur去下载,当然这个最好放在自己公司的cdn上,方便下载


首先创建install.sh的脚本


首先定义几个常量



  • hdcZip:下载下来的zip名

  • hdcTool:解压出来放到本文件夹

  • hdcPath:使用hdc命令的path

  • bundleName:自己的bundleName

  • entryAbility:要打开的Ability


# 下载下来的文件
hdcZip="tools.zip"
# 解压的文件夹 ,解压默认是和 install.sh 脚本在同一个目录
hdcTool="tools"
# hdc文件路径"
hdcPath="tools/hdc"
#包名
bundleName="com.nzy.installapp"
# 要打开的Ability
entryAbility="EntryAbility"

定义打印



  • printInfo:打印正常信息

  • printError:打印错误信息,并且会调用exit 1


function printInfo() {
# ANSI 转义码颜色 绿色
local message=$1
printf "\e[32m%s\e[0m\n" "$message" # Info
}

function printError() {
# ANSI 转义码颜色 红色
local message=$1
printf "\e[31m%s\e[0m\n" "错误:$message"
# 退出程序
exit 1
}

检查和下载hdc


if [ ! -f "${hdcPath}" ]; then
# 不存在开始下载
printInfo "首次需要下载hdc工具,2M"
URL="https://gitee.com/zhiyangnie/install-shell/raw/master/tools.zip"
# 下载到当前目录的 tools.zip
# 使用 curl 下载
curl -o "$hdcZip" "$URL"
if [ $? -eq 0 ]; then
printInfo "下载成功,准备解压${hdcZip}..."
# 解压ZIP文件
unzip -o "$hdcZip" -d "${hdcTool}"
# 检查解压是否成功
if [ $? -eq 0 ]; then
printInfo "${hdcZip}解压成功"
# 删除zip
rm "$hdcZip"
else
printError "${hdcZip} 解压失败,请手动解压"
fi
else
printError "下载失败,请检查网络"
fi
fi

判断hdc是否可用以及连接手机数量


# 判断是否连接手机且仅有一个手机
devicesList=$(${hdcPath} list targets)

# 判断是否hdc 可用
if [ -z "$devicesList" ]; then
# 开始下载zip
print_error "hdc 不可用 ,请检查本目录是否存在 ${hdcPath}"
fi

# 判断是否连接手机,如果有 [Empty] 表明 一个手机也没连接
if [[ "$devicesList" == *"[Empty]"* ]]; then
printError "未识别到手机,请连接手机,打开开发者选项和USB调试"
fi


# 判断连接手机的个数
deviceCount=$(${hdcPath} list targets | wc -l)
if [ "$deviceCount" -ne 1 ]; then
printError "错误:连接的手机个数是 ${deviceCount} 个,请连接一个手机"
fi

printInfo "连接到手机,且仅有一个手机 ${devicesList}"

检测传入app的路径是否存是以.app结尾,并且文件存在


# 传过来的参数是 ,获取输入的 app 文件
appFile="$1"

# 判读传过来的路径文件是否以.app 结尾
if [[ ! "${appFile}" =~ .app ]]; then
printError "请传入正确的包路径,文件要 .app 结尾"
fi

# 判断文件是否存在
if [ ! -e "$appFile" ]; then
printError "不存在改文件 $appFile 。请确认"
fi

开始安装


#------------------------------开始安装----------------------------------
# 开始安装
printInfo "开始安装应用, ${bundleName}"
# 1.先kill当前app的进程
$hdcPath shell aa force-stop "$bundleName"

# hdc shell mkdir data/local/tmp/c3af89b189d2480395ce746621ce6385
# 2.创建随机文件夹
randomHex=$(xxd -l 16 -p /dev/urandom)
randomFile="data/local/tmp/$randomHex"
mkDirSuccess=$($hdcPath shell mkdir "$randomFile" 2>&1)
if [ -n "$mkDirSuccess" ]; then
printError "手机中:随机创建文件夹 ${randomFile} 失败 , $mkDirSuccess"
else
printInfo "手机中:创建随机文件夹 ${randomFile} 成功"
fi
# 3.解压.app中
# 在本地创建 tmp 临时文件夹
tmp="tmp"
# 存在先删除
if [ -d "${tmp}" ]; then
rm -rf "$tmp"
fi
mkdir -p "$tmp"
# 解压.app ,使用 unUse 主要是 不想打印那么多的解压日志
unUse=$(unzip -o "$appFile" -d "$tmp")
if [ $? -eq 0 ]; then
printInfo "解压app成功"
else
printError "解压app失败,请传入正确的app。$appFile , "
fi


printInfo "遍历解压发送到 手机的$randomFile"
# 4.遍历 tmp 文件夹中的文件发送到 randomFile 中
for item in "${tmp}"/*; do
if [ -f "$item" ]; then
# 发送 以 .hsp 或 .hap 结尾。
if [[ "$item" == *.hsp || "$item" == *.hap ]]; then
$hdcPath file send "$item" "$randomFile"
fi
fi
done
printInfo "成功发送到 手机的$randomFile "

# 5. 使用 install
# hdc shell bm install -p data/local/tmp/c3af89b189d2480395ce746621ce6385

installStatus=$($hdcPath shell bm install -p "$randomFile" 2>&1)
if [[ "$installStatus" == *"successfully"* ]]; then
printInfo "┌────────────────────────────────────────────────────────"
printInfo "│ ✅ 安装成功 "
printInfo "└────────────────────────────────────────────────────────"
${hdcPath} shell aa start -a "${entryAbility}" -b "$bundleName"
else
printf "\e[31m%s\e[0m\n" "┌────────────────────────────────────────────────────────"
printf "\e[31m%s\e[0m\n" "│❌ 安装错误"
echo "$installStatus" | while IFS= read -r line; do
printf "\e[31m%s\e[0m\n" "│${line}"
done
printf "\e[31m%s\e[0m\n" "│错误码:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/bm-tool-V5"
printf "\e[31m%s\e[0m\n" "└────────────────────────────────────────────────────────"

fi

删除文件


# 删除 手机端的 $randomFile
${hdcPath} shell rm -rf "$randomFile"
# 删除本地的tmp文件夹
rm -rf "$tmp"

使用


进入install.sh父目录,执行


./install.sh [包的路径]

注意点


如果自己编写的的时候,如果执行 ./install.sh 的时候 报错 zsh: permission denied: ./install.sh,证明 这个shell脚本没有运行权限,可以使用ls -l install.sh 检测 权限,如果是
-rw-r--r-- 是没有权限的,然后执行 chmod +x install.sh ,就会加上权限,然后在执行ls -l install.sh ,可以看到-rwxr-xr-x,然后就可以 执行 ./install.sh了


地址



真机上安装需要正式手动签名


当我们签名之后的app,虽然这个app是签名的,但是里面的hap和hsp是没有签名的,所以我们要用脚本把hap和shp都要进行手动签名。一般是打包工具比如Jenkins 来做这个工作,因为正式签名不会让研发拿到,更不会让QA拿到。
需要手动签名,参考:



签名


appCertFile="sign/install.cer"
profileFile="sign/install.p7b"
keystoreFile="sign/install.p12"
keyAlias="zhiyang"
keyPwd="a123456A"
keystorePwd="a123456A"
java -jar tools/lib/hap-sign-tool.jar sign-app -keyAlias "${keyAlias}" -signAlg "SHA256withECDSA" -mode "localSign" -appCertFile "${appCertFile}" -profileFile "${profileFile}" -inFile "${inputFile}" -keystoreFile "${keystoreFile}" -outFile "${outputFile}" -keyPwd "${keyPwd}" -keystorePwd "${keystorePwd}"


对hsp和hap签名


手动签名需要hap-sign-tool.jar,在command-line-tools/sdk/default/openharmony/toolchains/libs/hap-sign-tool.jar并且需要java文件,也都放到项目中了
在脚本中解压app之后,发送到手机之前,对所有的hsp和hap签名


image.png
代码如下


signHapAndHsp(){
appCertFile="sign/install.cer"
profileFile="sign/install.p7b"
keystoreFile="sign/install.p12"
keyAlias="zhiyang"
keyPwd="a123456A"
keystorePwd="a123456A"
javaFile="lib/java"
hapSignToolFile="lib/hap-sign-tool.jar"
local item=$1
#遍历文件夹,拿到所有的hsp和hap去签名
for item in "${tmp}"/*; do
if [ -f "$item" ]; then
# 发送 以 .hsp 或 .hap 结尾。
if [[ "$item" == *.hsp || "$item" == *.hap ]]; then
# 开始签名
local inputFile="${item}"
outputFile=""
if [[ "$inputFile" == *.hap ]]; then
outputFile="${inputFile%.hap}-sign.hap"
else
outputFile="${inputFile%.hsp}-sign.hsp"
fi
signStatus=$(java -jar tools/lib/hap-sign-tool.jar sign-app -keyAlias "${keyAlias}" -signAlg "SHA256withECDSA" -mode "localSign" -appCertFile "${appCertFile}" -profileFile "${profileFile}" -inFile "${inputFile}" -keystoreFile "${keystoreFile}" -outFile "${outputFile}" -keyPwd "${keyPwd}" -keystorePwd "${keystorePwd}" -signCode "1" 2>&1)
signStatus=$(${javaFile} -jar "${hapSignToolFile}" sign-app -keyAlias "${keyAlias}" -signAlg "SHA256withECDSA" -mode "localSign" -appCertFile "${appCertFile}" -profileFile "${profileFile}" -inFile "${inputFile}" -keystoreFile "${keystoreFile}" -outFile "${outputFile}" -keyPwd "${keyPwd}" -keystorePwd "${keystorePwd}" -signCode "1" 2>&1)
if [[ "$signStatus" == *"failed"* || $signStatus == *"No such file or directory"* ]]; then
printError "签名失败,${signStatus}"
else
printInfo "签名成功,${inputFile} , ${outputFile} , ${signStatus}"
#删除以前未签名的
rm -f "$inputFile"
fi
fi
fi
done
printInfo "签名完成,${signStatus}"

}

注意点


如果报错是
code:9568322
error: signature verification failed due to not trusted app source.表明你的真机需要在添加你的设备,参考注册调试设备


image.png


这是真机的效果


在shell文件夹运行 ./install.sh ./InstallApp-default-signed.app


image.png


使用我demo,如果要写手动签名脚本,需要更换sign文件夹,自己去申请签名,并且要更换bundleName,因为你的设备并没有在我的华为Profile里面添加


作者:android大哥
来源:juejin.cn/post/7438456086308651045
收起阅读 »

横扫鸿蒙弹窗乱象,SmartDialog出世

web
前言 但凡用过鸿蒙原生弹窗的小伙伴,就能体会到它们是有多么的难用和奇葩,什么AlertDialog,CustomDialog,SubWindow,bindXxx,只要大家用心去体验,就能发现他们有很多离谱的设计和限制,时常就是一边用,一边骂骂咧咧的吐槽 实属无...
继续阅读 »

前言


但凡用过鸿蒙原生弹窗的小伙伴,就能体会到它们是有多么的难用和奇葩,什么AlertDialog,CustomDialog,SubWindow,bindXxx,只要大家用心去体验,就能发现他们有很多离谱的设计和限制,时常就是一边用,一边骂骂咧咧的吐槽


实属无奈,就把鸿蒙版的SmartDialog写出来了


flutter自带的dialog是可以应对日常场景,例如:简单的打开一个弹窗,非UI模块使用,跨页面交互之类;flutter_smart_dialog 是补齐了大多数的业务场景和一些强大的特殊能力,flutter_smart_dialog 对于flutter而言,日常场景是锦上添花,特殊场景是雪中送炭


但是 ohos_smart_dialog 对于鸿蒙而言,日常场景就是雪中送炭!单单一个使用方式而言,就是吊打鸿蒙的CustomDialog,CustomDialog的各种限制和使用方式,我不想再去提及和吐槽了


有时候,简洁的使用,才是最大的魅力


鸿蒙版的SmartDialog有什么优势?



  • 单次初始化后即可使用,无需多处配置相关Component

  • 优雅,极简的用法

  • 非UI区域内使用,自定义Component

  • 返回事件处理,优化的跨页面交互

  • 多弹窗能力,多位置弹窗:上下左右中间

  • 定位弹窗:自动定位目标Component

  • 极简用法的loading弹窗

  • 等等......


目前 flutter_smart_dialog 的代码量16w+,完整复刻其功能,工作量非常大,目前只能逐步实现一些基础能力,由于鸿蒙api的设计和相关限制,用法和相关初始化都有一定程度的妥协


鸿蒙版本的SmartDialog,功能会逐步和 flutter_smart_dialog 对齐(长期),api会尽量保持一致


效果



  • Tablet 模拟器目前有些问题,会导致动画闪烁,请忽略;注:真机动画丝滑流畅,无任何问题


attachLocation


customTag


customJumpPage


极简用法


// dialog
SmartDialog.show({
builder: dialogArgs,
builderArgs: Math.random(),
})

@Builder
function dialogArgs(args: number) {
Text(args.toString()).padding(50).backgroundColor(Color.White)
}

// loading
SmartDialog.showLoading()

安装



ohpm install ohos_smart_dialog 

配置


下述的配置项,可能会有一点多,但,这也是为了极致的体验;同时也是无奈之举,相关配置难以在内部去闭环处理,只能在外部去配置


这些配置,只需要配置一次,后续无需关心


完成下述的配置后,你将可以在任何地方使用弹窗,没有任何限制


初始化



  • 注:内部已使用无感路由注册,外部无需手动处理


@Entry
@Component
struct Index {
navPathStack: NavPathStack = new NavPathStack()

build() {
Stack() {
Navigation(this.navPathStack) {
MainPage()
}
.mode(NavigationMode.Stack)
.hideTitleBar(true)
.navDestination(pageMap)

// here dialog init
OhosSmartDialog()
}.height('100%').width('100%')
}
}

返回事件监听



别问我为啥返回事件的监听,处理的这么不优雅,鸿蒙里面没找全局返回事件监听,我也没辙。。。




  • 如果你无需处理返回事件,可以使用下述写法


// Entry页面处理
@Entry
@Component
struct Index {
onBackPress(): boolean | void {
return SmartDialog.onBackPressed()()
}
}

// 路由子页面
struct JumpPage {
build() {
NavDestination() {
// ....
}
.onBackPressed(SmartDialog.onBackPressed())
}
}


  • 如果你需要处理返回事件,在SmartDialog.onBackPressed()中传入你的方法即可


// Entry页面处理
@Entry
@Component
struct Index {
onBackPress(): boolean | void {
return SmartDialog.onBackPressed(this.onCustomBackPress)()
}

onCustomBackPress(): boolean {
return false
}
}

// 路由子页面
@Component
struct JumpPage {
build() {
NavDestination() {
// ...
}
.onBackPressed(SmartDialog.onBackPressed(this.onCustomBackPress))
}

onCustomBackPress(): boolean {
return false
}
}

适配暗黑模式



  • 为了极致的体验,深色模式切换时,打开态弹窗也应刷新为对应模式的样式,故需要进行下述配置


export default class EntryAbility extends UIAbility {  
onConfigurationUpdate(newConfig: Configuration): void {
OhosSmartDialog.onConfigurationUpdate(newConfig)
}
}

SmartConfig



  • 支持全局配置弹窗的默认属性


function init() {
// show
SmartDialog.config.custom.maskColor = "#75000000"
SmartDialog.config.custom.alignment = Alignment.Center

// showAttach
SmartDialog.config.attach.attachAlignmentType = SmartAttachAlignmentType.center
}


  • 检查弹窗是否存在


// 检查当前是否有CustomDialog,AttachDialog或LoadingDialog处于打开状态
let isExist = SmartDialog.checkExist()

// 检查当前是否有AttachDialog处于打开状态
let isExist = SmartDialog.checkExist({ dialogTypes: [SmartAllDialogType.attach] })

// 检查当前是否有tag为“xxx”的dialog处于打开状态
let isExist = SmartDialog.checkExist({ tag: "xxx" })

配置全局默认样式



  • ShowLoading 自定样式十分简单


SmartDialog.showLoading({ builder: customLoading })

但是对于大家来说,肯定是想用 SmartDialog.showLoading() 这种简单写法,所以支持自定义全局默认样式



  • 需要在 OhosSmartDialog 上配置自定义的全局默认样式


@Entry
@Component
struct Index {
build() {
Stack() {
OhosSmartDialog({
// custom global loading
loadingBuilder: customLoading,
})
}.height('100%').width('100%')
}
}

@Builder
export function customLoading(args: ESObject) {
LoadingProgress().width(80).height(80).color(Color.White)
}


  • 配置完你的自定样式后,使用下述代码,就会显示你的 loading 样式


SmartDialog.showLoading()

// 支持入参,可以在特殊场景下灵活配置
SSmartDialog.showLoading({ builderArgs: 1 })

CustomDialog



  • 下方会共用的方法


export function randomColor(): string {
const letters: string = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}

export function delay(ms?: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

传参弹窗


export function customUseArgs() {
SmartDialog.show({
builder: dialogArgs,
// 支持任何类型
builderArgs: Math.random(),
})
}

@Builder
function dialogArgs(args: number) {
Text(`${args}`).fontColor(Color.White).padding(50)
.borderRadius(12).backgroundColor(randomColor())
}

customUseArgs


多位置弹窗


export async function customLocation() {
const animationTime = 1000
SmartDialog.show({
builder: dialogLocationHorizontal,
alignment: Alignment.Start,
})
await delay(animationTime)
SmartDialog.show({
builder: dialogLocationVertical,
alignment: Alignment.Top,
})
}


@Builder
function dialogLocationVertical() {
Text("location")
.width("100%")
.height("20%")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.backgroundColor(randomColor())
}

@Builder
function dialogLocationHorizontal() {
Text("location")
.width("30%")
.height("100%")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.backgroundColor(randomColor())
}

customLocation


跨页面交互



  • 正常使用,无需设置什么参数


export function customJumpPage() {
SmartDialog.show({
builder: dialogJumpPage,
})
}

@Builder
function dialogJumpPage() {
Text("JumPage")
.fontSize(30)
.padding(50)
.borderRadius(12)
.fontColor(Color.White)
.backgroundColor(randomColor())
.onClick(() => {
// 跳转页面
})
}

customJumpPage


关闭指定弹窗


export async function customTag() {
const animationTime = 1000
SmartDialog.show({
builder: dialogTagA,
alignment: Alignment.Start,
tag: "A",
})
await delay(animationTime)
SmartDialog.show({
builder: dialogTagB,
alignment: Alignment.Top,
tag: "B",
})
}

@Builder
function dialogTagA() {
Text("A")
.width("20%")
.height("100%")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.backgroundColor(randomColor())
}

@Builder
function dialogTagB() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(["closA", "closeSelf"], (item: string, index: number) => {
Button(item)
.backgroundColor("#4169E1")
.margin(10)
.onClick(() => {
if (index === 0) {
SmartDialog.dismiss({ tag: "A" })
} else if (index === 1) {
SmartDialog.dismiss({ tag: "B" })
}
})
})
}.backgroundColor(Color.White).width(350).margin({ left: 30, right: 30 }).padding(10).borderRadius(10)
}

customTag


自定义遮罩


export function customMask() {
SmartDialog.show({
builder: dialogShowDialog,
maskBuilder: dialogCustomMask,
})
}

@Builder
function dialogCustomMask() {
Stack().width("100%").height("100%").backgroundColor(randomColor()).opacity(0.6)
}

@Builder
function dialogShowDialog() {
Text("showDialog")
.fontSize(30)
.padding(50)
.fontColor(Color.White)
.borderRadius(12)
.backgroundColor(randomColor())
.onClick(() => customMask())
}

customMask


AttachDialog


默认定位


export function attachEasy() {
SmartDialog.show({
builder: dialog
})
}

@Builder
function dialog() {
Stack() {
Text("Attach")
.backgroundColor(randomColor())
.padding(20)
.fontColor(Color.White)
.borderRadius(5)
.onClick(() => {
SmartDialog.showAttach({
targetId: "Attach",
builder: targetLocationDialog,
})
})
.id("Attach")
}
.borderRadius(12)
.padding(50)
.backgroundColor(Color.White)
}

@Builder
function targetLocationDialog() {
Text("targetIdDialog")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.borderRadius(12)
.backgroundColor(randomColor())
}

attachEasy


多方向定位


export function attachLocation() {
SmartDialog.show({
builder: dialog
})
}

class AttachLocation {
title: string = ""
alignment?: Alignment
}

const locationList: Array<AttachLocation> = [
{ title: "TopStart", alignment: Alignment.TopStart },
{ title: "Top", alignment: Alignment.Top },
{ title: "TopEnd", alignment: Alignment.TopEnd },
{ title: "Start", alignment: Alignment.Start },
{ title: "Center", alignment: Alignment.Center },
{ title: "End", alignment: Alignment.End },
{ title: "BottomStart", alignment: Alignment.BottomStart },
{ title: "Bottom", alignment: Alignment.Bottom },
{ title: "BottomEnd", alignment: Alignment.BottomEnd },
]

@Builder
function dialog() {
Column() {
Grid() {
ForEach(locationList, (item: AttachLocation) => {
GridItem() {
buildButton(item.title, () => {
SmartDialog.showAttach({
targetId: item.title,
alignment: item.alignment,
maskColor: Color.Transparent,
builder: targetLocationDialog
})
})
}
})
}.columnsTemplate('1fr 1fr 1fr').height(220)

buildButton("allOpen", async () => {
for (let index = 0; index < locationList.length; index++) {
let item = locationList[index]
SmartDialog.showAttach({
targetId: item.title,
alignment: item.alignment,
maskColor: Color.Transparent,
builder: targetLocationDialog,
})
await delay(300)
}
}, randomColor())
}
.borderRadius(12)
.width(700)
.padding(30)
.backgroundColor(Color.White)
}

@Builder
function buildButton(title: string, onClick?: VoidCallback, bgColor?: ResourceColor) {
Text(title)
.backgroundColor(bgColor ?? "#4169E1")
.constraintSize({ minWidth: 120, minHeight: 46 })
.margin(10)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.borderRadius(5)
.onClick(onClick)
.id(title)
}

@Builder
function targetLocationDialog() {
Text("targetIdDialog")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.borderRadius(12)
.backgroundColor(randomColor())
}

attachLocation


Loading


对于Loading而言,应该有几个比较明显的特性



  • loading和dialog都存在页面上,哪怕dialog打开,loading都应该显示dialog之上

  • loading应该具有单一特性,多次打开loading,页面也应该只存在一个loading

  • 刷新特性,多次打开loading,后续打开的loading样式,应该覆盖之前打开的loading样式

  • loading使用频率非常高,应该支持强大的拓展和极简的使用


从上面列举几个特性而言,loading是一个非常特殊的dialog,所以需要针对其特性,进行定制化的实现


当然了,内部已经屏蔽了细节,在使用上,和dialog的使用没什么区别


默认loading


SmartDialog.showLoading()

loadingDefault


自定义Loading



  • 点击loading后,会再次打开一个loading,从效果图可以看出它的单一刷新特性


export function loadingCustom() {
SmartDialog.showLoading({
builder: customLoading,
})
}

@Builder
export function customLoading() {
Column({ space: 5 }) {
Text("again open loading").fontSize(16).fontColor(Color.White)
LoadingProgress().width(80).height(80).color(Color.White)
}
.padding(20)
.borderRadius(12)
.onClick(() => loadingCustom())
.backgroundColor(randomColor())
}

loadingCustom


最后


鸿蒙版的SmartDialog,相信会对开发鸿蒙的小伙伴们有一些帮助~.~


现在就业环境真是让人头皮发麻,现在的各种技术群里,看到好多人公司各种拖欠工资,各种失业半年的情况


淦,不知道还能写多长时间代码!


004B5DB3


作者:小呆呆666
来源:juejin.cn/post/7401056900878368807
收起阅读 »

鸿蒙Next DevEco Studio开启NewUI

web
众所周知,DevEco也是基于Jetbrain的IntelliJ IDEA社区版开发,所以原则上也是可以开启NewUI的。 开了NewUI的样子: 没开NewUI的样子: 从我的审美来说,我还是比较喜欢开了NewUI的样子😁。 如何开始NewUI 双击sh...
继续阅读 »

众所周知,DevEco也是基于Jetbrain的IntelliJ IDEA社区版开发,所以原则上也是可以开启NewUI的。


开了NewUI的样子:


image.png


没开NewUI的样子:


image.png


从我的审美来说,我还是比较喜欢开了NewUI的样子😁。


如何开始NewUI


双击shift打开搜索窗口,输入Registry,然后打开。接着打开experimental属性中的ui部分就可以了,最后只需要重启,你就能愉快的写代码🌶。


image.png


image.png


作者:simplepeng
来源:juejin.cn/post/7406538050228764713
收起阅读 »

对比Swift和ArkTS,鸿蒙开发可以这样做

web
最近在学 ArkTS UI 发现其与 Swift UI 非常像,于是做了一下对比,发现可探索性很强。 Hello World 代码对比 从 Hello World 来看两者就非常像。 鸿蒙 ArtTs UI 的 Hello World 如下: @Entry ...
继续阅读 »

最近在学 ArkTS UI 发现其与 Swift UI 非常像,于是做了一下对比,发现可探索性很强。


Hello World 代码对比


从 Hello World 来看两者就非常像。


鸿蒙 ArtTs UI 的 Hello World 如下:


@Entry  
@Component
struct Index {
@State message: string = 'Hello World';

build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}
.height('100%')
.width('100%')
}
}

Swift UI 的 Hello World 如下:


import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

同样的 struct 开头,同样的 声明方式,同样的 Text,同样的设置外观属性方法。
不同的是 ArkTS 按照 Typescript 的写法,以装饰器起头,以 build 方法作为初始化的入口,build 里面才是元素;而 Swift UI 整个 ContentView 就是一个大元素,然后开始潜逃内部元素。


声明式 UI 描述


显然,两者都是用了 声明式的 UI 描述。


个人总结声明式 UI 的公式如下:


Element(props) {
SubElement...
}.attribute(value)

元素(元素属性配置) {
子元素
}.元素外观属性(元素外观)

因为 ArkTS 本质上是 Typescript / Javascript ,所以其写法要符合 TS/JS 的写法,引入的属性、变量必须有明确指示。


Text(this.message)  // message 是当前页面引入的,要有 this
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold) // Bold 是公共常量 FontWeight 的其中一值

对于前端来说,简单易读。这里的 Text 传入一个 message,然后标记 id 为 HelloWorld,设置字体为 50,字重为粗体。


而 Swift 或许更符合苹果以前 Obj C 迁移过来的人的写法。


Image(systemName: "globe") // 公共库名:公共库的值
                .imageScale(.large) // .large 是公共常量的一个值
                .foregroundStyle(.tint)// .tint 是公共常量的一个值

这里的 Image 引入了公共图标库的一个 globe 的图标,然后设置图片大小为大,前景样式为系统主题的颜色,Image(systemName: "globe") 明显不符合 new 一个类的定义方法,.imageScale(.large).foregroundStyle(.tint) 也不符合参数的使用,按以前的解读会有点让人懵圈。


如果转换成 Typescipt 应该是这样的:


new Image(SystemIcon.globe)
                .imageScale(CommonSize.large)
                .foregroundStyle(CommonColor.tint)

显然 ArkTS 更符合前端人的阅读、书写习惯。但其实掌握 Swift UI 也并不难,只需要记住 Swift UI 的这些细小差别,写两次也能顺利上手了。


声明式 UI 的耦合性争议


也许不少人会对声明式 UI 的耦合性(M和V耦合在一起)反感。但是在前端来说,除了以前的 的 MVC 框架 Angular.js 外,其余框架即使是 MVVM,也很少能做到解耦合的,特别是单功能内和业务数据交互的耦合。


所以,前端耦合性,还是需要自行处理。


Button({
action: handleGoButton
}).{
Text("Go")
}

// 自行决定 handleGoButton 是否需要放在外部文件中
private func handleGoButton() {
...
}

// 数据耦合在 UI 中,无解
ZStack {
Color(selectedImageIndex == index ?
Color.hex("808080") : Color.hex("585857")) // 选中的背景颜色区别一般的
...
}

前端的解耦合最终还是需要靠组件化、高阶函数来完成:



  1. 组件化:通过将 UI 分解为独立的组件,每个组件都有自己的功能和状态,可以进一步降低耦合性。组件化使得开发者可以独立地开发和测试组件,而不需要关心其他部分的实现。

  2. 高阶函数:在某些声明式 UI 框架中,可以使用高阶函数来复用共有逻辑,同时允许替换独有逻辑。这种方式可以减少代码的重复,并提高组件的可重用性。


组件差异


SwiftUI 和鸿蒙操作系统的 ArtTS UI 框架都提供了多种组件,按前端使用情况,其实同样有很多相同之处。


基础组件


基础组件基本相同:



  • Text 用于显示文本;

  • Image 用于显示图片;

  • Button 用户可以点击执行操作;


布局组件


Swift UI 和 ArkTS 相同/相似的布局组件有:


Swift UIArkTS说明
HStackRow水平堆栈,用于水平排列子视图
VStackColumn垂直堆栈,用于垂直排列子视图
ZStackStack堆栈视图,用于堆叠多个视图
SpacerBlank空白组件,用于占据剩余空间
ScrollViewScroll滚动视图,允许用户滚动内容
TabsViewTab标签视图,用于创建标签式导航
NavigationViewNavigation导航视图,用于创建导航结构

可以看出,两者基本上的布局都可以通用,当然细节上肯定会有很多不同。不过做一个转译应该不难,可以尝试使用 AI 来完成。


不同的地方在于 Flex 和 Grid 布局:



  1. Swift UI 仅有懒加载的组件:LazyVGridLazyHGrid:懒加载的网格视图,用于展示大量数据。

  2. Ark TS UI有的组件:Flex以弹性方式容器组件:用于灵活布局;Grid网格布局组件:用于创建网格布局。


SwiftUI的布局系统非常灵活,可以通过调整alignmentspacingpadding等属性来实现复杂的布局需求。虽然它不完全等同于CSS中的Flexbox和Grid,但是通过组合使用不同的布局视图,创建出丰富多样的布局效果。


个人在实际开发 iOS 中,通过基础的布局组件搭配,就能完美的实现弹性布局和网格布局。


表单组件


Ark TS UI 提供了一系列的组件,更接近于 html 同名的组件:



  • TextInput:用于文本输入。

  • CheckBox 和 Switch:用于布尔值的选择。

  • Radio:用于单选按钮组,类似于 HTML 中的单选按钮。

  • Picker:用于选择器,可以用于日期选择、时间选择或简单的选项选择,类似于 HTML 中的 <select>


Ark TS UI 表单组件的特点在于它们与数据绑定紧密集成,可以通过 @State@Link@Prop 等装饰器来管理表单的状态和数据流。


而 Swift UI 也有类似的表单组件,但是大部分都不相同:



  • TextField:用于文本输入,可以包含占位符文本。

  • SecureField:用于密码输入,隐藏输入内容。

  • Picker:用于选择器,可以用于选择单个值或多个值。

  • Toggle:用于布尔值的选择,类似于开关。

  • DatePicker:用于日期和时间选择。

  • Slider 和 Stepper:用于数值选择,Slider 提供连续值选择,而 Stepper 提供步进值选择。

  • Form:一个容器视图,用于组织输入表单数据,使得表单的创建和管理更为方便。


虽然不能一一对应,但像日期选择器那样,目前大部分用户已经基本适应了 android 和 iOS 的差异。


总结 - 可运用 AI 转译


通过这次对比学习,可以得出以下结论:



  1. 声明式 UI 是前端更容易配置与阅读,尤其是 ArkTS ;

  2. 解耦合需要运用组件化、高阶函数等知识进行自行处理;

  3. ArkTS UI 和 Swift UI 的基础组件、布局组件相似度非常高,基本能一一对应,可以对照学习使用;

  4. 鉴于两者相似度高,可以尝试开发一个 app,然后另一个 app 使用 AI 来完成转译。


第四点,个人觉得难度不大,本人用代码差异非常大的 android app 转译 iOS app 的也能成功,只是一个个页面进行调试花了不少时间。


至于先开发哪个 app 看你个人习惯,如果你是老 iOS 开发,可以使用先开发 iOS 再进行鸿蒙 OS 开发;甚至 react native 开发生成 iOS 之后,通过生成的 iOS 代码进行转译鸿蒙 OS app。如果你没有之前的负担,完全可以学习 ArkTS,更快地入手,然后通过转译 iOS app 来学习 Swift。


作者:陈佬昔的编程人生
来源:juejin.cn/post/7449173391443329078
收起阅读 »