注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

从热衷到放弃:我的 Cursor 续费终止之路

前言 从我最开始用 Cursor 到现在已经有几个月了,然而随着对它的使用时间越来越长,我感觉帮助反而慢慢变小了,一度我这个月想着不续费了,然后我以为 13 到期、结果却是 7 号到期,所以又自动续费了。 最开始接触 我已经用了 Cursor 好几个月了,而...
继续阅读 »

前言


从我最开始用 Cursor 到现在已经有几个月了,然而随着对它的使用时间越来越长,我感觉帮助反而慢慢变小了,一度我这个月想着不续费了,然后我以为 13 到期、结果却是 7 号到期,所以又自动续费了。


image.png


最开始接触


我已经用了 Cursor 好几个月了,而我最开始用 Cursor 是在好几个月前。


最开始在社交平台上了解到它的功能后,我就很激动了,感觉这也太神了,就一直想体验。但那时也了解到它的价格的何等的贵,所以我开始并没有直接去下载它,而是找了平替 Windsurf


image.png


但别说用了,连注册都注册不了,反复试了几天,也没有注册成功。换手机、换科学上网的不同提供商,都没用。这不,才开始用 Cursor。


无限续杯 之 走到终点


等我用了之后,体验了它的14天免费时间。之后我就觉得它太强了。很多我可以写的功能,它可以以更好的方式,很快的方式生成出来。然而很多我还不会写的。它依然可以去实现。但14天很快就过去了,接着就是一段时间 帐号的删除与注册。但好景不长...


终于在多次反复删除账号又注册。这种操作对我来说已经失效了。


看过了掘金的大多教程,都没有什么用,最后一次有用,但第二天就又不行了。


但那段时间刚巧,我之前在找工作,那几天刚好入职。我就在工作中用了它,有意思的是,我们公司并没有人知道 cursor,甚至没人知道有 ai编辑器。过了几天,无限续杯 也刚好达到极限。


充会员 -> 早下班


当我体验了 Cursor 的威力后,我已经要离不开它了。


我是前端,而 Cursor 可以支持发给它图片,让它画页面。其实画的还挺不错的,这一点就深得我心,所以我痛定思痛充了会员。


image.png


那段时间,我就没加过班、自在的我自己都不知道该如何描述了。但刚开始还是不怎么舒服,因为它老是给我生成用 element-plus 组件直接写的,而我们公司有不少组件是二次封装的,导致我总是要改它。但用了十几次后,它就知道我要什么了。


用了哪些功能 与 自己的感受


Tab 的好与不好


我常用的功能就是tab,主要的是它比较灵活,生成的速度也比较快。而且用的越多它就越可以生成想要的代码。比如项目中自己封装的组件。用 tab 几次之后,它就自己可以去用这个组件,但更多情况下,它生成的会是之前写过的内容。


不过它也有一些缺点,比如:不能去预判一些复杂的思路。如果我们写了一个按钮,并在按钮身上绑定一个 Click 事件,名称叫做 search。Cursor 的 Tab 就可以自动会生成 search函数。但如果你只是在这里写了一个按钮,想要做的功能是导出。你没有在按钮上写导出两个字,也没有去绑定一个 Click事件 叫 export。那 Cursor 根本就不知道你要做的是导出,也就不会去自动实现这些功能。


另一个 Tab 的缺点,那就是影响复制功能。经常准备复制内容时,Tab 就给出了它的预判,然而原本你打算复制10个字,此时它的预判在 10个字中间加了 30个字。你要是想复制,正常就会用鼠标选中字,可一旦你鼠标点下那个位置,Tab就来了。我多次遇到这个问题,如果你没有遇到过,请教我一下方法。


对话模式


对话分两种,一种是全局,一种是局部。


先来说一下全局。


全局对话 cmd + i


image.png


由于 Cursor 默认会将所有文件自动追踪索引。所以当我们进行全局对话时, Cursor 会基于全局所有文件的索引为基础。去修改现在的代码,但如果我们只想改当前一个页面,它依然会去分析全局,增加了要处理的数据量,就导致时间比较长。


不知道是不是我的科学上网工具问题,我几乎只要用全局问答,就要好几分钟,要是改错了,又要重来,所以现在几乎就不用了。


另一个是后面代码变多了,时间就更长,而且它老是给我优化我不要优化的,因为它经常优化错了。比如关于接口的 type,我都是在 api 文件夹中定义的。但它总说在那个文件中没有这个 type,然后就自动在当前文件附近又创建 types.ts ,然后声明的类型和接口都不是对应的。


当然了,它的好处是分析的全面、如果要跨多个文件修改同一个功能,则它再慢,也得等着。


之后我就又想起了 局部 对话


局部对话 cmd + k


image.png


我是上段时间才开始用这个的,因为全局的太慢了,就突然想起来还有局部的 cmd ➕ k 。这个还不错,我最初是用来写 API 数据的。


因为我们是用 ApiPost,我就直接在左边接口标题处,点击复制,然后进代码,在局部问答中发给他,然后说,写出接口和类型。基本没出过错。


用了几天后也发现它的局限性了,就是它貌似只能在问的位置下方生成,如果我要它跨越几个地方添加就没用了。如:在template中生成页面展示的,在JS中生成脚本,在style中生成样式。


但之后发现这种方式不仅能生成,如果你选中了内容,它还能修改。然后我就随机一动,直接全选当前文件,则实现了对一整个文件的局部修改。但说实话,速度也并没有太快。


cursorrules


后来我又加了cursorrules,最初我以为只能用一个rules文件,直到在一个微信群里看见别人分享的照片,他有6个左右的rules。之后我就用了两天时间自己写了4个rules。但经常没有效果,而且还开启了always。


image.png


之后,我就在开始的位置写上这样这句话:


自动激活

这些指令在本项目的所有对话中自动生效。当使用到该 rules 时,要打印出这个rules的名字,如"使用了 项目规则.mdc 文件",让我知道你使用了这个文件。


之后有一次就突然出现了这句话


image.png


可是,只出现过这个 项目规则.mdc ,其它的mdc 都没有出现过,但其它的文件中 我也写了类型的 自动激活的话。不知道为什么没有生效。


MCP


server-sequential-thinking


MCP 之前使用过,那时主要火的是 server-sequential-thinking, 它的主要功能是思维更有条理。如果你在对话中 说了类似 " 思考 " 的话,那就会激活它。之后它就一句话一句话的分析,也一句一句的解释。因为工作中比较少的有这么有深度的思考,我几乎没用过它。而且用了它之后,话也变多了,导致效率也慢,外加 科学上网 的工具并不好,就更慢了。 上段时间我又开始使用它了,但一直没生效,不知道为啥?


playwright 自动化测试


用这个可能比较复杂,其实我就是希望 Cursor 可以自己调接口,然后根据 api 文件中的 对接口的声明、参数类型与返回类型。自动帮我实现 增删改查 ,如果一个表单,我的字段写错了,它就自动修改,然后继续填写数据再调接口。直到跑通为止。 因为这确实很费时间,也没意思。但至今也没有做到。


browser-tool-mcp


这个是用来让 Cursor 监控浏览器,它可以查看浏览器的 控制台、DOM 结构 等等,但用了一段时间后,发现直接把 控制台的报错 发给 Cursor 更快,也就没怎么用了。


结语


上面 MCP 用的不怎么好的一个原因,是因为没有打通 自动化的流程,所以总是需要我手动的操作。


这个星期打算把 claude 的提示词看一下,看看能不能改善一下 Cursor 的使用情况。


作者:自学前端_又又
来源:juejin.cn/post/7501966297334497290
收起阅读 »

Android 16 适配重点全解读 | OPPO 技术专场直播回顾

5月22日,OPPO举办「OTalk | Android 16 开发者交流专场」,特邀OPPO高级工程师团队深度解读Android 16核心技术要点与适配策略。活动以线上直播形式展开,吸引了众多开发者实时观看并参与讨论,为他们提供了从技术解析到工具支持的全流程...
继续阅读 »

5月22日,OPPO举办「OTalk | Android 16 开发者交流专场」,特邀OPPO高级工程师团队深度解读Android 16核心技术要点与适配策略。活动以线上直播形式展开,吸引了众多开发者实时观看并参与讨论,为他们提供了从技术解析到工具支持的全流程适配解决方案。

、Android 16开发者适配计划

根据Google规划,Android 16.0及16.1版本将于2025年分阶段发布,所有应用须在7月1日前完成适配,覆盖目标版本为36(API 36)的新开发及存量应用,涉及行为变更、API 调整和新功能兼容。开发者可尽早启动测试,以免适配延迟对应用上架和用户体验造成不利影响。

配图1.jpeg

、Android 16核心新特性及适配建议

自适应适配:大屏设备体验的优化

随着折叠屏、平板等多样化设备形态的普及,大屏适配已成为开发者面临的重要技术挑战。在 Android 16.0 中,当应用 Target SDK=36 且运行在最小宽度≥600dp 的设备时,系统将忽略传统的屏幕方向、尺寸可调整性等设置限制,为大屏设备带来更出色的视觉体验。

不过,以下三种情况不在新特性的范围内:

  1. 游戏类应用(需要在清单属性中配置 android:appCategory);

  2. 小于 sw600dp 的屏幕(常见手机设备不受影响);

  3. 用户在系统设置中启用了宽高比配置。

适配建议:

遵循谷歌适配指南,完成大屏布局优化,以提供更佳的用户体验;

若暂不支持,可在 Activity 或 Application 节点添加 PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY 属性临时豁免,但需注意,该配置可能会在 Android 17 中被取消,因此建议开发者优先完成适配。

针对大屏适配,开发者可以参考由 OPPO、vivo、小米等厂商共同制定的《ITGSA 大屏设备应用适配白皮书 2.0》。同时,建议开发者逐步迁移到 Compose 开发,使后续适配工作更加简单高效。

配图2.jpg

预测性返回:手势导航的交互变革

预测性返回是 Android 13 引入的手势导航增强功能,用户在侧滑返回时可以预览目标界面。在 Android 16 中,目标 SDK≥36 的应用默认启用预测性返回动画,系统不再调用 onBackPressed 也不会再调度 KeyEvent.KEYCODE_BACK。

适配建议:迁移至 onBackInvokedCallback 回调处理返回逻辑;若需保留原有逻辑,可在清单中设置 android:enableOnBackInvokedCallback="false" 停用。

配图3.jpg

ART 内部变更:提升性能与兼容性

Android 16 包含 Android 运行时(ART)的最新更新,这些更新旨在提升 ART 的性能,并支持更多的 Java 功能。依赖 ART 内部结构的代码(如私有反射、非 SDK 接口)将全面失效。

适配建议:全面测试应用稳定性,替换非公开 API 为系统提供的公共 API。

配图4.jpg

JobScheduler 配额优化:后台任务的效率革命

为了降低系统负载,Android 16 对 JobScheduler 的执行配额进行了动态管理,根据应用待机分桶和前台服务状态动态分配 JobScheduler 执行配额,活跃应用获得更多配额,后台任务仍需遵守配额限制。

适配建议:减少非必要后台任务,高优先级任务使用 setExpedited() 标记;通过 WorkInfo.getStopReason() 记录任务终止原因并调整调度策略。

配图5.jpg

健康与健身权限:隐私管控的升级

Android 16 将 BODY_SENSOR 权限迁移至 “健康数据共享” 权限组。对于 Target SDK≥36 的应用,需要请求新的权限。

适配建议:更新权限请求逻辑,引导用户在系统级 “健康数据共享” 页面授权。

配图6.jpg

setImportantWhileForeground 接口失效:后台任务的约束

setImportantWhileForeground 接口曾用于让前台任务豁免后台限制,但从 Android 16 开始,该接口的功能已被彻底移除。依赖此接口的下载任务、实时同步等场景可能出现延迟,影响用户体验。

适配建议:改用 jobInfo.setExpedited() 标记加急任务,确保关键操作优先执行。

配图7.jpg

息屏场景自动停止屏幕分享:隐私与管控的平衡

为提升隐私安全,Android 16会在手机息屏或通话结束后,自动释放 MediaProjection。

适配建议:在 onStop 回调中处理异常,如需持续投屏,需重新获取 MediaProjection 权限。

配图8.jpg

此外,在 Android 16 中,多项关键特性同样值得注意。优雅字体 API 被废弃,开发者需手动调整文字布局以确保显示效果。更安全的 Intent 机制要求显式 Intent 与目标组件的 Intent 过滤器相匹配,提升应用安全性。以进度为中心的通知功能增强,通过Notification.ProgressStyle实现更直观的进度可视化。MediaProvider 扩展了能力,PhotoPicker 支持 PDF 读取并增强权限鉴权,同时统一了界面风格。这些变更体现了 Android 16 在安全性、用户体验和功能上的优化。

配图9.jpg

配图10.jpg

在互动答疑环节,有开发者提出预测性返回动画是否是系统强制的问题,纪昌杰表示预测性返回特性需要应用 targetsdk 升级到 36 才会强制生效,未升级的应用则需通过配置使其生效,应用要主动适配,适配重点在于防止系统不再调用 onBackPressed 和不再调度 KeyEvent.KEYCODE_BACK 导致应用逻辑异常。而对于一个开发人员如何高效适配大屏的问题,纪昌杰再次强调,建议开发者逐步迁移到 Compose 平台开发,以获得谷歌更多支持,开发资源有限的开发者可以参考金标联盟制定的大屏适配 2.0 标准,其内容大多基于 View + XML 开发模式进行指导。

OPPO一站式支持体系

在本次交流专场中,纪昌杰还介绍了 OPPO 为助力 Android 16 适配所构建的一站式开发者支持体系。该体系涵盖了详尽的兼容性适配文档,为开发者提供了清晰明确的适配指引;免费的云真机 / 云测服务,赋能开发者随时随地开展高效调试与验证工作。此外,还包括开发者预览版,便于开发者提前评估应用在新系统上的表现,以及应用商店新特性检测,确保应用完全符合 Android 16 的各项标准。同时,开发者可借助适配答疑交流社群和 OPPO 开放平台支持专区等多元渠道,获取全方位支持,有效提升适配效率。

配图11.jpg

此次「OTalk | Android 16 适配开发者交流专场」聚焦前沿技术洞察与实战指南,开发者提供了系统性适配路径与高效解决方案。活动分享的适配策略、高频问题解答等核心资料,将在「OPPO开放平台」公众号及OPPO开发者社区官网发布,开发者可免费查阅并应用于实际开发流程。

作为Android生态的重要推动者,OPPO将持续提供全链路适配支持服务,并通过技术沙龙、开发者社群及线上交流平台,与开发者紧密协作,共同探索Android 16的创新边界,助力移动应用生态实现高质量演进。

收起阅读 »

开源TTS领域迎来重磅新星!Dia-1.6B:超逼真对话生成,开源2天斩获6.5K Star!

文本转语音(TTS)领域又迎来一位重磅新星! 由 Nari Labs 开发的 Dia-1.6B 以其超逼真的对话生成能力引发热议,仅开源两天便在 GitHub 收获 6.5K+ Star! 据说其能力超越了 ElevenLabs 和 Sesame,仅凭借 ...
继续阅读 »

文本转语音(TTS)领域又迎来一位重磅新星!


图片


Nari Labs 开发的 Dia-1.6B 以其超逼真的对话生成能力引发热议,仅开源两天便在 GitHub 收获 6.5K+ Star!


图片


据说其能力超越了 ElevenLabs 和 Sesame,仅凭借 1.6B 参数就可实现情感控制、非语言声音(如笑声、咳嗽)和零样本声纹克隆,运行效率惊人。


支持从文本脚本生成多角色对话,通过 [S1]、[S2] 标签区分角色,生成自然语音,并支持非语言表达和声纹克隆,目前仅限英语。


它在 Hugging Face 也提供了模型权重和 Gradio Demo 体验。


主要功能



  • 多角色对话生成:使用 [S1]、[S2] 等标签区分角色,单次生成多角色对话,保持自然节奏和情感过渡。

  • 拟人化表达:支持笑声(laugh)、叹气(sigh)、咳嗽(cough)等非语言情绪

  • 零样本声纹克隆:可微调或指定语音风格,克隆用户或角色声音

  • 高质量语音合成:音质媲美 ElevenLabs、Sesame,细节自然,情绪变化真实

  • 实时推理速度:A4000 显卡上约 40 tokens/s,体验流畅无等待

  • Gradio 界面支持:自带可用的 Web UI 界面,输入文本立即试听


快速上手


Dia-1.6B 官方实验室提供了详细安装指南和 Gradio 演示。


在线体验:


无需配置环境,打开 Hugging Face Demo 即可输入脚本或音频试听效果:


Demo:huggingface.co/spaces/nari…


安装部署与使用步骤:


1、克隆项目


git clone https://github.com/nari-labs/dia.git
cd dia

2、创建虚拟环境


python -m venv .venv
source .venv/bin/activate

3、安装依赖


pip install -e .

4、启动 Gradio UI


python app.py

访问 http://localhost:7860,输入脚本或上传音频,生成对话。


示例脚本:


[S1] Dia is amazing! [S2] Yeah, it generates laughs too! (laughs)

图片


还可以通过 Python 包安装 Dia 的API


# Install directly from GitHub
pip install git+https://github.com/nari-labs/dia.git

Python 调用示例:


import soundfile as sf

from dia.model import Dia


model = Dia.from_pretrained("nari-labs/Dia-1.6B")

text = "[S1] Dia is an open weights text to dialogue model. [S2] You get full control over scripts and voices. [S1] Wow. Amazing. (laughs) [S2] Try it now on Git hub or Hugging Face."

output = model.generate(text)

sf.write("simple.mp3", output, 44100)

后续也将推出 pypi包和CLI工具。


使用场景推荐



  • 有声读物 / 小说播报:让不同角色“开口说话”,配合情绪词还原真实语境

  • 播客配音:快速合成带感情、有风格的访谈类语音

  • AI 角色扮演:配合 Agent,多角色模拟对话系统

  • TTS 研究与微调:声音克隆、情绪控制、非语言表达


写在最后


Dia-1.6B 是开源 TTS 领域的最新研究成果,其逼真对话和低资源需求让人惊叹。


小体量(1.6B 参数)却能生成媲美 ElevenLabs、Sesame 的高保真语音,甚至能区分角色、模拟非语言情绪(比如:[咳嗽]、[叹气]、[笑声])。


其免费开源 + 超高拟真度 + 多角色/非语言表达全支持,使其成为目前开源TTS领域最值得关注的TTS项目之一!


不过目前仅支持英文,期待后续能够支持中文及更多语言。


GitHub 项目地址:github.com/nari-labs/d…


HF 模型地址:huggingface.co/nari-labs/D…


在线Demo:huggingface.co/spaces/nari…


如果你有个 AI 角色,想让它“开口说话”,甚至“笑一下”,Dia-1.6B 那就太适合了。


作者:开源星探
来源:juejin.cn/post/7496475277187498011
收起阅读 »

这么好看的 Flutter 搜索框,快来看看是怎么实现的

最近项目中在实现一个搜索的功能,根据 Flutter 的类似组件的调用习惯,输入 showSearch 后发现还真有,跳进源码中一看,Flutter 已经实现了相应的 Widget 和交互,简直不要太方便了,先来看看如何调用的。 showSearch 方法介绍...
继续阅读 »


最近项目中在实现一个搜索的功能,根据 Flutter 的类似组件的调用习惯,输入 showSearch 后发现还真有,跳进源码中一看,Flutter 已经实现了相应的 Widget 和交互,简直不要太方便了,先来看看如何调用的。


showSearch 方法介绍


Future<T?> showSearch<T>({
required BuildContext context,
required SearchDelegate<T> delegate,
String? query = '',
bool useRootNavigator = false,
}) {
assert(delegate != null);
assert(context != null);
assert(useRootNavigator != null);
delegate.query = query ?? delegate.query;
delegate._currentBody = _SearchBody.suggestions;
return Navigator.of(context, rootNavigator: useRootNavigator).push(_SearchPageRoute<T>(
delegate: delegate,
));
}

上面函数定义在源码 flutter/lib/src/material/search.dart 文件中,根据该函数要求须传入一个 contextdelegatecontext 是我们的老朋友,这里就无需过多介绍了。但是这个 delegate (SearchDelegate 类)是干啥的?继续跳到 SearchDelegate 发现SearchDelegate 是一个抽象类,SearchDelegate 第一句介绍 Delegate for [showSearch] to define the content of the search page. 定义搜索页面的内容,也就是说需要我们创建一个继承自 SearchDelegate 的子类来实例化参数 delegate,下面是这个子类CustomSearchPageDelegate的代码。


class CustomSearchPageDelegate extends SearchDelegate<String> {
// 搜索框右边的显示,如返回按钮
@override
List<Widget>? buildActions(BuildContext context) {}

// 搜索框左边的显示,如搜索按钮
@override
Widget? buildLeading(BuildContext context) {}

// 搜索的结果展示,如列表 ListView
@override
Widget buildResults(BuildContext context) {}

// 输入框输入内容时给出的提示
@override
Widget buildSuggestions(BuildContext context) {}
}

从上面可以看出我们需要返回4个 Widget 来显示内容,其中 buildLeadingbuildActions 分别对应搜索框左右两边的内容,通常是 button,如 buildLeading 是返回按钮,buildActions 右边是搜索按钮。buildResults 则表示搜索的结果展示,通常是一个列表,而 buildSuggestions 展示当用户在输入框输入内容时给出的提示,展示多条提示内容时也会用到列表(ListView)。


实现 CustomSearchPageDelegate


接下来以搜索文章为例子利用自定义的CustomSearchPageDelegate 类实现一下搜索功能。


import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_todo/pages/search_page/data_item_model.dart';
import 'package:flutter_todo/pages/search_page/search_item_widget.dart';

class CustomSearchPageDelegate extends SearchDelegate<DataItemModel> {
CustomSearchPageDelegate({
String? hintText,
required this.models,
}) : super(
searchFieldLabel: hintText,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
);

/// 搜素提示
List<String> suggestions = [
"Flutter",
"Flutter开发7个建议,让你的工作效率飙升",
"浅谈 Flutter 的并发和 isolates",
"Flutter 技术实践",
"Flutter 中如何优雅地使用弹框",
"Flutter设计模式全面解析:单例模式",
"Flutter Dart",
"Flutter 状态管理",
"Flutter大型项目架构:UI设计系统实现(二)",
"Flutter大型项目架构:分层设计篇",
"Dart 语法原来这么好玩儿"
];

/// 模拟数据,一般调用接口返回的数据
List<DataItemModel> models = [];

/// 搜索结果
List<DataItemModel> results = [];

/// 右边的搜索按钮
@override
List<Widget>? buildActions(BuildContext context) {
return [
InkWell(
onTap: () {},
child: Container(
margin: const EdgeInsets.all(10),
height: 30,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(30)),
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
child: const Text(
"搜索",
style: TextStyle(color: Colors.white, fontSize: 14),
),
),
),
];
}

/// 左边返回按钮
@override
Widget? buildLeading(BuildContext context) {
return InkWell(
onTap: () {
/// 返回操作,关闭搜索功能
/// 这里应该返回 null
close(context, DataItemModel());
},
child: Container(
padding: const EdgeInsets.all(15.0),
child: SvgPicture.asset(
"assets/images/arrow.svg",
height: 22,
color: Colors.black,
),
));
}

/// 搜索结果列表
@override
Widget buildResults(BuildContext context) {
return ListView.separated(
physics: const BouncingScrollPhysics(),
itemCount: results.length,
itemBuilder: (context, index) {
DataItemModel item = results[index];
/// 自定义Widget,用来显示每一条搜素到的数据。
return SearchResultItemWidget(
itemModel: item,
click: () {
/// 点击一条数据后关闭搜索功能,返回该条数据。
close(context, item);
},
);
},
separatorBuilder: (BuildContext context, int index) {
return divider;
},
);
}

/// 提示词列表
@override
Widget buildSuggestions(BuildContext context) {
List<String> suggestionList = query.isEmpty
? []
: suggestions
.where((p) => p.toLowerCase().contains(query.toLowerCase()))
.toList();
if (suggestionList.isEmpty) return Container();
return ListView.separated(
itemBuilder: (context, index) {
String name = suggestionList[index];
return InkWell(
onTap: () {
/// 点击提示词,会根据提示词开始搜索,这里模拟从models数组中搜索数据。
query = name;
results = models
.where((e) =>
(e.title?.toLowerCase().contains(name.toLowerCase()) ??
false) ||
(e.desc?.toLowerCase().contains(name.toLowerCase()) ??
false))
.toList();

/// 展示结果,这个时候就调用 buildResults,主页面就会用来显示搜索结果
showResults(context);
},
child: Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
child: Row(
children: [
SvgPicture.asset(
"assets/images/search.svg",
height: 16,
color: const Color(0xFF373737),
),
const SizedBox(
width: 4,
),
RichText(
text: TextSpan(children: getSpans(name)),
),
],
),
),
);
},
itemCount: suggestionList.length,
separatorBuilder: (BuildContext context, int index) {
return divider;
},
);
}

/// 分割线
Widget get divider => Container(
color: const Color(0xFFAFAFAF),
height: 0.3,
);

/// 富文本提示词,其中如果和输入的文本匹配到的关键字显示红色。
List<TextSpan> getSpans(String name) {
int start = name.toLowerCase().indexOf(query.toLowerCase());
String end = name.substring(start, start + query.length);
List<String> spanStrings = name
.toLowerCase()
.replaceAll(end.toLowerCase(), "*${end.toLowerCase()}*")
.split("*");
return spanStrings
.map((e) => (e.toLowerCase() == end.toLowerCase()
? TextSpan(
text: e,
style: const TextStyle(color: Colors.red, fontSize: 14))
: TextSpan(
text: e,
style:
const TextStyle(color: Color(0xFF373737), fontSize: 14))))
.toList();
}
}

这里要说明一下,query 关键字是输入框的文本内容。调用的时候实例化一下该类,传递给 shwoSearchdelegate 参数。下图就是我们看到的效果:
521730704175_.pic.jpg


总结问题


以上图片的搜索框还可以通过重写 appBarTheme 来定制自己想要的 UI 效果,虽然可以这样,但是和我们要实现的效果比起来还相差甚远,尤其是顶部的搜索框,其左右两边的留白区域过多,背景颜色无法调整,内部的输入框 TextField 也无法定制自己想要的效果,如不能调整其圆角、背景颜色以及添加额外控件等等。


还有一点就是当我们点击返回按钮调用 close 时,这里返回值是泛型 T 却不支持 null 类型,在文章的开头,我们可以看到 shwoSearchdelegate 参数类型是 SearchDelegate<T>,所以创建 CustomSearchPageDelegate 时必须这样去声明。


class CustomSearchPageDelegate extends SearchDelegate<DataItemModel>

而我们想要实现这样去声明


class CustomSearchPageDelegate extends SearchDelegate<DataItemModel?>

这样当我们调用 close 时可以做到传 null,在外面调用的位置可以对返回值进行判断,返回值为 null 就不作任何处理。


交互上,在点击键盘上的 搜索 按键时,直接调用的是 showResults 函数,而通常的操作是需要调用搜索的接口拿到数据后,再去调用 showResults 函数来展示搜索结果的数据。


对于上述问题,我们可以做什么呢?


源码分析


想要到达我们需要的效果,还是需要看看 Flutter 的源码是怎么实现的,我们再次来到 flutter/lib/src/material/search.dart 文件中,可以看到该文件中定义了除上面提到的抽象类 SearchDelegate 和全局函数 showSearch 之外,还有内部类 _SearchPageRoute_SearchPage_SearchPageRoute 继承自 PageRoute,顾名思义就是负责路由跳转及转场动画的。


以下是 _SearchPageRoute 部分代码:


class _SearchPageRoute<T> extends PageRoute<T> {
_SearchPageRoute({
required this.delegate,
}) : assert(delegate != null) {
delegate._route = this;
}

final SearchDelegate<T> delegate;

/// ...

@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return _SearchPage<T>(
delegate: delegate,
animation: animation,
);
}

/// ...
}

重写父类的 buildPage 方法,将 delegate 传递给 _SearchPage 并将其返回,而所有的 UI 逻辑都在这个 _SearchPage 中,来到 _SearchPagebuild 函数中就可以看到下面的实现。


_SearchPagebuild 函数代码


@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final ThemeData theme = widget.delegate.appBarTheme(context);
final String searchFieldLabel = widget.delegate.searchFieldLabel
?? MaterialLocalizations.of(context).searchFieldLabel;
Widget? body;
// _currentBody 枚举类型_SearchBody,用来区分当前body是展示提示列表还是搜索结果列表,
// 当调用 SearchDelegate 中 showResults 函数时,_currentBody = _SearchBody.results
// 当调用 SearchDelegate 中 showSuggestions 函数时,_currentBody = _SearchBody.suggestions
switch(widget.delegate._currentBody) {
case _SearchBody.suggestions:
body = KeyedSubtree(
key: const ValueKey<_SearchBody>(_SearchBody.suggestions),
child: widget.delegate.buildSuggestions(context),
);
break;
case _SearchBody.results:
body = KeyedSubtree(
key: const ValueKey<_SearchBody>(_SearchBody.results),
child: widget.delegate.buildResults(context),
);
break;
case null:
break;
}

return Semantics(
explicitChildNodes: true,
scopesRoute: true,
namesRoute: true,
label: routeName,
child: Theme(
data: theme,
child: Scaffold(
appBar: AppBar(
leading: widget.delegate.buildLeading(context),
title: TextField(
controller: widget.delegate._queryTextController,
focusNode: focusNode,
style: widget.delegate.searchFieldStyle ?? theme.textTheme.titleLarge,
textInputAction: widget.delegate.textInputAction,
keyboardType: widget.delegate.keyboardType,
onSubmitted: (String _) {
widget.delegate.showResults(context);
},
decoration: InputDecoration(hintText: searchFieldLabel),
),
actions: widget.delegate.buildActions(context),
bottom: widget.delegate.buildBottom(context),
),
body: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: body,
),
),
),
);
}

_SearchPage 中的实现也非常简单,就是一个嵌入到 AppBar 中的搜索框和呈现 suggestion listresult listbody。想要定制自己的 UI 效果,改的也是该位置的代码。


优化实现


UI 方面主要针对 TextFieldAppBar 代码修改,怎么改就看想要实现什么效果了。参考 Flutter 官方的源码,重新实现一个的 _SearchPage 类,然后在 _SearchPageRoute 替换成自己写的 _SearchPage,再去 SearchDelegate 替换一下修改过的 _SearchPageRoute


还一个问题怎么实现调用 close 时可以返回 null 的结果内呢?除了上面提到的这样去声明


class CustomSearchPageDelegate extends SearchDelegate<DataItemModel?>

之外,还需要修改 _SearchPageRoute


// 改之后
final CustomSearchDelegate<T?> delegate;
// 改之前
// final CustomSearchDelegate<T> delegate;

重新定义一个全局函数 showSearchWithCustomiseSearchDelegate,和官方的区分开来。


Future<T?> showSearchWithCustomiseSearchDelegate<T>({
required BuildContext context,
// 这里的泛型由原来的 T 改成了 T?
required CustomSearchDelegate<T?> delegate,
String? query = '',
bool useRootNavigator = false,
}) {
delegate.query = query ?? delegate.query;
delegate._currentBody = _SearchBody.suggestions;
// 这里的 _SearchPageRoute 是我们自己实现的类
return Navigator.of(context, rootNavigator: useRootNavigator)
.push(_SearchPageRoute<T>(
delegate: delegate,
));
}

来看看最终调用上面的函数


DataItemModel? result =
await showSearchWithCustomiseSearchDelegate(
context: context,
delegate: SearchPageDelegate(
hintText: "Flutter 技术实践", models: models));
if (result != null) {
/// to detail page
}

解决交互上的问题,需要在我们自己抽象类 SearchDelegate 单独定义一个函数 onSubmit,点击键盘上的搜索按键和右边的搜索按钮调用 onSubmit 函数,如:widget.delegate.onSubmit(context, text);,在 SearchDelegate 子类的 onSubmit 中来实现具体的逻辑,如发送网络请求,返回数据后在调用 showResults


@override
void onSubmit(BuildContext context, String text) {
// 发送网络请求,拿到数据。
// showResults(context);
}

整体实现的代码量多,就不在文中贴出来了,具体实现大家可以参考这里的代码:
github.com/joedrm/flut…


下图是最终实现效果:



小结


自定义搜索框的实现整体来说还是比较简单的,相比于源码改动的地方并不多,就可以显示想要的效果。当然还有其它更多的实现方式,这里只是提供了一种分析思路。我们还可以发散一下,去看看其它的如:showBottomSheetshowDialog 等等和 SearchDelegate,他们直之间也有不少类似的地方,当我想要自定义自己的控件时,会发现其实很多答案就在官方的源码里,动手改吧改吧就出来了。最后聊一下近况,近期有一些想法在忙着实现,时间有点安排不过来,文章的更新就有点儿偷懒了,跟大家说声抱歉,后面有机会单独来分享一下最近忙的事情,最后感谢大家耐心的阅读!


作者:那年星空
来源:juejin.cn/post/7433469126629081140
收起阅读 »

短信为何没落?

短信,从曾经的日常对话,变成了今天只在收验证码、收通知时才偶尔想起的工具。我们不再用它聊天,但它却以另一种方式,继续默默留在了每一部手机里——像一条永不熄灭的应急通道,安静又顽强。 为什么传统短信衰落了? 我想,原因显而易见: 体验落后:纯文本,沉闷乏味,与...
继续阅读 »

短信,从曾经的日常对话,变成了今天只在收验证码、收通知时才偶尔想起的工具。我们不再用它聊天,但它却以另一种方式,继续默默留在了每一部手机里——像一条永不熄灭的应急通道,安静又顽强。


为什么传统短信衰落了?


我想,原因显而易见:



  • 体验落后:纯文本,沉闷乏味,与今天充满表情包、短视频的社交体验格格不入。

  • 安全问题:垃圾信息与钓鱼链接充斥,带来隐私泄露和金融诈骗风险。

  • 替代者崛起:OTT应用(如微信等即时通讯)越过运营商,构建了更强大且丰富的通讯体验。



一句话总结:自第一代iPhone发布以来,短信从未进化过。


但近两年,5G消息的概念强势崛起:试图“复活”短信。


它被认为是短信的升级版,是一种长得更好看的短信,支持文本、图片、音频、视频、定位、支付、卡片应用等丰富媒介内容,如图。它试图成为新的“杀手级应用”,补齐传统短信缺失的智能与体验。



但问题来了 —— 对用户来说,这些体验在APP应用里早已稀松平常,“了无新意”。。。5G消息远远谈不上颠覆,只是跟上了过去的潮流。


思考


这里想问大家一句:短信最大的优势是什么?


—— 我认为可能是有一点:那就是 “强推送“ 。即只要拿到了手机号,不管你愿意不愿意,就能给你发短信!


—— 这个可以是正向积极的、比如,如果在地震台风来临时,谁也不想错过“被强推送”一条预警消息。或者:你订阅了关注金价走势的服务,每天早上9点,直接推送一条金价信息,也不会让人太反感尔尔。


—— 也可以是负面消极的,比如消息轰炸、低劣推销、骚扰信息的等。



本瓜认为:真正的变化不应该是“形式”,应该发生在基础设施层



现在的消息生态,并不是单一平台控制的,而是:运营商、手机厂商、MaaP平台、AI服务商、HUB厂商、SDK厂商、企业用户等多方共建。


多方参与虽然打破了垄断,但也带来了潜在的协调低效与体验分裂问题。



移动互联网的成功经验告诉我们:轻应用,才是黏住用户、构建生态的核心。


1、对用户而言:免安装、免更新、即用即走、无缝切换。—— 是好的!


2、对开发者而言:开发简单、成本低、快速迭代。—— 也是好的!


在App平台上,代表性的轻应用是小程序。而在5G消息体系里,对应的是Chatbot(聊天机器人) 。—— 就有点像把如今火爆的智能体加入到短信里。


虽然聊天机器人前端看似简单,但背后隐藏着巨大的开发挑战:不仅是UI,而是要设计对话剧本、逻辑分支;还有自然语言输入不确定,需要NLP、NLU技术精准理解;需要训练、调优,否则容易出错让用户失望等等。


对企业来说,想开发一个成熟Chatbot的难度和投入,远超表面。所以,这也是为什么现在你还没有开始在短信界面呼起各类机器人/应用。


未来企业很可能同时包含:



  • App:重应用,沉淀核心用户。

  • 小程序:轻应用,覆盖广泛场景。

  • Chatbot:对话应用,增强即时服务与主动触达能力:智能客服、营销互动



问题又来了: 要支撑这样分散、灵活、快速演化的格局,需要怎样的基础设施?



可能需要一套完善的 沙箱计划:即允许应用以轻量方式接入、运行、管理,同时保障数据隔离、安全合规。


作者:掘金安东尼
来源:juejin.cn/post/7503462035272073266
收起阅读 »

拿去吧你!Flutter 仿抖音个人主页下拉拖拽效果

引言 最近产品经理看到抖音的个人主页下拉效果很不错,让我也实现一个,如果是native还好办,开源成熟的库一大堆,可我是Flutter呐🤣,业内成熟可用的库非常有限,最终跟产品经理batte失败后,没办法只能参考native代码硬肝出来。 效果图 整体构思 ...
继续阅读 »

引言


最近产品经理看到抖音的个人主页下拉效果很不错,让我也实现一个,如果是native还好办,开源成熟的库一大堆,可我是Flutter呐🤣,业内成熟可用的库非常有限,最终跟产品经理batte失败后,没办法只能参考native代码硬肝出来。


效果图


掘金素材.gif


整体构思


实现拖拽滑动功能,关键在于对手势事件的识别。在 Flutter 中,可使用Listener来监听触摸事件,如下所示:


Listener(
onPointerDown: (result) {

},
onPointerMove: (result) {

},
onPointerUp: (_) {

}

在手指滑动的过程中不断的刷新背景图高度是不是就可以实现图片的拉伸效果呢?我们这里图片加载库使用CachedNetworkImage,高度在156的基础上动态识别手指的滑动距离extraPicHeight


CachedNetworkImage(
width: double.infinity,
height: 156 + extraPicHeight,
imageUrl: backgroundUrl,
fit: fitType,
)

识别到手指滑动就不断的刷新拉伸高度extraPicHeight,flutter setState 内部已经做了优化,不用担心性能问题,实际效果体验很不错。


setState(() {
extraPicHeight;
});

经过实验思路是没有问题,那么监听哪些事件,extraPicHeight到底怎么计算,有什么边界值还考虑到呢?我们从手势的顺序开始梳理一下。


首先按压屏幕会识别到触碰屏幕起点,也就是initialDx initialDy,对于下拉拖拽我们关心更多的是纵向坐标result.position.dy


onPointerDown: (result) {
initialDy = result.position.dy;
initialDx = result.position.dx;
},

当手指在屏幕滑动会触发onPointerMovew,result.position.dy代表的就是手势滑动的位置


onPointerMove: (result) {
//手指的移动时
// updatePicHeight(result.position.dy); //自定义方法,图片的放大由它完成。
},

这边处理逻辑比较复杂,我们先抽成函数updatePicHeight


updatePicHeight(changed) {
//。。。已省略不重要细节代码
extraPicHeight += changed - prev_dy; //新的一个y值减去前一次的y值然后累加,作为加载到图片上的高度。
debugPrint('extraPicHeight updatePicHeight : $extraPicHeight');
//这里是为了限制我们的最大拉伸效果
if (extraPicHeight > 300) {
extraPicHeight = 300;
}
if (extraPicHeight > 0) {
setState(() {
prev_dy = changed;
});
}
}

这里简化了很多细节逻辑,核心目的就是要不断的累加我们的拖动距离来计算extraPicHeight高度,这里的changed是我们手指的y坐标,滑动的距离需要减去上次滑动的回调y,所以我们必须声明一个过去y坐标的变量也就是prev_dy,通过通过 changed - prev_dy就可以得出真正滑动的距离,然后我们不断累加 extraPicHeight += changed - prev_dy就是图片的拉伸距离。


手指下拉以后图片确实拉伸了,但是松开手后发现回不去了🤣因为我们还需要处理图回去的问题,既然可以通过setState把图片高度拉高,我们也可以通过setState把图片高度刷回去,核心要思考的是如何平滑的让图片自己缩回去呢?有经验的你一定想到动画了。


flutter这里的动画库是TweenTween可以通过addListener监听距离的回调,当距离变化不断刷新图片高度


anim = Tween(begin: extraPicHeight, end: 0.0).animate(animationController)
..addListener(() {
setState(() {
extraPicHeight = anim.value;
fitType = BoxFit.cover;
});
});
prev_dy = 0; //同样归零

动画的效果最终由控制器animationController来决定,这里给了一个300ms的时间还不错,可以根据自己业务扩展


animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));

所有在手抬起的时候执行我们的动画runAnimate函数即可


onPointerUp: (_) {
//当手指抬起离开屏幕时
if (isVerticalMove) {
if (extraPicHeight < 0) {
extraPicHeight = 0;
prev_dy = 0;
return;
}
debugPrint('extraPicHeight onPointerUp : $extraPicHeight');
runAnimate(); //动画执行
animationController.forward(from: 0); //重置动画
}
},

整体的技术方案履完了,之后就是细节问题了


问题1:横行稍微有倾角的滑动也会导致页面拖拽,比如侧滑返回上一页面


这是由于手指滑动的角度没有限制, 这里我们计算一下滑动倾角,超过45度无效,角度计算通过x,y坐标计算tan函数即可


onPointerMove: (result) {
double deltaY = result.position.dy - initialDy;
double deltaX = result.position.dx - initialDx;
double angle =
(deltaY == 0) ? 90 : atan(deltaX.abs() / deltaY.abs()) * 180 / pi;
debugPrint('onPointerMove angle : $angle');
if (angle < 45) {
isVerticalMove = true; // It's a valid vertical movement
updatePicHeight(result
.position.dy); // Custom method to handle vertical movement
} else {
isVerticalMove =
false; // It's not a valid vertical movement, ignore it
}
}

问题2:图片高度变了,为啥没有拉伸啊!


图片拉伸取决于你图片库的加载配置,以flutter举例,我们的图片库是CachedNetworkImage


 CachedNetworkImage(
width: double.infinity,
height: 156 + extraPicHeight,
imageUrl: backgroundUrl,
fit: fitType,
)

加载效果取决于fit,默认不变形我们使用cover,拉伸时使用fitHeight或者fill


updatePicHeight(changed) {
if (prev_dy == 0) {
//如果是手指第一次点下时,我们不希望图片大小就直接发生变化,所以进行一个判定。
prev_dy = changed;
}
if (extraPicHeight > 0) {
//当我们加载到图片上的高度大于某个值的时候,改变图片的填充方式,让它由以宽度填充变为以高度填充,从而实现了图片视角上的放大。
fitType = BoxFit.fitHeight;
} else {
fitType = BoxFit.cover;
}
extraPicHeight += changed - prev_dy; //新的一个y值减去前一次的y值然后累加,作为加载到图片上的高度。
debugPrint('extraPicHeight updatePicHeight : $extraPicHeight');
if (extraPicHeight > 300) {
extraPicHeight = 300;
}
if (extraPicHeight > 0) {
setState(() {
prev_dy = changed;
fitType = fitType;
});
}
}

最后看下组件如何布局


 CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: buildTopWidget(),
),
SliverToBoxAdapter(
child: Column(
children: contents,
),
)
]
),
)

整个列表使用CustomScrollView,因为在flutter上用他才能实现这种变化效果,未来还可以扩展顶部导航栏的变化需求。buildTopWidget就是我们头部组件,包括内部的背景图,但是整个组件和背景图的高度都是依赖extraPicHeight变化的,contents是我们的内容,当头部组件挤压,会正常跟随滑动到底部。


全局变量依赖以下参数就够了,核心要注意的就是边界值问题,什么时候把状态值重置问题。


//初始坐标
double initialDy = 0;
double initialDx = 0;
double extraPicHeight = 0; //初始化要加载到图片上的高度
late double prev_dy; //前一次滑动y
//是否是垂直滑动
bool isVerticalMove = false;
//动画器
late AnimationController animationController;
late Animation<double> anim;


技术语言不是我分享的核心,解决这个需求的技术思维路线是我们大家可以借鉴学习的。



如果你有任何疑问可以通过掘金联系我,如果文章对你有所启发,希望能得到你的点赞、关注和收藏,这是我持续写作的最大动力。Thanks~


作者:小虎牙007
来源:juejin.cn/post/7419248277382021135
收起阅读 »

请放弃使用JPEG、PNG、GIF格式的图片!

web
随着互联网的发展,图片作为最直观的内容展示方式逐渐在系统中占用越来越多的版面,但是随之而来的就是系统性能的大幅度下滑。传统的JPEG、PNG、GIF各有优点,也各有弊端,“大一统”的图片格式被需要,于是WebP诞生了。 需求 WebP格式文件产生的原因主要是源...
继续阅读 »

随着互联网的发展,图片作为最直观的内容展示方式逐渐在系统中占用越来越多的版面,但是随之而来的就是系统性能的大幅度下滑。传统的JPEG、PNG、GIF各有优点,也各有弊端,“大一统”的图片格式被需要,于是WebP诞生了。


需求


WebP格式文件产生的原因主要是源于对网络图像传输效率的需求以及现有图像格式在某些方面的局限性


在现代互联网网页中图片和视频占据了很大比例。为了提供更吸引人的用户体验,网站需要加载大量的高质量图像


image.png


同时智能手机和平板电脑的普及推动了移动互联网的快速发展。在移动设备上,网络速度通常比桌面端慢,且用户的流量是有限的。


而JPEG、PNG和GIF等传统图像格式各有其优点,但也存在不足之处。


例如,JPEG虽然非常适合照片,但仅支持有损压缩且不支持透明度;PNG支持透明度但文件大小通常较大;GIF支持动画但色彩范围有限,且文件体积相对较大。


产生


WebP是一种由Google开发的图像文件格式,旨在提供更高效的图片压缩,适用于网络图像传输和展示。



  1. 高压缩效率:WebP采用了先进的压缩算法,可以提供比JPEG更高的压缩率而不会明显损失图像质量。这意味着使用WebP格式可以在不牺牲视觉体验的情况下显著减少图片文件的大小,从而加快网页加载速度。

  2. 支持透明度:与JPEG不同,WebP支持alpha通道(即透明度),这使得它在需要背景透明效果的应用场景中成为PNG的一个有力替代者,同时还能以更低的文件大小实现这一功能。

  3. 动画支持:除了静态图像外,WebP还支持动画,作为一种更加有效的替代GIF的方案。相比GIF,WebP能够以更小的文件尺寸提供更高品质的动画效果和更多的色彩支持。

  4. 广泛兼容性:虽然WebP最初由Google推出,但它逐渐获得了广泛的浏览器和其他平台的支持,包括Chrome、Firefox、Edge、Safari等主流浏览器,以及各种操作系统和图像处理软件。


image.png


局限



  1. 浏览器兼容性:虽然大多数现代浏览器已经支持WebP格式,但仍有少数旧版浏览器可能不完全支持或根本不支持这种格式。在转换的同时也需要准备适当的回退方案(如提供JPEG或PNG版本的图像)。

  2. 性能问题:尽管WebP通常能提供更好的压缩率和质量比,但在某些情况下,转换过程可能会增加服务器负载,尤其是在需要实时生成WebP图像的情况下。

  3. 特定需求和偏好:一些网站可能基于设计、品牌或其他技术要求而选择特定的图像格式。例如,对于需要极高保真度的专业摄影展示,可能仍然倾向于使用TIFF或高质量JPEG格式。


使用


在线格式转换



ced075b010f14508be723fb7830d3287_2.png


程序格式转换


Python:可以使用Pillow库(PIL的一个分支)结合webp的支持来进行转换。


// 安装 pip install Pillow

from PIL import Image
im = Image.open("input.png")
im.save("output.webp", "WEBP")

也可以使用Node.js来转换。


这里使用egg.js作为服务端框架


前端


<template>
<div class="wrap">
<a-upload
v-model:file-list="fileList"
name="file"
action="/api/uploadImg"
:accept="['.jpeg','.png','.jpg','.gif']"
@change="handleChange"
>
<a-button>
上传文件
</a-button>
</a-upload>

<div class="diff-wrap">
<div class="old-img">
<img style="max-width: 400px;max-height: 500px;" :src="oldImg" alt=""/>
</div>
<div class="new-img">
<img style="max-width: 400px;max-height: 500px;" :src="newImg" alt=""/>
</div>
</div>
</div>
</template>

<script setup>
import { ref } from 'vue';
const oldImg = ref('');
const newImg = ref('');

const handleChange = info => {
const file = info.file;

// 使用 FileReader 进行本地文件预览(无论上传是否成功)
const reader = new FileReader();
reader.onload = () => {
oldImg.value = reader.result; // 将本地文件的 Base64 赋值给 oldImg
};
reader.readAsDataURL(file.originFileObj); // 读取原始文件对象

// 原有上传完成逻辑可保留用于处理服务器返回结果
if (file.status === 'done' && file.response) {
console.log(file)
newImg.value = file.response.url; // 如果上传成功,使用服务器返回的 URL
}
};

const fileList = ref([]);
</script>

<style scoped>
.diff-wrap {
width: 800px;
margin: 20px auto;
border: 1px solid #ddd;
display: flex;
}

.old-img {
flex: 1;
height: 500px;
border-right: 1px solid #ddd;
}

.new-img {
flex: 1;
height: 500px;
}
</style>

服务端


使用 Node.js 的图像处理库 sharp 进行格式转换,安装 sharp。


npm install sharp

示例代码


const { Service } = require('egg');
const fs = require('fs');
const path = require('path');
const sharp = require('sharp');

class HomeService extends Service {
async index() {
return 'hello world';
}

async uploadImg() {
const { ctx } = this;

try {
// 1. 获取上传的文件流
const stream = await ctx.getFileStream();

// 2. 检查是否为支持的图片格式(可选)
const allowedMimes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ];
if (!allowedMimes.includes(stream.mime)) {
throw new Error('Unsupported image format');
}

// 3. 定义路径
const tempInputPath = path.join(this.config.baseDir, 'app/public', `temp_${Date.now()}.tmp`);
const outputFilename = `converted_${Date.now()}.webp`;
const outputFilePath = path.join(this.config.baseDir, 'app/public', outputFilename);

// 4. 写入临时原始文件
const writeStream = fs.createWriteStream(tempInputPath);
await new Promise((resolve, reject) => {
stream.pipe(writeStream);
stream.on('end', resolve);
stream.on('error', reject);
});

// 5. 使用 sharp 转换为 webp
await sharp(tempInputPath)
.webp({ quality: 80 }) // 可设置压缩质量
.toFile(outputFilePath);

// 6. 清理临时文件
fs.unlinkSync(tempInputPath);

// 7. 返回 WebP 图片地址
return {
url: `/public/${outputFilename}`,
filename: outputFilename,
};
} catch (err) {
ctx.logger.error('Image upload or conversion failed:', err);
throw new Error('Image processing failed: ' + err.message);
}
}
}

module.exports = HomeService;

作者:李剑一
来源:juejin.cn/post/7503017777064362010
收起阅读 »

纯血鸿蒙 璀璨星河

华为最近可谓是加班加点,前段时间华为给出了鸿蒙系统的计划表,让大家都摩拳擦掌,想看看华为到底有几把刷子,这次的621HDC大会,就是华为给全世界的第一个交待。 最近在网上经常流传着这样一张图:大体意思就是:根据Counterpoint Research报告显示...
继续阅读 »

华为最近可谓是加班加点,前段时间华为给出了鸿蒙系统的计划表,让大家都摩拳擦掌,想看看华为到底有几把刷子,这次的621HDC大会,就是华为给全世界的第一个交待。


最近在网上经常流传着这样一张图:
image.png
大体意思就是:根据Counterpoint Research报告显示,今年一季度,鸿蒙在中国市场份额为17%,而苹果的iOS为16%。这意味着,鸿蒙在中国市场首次超越iOS,成为中国市场第二大智能手机操作系统。


这张图呢,可以说没问题,但也有问题,具体是什么问题呢,我们后面再说。


首先,我们要先来了解下,什么是「鸿蒙」。


对于一般用户来说,当前对鸿蒙可能没有什么概念,因为现在的华为手机上,跑的就是鸿蒙系统,但此鸿蒙,非彼之鸿蒙,当前市面上的所有在售华为设备,运行的都是「基于Android系统的鸿蒙系统」,也就是说,图中的鸿蒙系统,实际上就是华为的基于Android系统修改的鸿蒙系统。所以这图中的操作系统对比,实际上就是在偷换概念,这其实就是华为设备的占比。


那么为什么现在还在炒作什么国产新系统呢?这不妥妥的套壳吗?确实,现在的鸿蒙还是在套壳,但是今年以后的鸿蒙,可能就不是了。


这就是我们今天的主角——纯血鸿蒙。



鸿蒙采取的策略,实际上和当初的Android类似,先以Android系统为底,建立华为生态和用户群,再通过建立全新的操作系统,来和Android割裂,完整的建立自己的生态。



所以,现在的纯血鸿蒙,实际上就是「HarmonyOS NEXT」系统,这个系统,与现有Android系统完全不兼容,属于一个完整的自建操作系统,再也不是套壳了。


根据我多年的鸿蒙开发经验(特别是这3个月的),纯血鸿蒙确实博采众家之长,有点东西。


其实,做一个操作系统不难,难的是做一个生态,华为的投入可谓空前巨大,从开发工具链到社区,从硬件设备到软件开发,整体的投入都是前所未有的,这也体现了高层,对国产操作系统的信心和决心。


虽然现在纯血鸿蒙还处于初级阶段,但带着发展的眼光来看,确实是极具潜力的,根据我的经验来看:



  • 渲染系统吸取了各大平台的特点,这么多年以来,各大渲染体系该踩得坑都踩完了,现在华为可谓是一将功成

  • 响应式开发体系也做了很大改进,但是感觉设计有些过于复杂,颗粒度太细,上手成本较高,同时由于现在开发API还不稳定,变动较多

  • 开发生态逐渐完善,和苹果类似,华为又做硬件又做软件,所以它自己可以很方便的整合自家资源,它绝不仅仅是一个操作系统,更是一个生态平台,可以融合自己的AI大模型、多端平台共享、多端接续等等生态功能

  • 开发语言包容性强,js、c、TS、ArkTS、dart、kotlin等等不同的语言生态,都有社区对鸿蒙的支持,这些社区中,有很多都是华为的开发人员,在帮助社区共建,所以,各个不同的平台系统,都可以很快的用自己的技术栈,接入鸿蒙系统

  • API很丰富,但也是一把双刃剑,华为在对接很多开发者,为他们提供了很多便利的API,但是也由于快速迭代,API的设计可能缺少深思熟虑,有些API会有些冗余、随意

  • 整体架构来说,有很多Android的影子,也借鉴了很多Flutter的渲染机制,开发者在开发的时候,总能不在意的会心一笑


621之后,纯血鸿蒙中会有大量的APP上线,基本已经涵盖了日常使用功能,作为开发者,我们来看看这些常用的APP,看下它们都是用的哪些技术栈进行开发的。


Screenshot_20240621102104400.jpegScreenshot_20240621102132997.jpeg
Screenshot_20240621102240537.jpeg
Screenshot_20240621102352584.jpegScreenshot_20240621102402095.jpeg
Screenshot_20240621102422429.jpeg
Screenshot_20240621102437862.jpegScreenshot_20240621102447012.jpeg
Screenshot_20240621102500363.jpegScreenshot_20240621102510978.jpeg
Screenshot_20240621102524579.jpegScreenshot_20240621102535512.jpegScreenshot_20240621102545314.jpeg
Screenshot_20240621102609562.jpegScreenshot_20240621102622761.jpeg
Screenshot_20240621102641945.jpegScreenshot_20240621102656395.jpeg
Screenshot_20240621102713911.jpeg
Screenshot_20240621102735295.jpeg
Screenshot_20240621102750312.jpegScreenshot_20240621102804860.jpeg

这里我找了市场中已经上线的一些典型APP,利用纯血鸿蒙的开发者选项中的布局边界,来大致揣测下它们的技术方案。


从业务形态上来看,这些APP大致分为几种:



  • 信息流:这类的APP,其实就是纯粹堆UI,大部分都利用ArkTS进行开发,只要熟悉ArkTS语法,开发难度不大,但需要堆人力,因为很多组件都需要从零开始开发。

  • 自渲染:这类的APP,以视频、小说类为主,这些APP相对来说比较复杂,但是视频类,相对来说会好一些,比较大部分复杂的音视频处理,都在C层时处理了,所以上层业务,提供鸿蒙的适配方案就行了,而小说类的,文字渲染相关的API和刷新方案,都需要重新按照鸿蒙的架构思路来重新实现,所以开发难度会大一些。

  • 跨平台:通常基于Flutter的方案来实现,这里的典型就行闲鱼,现有业务可以直接移植过来,只需要在Channel层适配即可,如果公司当前有Flutter架构,那这是适配鸿蒙的最快方案。


总的来说,当前纯血鸿蒙的设备已经支持高刷,各大应用的完成度也很高,同时,由于时间紧任务重,很多营收类需求都没做,比如广告,所以这个版本应该是非常「纯净」的版本了,整体质量和体验都非常好。


当然,不得不说,现在的纯血鸿蒙依然是初级阶段,现在不管是开发上还是系统上,都还有很多问题,不过,正如那句话说的——「没有人能够熄灭漫天星光,每一位开发者,都是华为要汇聚的星星之火」。


image.png


欢迎大家关注我的公众号——【群英传】,专注于「Android」「Flutter」「Kotlin」
我的语雀知识库——http://www.yuque.com/xuyisheng


作者:xuyisheng
来源:juejin.cn/post/7382892371224494120
收起阅读 »

跨平台开发的新纪元:Tauri 2.0 横空出世,移动端、桌面端一网打尽!

前言 Tauri 2.0 正式版终于在 2024 年 10 月 2 日正式发布了。这次重大更新不仅带来了令人兴奋的新特性,更是为跨平台应用开发开辟了一条全新的道路。让我们一起来看看这个重量级版本带来了哪些惊喜! 1.移动端支持:拥抱全平台时代 Tauri 2...
继续阅读 »


前言


Tauri 2.0 正式版终于在 2024 年 10 月 2 日正式发布了。这次重大更新不仅带来了令人兴奋的新特性,更是为跨平台应用开发开辟了一条全新的道路。让我们一起来看看这个重量级版本带来了哪些惊喜!


1.移动端支持:拥抱全平台时代



Tauri 2.0 最引人注目的特性莫过于对 iOS 和 Android 的全面支持。现在,您可以用同一套代码库开发桌面端(Windows、macOS、Linux)和移动端应用,真正实现"一次编写,到处运行"的梦想。这不仅大大提高了开发效率,还为您的应用打开了更广阔的市场。


2.插件系统升级:灵活性与可扩展性的完美结合



新版本中,Tauri 将大量核心功能转移到了插件中。这意味着您可以根据需求自由选择功能,让应用更加轻量化。同时,插件系统的改进也为社区贡献打开了大门,期待看到更多创新的插件涌现。Tauri 2.0 的插件系统不仅更加灵活,还提供了丰富的官方插件,满足各种开发需求。以下是部分官方插件及其功能:



  • 自动启动 (Autostart): 让您的应用在系统启动时自动运行。

  • 条形码扫描器 (Barcode Scanner): 在移动应用中使用相机扫描二维码和条形码。

  • 生物识别 (Biometric): 在Android和iOS上进行生物识别认证。

  • 剪贴板 (Clipboard): 读取和写入系统剪贴板。

  • 命令行接口 (CLI): 解析命令行参数。

  • 深度链接 (Deep Linking): 将您的Tauri应用设置为特定URL的默认处理程序。

  • 对话框 (Dialog): 用于打开/保存文件和显示消息的原生系统对话框。

  • 文件系统 (File System): 访问文件系统。

  • 全局快捷键 (Global Shortcut): 注册全局快捷键。

  • HTTP客户端: 使用Rust编写的HTTP客户端。

  • 本地主机 (Localhost): 在生产应用中使用本地主机服务器。

  • 日志 (Logging): 可配置的日志记录。

  • NFC: 在Android和iOS上读写NFC标签。

  • 通知 (Notifications): 向用户发送原生通知。

  • 操作系统信息 (OS Information): 读取操作系统信息。

  • 持久化作用域 (Persisted Scope): 在文件系统中持久化运行时作用域更改。

  • 定位器 (Positioner): 将窗口移动到常用位置。

  • 进程 (Process): 访问当前进程。

  • Shell: 使用默认应用程序管理文件和URL,以及生成子进程。

  • 单实例 (Single Instance): 确保Tauri应用同时只运行一个实例。

  • SQL: 提供前端与SQL数据库通信的接口。

  • 存储 (Store): 持久化的键值存储。

  • Stronghold: 加密、安全的数据库。

  • 系统托盘 (System Tray): 系统托盘功能。

  • 更新器 (Updater): Tauri应用的应用内更新。

  • 上传 (Upload): 通过HTTP进行文件上传。

  • WebSocket: 在JavaScript中使用Rust客户端打开WebSocket连接。

  • 窗口自定义 (Window Customization): 自定义窗口外观和行为。

  • 窗口状态 (Window State): 保存窗口大小和位置。


这些插件涵盖了从基础功能到高级特性的广泛范围,让开发者能够根据项目需求灵活选择。通过这种模块化的方式,Tauri不仅保持了核心框架的轻量级,还为开发者提供了强大的扩展能力。无论您是开发一个简单的工具还是复杂的企业级应用,Tauri的插件系统都能满足您的需求。


3.安全性大幅提升:告别allowlist,迎接新的权限系统


Tauri 2.0 抛弃了旧的 allowlist 系统,引入了更加灵活和强大的权限、作用域和功能系统。这不仅提高了安全性,还让开发者能够更精细地控制应用的权限。值得一提的是,Tauri 还通过了独立的安全审计,让您使用起来更加放心。


4.性能优化:IPC层重写,更快更强


通过重写进程间通信(IPC)层,Tauri 2.0现在支持原始有效载荷,这意味着在前端和后端之间传输大量数据时,性能得到了显著提升。如果您的应用需要处理大量数据,这个特性绝对不容错过。


5.开发体验升级:HMR支持更给力


热模块替换(HMR)现在扩展到了移动设备和模拟器。这意味着您可以实时预览应用在不同设备上的表现,大大加速了开发和调试过程。


6.分发更简单:一站式解决方案



Tauri 2.0提供了详尽的分发指南,覆盖了从App Store到Google Play,再到Microsoft Store等多个平台。无论您的目标市场在哪里,Tauri都能帮您轻松应对。


结语


Tauri 2.0 的正式发布无疑是跨平台开发领域的一个重要里程碑。它不仅延续了 Tauri 一贯的轻量、快速的特点,还通过移动端支持、增强的插件系统和改进的安全机制等特性,为开发者提供了更强大、更灵活的工具。


如果您正在寻找一个能够同时覆盖桌面端和移动端的开发框架,Tauri 2.0绝对值得一试。它不仅能帮您节省时间和资源,还能为您的应用带来卓越的性能和安全性。


参考文章


v2.tauri.app/blog/tauri-…


v2.tauri.app/blog/tauri-…


作者:前端徐徐
来源:juejin.cn/post/7423231530498031631
收起阅读 »

Flutter GPU 是什么?为什么它对 Flutter 有跨时代的意义?

Flutter 3.24 版本引入了 Flutter GPU 概念的新底层图形 API flutter_gpu ,还有 flutter_scene 的 3D 渲染支持库,它们目前都是预览阶段,只能在 main channel 上体验,并且依赖 Impel...
继续阅读 »

Flutter 3.24 版本引入了 Flutter GPU 概念的新底层图形 API flutter_gpu ,还有 flutter_scene 的 3D 渲染支持库,它们目前都是预览阶段,只能在 main channel 上体验,并且依赖 Impeller 的实现。



Flutter GPU 是 Flutter 内置的底层图形 API,它可以通过编写 Dart 代码和 GLSL 着色器在 Flutter 中构建和集成自定义渲染器,而无需 Native 平台代码。


目前 Flutter GPU 处于早期预览阶段并只提供基本的光栅化 API,但随着 API 接近稳定,会继续添加和完善更多功能。



详细说,Flutter GPU 是 Impeller 对于 HAL 的一层很轻的包装,并搭配了关于着色器和管道编排的自动化能力,也通过 Flutter GPU 就可以使用 Dart 直接构建自定义渲染器。


Flutter GPU 和 Impeller 一样,它的着色器也是使用 impellerc 提前编译,所以 Flutter GPU 也只支持 Impeller 的平台上可用。



Impeller 的 HAL 和 Flutter GPU 都没打算成为类似 WebGPU 这样的正式标准,相反,Flutter GPU 主要是由 Flutter 社区开发和发展,专职为了 Flutter 服务,所以不需要考虑「公有化」的兼容问题。



在 Flutter GPU 上,可直接从 Dart 与 Impeller 的 HAL 对话,甚至 Impeller Scene API(3D)也将作为重写的一部分出现。



说人话就是,可以用 Dart 通过 Flutter GPU 直接构建自定义渲染效果,未来直接支持 3D



可能有的人对于 Impeller 的整体结构和 HAL 还很模式无法理解,那么这里我们简单过一下:



  • 在 Framework 上层,我们知道 Widget -> Element -> RenderObject -> Layer 这样的过程,而后其实流程就来到了 Flutter 自定义抽象的 DisplayList

  • DisplayList 帮助 Flutter 在 Engine 做了接耦,从而让 Flutter 可以在 skia 和 Impeller 之间进行的切换

  • 之后 Impeller 架构的顶层是 Aiks,这一层主要作为绘图操作的高级接口,它接受来自 Flutter 框架的命令,例如绘制路径或图像,并将这些命令转换为一组更精细的 “Entities”,然后转给下一层。

  • Entities Framework,它是 Impeller 架构的核心组件,当 Aiks 处理完命令时生成 Entities 后,每一个 Entity 其实就是渲染指令的独立单元,其中包含绘制特定元素的所有必要信息(编码位置、旋转、缩放、content object),此时还不能直接作用于 GPU

  • HAL(Hardware Abstraction Layer) 则为底层图形硬件提供了统一的接口,抽象了不同图形 API 的细节,该层确保了 Impeller 的跨平台能力,它将高级渲染命令转换为低级 GPU 指令,充当 Impeller 渲染逻辑和设备图形硬件之间的桥梁。


所以 HAL 它包装了各种图形 API,以提供通用的设备作业调度接口、一致的资源管理语义和统一的着色器创作体验,而对于 Impeller , Entities (2D renderer) 和 Scene (3D renderer) 都是直接通过 HAL 对接,甚至可以认为,Impeller 的 HAL 抽象并统一了 Metal 和 Vulkan 的常见用法和相似结构。



Unity 现在也有在 C# 直接向用户公开其 HAL 版本,称为 "Scriptable Render Pipeline" ,并提供了两个基于该 API 构建的默认渲染器 "Universal RP" / "High Definition RP" 用于服务不同的场景,所以 Unity 开发可以从使用这些渲染器去进行修改或扩展一些特定渲染需求。





而在 Flutter 的设计上,Flutter GPU 会作为 Flutter SDK 的一部分,并以 flutter_gpu 的 Dart 包的形式提供使用。


当然,Flutter GPU 由 Impeller 支持,但重要的是要记住它不是 Impeller ,Impeller 的 HAL 是私有内部代码与 Flutter GPU 的要求非常不同, Impeller 的私有 HAL 和 Flutter GPU 的公共 API 设计之间是存在一定差异化实现,而前面的流程,如 Scene (3D renderer) ,也可以被调整为基于 Flutter GPU 的全新模式实现。


而通过 Flutter GPU,如曾经的 Scene (3D renderer) 支持,也可以被调整为基于 Flutter GPU 的全新模式实现,因为 Flutter GPU 的 API 允许完全控制渲染通道附件、顶点阶段和数据上传到 GPU。这种灵活性对于创建复杂的渲染解决方案(从 2D 角色动画到复杂的 3D 场景)至关重要。



Flutter GPU 支持的自定义 2D 渲染器的一个很好的用例:依赖于骨骼网格变形的 2D 角色动画格式。


Spine 2D 就是一个很好的例子,骨骼网格解决方案通常具有动画剪辑,可以按层次结构操纵骨骼的平移、旋转和缩放属性,并且每个顶点都有几个相关的“bone weights”,这些权重决定了哪些骨骼应该影响顶点以及影响程度如何。



使用像 drawVertices 这样的 Canvas 解决方案,需要在 CPU 上对每个顶点应用骨骼权重变换,而 使用 Flutter GPU,骨骼变换可以用统一数组或纹理采样器的形式发送到顶点着色器,从而允许根据骨架状态和每个顶点的 “bone weights” 在 GPU 上并行计算每个顶点的最终位置。


使用 Flutter GPU


首先你需要在最新的 main channel 分支,然后通过 flutter pub add flutter_gpu --sdk=flutter 将 flutter_gpu SDK 包添加到你的 pubspec。


为了使用 Flutter GPU 渲染内容,你会需要编写一些 GLSL 着色器,Flutter GPU 的着色器与 Flutter 的 fragment shader 功能所使用的着色器具有不同的语义,特别是在统一绑定方面,还需要定义一个顶点(vertex)着色器来与 fragment shader 一起使用,然后配合 gpu.ShaderLibrary 等 API 就可以直接实现 Flutter GPU 渲染。


当然,本篇不会介绍详细的 API 使用 ,这里只是单纯做一个简单的介绍,目前 Flutter GPU 进行光栅化的简单流程如下:



  • 获取 GPUContext。

  • GpuContext.createCommandBuffer 创建一个 CommandBuffer

  • CommandBuffer.createRenderPass 创建一个 RenderPass

  • 使用各种方法设置状态/管道并绑定资源 RenderPass

  • 附加绘图命令 RenderPass.draw

  • CommandBuffer 使用 CommandBuffer.submit (异步)提交绘制,所有 RenderPass 会按照其创建顺序进行编码


·····
///导入 flutter_gpu
import 'package:flutter_gpu/gpu.dart' as gpu;

ByteData float32(List<double> values) {
return Float32List.fromList(values).buffer.asByteData();
}

ByteData float32Mat(Matrix4 matrix) {
return Float32List.fromList(matrix.storage).buffer.asByteData();
}

class TrianglePainter extends CustomPainter {
TrianglePainter(this.time, this.seedX, this.seedY);

double time;
double seedX;
double seedY;

@override
void paint(Canvas canvas, Size size) {
/// Allocate a new renderable texture.
final gpu.Texture? renderTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate, 300, 300,
enableRenderTargetUsage: true,
enableShaderReadUsage: true,
coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture);
if (renderTexture == null) {
return;
}

final gpu.Texture? depthTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient, 300, 300,
format: gpu.gpuContext.defaultDepthStencilFormat,
enableRenderTargetUsage: true,
coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture);
if (depthTexture == null) {
return;
}

/// Create the command buffer. This will be used to submit all encoded
/// commands at the end.
final commandBuffer = gpu.gpuContext.createCommandBuffer();

/// Define a render target. This is just a collection of attachments that a
/// RenderPass will write to.
final renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: renderTexture),
depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthTexture),
);

/// Add a render pass encoder to the command buffer so that we can start
/// encoding commands.
final encoder = commandBuffer.createRenderPass(renderTarget);

/// Load a shader bundle asset.
final library = gpu.ShaderLibrary.fromAsset('assets/TestLibrary.shaderbundle')!;

/// Create a RenderPipeline using shaders from the asset.
final vertex = library['UnlitVertex']!;
final fragment = library['UnlitFragment']!;
final pipeline = gpu.gpuContext.createRenderPipeline(vertex, fragment);

encoder.bindPipeline(pipeline);

/// (Optional) Configure blending for the first color attachment.
encoder.setColorBlendEnable(true);
encoder.setColorBlendEquation(gpu.ColorBlendEquation(
colorBlendOperation: gpu.BlendOperation.add,
sourceColorBlendFactor: gpu.BlendFactor.one,
destinationColorBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha,
alphaBlendOperation: gpu.BlendOperation.add,
sourceAlphaBlendFactor: gpu.BlendFactor.one,
destinationAlphaBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha));

/// Append quick geometry and uniforms to a host buffer that will be
/// automatically uploaded to the GPU later on.
final transients = gpu.HostBuffer();
final vertices = transients.emplace(float32(<double>[
-0.5, -0.5, //
0, 0.5, //
0.5, -0.5, //
]));
final color = transients.emplace(float32(<double>[0, 1, 0, 1])); // rgba
final mvp = transients.emplace(float32Mat(Matrix4(
1, 0, 0, 0, //
0, 1, 0, 0, //
0, 0, 1, 0, //
0, 0, 0.5, 1, //
) *
Matrix4.rotationX(time) *
Matrix4.rotationY(time * seedX) *
Matrix4.rotationZ(time * seedY)));

/// Bind the vertex data. In this case, we won't bother binding an index
/// buffer.
encoder.bindVertexBuffer(vertices, 3);

/// Bind the host buffer data we just created to the vertex shader's uniform
/// slots. Although the locations are specified in the shader and are
/// predictable, we can optionally fetch the uniform slots by name for
/// convenience.
final mvpSlot = pipeline.vertexShader.getUniformSlot('mvp')!;
final colorSlot = pipeline.vertexShader.getUniformSlot('color')!;
encoder.bindUniform(mvpSlot, mvp);
encoder.bindUniform(colorSlot, color);

/// And finally, we append a draw call.
encoder.draw();

/// Submit all of the previously encoded passes. Passes are encoded in the
/// same order they were created in.
commandBuffer.submit();

/// Wrap the Flutter GPU texture as a ui.Image and draw it like normal!
final image = renderTexture.asImage();

canvas.drawImage(image, Offset(-renderTexture.width / 2, 0), Paint());
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

class TrianglePage extends StatefulWidget {
const TrianglePage({super.key});

@override
State<TrianglePage> createState() => _TrianglePageState();
}

class _TrianglePageState extends State<TrianglePage> {
Ticker? tick;
double time = 0;
double deltaSeconds = 0;
double seedX = -0.512511498387847167;
double seedY = 0.521295573094847167;

@override
void initState() {
tick = Ticker(
(elapsed) {
setState(() {
double previousTime = time;
time = elapsed.inMilliseconds / 1000.0;
deltaSeconds = previousTime > 0 ? time - previousTime : 0;
});
},
);
tick!.start();
super.initState();
}

@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Slider(
value: seedX,
max: 1,
min: -1,
onChanged: (value) => {setState(() => seedX = value)}),
Slider(
value: seedY,
max: 1,
min: -1,
onChanged: (value) => {setState(() => seedY = value)}),
CustomPaint(
painter: TrianglePainter(time, seedX, seedY),
),
],
);
}
}

GpuContext 是分配所有 GPU 资源并调度 GPU 的存在,而 GpuContext 仅有启用 Impeller 时才能访问。


DeviceBuffer 和 Texture 就是 GPU 拥有的资源,可以通过 GPUContext 创建获取,如 createDeviceBuffercreateTexture



  • DeviceBuffer 简单理解就是在 GPU 上分配的简单字节串,主要用于存储几何数据(索引和顶点属性)以及统一数据

  • Texture 是一个特殊的设备缓冲区


CommandBuffer 用于对 GPU 上的异步执行进行排队和调度工作。


RenderPass 是 GPU 上渲染工作的顶层单元。


RenderPipeline 提供增量更改绘制所有状态以及附加绘制调用的方法如 RenderPass.draw()


可以想象,通过 Flutter GPU,Flutter 开发者可以更简单地对 GPU 进行更精细的控制,通过与 HAL 直接通信,创建 GPU 资源并记录 GPU 命令,从而最大限度的发挥 Flutter 的渲染能力。


另外,对于 3D 支持的 Flutter Scene , 可以通过使用 native-assets 来设置 Flutter Scene 的 3D 模型自动导入,通过导入编译模型 .model 之后,就可以通过 Dart 实现一些 3D 的渲染。


import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_scene/camera.dart';
import 'package:flutter_scene/node.dart';
import 'package:flutter_scene/scene.dart';
import 'package:vector_math/vector_math.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatefulWidget {
const MyApp({super.key});

@override
MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
double elapsedSeconds = 0;
Scene scene = Scene();

@override
void initState() {
createTicker((elapsed) {
setState(() {
elapsedSeconds = elapsed.inMilliseconds.toDouble() / 1000;
});
}).start();

Node.fromAsset('build/models/DamagedHelmet.model').then((model) {
model.name = 'Helmet';
scene.add(model);
});

super.initState();
}

@override
Widget build(BuildContext context) {
final painter = ScenePainter(
scene: scene,
camera: PerspectiveCamera(
position: Vector3(sin(elapsedSeconds) * 3, 2, cos(elapsedSeconds) * 3),
target: Vector3(0, 0, 0),
),
);

return MaterialApp(
title: 'My 3D app',
home: CustomPaint(painter: painter),
);
}
}

class ScenePainter extends CustomPainter {
ScenePainter({required this.scene, required this.camera});
Scene scene;
Camera camera;

@override
void paint(Canvas canvas, Size size) {
scene.render(camera, canvas, viewport: Offset.zero & size);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}



目前 Flutter GPU 和 Flutter Scene 的支持还十分有限,但是借助 Impeller ,Flutter 开启了新的可能,可以说是,Flutter 团队完全掌控了渲染堆栈,在除了自定义更丰富的 2D 场景之外,也为 Flutter 开启了 3D 游戏的可能,2023 年 Flutter Forward 大会的承诺,目前正在被落地实现




详细 API 使用例子可以参看 :medium.com/flutter/get…



如果你对 Flutter Impeller 和其着色器感兴趣,也可以看:



作者:恋猫de小郭
来源:juejin.cn/post/7399985723673821193
收起阅读 »

Vue3 首款 3D 数字孪生编辑器 正式开源!

web
作者:前端开发爱好者 对于多数前端开发者而言,用 ThreeJS 打造炫酷的数字孪生场景并非易事,需掌握大量专业知识。 如今,一款基于 Vue3、ThreeJS 和 Naive UI 的数字孪生开发框架 ——Astral 3D Editor 正式开源,为 W...
继续阅读 »

作者:前端开发爱好者


对于多数前端开发者而言,用 ThreeJS 打造炫酷的数字孪生场景并非易事,需掌握大量专业知识。


图片


如今,一款基于 Vue3ThreeJS 和 Naive UI 的数字孪生开发框架 ——Astral 3D Editor 正式开源,为 Web3D 开发带来新转机。


Astral 3D Editor 是什么?


Astral 3D Editor 是一款免费开源的三维可视化孪生场景编辑器,主要服务于 Web3D 开发,支持多种常见 3D 模型格式


图片


还具备轻量化 BIM 模型解析及 CAD 图纸预览功能。


图片


Astral 3D Editor 的优势



  • 功能丰富 :支持多种 3D 模型格式,可导入导出多类型模型,方便资源整合。它还提供插件系统,可扩展更多功能。同时,支持在线预览 BIM 模型和 CAD 图纸,为建筑、工程等领域提供便利。粒子系统、动画编辑器等功能一应俱全,满足多样化创作需求。

  • 技术先进 :以 ThreeJS 为底层 3D 渲染库,结合 Vue3 响应式编程和组件化开发,以及 Naive UI 的丰富组件,构建高效稳定的编辑器框架。其场景数据无损压缩和网络分包渐进存取技术,优化了大规模场景的加载效率。

  • 开发门槛低 :作为 3D 低代码创作工具,降低了 Web3D 开发难度,前端开发者无需深入掌握 3D 图形学知识,也能快速创建高质量 3D 场景,提高开发效率。

  • 开源友好 :采用 Apache-2.0 License 开源协议,吸引众多开发者参与,形成活跃开源社区,便于交流分享和共同推动项目发展。


Astral 3D Editor 快速上手


环境准备


在开始使用 Astral 3D Editor 之前,确保已经安装了以下软件和工具:



  • Node.js :建议安装 Node.js ≥ 18.x,可以通过官方链接下载安装。

  • Yarn :一个高效的包管理工具,可以通过官方链接进行安装。


图片


项目克隆与安装


通过 Git 将 Astral 3D Editor 的项目代码克隆到本地:


git clone https://github.com/mlt131220/Astral3DEditor.git

进入项目目录


cd Astral3DEditor

使用 Yarn 安装项目依赖:


yarn install

项目运行与构建


在开发环境中启动项目:


yarn run dev

这将启动本地开发服务器,通常会自动在浏览器中打开 Astral 3D Editor 的界面,若未自动打开,可在浏览器中访问 http://localhost:3000


基础操作指南


Astral 3D Editor 的界面简洁直观,主要包含以下几个关键区域:



  • 工具栏 :提供了各种工具按钮,可进行模型导入、视图切换、对象选择和变换等操作。


图片



  • 视图区域 :用于显示和编辑 3D 场景,支持多种视图模式,如透视图、正交图,以及前置、后置、左置、右置等不同视角的切换。


图片



  • 属性面板 :用于查看和编辑当前选中对象的属性,可根据不同对象类型进行相应属性的调整。


图片


Astral 3D Editor 在线编辑器


Astral 3D Editor 的在线编辑器是其一大亮点,提供了便捷的在线 3D 场景创作体验。


图片


在线编辑器无需安装额外软件,只要有浏览器和网络连接,用户就能直接在浏览器中打开: https://editor.astraljs.com/#/,随时随地进行 3D 场景的创作和编辑。


图片


界面设计简洁直观,操作流程简单易懂,降低了学习成本,初学者也能快速上手,轻松进行模型导入、场景编辑、动画添加等操作,迅速构建出想要的 3D 场景。


图片


此外,在线编辑器还具有出色的跨平台兼容性,支持在 WindowsmacOS 以及 Linux 等多种操作系统上运行,兼容各大主流浏览器,包括 ChromeFirefoxSafari 等,用户可自由选择浏览器进行创作。


值得一提的是,在线编辑器支持通过拖拉拽形式创建场景,操作简单直观,大大降低了 3D 场景创作的门槛。


同时,官方还提供了大量可视化案例展示,这些案例不仅丰富多样,而且具有很高的学习价值,可供用户参考学习,帮助用户更好地掌握 3D 场景创作的技巧和方法。


Astral 3D Editor 的开源,为 Web3D 领域注入新活力。


其功能、技术、开发难度、应用场景和开源优势,使其有望在数字孪生领域发挥重要作用,推动 Web3D 技术持续进步。



作者:独立开阀者_FwtCoder
来源:juejin.cn/post/7497821254205816858
收起阅读 »

使用 Flutter 开发数字钱包应用(Dompet App)

感谢由 Pixso 资源社区提供的共享作品 《Dompet数字页面设计》 推荐 使用 Harmony ArkTS 开发数字钱包应用(Dompet App) 前言 早在几年前,我有幸参与了公司 App 的技术选型,并决定采用 Flutter 技术栈,快速从零...
继续阅读 »

design.png


感谢由 Pixso 资源社区提供的共享作品 《Dompet数字页面设计》




推荐


使用 Harmony ArkTS 开发数字钱包应用(Dompet App)




前言


早在几年前,我有幸参与了公司 App 的技术选型,并决定采用 Flutter 技术栈,快速从零搭建出能够集成 H5 应用的 混合应用框架(Hybrid App)。在这个过程中,得益于 Flutter 跨平台特性和高效的 UI 构建能力,我们在短时间内实现了一个功能丰富的混合应用,支持 H5 应用扫码、支付、定位等设备能力。


随着需求的不断迭代与升级,原本的 App 逐渐变得 “臃肿” 起来。回顾当初搭建的 Flutter 混合应用框架,虽然满足了业务上的需求,但在架构设计上显得略为粗糙。面对日益增加的技术负债,依靠业余时间进行优化和重构已然心有余而力不足。是时候重新搭建开发 Flutter 混合应用框架,以便更好的引入最新技术,快速适配各业务场景,提升 App 开发和运行效率。




选型


在构建 Flutter 混合应用 App 的过程中,基于 状态管理依赖注入路由管理国际化(i18n) 等考虑,我们首先选择了 GetX 作为核心框架。随后,为了确保 App 具备 网络请求本地持久化存储本地数据库管理 以及 Webview 等能力,我们进一步引入了 dioget_storagesqfliteflutter_inappwebview 等插件。


然而,开发 App 仅仅依靠 Flutter 技术栈是远远不够的,我们还需要一个具有良好用户体验和精美 UI 界面的设计稿。可惜设计不是我所擅长的领域,好在 Pixso 资源社区提供了丰富的共享作品,在那里我找到了适合我的 UI 设计 《Dompet 数字钱包》


这套设计稿具备以下优点:



  1. 设计精美:界面简洁清晰,用户体验良好,能够给用户带来直观的操作体验。

  2. 便捷登录:设计了 Google、Facebook 第三方登录,免去了繁琐的注册流程。

  3. 功能丰富:包含多种常用组件,具备一定的通用性,便于快速适配不同业务场景。




特性


在技术上,我们选择了 GetXdiosqfliteget_storage 以及 flutter_inappwebview 作为构建框架的核心基础设施,而在 UI 设计方面,则是选择了 《Dompet 数字钱包》 设计稿作为我们 App 用户界面。


在确定了 Flutter 技术栈、核心插件以及 UI 设计稿后,我们开始着手梳理 Dompet 数字钱包 的 UI 界面和功能需求,以确保高效的开发效率,并成为一个用户体验极佳的 App。



  1. 使用 GetX 中 GetMaterialApp 来接管 App 的路由管理、权限拦截和认证

  2. 使用 GetX 中 .tr,以及自定义 JsonTranslations 实现 App 中英文的切换。

  3. 使用 GetX 中 .obsObx 来取代 GetBuilderupdate 手动更新 UI 的方式

  4. 封装 sqflite 实现本地数据库,存储读取用户、账单、消息、银彳亍卡的数据,模拟服务端处理

  5. 对于 Google 这类第三方登录,我们通过借助 Firebase 平台,实现 Google、Github 便捷登录

  6. 对于 UI 设计稿中的折线图表,我们使用 fl_chart 插件实现,这也是对 Flutter Chart 的首次尝试

  7. 借助 GetX .obsObx 的数据与 UI 更新机制,自定义 extension 单位,自动适配不同设备屏幕

  8. 借助 intl 和 GetX 的 updateLocale 自动处理不同语言和区域格式的数据 (例如 日期不同地区显示)

  9. 借助 image_picker 插件,实现 App 上传来自相册或拍照而的得图片,进而更新用户头像 avatar

  10. 虽然 sqflite 模拟了服务端数据的处理和流转,但网络请求作为 App 基础设施,我们依旧对 dio 进行了封装,完善了 Request 和 Response 拦截处理 (携带 Token、异常处理等)

  11. ......


重要插件依赖如下:



  • flutter_ringtone_player: (系统提示音,用于转账、支付、充值成功时的系统提示)

  • flutter_inappwebview: (App Webview, 用于访问 H5 Web 应用)

  • google_sign_in: (Google 账号免密登录,用于实现便捷的用户身份验证)

  • firebase_core: (Firebase 的核心依赖,为其他 Firebase 服务提供基础支持)

  • firebase_auth: (Firebase 平台认证,支持多种认证方式,目前用到了 Google 和 GitHub)

  • path_provider: (获取应用的常用存储路径,例如:临时目录、文档目录)

  • fluttertoast: (用于在应用中显示短暂的消息提示,支持error、warnning、 success)

  • image_picker: (用于选择和拍摄图片或视频,方便用户上传和处理媒体文件)

  • get_storage: (轻量级的本地存储解决方案,用于存储简单数据,如用户登录状态)

  • fl_chart: (用于绘制各类图表,如折线图、柱状图等,方便可视化数据展示)

  • sqflite: (Flutter 的 SQLite 插件,用于在本地存储和管理结构化数据)

  • intl: (国际化和本地化支持库,帮助处理不同语言和区域格式的数据)

  • dio: (强大的 HTTP 客户端,支持各种请求方式、拦截器和响应处理,适用于网络请求)

  • get: (用于状态管理、依赖注入和路由管理,提供简单而高效的开发体验)


源码 - 插件封装



源码 - Extension 扩展



  • lib/extension/date: 为不同语言和地区提供日期本地化格式方案 (eg. US: Oct 4, 2024)

  • lib/extension/bool: 为 Dart 所有类型转换 bool 类型,提供了便捷方案 (eg. 'string'.bv => true)

  • lib/extension/money: 定义提供了金额显示的多种方式 USD、usd2 (eg. 1000.usd2 => '$1,000.00')

  • lib/extension/size: 适配不同设备的屏幕以及屏幕翻转时自适应,定义多种单位 (wdp、wmax、sr ...)


源码 - 全局应用服务



  • lib/service/event: 定义了全局的事件。例如 登录、退出、更新用户信息,创建订单、创建消息等

  • lib/service/locale: 定义存储了全局的 locale、以及语言国际化处理。如切换中英文、使用系统语言

  • lib/service/media: 定义储存了 mediaQuery 状态,借助 .obs 实现 UI 自动更新 (如 设备翻转时)

  • lib/service/native: 定义创建了 Flutter Dart 与 原生 IOS、Android 端 的 MethodChannel 消息通道

  • lib/service/sqlite: 定义创建了 App 本地数据库(sqlite),并为不同的用户账号分配各自数据库资源

  • lib/service/store: 定义维护了全局数据存储 (例如 用户、订单、银彳亍卡...),并自动同步 UI 更新

  • lib/service/webview: 定义创建了 Flutter 与 Webview 端的 WebviewChannel,类似 JS Bridge




遗憾


在开发过程中,虽然我们通过 Flutter 技术栈和丰富的插件实现了许多功能,但仍遇到了一些不尽如人意的地方,特别是在集成 国内支付 (微信支付、支付宝) 上。国内支付必须通过企业或第三方服务商,且得有资质情况下,才能完成支付认证和集成,对个人开发者并不太友好。


此外,这次 数字钱包应用 的开发,主要目的是为了探索和搭建 Flutter App 框架,并没有计划将其上架。因此,我们暂时没有进行 App 订阅 功能的集成和测试,这也算是项目中的一个小遗憾。对于 App 订阅 的实现与集成,我们没有实际的开发经验。如果有熟悉这方面的朋友,欢迎在评论区留言分享,感谢!




注意


如果你对 数字钱包应用 感兴趣,想从 Github 下载下来,运行在自己的设备上。需要注意的是,无论是账号密码的登录注册,还是 Google 或 Github 的便捷登录,因为使用了 Firebase 平台的认证,在国内你可能需要 VPN,否则 数字钱包应用 可能会因网络问题而卡死。


有关这个问题的 issues 如下:


https://github.com/flutter/flutter/issues/70427 前往




演示


https://linpengteng.github.io/resource/dompet-app/app.gif 前往




GitHub


Dompet App: https://github.com/DompetApp/Dompet.flutter 前往

Webview SDK: https://github.com/DompetApp/Dompet.webview 前往


前往这个项目,给个 Star 👍


作者:linpengteng
来源:juejin.cn/post/7420814883576938547
收起阅读 »

鸿蒙 PC 发布之后,想在技术上聊聊它的未来可能

最近鸿蒙 PC 刚发布完,但是发布会没公布太多技术细节,基本上一些细节都是通过自媒体渠道获取,首先可以确定的是,鸿蒙 PC 本身肯定是无法「直接」运行 win 原本的应用,但是可以支持手机上「原生鸿蒙」的应用,细节上无非就是 UI 兼容下大屏模式的支持,比如下...
继续阅读 »

最近鸿蒙 PC 刚发布完,但是发布会没公布太多技术细节,基本上一些细节都是通过自媒体渠道获取,首先可以确定的是,鸿蒙 PC 本身肯定是无法「直接」运行 win 原本的应用,但是可以支持手机上「原生鸿蒙」的应用,细节上无非就是 UI 兼容下大屏模式的支持,比如下图是来自 差评XPIN 的鸿蒙 PC 截图:





那么问题来了,HarmonyOS 「卓易通 」 作为生态过渡的丰富支持,甚至在应用商店都可以无缝衔接,那么鸿蒙 PC 是否也可以有类似的场景?



因为目前得到的消息是,鸿蒙 PC 不支持侧载 ,这个结论我也不保熟,只是在这个大条件下讨论,那么 鸿蒙 PC 是不是也可以有个 「W易通」?技术上是否可以支持?



当然,你要说鸿蒙 PC 和鸿蒙手机,本质它们是同源的,App 之间互通理论上没毛病,从这个角度看,好像又支持侧载?


1a187a9eafc3edd514f8e310f5ec8d3b.png


那「W易通」?技术上是否可以支持?答案上还真可以,从某些媒体上说的,通过定制 Wine 来兼容已有的 win 软件,这个或许是一条可行的路,但是其实我也并没有找到官方下图的说法和出处,但是不妨碍我们讨论可行性。



Wine 这个名字本身就揭示了它核心特性:“Wine Is Not an Emulator”(Wine 不是模拟器),它其实已经被应用很久了,例如:



  • Steam Deck 和 Proton,就是那个 Steam 的掌机,它用的就是为极致游戏定制的 Wine ,Proton 是一个为 Windows 游戏能在 Linux 上流畅运行而深度定制和优化的发行套件

  • CrossOver 和 Whiskey,在 macOS 上运行 win 游戏,也是 Wine 的定制


所以 Wine 确实是一个可行的途径,Wine 在实际场景里主要是充当一个兼容层,实时地将 Windows 应用的 API 调用转换为宿主操作系统(如 Linux 或 macOS)能够理解的等效 POSIX 调用 。


当然,这种设计也意味着 Wine 的兼容性直接取决于其对 Windows API 的重实现程度,所以 Wine 的核心就是重塑 Windows API ,在某种程度上镜像了 Windows 的结构,例如:



  • wineserver:在 Windows 中主要是由内核提供核心服务,在 Wine 中会由 wineserver 在用户空间实现 ,它的职责包括实现基本的 Windows 功能,如进程和线程管理、对象管理、进程间通信(IPC)、同步原语、将 Unix 信号转换为 Windows 异常,处理窗口管理和输入事件等

  • 核心 DLL 的重实现:Wine 提供了大量核心 Windows DLL 版本,例如 NTDLL.DLL(Windows NT 内核功能的核心接口)、KERNEL32.DLL(基础操作系统功能,如内存管理、文件 I/O)、GDI32.DLL(图形设备接口,负责 2D 绘图)、USER32.DLL(用户界面元素、窗口管理、消息传递)等 ,这些 Wine 实现的 DLL 通常以 Unix 共享对象(.so 文件)的形式存在,它们可以直接调用宿主操作系统的函数


另外 还有 WineD3D ,将 Direct3D 和 DirectDraw API 调用翻译成 OpenGL 调用的核心组件 ,另外还有 DXVK 这种专注于将 Direct3D API 调用高效地翻译成 Vulkan 调用的支持。



前面的 Steam 的 Proton 也是一个针对游戏优化的 vkd3d 分支,负责将 D3D12 调用翻译为 Vulkan ,而 macOS 上或者还需要比如 MoltenVK 将 Vulkan 转为 Metal ?



当然,翻译 API 的局限性就不用多说了,还有一些依赖底层驱动支持的场景,很难在通用性上做到完美,当时理论上做到部分应用通用的场景应该可以,甚至在游戏领域反而更有优势?


当然,还有另外一条途径就是直接跑虚拟机,或者说虚拟桌面,目前已经有不少人运行成功,比如就有博主用 Os-easy 虚拟机装上了Windows 11 :




事实上 Linux 上运行 Win 虚拟机一直以来就有,用户只需选择镜像文件并完成基础配置,同样也可以在鸿蒙 PC 上使用Windows系统。


安装完成后,用户可以在鸿蒙与 Windows 系统之间便捷切换,类似切换桌面的效果,这样也算是一种场景支持:



当然,虚拟桌面的割裂感会更重,但是在通用软件场景下会相对更好,但是性能也许会更差一下?


另外,目前也挺多觉得鸿蒙 PC 就是一个平板 PC 化的场景,其实这样也算是一个趋势?类似我前段时间一直在聊的 Android PC 化支持,目前 Android 桌面化已经集齐:



  • Linux 终端控制台支持

  • 桌面模式

  • 外部显示器支持

  • 窗口多任务,最小化,多实例支持

  • 尚未明确的 Desktop View

  • 外部显示器排列和切换

  • ·····


例如下方就是 Android 下的外部显示器排列和切换支持:



最后,貌似目前鸿蒙 PC 虽然能进终端,但是不开放 sudo 权限,apt 也没有?这部分能力不知道后续是否会开放,从 PC 角度看这部分能力还是有必要的:



比如小米这个 winplay 是不是也是 wine 的定制魔改?我个人感觉,鸿蒙 pc 上游戏通过 wine 体系支持可能会比虚拟桌面更好


a723d34463601e3adb5da2daa42c024.png


那么,对于鸿蒙 PC 场景,你有什么技术方向想聊的?


作者:恋猫de小郭
来源:juejin.cn/post/7503450078159470646
收起阅读 »

个人开发者如何发送短信?这个方案太香了!

还在为无法发送短信验证码而烦恼?今天分享一个超实用的解决方案,个人开发者也能用! 最近国内很多平台暂停了针对个人用户的短信发送,这给个人开发者带来了不少困扰。不过别担心,我发现了一个超实用的解决方案——Spug推送平台,它能很好地满足我们发送短信等需求。 为...
继续阅读 »

还在为无法发送短信验证码而烦恼?今天分享一个超实用的解决方案,个人开发者也能用!



最近国内很多平台暂停了针对个人用户的短信发送,这给个人开发者带来了不少困扰。不过别担心,我发现了一个超实用的解决方案——Spug推送平台,它能很好地满足我们发送短信等需求。


为什么选择这个方案?



  1. 无需企业认证:个人开发者直接可用

  2. 新用户福利:注册即送测试短信

  3. 价格实惠:0.05元/条,按量计费

  4. 接口简单:几行代码就能搞定

  5. 支持丰富:短信、电话、微信、企业微信、飞书、钉钉、邮件等


三步搞定短信发送


第一步:注册账户


打开push.spug.cc,使用微信扫码直接登录,无需繁琐的认证流程。


第二步:创建模板



  1. 点击"消息模板" → "新建"

  2. 输入模版名称

  3. 选择推送通道

  4. 选择短信模板

  5. 选择推送对象

  6. 保存模板


第三步:发送短信


复制模版ID,通过API调用即可发送短信。


发送短信验证码代码示例(多种语言)


Python版(推荐)


import requests

def send_sms(template_id, code, phone):
url = f"https://push.spug.cc/send/{template_id}"
params = {
"code": code,
"targets": phone
}
response = requests.get(url, params=params)
return response.json()

# 使用示例
result = send_sms("abc", "6677", "151xxxx0875")
print(result)

Go版


package main

import (
"fmt"
"net/http"
"io/ioutil"
)

func sendSMS(templateID, code, phone string) (string, error) {
url := fmt.Sprintf("https://push.spug.cc/send/%s?code=%s&targets=%s",
templateID, code, phone)

resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}

return string(body), nil
}

func main() {
result, err := sendSMS("abc", "6677", "151xxxx0875")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(result)
}

Java版


import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class SMSSender {
public static String sendSMS(String templateId, String code, String phone) throws Exception {
String url = String.format("https://push.spug.cc/send/%s?code=%s&targets=%s",
templateId, code, phone);

URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");

BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();

while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();

return response.toString();
}

public static void main(String[] args) {
try {
String result = sendSMS("abc", "6677", "151xxxx0875");
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}

使用技巧



  1. 参数说明



    • code:验证码内容

    • targets:接收短信的手机号

    • 使用targets参数会覆盖模板配置的手机号



  2. 最佳实践



    • 选择合适的短信模版

    • 验证手机号格式

    • 管理验证码有效期

    • 添加错误处理

    • 确保账户余额充足




作者:外滩运维专家
来源:juejin.cn/post/7495570300124119052
收起阅读 »

这只是一罐过期了七年的红牛……

可能是临近假期,昨天眼睛乱晃时,偶然间在我的办公桌角落上发现一罐红牛。当我看到它的第一眼,就想起了很多事情。我知道它已经过期很久了,我还能模糊地想起当年隔壁部门领导送给我们的那个场景,也还能想起早已离职的同事们相处的日常…… 这罐红牛啊,过期很久咯 但当我拿过...
继续阅读 »

可能是临近假期,昨天眼睛乱晃时,偶然间在我的办公桌角落上发现一罐红牛。当我看到它的第一眼,就想起了很多事情。我知道它已经过期很久了,我还能模糊地想起当年隔壁部门领导送给我们的那个场景,也还能想起早已离职的同事们相处的日常……


这罐红牛啊,过期很久咯


但当我拿过来仔细一看的时候,才有点感慨,我知道它过期很久了,但没想到已经过期快 7 年了。也就是说,我来这个公司已经快 9 年了。作为一个2016年软件工程985本科毕业的年轻人,守在一个工厂拿着远远远低于行业平均水平的死工资,居然已经快 9 年了。我甚至敏感到与这罐红牛“感同身受”。



  • 年轻时,无穷的精力和旺盛的斗志,对未来也是无限美好的憧憬,根本不需要红牛来提神。

  • 过几年,经历越来越多的挫败和不如意,发现事情越来越不受自己控制,一罐红牛能起什么用。

  • 后来啊,感觉自己前途和命运(身体与灵魂)都不属于自己、无法掌握,一罐红牛又算什么东西。

  • 现在啊,孙连城是对的,就应该多看星星,“胸怀宇宙”,相比之下一切都是小事,红牛本来就只是用来提神的。


可惜的是,这罐红牛过期快七年了,它还能再提神吗?就和薛定谔的猫一样,不在困倦的时候去喝掉这罐红牛,我根本不会知道它还能不能提神。但是呢,我不敢喝。



  • 如果只是想要提神,再买一罐没过期的就好了。但是这罐过期的红牛,再也回不到没过期了。

    • 就像我在这工厂呆了九年,也三十多了,再也回不到二十多岁的时候了。但永远都有充满无限精力、天真稚嫩的二十多岁的小伙子。



  • 或许只是我还把它遗忘在办公桌的角落,所以它还存在;换成别人,可能早就喝掉了;或者也放过期,但是也早扔掉了;罐子可能回收利用,继续发光发热,也可能埋在哪个角落,慢慢副食氧化……

    • 会不会也偶尔觉得自己“空有一身济世救人的才华和抱负却无处施展”?像个废物不被重视。



  • 既然已经放在角落那么多年了,我也打算继续放在那里。那个不起眼的角落,只会占那么小小的一点地方,我不会在意,别人更不会在意。

    • 我不知道我还能在这里待多久,到时候我会带它走吗?可能不会了吧,毕竟它也没什么用了。象征意义大于实际,就像我也不主动联系那些没什么交集、感情不深的老同学、前同事。




过期红牛.jpg


自怨自艾啊,都知道是没用的


我觉得“最近行情不太好”这个话术,我很久没有去听了,也可能是刻意没有去关注。原因倒也简单,之前还想着说不定好好准备换个“钱多事少离家近”的工作。后来就是“我先保住这份工”,什么事情到时候再说吧。现在我多“乐观”,不管什么事,到时候再说吧。


就像也偶尔看到什么统计数据,结婚登记人数越来越少,离婚登记人数越来越多,“或许大家都还在纳闷不知道为什么呢。”


这很难的,就像我表弟刚工作 3 年,就开始在担心“35 岁危机”。未雨绸缪、眼光长远一点问题没有, 但是呢:



  • 要命的是光担心肯定是一点用没有,因为你只是担心、忧虑这个情况,并没有想到应对策略,那真正面临的时候,还是无所适从。

  • 更要命的是,不管你担心与否、忧虑与否,这个“危机”一定存在。

  • 更更要命的是,你知道,但是解决不了。

  • 就像我表妹指着我对表弟所说:“你慌什么,到时候先死的是他。”这一点毛病没有,我是没几年就 35 了,而且我到现在也没有具体应对策略。但更更更要命的是 ,我死了并不能缓解我表弟的担忧,看着我毫无办法或许更加重他的忧虑。


那我还会担心吗?偶尔吧。毕竟就像以前所说的,我与自己和解了,认清了现实,接受了平凡而普通的自己。可惜的是自己看清楚了,好像并没有很大的用处,因为似乎现代人很难作为一个独立的个体而生存。简单说起来就是"帅哥/美女与金钱"(爱情和事业)。



  • 我一个月倒还想拿个几百万呢,但是老板不给我啊?什么,我为什么不去当老板?

  • 我也想相敬如宾、举案齐眉、儿孙满堂、本枝百世,可还是单身。什么,我为什么不去找对象?


八旬老头老太太经常打电话问我:“你要好久才找对象哦”、“你结婚了我们才放得下心哦”、“我们天天焦得瞌睡都睡不着啊”……


我是很难去回应这种期盼了,甚至有时候不知道他们是真关心我生活,还是因为“你大孙 30 多了还没结婚啊,好搞笑哦”太没面子。毕竟时代不一样了,我可能不接受但可以去理解他们的老思想,但他们却不理解也无法接受我们新时代,就好像他们真的觉得想找对象马上就找的到,想换工作马上就换得到,想长生不老喝口水就可以了一样。


说多了大家都不爱听,所以我一般只会阿巴阿巴。


与自己和解?我只是随口一说


偶尔我会觉得,大部分普通人的人生经历都是类似的,只有那些真正“我命由我不由天”的少数人才的成功,比较难以复制。这也是我认为“经验之谈”大部分时候都是有效的。


可惜的是,经验之谈往往只有个结果,每个人的真正经历的细节不太一样,每个人的理解能力不太一样,时代背景也不一样,导致处理过程可能就不通用了。我说“与自己和解”之后,我的人生畅快了很多,但有些陌生的朋友来和我交流的时候,我却爱莫能助,也只能提个结论。



  • 深情是不是舔狗?没钱是不是铁废物?

  • “事前同意不代表事后同意”到底是什么原理?

  • “我只是希望你上进”到底是什么意思?

  • “我真的不想耽误你”到底是不是一片好心?

  • “我真的希望你有更好的未来”是不是真心的祝愿?

  • ……


“乱花渐欲迷人眼”,很多时候我看不明白,我就不爱看,我就做个“逃避者”,你觉得可耻关我什么事呢。这个时代到底需不需要阿 Q 精神?“只有对社会没用的废物才会想着放假,我们这些栋梁之才一心做建设,根本不需要放假。”


……


这样的生活多累啊,但是作为人的话,本来就很复杂。就像我曾以为我可以大展宏图发光发热,但是我错了;也以为自己遇到了对的人,然后又错了……


事实上,那罐红牛没有思想,我摆在那里,它就在那里,而且一直在那里。它不会开心、不会难过,也不会发表长篇大论咋咋呼呼。而且在那已经过去的8年,不管它是否过期,它都没有意识,它根本不关心,而且其实我也TM根本就不在乎它,全都是我顾影自怜而已。


或许可以试着当一罐过期的红牛?


就像我记得去年四五月的时候,各个云平台的AI大模型API或大幅度降价或者免费,我就在想能不能借助这些资源做个什么东西出来,还给好几个平台充了钱。一年过去了,但还是不知道能拿来做什么,依旧和最初的想法一样,调用各个平台API进行聊天对话而已,其他富有创造性的想法,完全没有。甚至在年初DeepSeek爆火之后,像我充钱的零一万物、智谱AI啥的,都没有特别大的声音。


或许有能力的人,先有超凡的思想,然后又脱俗的能力,所以才是弄潮儿。一年过去了,我还是没有想到能借助AI浪潮搞点什么,最多是用上了cursor并逐渐接纳了它。


但是,不管我怎么看待AI浪潮,它都会发展。就像5G、Web3、元宇宙……你不管以后对它们的定义是什么,它们都有着自己的发展轨迹,可能有的生命周期长点,有的Y轴高点,有的依旧不被很多人看好……


虽然我不是“宇宙区长”,我的生命还有那么长,“人生在世屈指算,也就三万六千天”。但就算到了35岁,也才三分之一,到目前的法定退休也才二分之一,我才进社会多少年,那后面的那些年就不过了?


或许可以当一罐过期的红牛,就默默地待在角落,世间的美好,人性的险恶,都如过眼云烟,我自岿然不动。


或许我只是想当这一罐过期了 7 年、却还没被我丢掉的红牛,可以长久在无人关注的角落,享受着自己孤独的快乐。




最后,还是分享一下我最新版本调用云平台大模型API的聊天应用,万一正好有朋友需要呢。


桌面主页面预览:


screenshot-home-desktop.png


移动端截图:


screenshot-mobile.jpg


若有兴趣,更具体内容可访问github查看:Sanotsu/SuChat-Lite


以上,感谢垂阅。


作者:小流苏生
来源:juejin.cn/post/7498292516492476450
收起阅读 »

我用AI+高德MCP 10分钟搞定苏州三日游

清明节后回到工作岗位,同事们都在讨论"五一"小长假要去哪里,我悄悄地笑了——作为一名AI玩家,旅行规划这种事,早就甩手给AI工具了!前两天,我用AI+高德地图MCP不到10分钟就搞定了一份详细的苏州三日游攻略,发给朋友们看了规划都惊呆了。"这...这么详细?连...
继续阅读 »

清明节后回到工作岗位,同事们都在讨论"五一"小长假要去哪里,我悄悄地笑了——作为一名AI玩家,旅行规划这种事,早就甩手给AI工具了!

前两天,我用AI+高德地图MCP不到10分钟就搞定了一份详细的苏州三日游攻略,发给朋友们看了规划都惊呆了。

"这...这么详细?连每天天气、门票价格、交通方式都安排好了?"

没错,它全都搞定了!想当年我策划旅行,那可是"人间疾苦":

  • 在小红书翻攻略翻到眼睛发酸
  • 在地图上反复规划路线怀疑人生
  • 十几个浏览器标签切换到想砸电脑

现在?10分钟搞定,而且比人工规划更合理、更高效。

想学吗?我现在就手把手教你,怎么让AI+高德MCP为你定制完美旅行计划。

四步上手,成为旅行规划大师

步骤1:获取高德地图开发权限(超简单)

先去高德开发者平台(lbs.amap.com)注册个账号。

怕麻烦?直接用支付宝扫码就能登录,一分钟搞定!

注册完成后,系统会让你验证身份——这是为了确认你不是机器人,讽刺的是我们要用这个来教AI做事🤣

验证过程很简单,照着提示操作就行,最终你会成为一名光荣的"高德地图开发者"。

步骤2:创建应用并获取API Key

登录成功后,进入控制台:

  1. 点击"应用管理",创建一个新应用

  1. 应用名称随便填,比如"我的旅行助手"
  2. 平台选择"Web服务"

  1. 创建应用后点击"添加Key",复制生成的密钥

这个Key就是打开高德地图宝库的钥匙,下面要把它交给我们的AI助手。

步骤3:配置AI的地图能力

这一步的关键——我们要让AI获得调用高德地图的超能力:

  1. 打开Claude Desktop(或其他支持MCP的AI,比如Cursor)
  2. File->Setting->Developer->Edit Config
  3. 配置MCP配置文件,配置高德地图MCP服务,贴入刚才获取的API Key
  4. 保存配置,重启应用

如果你使用的是Claude,添加下面的代码(记得替换成你自己的key)建议重启下应用:

{
"mcpServers": {
"amap-maps": {
"command": "npx",
"args": ["-y", "@amap/amap-maps-mcp-server"],
"env": {"AMAP_MAPS_API_KEY": "这里粘贴你的key"}
}
}
}

确认配置无误后,AI现在已经具备了调用高德地图的能力,它可以查询实时天气、景点信息、路线规划和交通状况等数据。

步骤4:一句指令,生成完美攻略

现在是见证奇迹的时刻!在对话框中输入:

用高德MCP,做苏州三天旅游指南

然后静静等待几秒钟,AI会开始调用高德地图API,搜集各种数据并为你生成一份详尽的旅行规划。

我的苏州三日游攻略包含了:

  • 每天详细的行程安排和时间规划
  • 景点介绍、门票价格和开放时间
  • 周边餐厅推荐和特色美食
  • 不同景点间的交通方式和预计用时
  • 三天的天气预报
  • 住宿和购物建议
  • 各种实用小贴士

最妙的是,AI还能根据天气情况自动调整行程——我看到第二天苏州预报有大雨,它贴心地提醒我准备雨具,并建议安排更多室内活动。

锦上添花:生成打印版旅行攻略

如果你想更进一步,可以让AI为你生成一份精美的A4旅行规划表,方便打印随身携带。

只需输入: 帮我设计一个A4纸张大小的旅行规划表,适合打印出来随身携带

这是我的提示词

# 旅行规划表设计提示词
你是一位优秀的平面设计师和前端开发工程师,具有丰富的旅行信息可视化经验,曾为众多知名旅游平台设计过清晰实用的旅行规划表。现在需要为我创建一个A4纸张大小的旅行规划表,适合打印出来随身携带使用。请使用HTML、CSS和JavaScript代码实现以下要求:
## 基本要求
尺寸与基础结构
- 严格符合A4纸尺寸(210mm×297mm),比例为1:1.414
- 适合打印的设计,预留适当的打印边距(建议上下左右各10mm)
- 采用单页设计,所有重要信息必须在一页内完整呈现
- 信息分区清晰,使用网格布局确保整洁有序
- 打印友好的配色方案,避免过深的背景色和过小的字体
技术实现
- 使用打印友好的CSS设计
- 提供专用的打印按钮,优化打印样式
- 使用高对比度的配色方案,确保打印后清晰可读
- 可选择性地添加虚线辅助剪裁线
- 使用Google Fonts或其他CDN加载适合的现代字体
- 引用Font Awesome提供图标支持
专业设计技巧
- 使用图标和颜色编码区分不同类型的活动(景点、餐饮、交通等)
- 为景点和活动设计简洁的时间轴或表格布局
- 使用简明的图示代替冗长文字描述
- 为重要信息添加视觉强调(如框线、加粗、不同颜色等)
- 在设计中融入城市地标元素作为装饰,增强辨识度
## 设计风格
- 实用为主的旅行工具风格:以清晰的信息呈现为首要目标
- 专业旅行指南风格:参考Lonely Planet等专业旅游指南的排版和布局
- 信息图表风格:将复杂行程转化为直观的图表和时间轴
- 简约现代设计:干净的线条、充分的留白和清晰的层次结构
- 整洁的表格布局:使用表格组织景点、活动和时间信息
- 地图元素整合:在合适位置添加简化的路线或位置示意图
- 打印友好的灰度设计:即使黑白打印也能保持良好的可读性和美观
## 内容区块
1. 行程标题区:
- 目的地名称(主标题,醒目位置)
- 旅行日期和总天数
- 旅行者姓名/团队名称(可选)
- 天气信息摘要
2. 行程概览区:
- 按日期分区的行程简表
- 每天主要活动/景点的概览
- 使用图标标识不同类型的活动
3. 详细时间表区:
- 以表格或时间轴形式呈现详细行程
- 包含时间、地点、活动描述
- 每个景点的停留时间
- 标注门票价格和必要预订信息
4. 交通信息区:
- 主要交通换乘点及方式
- 地铁/公交线路和站点信息
- 预计交通时间
- 使用箭头或连线表示行程路线
5. 住宿与餐饮区:
- 酒店/住宿地址和联系方式
- 入住和退房时间
- 推荐餐厅列表(标注特色菜和价格区间)
- 附近便利设施(如超市、药店等)
6. 实用信息区:
- 紧急联系电话
- 重要提示和注意事项
- 预算摘要
- 行李清单提醒
## 示例内容(基于深圳一日游)
目的地:深圳一日游
日期:2025年4月15日(星期二)
天气:晴,24°C/18°C,东南风2-3级
时间表:
| 时间 | 活动 | 地点 | 详情 |
|------|------|------|------|
| 09:00-11:30 | 参观世界之窗 | 南山区深南大道9037号 | 门票:190元 |
| 12:00-13:30 | 海上世界午餐 | 蛇口海上世界 | 推荐:海鲜、客家菜 |
| 14:00-16:00 | 游览深圳湾公园 | 南山区滨海大道 | 免费活动 |
| 16:30-18:30 | 逛深圳欢乐海岸 | 南山区白石路 | 购物娱乐 |
| 19:00-21:00 | 福田CBD夜景或莲花山夜游 | 福田中心区 | 免费活动 |
交通路线:
- 世界之窗→海上世界:乘坐地铁2号线(世界之窗站→海上世界站),步行5分钟,约20分钟
- 海上世界→深圳湾公园:乘坐公交线路380路,约15分钟
- 深圳湾→欢乐海岸:步行或乘坐出租车,约10分钟
- 欢乐海岸→福田CBD:地铁2号线→地铁4号线,约35分钟
实用提示:
- 下载"深圳地铁"APP查询路线
- 准备防晒用品,深圳日照强烈
- 世界之窗建议提前网上购票避免排队
- 使用深圳通交通卡或移动支付
- 深圳湾傍晚可观赏日落美景和香港夜景
- 周末景点人流较大,建议工作日出行
重要电话:
- 旅游咨询:0755-12301
- 紧急求助:110(警察)/120(急救)
请创建一个既美观又实用的旅行规划表,适合打印在A4纸上随身携带,帮助用户清晰掌握行程安排。

AI会立刻为你创建一个格式优美、信息完整的HTML文档,包含所有行程信息,分区清晰,配色考虑了打印需求,真正做到了拿来即用!

告别旅行规划焦虑症

这套方法彻底改变了我规划旅行的方式。以前要花半天甚至几天的工作,现在10分钟就能完成,而且质量更高:

  • 基于实时数据:不会推荐已关闭的景点或过时信息
  • 路线最优化:自动计算景点间最合理的游览顺序
  • 个性化定制:想要美食之旅?亲子游?文艺路线?只需一句话
  • 省时又省力:把宝贵时间用在享受旅行上,而不是规划过程中

最让我满意的是,这整套流程不需要任何编程知识,人人都能轻松上手。我妈妈都能用!

更多玩法等你探索

除了基础攻略,你还可以用更具体的指令获取定制内容:

"我想了解苏州有什么值得打卡的特色美食" "帮我规划一条适合老人和小孩的苏州慢游路线" "我只去苏州一天,哪些景点必须打卡?" "设计一条苏州园林主题的摄影路线"

每一个问题,AI都能结合高德地图的数据给你最专业的建议。

以后旅行前,不用再痛苦地翻攻略、对比信息、反复规划了。一杯咖啡的时间,完美行程就在你手中。

这大概就是科技改变生活的最好证明吧!下次出行,不妨也试试这个方法,让AI做你的专属旅行规划师!

阿里云宣布全面支持MCP服务部署和调用

前天群里还有小伙伴想玩下MCP服务呢,昨天阿里云百炼平台就宣布全面支持MCP服务部署与调用,打通AI应用爆发的最后一公里。

这里是地址:bailian.console.aliyun.com/?tab=mcp#/m…

当然昨晚我也研究了下,简直不要太简单,连注册都省了,下面点立即开通呢就能玩了

下面这个知名爬虫服务我也体验了把,非常简单易懂

创建完应用,提示词录入进去就能用了,连cursor,claude的mcp配置都免了,感兴趣的朋友可以去体验下。


作者:易安说AI
来源:juejin.cn/post/7491553973112111115
收起阅读 »

不容易,35岁的我还在小公司苟且偷生

前言 前几天和前同事闲时聚餐,约了两个月的小聚终于达成了,程序员行业聚少离多,所幸大家的发量还坚挺着。 期间不可避免地聊到了自己的公司、行业状况以及对未来的看法,几杯老酒之后,大家畅所欲言,其中一位老哥侃起了他的职业生涯,既坎坷又无奈,饭后想起来挺有代表性的,...
继续阅读 »

前言


前几天和前同事闲时聚餐,约了两个月的小聚终于达成了,程序员行业聚少离多,所幸大家的发量还坚挺着。

期间不可避免地聊到了自己的公司、行业状况以及对未来的看法,几杯老酒之后,大家畅所欲言,其中一位老哥侃起了他的职业生涯,既坎坷又无奈,饭后想起来挺有代表性的,征得他同意故记录在此。

以下是老哥的历程。



cold.jpg


程序员的前半生


我今年35岁,有房有贷有妻女有老父母。


出生在90年代的农村,从小中规中矩,不惹事不喧哗不突出,三好学生没有我,德智体美没有全面发展。学习也算努力,不算小题做题家,因为只考了个本科。


大学学费全靠助学贷款,勤工俭学补贴日用,埋头苦干成绩也只在年级中等偏下水平。有些同学早早就定下了大学的目标,比如考研、比如出国、比如考公,到了大三的时候大家基本都有了自己的目标。而我的目标就是尽早工作,争取早日还完贷款,因此早早就开始准备找工作。

也许是上天眷顾,不知道怎么就被华为看重了(那会华为还没现在的如日中天,彼时是BAT的天下),稀里糊涂的接受了offer,没想到却是改变了后面十年的决定。


2013年,深圳的夏天阳光明媚,热气扑鼻,提着一个简单的箱子进入了坂田基地。

刚开始,工作上的一切都很新鲜,每个人都在忙碌,虽然不知道他们在忙什么,但感觉很高级的样子。同期入职的同事都比较厉害,很快就适应了工作,而自己还是没完全应对工作内容,于是下班之后继续留在公司学习,顺便蹭饭。

就这样,很快就一年过去了,自己也慢慢熟悉了工作节奏,但是加班也越来越多了。对于自己来说,为了过节点,6点是晚饭时间,9点是下班时间,12点正式下班。

平凡的日子没什么值得留恋,过一天、一个月、一年、四年都没什么两样,四年里学习到了不少的知识,也数了很多次深圳凌晨的路灯数。


作为深漂,没有遇到深圳爱情故事,也对高昂的房价绝望,于是决定回到二线城市,成为一名蓉漂。
2017年,还是和四年前一样的行李箱,出现在了老家的省会城市,只是那时的我没有了助学打款,怀里也攒下了一些血汗钱。

那时互联网行业发展还是如火如荼,前端的需求量也很大,也得益于华为公司发展越来越好,自己的华为经历很快就拿到了几个offer,选了一家初创公司,幻想着能有一番成就。


2018年底,眼看着房价越长越高,某链中介不断地灌输再不买明天就是另一个价了,错过这个村就没这个店了,也许是想有个家,也许是想着父母能到省会里一起住,拿出自己做牛马几年的积蓄加上父母一辈子辛苦攒的小十万的养老钱购买了城区里的新房,那会儿的价格已经比前两年涨了一倍多,妥妥的高位站岗,不过想着自己是刚需也不会卖,因此咬咬牙掏出了全部的积蓄怒而背上了三十年的房贷。


房子的事暂时落定了,全身心的投入到工作中,没想到老板只想骗投资人的钱,产品没弄好投资人不愿跟进了,坚持了三年,期间各种断臂求生,最终还是落了个司破人走的境地。


2020年,30岁的我第一次被动失业了,幸运的是也找到了另一半。为了尽可能节省支出,房子装修的事我们都是亲力亲为,最后花了十多万终于将房子装好了,虽然很简单但毕竟是自己在大城市里的第一套房子,那一刻,感觉十年的付出都是值得的。

背着沉重的房贷,期望能找到一份薪资稍微过得去的工作,于是在简历上优势那行写了:“可加班”。依稀记得有些HR对我进行了灵魂拷问:结婚了吗?有小孩了吗?你都30岁了还能加班吗?。我斩钉截铁地说:只要公司有需要,我定会全力以赴!


2022年,我们的孩子出世了,队友辞去了工作全心全意带小孩,而我更加努力了,毕竟有了四脚吞金兽,不得不肝。

虽然工作很努力,但成果一般,不是公司的技术担当,也不会是技术洼地。


2023年的某一天,和之前的364天一样的平淡,在座位上解Bug的我突然感觉到一阵心悸,呼吸不畅,实在不行了呼唤同事叫了120,去医院一套检查下来没发现什么大问题。医生询问是不是工作压力太大,平时加班很多?我说还好,平时也就加班到9点。医生笑了笑说你这种年轻人我见多了,都是压力大的毛病,平时工作不要久坐盯着屏幕多站起来走走。他让我回家多休息,回去后观察了几天还是偶尔会有心悸,再去了另一个医院进行检查,也是没有明确的诊断结果,只是说可能是这个问题,又可能是另一个问题。

过了1个月后,身体上的问题不见好转,我辞去了工作。


2023年末,找了一家小公司,也就是我现在的公司,工资没有涨,仔细算起来还变相下降了。

还是做的业务需求,也没有领导什么人,管好自己就行,直属上级还是个工作几年的小伙。这家公司主要的特点是不加班,技术难度不高,能做多少就是多少,前提是要报风险,领导也不会强迫加班。


就这样到了2024,神奇的是我已经很久没有心悸的感觉了,不知道是不加班还是心态转变的原因。
家里的小朋友也长大了,会说话了。我现在每天下班最温馨的的是她开着门期待我回家的那一刻,她的期盼的眼神就是我回家的动力。


公司在2024年也裁了不少人,领导也找我谈过问问我的想法,我说:我还是能胜任这份工作的。领导说:公司觉得你年级大了一些,工资虽然不是最高,但不太符合行情,你懂的。我说:我懂,可以接受适当的降薪。
就这样,我挺过了2024,然而过了一周领导走了。


2025年,我35周岁了。
现在的我已经彻底接受自己的平庸的事实了。在学生时代,从来都不出色,也不会垫底,就是那类最容易被忽略的人。在工作时代,不是技术大牛,也不是完全的水货,就是普普通通的程序员。


如果说上半生吃到了什么红利,只能说入坑了计算机这行业,技术给我带了收入,有了糊口的基础。没进股市,却被房价狠狠割了一道。


35岁的我,没有彻底躺平摆烂,也没有足够奋发进取。

35岁的我,有着24年的房贷,还好61岁的时候我还在工作,应该还能还房贷。

35岁的我,不吃海鲜不喝酒,尿酸500+。

35岁的我,人体工学椅也挽救不了腰椎间盘突出。

35岁的我,头发依然浓密,只是白发越来越多。

35岁的我,已经不打游戏,只是会看这各种小说聊以慰藉。

35岁的我,两点一线,每天挤着地铁,看众生百态。

35岁的我,早睡早起,放空自己。

35岁的我,暂时还没有领取毕业大礼包,希望今年还能苟过。

35岁的我,希望经济能够好起来,让如我一般平凡的人能够有活下去的勇气。


诸君,下一年再会~祝你平安喜乐,万事顺遂!


太极分两仪,有程序员也有程序媛:30岁的程序媛,升值加薪与我无缘


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

✨说说私活,前端特效开发,以及报价策略✨

web
为啥要写 最能唬人的前端工种是啥?最能出活的前端模块是啥?跟大家讲,真的是搞动画那一块,搞特效那一块,搞3d、webgl那一块。出活,真的出活。 吭哧吭哧一些高深的框架或者死磕一个难啃的技术硬骨头老半天,给不懂技术的人看,他未必能懂,可能他还会心想:”做老半天...
继续阅读 »

为啥要写


最能唬人的前端工种是啥?最能出活的前端模块是啥?跟大家讲,真的是搞动画那一块,搞特效那一块,搞3d、webgl那一块。出活,真的出活。


吭哧吭哧一些高深的框架或者死磕一个难啃的技术硬骨头老半天,给不懂技术的人看,他未必能懂,可能他还会心想:”做老半天,啊?就这...要我来我也会(翻白眼🙄)“。真是这样的。


所以说,唬得住人的,绝b有视觉滚动这一块。


搜一些外国佬的一些产品官网,十个有八个是类似这样的。我们熟悉的苹果官网也是这样的。


封面图.gif


封面图.gif


再多的,我不举例了。


那身为前端的,切图仔的,小卡拉米的,千千万万个我,绝不能说不会。一般人会看到一个官网有这种视差效果,就会打开给你看,问:”你说你是前端,那你会做这个效果吗“。


这个时候,咱一定得把这个ac中间那个字母给支棱起来。不能丢了面。去搜库搜包,借助工具给它搞出来。


不要说不行


要做这种视觉滚动效果,给jym推荐一个库,啥库呢?gsap


这个玩意,能通过最少的代码实现令人惊叹的滚动动画。


外国佬很多网站,甚至我们国内很多官网,搞这种装ac中间那个字母的官网,离不开这个库。


咋用


写原生,不搞框架的:


就用cdn,引就行了:


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

引完这个gsap的库的js呢,完呢,再引入一个插件,叫ScrollTrigger,两者一结合,啥滚动动画能做不出来你说,官网都说了:GSAP的ScrollTrigger插件通过最少的代码实现令人惊叹的滚动动画。


<html lang="en">
// 每个section都有个背景,和一个h1的文字
<section>
<h1>111</h1>
</section>
<section>
<h1>222</h1>
</section>
<section>
<h1>333</h1>
</section>

<body>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/ScrollTrigger.min.js"></script>
</body>
</html>

上面就把html那一块写完了,接着写js:


const sections = document.querySelectorAll('section'); // 获取页面中所有的<section>元素

sections.forEach(se => {
// 从初始到目标的动画
gsap.fromTo(se, {
backgroundPositionY: `-${window.innerHeight / 2}px`, // 向上偏移(初始背景垂直位置为视口高度一般的负值)
}, {
backgroundPositionY: `${window.innerHeight / 2}px`, // 向下偏移(视口高度的一般的正值)
duration: 3, // 动画持续3s
ease: 'none', // 线性曲线
})
})

接下来用到ScrollTrigger插件:


const sections = document.querySelectorAll('section'); // 获取页面中所有的<section>元素

gsap.registerPlugin(ScrollTrigger); // gsap注册插件ScrollTrigger

sections.forEach(se => {
// 从初始到目标的动画
gsap.fromTo(se, {
backgroundPositionY: `-${window.innerHeight / 2}px`, // 向上偏移(初始背景垂直位置为视口高度一般的负值)
}, {
backgroundPositionY: `${window.innerHeight / 2}px`, // 向下偏移(视口高度的一般的正值)
duration: 3, // 动画持续3s
ease: 'none', // 线性曲线
scrollTrigger: {
trigger: se, // 触发的是当前的section
scrub: true, // 按滚动条去做这个视觉效果
}
})
})

总结


好了,如果你觉得神奇,停止往下看,自个儿去官网瞅一眼,别听我在这瞎叨叨,自己去瞅一眼,它啊,不单单是我说的这种视差滚动效果可以做,还有很多动画可以搞,它是一个动画库,我只是说的其一。


动画滴本质实质上就是数字的变动。动来动去就有了动画。这个库帮我们做了很多活,我们拿来用,我们就关注应该怎么变动数字,哪些数字,就完事。前人栽树,后人乘凉,有时候有效地聪明地灵活地有思路地去运用一些库一些包会让事情事半功倍,让效率提升,让时间缩减,让效果更美妙。


特效


往往,玩这些特效的,💰会报得高:



  • 视差滚动(Parallax Scrolling):通过背景层与内容层滚动速度差异营造空间感,适用于官网、营销页等场景,开发成本低但感知价值高。

    • 3D交互(Three.js/WebGL):如产品展示、虚拟展厅等,结合Canvas或WebGL实现,技术门槛较高但报价可达数万元。

    • 粒子动画(Particles.js/GSAP):用于登录页、Loading动画等,开发周期短但视觉效果突出,适合按模块打包报价。

    • SVG路径动画:适用于图标、数据可视化等场景,通过GreenSock等工具实现,复杂度可控且客户感知专业性强。




推荐大家工具,或许对你当前比较恼火的无法着手的特效,可能有思路:GSAP(动画库)、Three.js(3D渲染)、Pixi.js(2D渲染)。


报价策略:优先选择视觉效果显著、开发效率高的特效类型(如视差滚动、3D交互),报价可以溢出点,客户会觉得干了很多活才出这么牛的效果,交付的时候,特别是官网,也不会说太干巴巴,就只是图垒字,字垒图这种。有动画的官网,会使得更多的阅览量,触动更多的购买欲。


作者:小old弟
来源:juejin.cn/post/7495938507212177448
收起阅读 »

中年程序员,看不惯大公司,去小公司了

我想多些技术传奇,但是为啥总是那么多曲折的世俗故事,这也是别扭极了。 我博客这儿有技术方案,也好多IT行业的狗血故事。盘点了一下,都快成系列短剧了。 再往前,故事更多。而且总是引发着网友的讨论,以及成千上万的关注量。 今天这个故事,讲的是我放弃了大公司,...
继续阅读 »

我想多些技术传奇,但是为啥总是那么多曲折的世俗故事,这也是别扭极了。



我博客这儿有技术方案,也好多IT行业的狗血故事。盘点了一下,都快成系列短剧了。



再往前,故事更多。而且总是引发着网友的讨论,以及成千上万的关注量。



今天这个故事,讲的是我放弃了大公司,去了小公司的故事。


刚辞职离开的公司,是超过500人的企业,不缺钱,很土豪。再之前的公司也都是几百人的公司,最次也是新三板挂牌。而现在,我却入职了一家不到10个人的小公司。


我没疯。


咋说呢?我从大公司干了十多年,又能怎样呢?无非就是混日子,混工资。但是,随着年龄的增长,这种状态还能维持多久呢?我并不想混一天是一天。


很多朋友说,你怎么不继续去大公司?目前的大环境,大公司稳定,抗风险能力强。


但是,我刚辞职的这家就是挺大的公司,其实也并不“稳定”。


我依然是修炼不够,同时不愿躺平之心游丝尚存。



上一份工作,我以为找到了职业生涯的终极归宿,因为它太契合我了。可以说,它需要我,我也需要它。


我做教育信息化10年了,从手机到平板,从客户端到服务端,从硬件到软件,从团队管理到技术方案,再到后来将人工智能融入教育领域。说实话,即便如此,我想找份称心如意的工作依然不容易。因为,一个行业待久了,技能窗口变得既深又窄。比如试卷的电子化和批阅,我就研究了3年,可以说做到了行业领先。


但是,并没有几家公司需要这类冷门的技能。


同样,公司这边也面临同样的问题:找一个经验丰富的业内人,也很难。


我需要一个平台,让我继续研究教育领域的人工智能,我有信心让他成为行业领先。而公司需要一个员工帮它打造专业领域的业务,它也有能力供给我可观的收入。


因此,我俩一拍即合。


这家公司规模大,资金链充足,福利待遇又好。我一度认为,我的职业生涯终于可以稳定下来了,这是个好平台,我立志干到退休。


但是,我错了。


有一天,突然一个同事找到我,说要跟我谈谈。


这个同事不是行政不是HR,也不是我的领导,工作上和我也没有交集。离下班还有1分钟,给我发钉钉让我下班别走。



我当然不惯着他,我问他什么事情。他回复说,等他写完代码。


我说,我还有事,要去车站接人,请尽快。


他叫我去会议室,他告诉我说,我这个岗位是因为有临时需求才招聘的,现在我把活干完了,他也能接手,我就可有可无了。


我问什么意思,要裁我吗?


他说不是,现在只有他能救我,他一句话,我就能留,另一句话,我就得走。


第一次谈话,就这么结束了。算是一个下马威。


第二天,我去找我的直属领导。我问昨天找我谈话的那个白胖子什么角色,他说的话可不可信。


领导说,一半可信,一半不可信。


可信之处在于他是新来高管老郭身边的红人,老郭对他言听计从。不可信之处是他什么都不懂,一动代码就捅娄子,根本接不了我写的AI项目。


后来,我也见识到白胖子的能力,就是特别自信,他知道fastjson是阿里的开源项目,所以它笃定fastapi也必定是阿里开源的。这一点让老郭非常欣赏,因为白胖子总是有肯定的回答。而其技术只会说:可能……也许……大概……不敢保证……


有一次白胖子专门设计陷害一个老实同事,结果得到了老郭的大力支持。这一举动让其他同事既不满又无奈,更多的是害怕。而白胖子则以此示威,更加自信。


后来,白胖子又找我谈话。


他说他不想找我,但是老郭老问,他不得不找我。


他找我的主要目的是跟我谈我该怎么被安排的问题。他说,我现在很尴尬,原因是我的工作很差。主要体现在负责的项目太少,因为目前我手头就一个从0到1自研AI的项目。


我很震惊,手敲一个从0到1的自研AI项目,而且我只报了1个月的工期,保证保质保量地完成,这还不够炸裂吗?!做完了,就是公司的自主知识产品,可以申请专利的那种!这可是一个大项目,类似的项目,我上家公司投入几十万都没干成,后来我用2个月时间做了出来,当时我还受到了集团董事长的接见。


而在这里,我分析需求做定制开发,居然被谈话,理由是活太少?!


他说,你看看别人:小张,这个月干了5件事:修改A项目首页的弹窗文案,将“你好”改为“您好”;给B项目一个500行代码的类加了35条注释;C项目实现了登录功能,代码提交量为1500行……


你说说,人家手里5个项目,而你1个项目,这是5倍的差距。


我说,郭总也这么看吗?白胖子说,当然,郭总多厉害,肯定知道5大于1,小学生都知道。


那怎么办……我问他。他说,我早就说过了,现在只有我能救你。你要有空,我们先吃个饭聊聊,合适的话,我叫上郭总再一起吃个饭……


我说,如果不合适呢?他说,我现在站在个人角度给你聊,要是站在公司角度……


“公司角度怎么样?”


“现在就可以走流程了!你个人能和法人玩得起吗?”


我说,我再想想。


后来,他又找我。



这次,我留了个心眼,打开了手机录音。他还是哪些话,基本就是打压和威胁,只不过更重了一些。


这次我跟他顶嘴了,我说要谈离职也是人事跟我谈,你既不是人事也不是我领导,你没有资格。


最后不欢而散。


回到座位后,我听到从远处传来郭总的声音:怎么这么过分……那怎么能行……简直是不像话……


次日,我已经不打算在这干了。理由是小白胖子并没有什么可怕的,这种人我见多了。但是你一个集团高管居然受制于他,这就奇葩了。


我就把录音发给了HR总监,面谈了一会儿。总监说,我们了解下情况。


我感觉这类情况很严重,没想到公司会这么对待员工。如果不是公司的意思,那么个人行为更恶劣。


结果,HR将情况告知了郭总,让郭总找我聊聊。


我原来以为只是白胖子的个人想法,因为一个花十几万猎头费聘来的大集团高管,不至于境界那么低,偏听偏信,一点技术上的判断力都没有。但是,后来跟郭总一聊发现,他的想法完全受白胖子影响。换句话说,郭就是个傀儡,他也不了解该如何工作,完全听白胖子的。也就是郭当皇帝拿工资,任命白胖子为常务皇帝干活。


郭说,现实情况就是你可有可无。如果你足够听话,我肯定会保你。如果不听话,那么他只能重新招人。


这还有什么理由待下去呢?我提出了辞职。


一开始,白胖子一派很开心,马上就批准我的辞职。后来,他们开始紧张了起来。


据说,白胖子原计划将我搞走后,引入他的认识一个熟人进来。



但是他的人来面试之后,履历不匹配,还要价太高,远超出我的薪资,公司没有同意。结果,“人傻、钱多、速来”的换人计划落空,我又被搞走了,导致AI项目无人接手了。


现在,他们赶紧启动招人,并且尽量拖着不让我走。


这个公司离职交接很快,这是几十年的传统。你一旦提出离职,不管何种岗位,是否有交接人,三两天必定办完手续走人。


但是,我破纪录了。我被卡了半个多月,愣是不给我办离职手续。一催就说,法律规定30天才能走。



这就矛盾了,前期你说我可有可无,是个可怜虫,现在又不让我走。


后来,我就催。我说没活了,还留我干什么?结果他们给我安排改bug的工作。我的AI项目识别率99%,他们测出那识别不了的1%算bug,让我修改。



改不出来,就不办手续。


搞得我都想留下来了,留下来让你们辞退我得了,还能拿赔偿。
最后,多方运动,终于办完了手续。
就在最后运维、财务、人力全部都签完字了,还是不给离职证明,说要改bug到满意为止。



从业这么多年来,没有经历这么恶心的事情。


最后,总算是走了。


一个月后,发了离职工资,金额不对。我找公司专门负责工资核算的员工,打电话不接,加微信不通过。托其他同事的关系,才要到了工资条。后来,他们居然还想问我工作上的问题。



这就是大公司,以及大公司里的人情世故。


后来,也听到很多同事的吐槽,说公司如何如何。



一入职白胖子就会鼓动新人去请领导吃饭,表示站队。我就是没有站队,导致被针对。


还有,一动代码就捅娄子的白胖子,曾经动过一次代码,结果捅了大篓子。导致集团追责,技术部全体员工都被扣了工资。因为白胖子通过口若悬河的说辞,让自己完全撇清了关系。因此部门领导扣了全体同事的,唯独没扣白胖子的。结果,同事找到了Git代码提交记录,发现完全就白胖子自己一个人的疏忽造成的。这就是“集体闹绩效事件”。但是,即便集体闹,也没什么用。


我最反感的是即便大家都知道此人横行霸道,但是大家依然无能为力。


因为这个公司太大了,管事的人太多了,导致都管不了,也改变不了。现实情况是只有老板能管,但是老板很信任他建立的体系。


如果有人把白胖子的事迹告诉老板,那老板会狠批告密者,这叫打小报告,是严重违纪。因为公司有七八个部门,大约上百人专门做人员的评价工作,他们每天的工作就是进行风气整治和文化的净化。


但是,这些部门都知道白胖子的事迹,就算都觉得不对,谁来下这个手?肯定不是自己!这是得罪用人部门的事情。那就等老板知道了,老板发了话,大家才抢着干。但是老板又不让打小报告,永远不会知道。那就是用人部门说了算。用人部门又听白胖子。


所以,最后的结果就是白胖子没人能管得了。


在一个很大的体系里,最大的成本就是管理成本,也就是让这个体系能运转的成本。得有200人做服务才能支撑另外200人能正常上下班。


我的另一个感觉就是,大公司的反应速度太慢了。


我从0到1做一个同样需求的AI项目,我在10人企业只需要1个月,在100人企业需要3个月,而在这里,可能1年都干不完。因为周边的东西太多了。会有太多的部门卡你,有功都来抢,有锅都来甩。开不完的罗圈会,写不完申请,以及数不清的明枪暗箭。咱们是技术人,不是朝堂上的政治家。


我实在是不愿意浪费时间在这些人身上,我更愿去研究AI,研究教育。


而小公司就不一样了。可以直面老板,可以直面客户,可以直面技术。行动迅速敏捷,效率高。


如今的市场变化很快,需要快速响应,需要快速迭代,需要快速试错。新模式的爆发速度,让人猝不及防。


跟着小公司老板有很多可以学习的地方,会亲历一个想法从诞生到落地,这些我全程都会亲历,这很有可能就是我的以后。


随着年龄的增长,越来越不愿意待在能养老的地方,反而愿意去折腾,去挑战。因为剩下的时间不多了。


目前入职了一家小公司,不用打卡,不用写各种汇报材料,不用请领导吃饭,同事间关系简单,老板有活力,有想法,有干劲。中午吃完饭,会去园区里走走,晒晒太阳,听听鸟叫,高效率工作,大家也经常和老板有那种面红耳赤的讨论,这在集团企业内是不敢想的,更有一种独立人的自主感。我所写的代码,客户、同事和自己都满意,这是我非常喜欢的。


我一直是跟着自己的感觉走,从不委屈自己的内心。我一个老领导生病了,心脑血管方面的问题。术后,医生不让他抽烟喝酒。后来,他轻度抑郁。医生开始让他每周不能超过2顿酒,每天不超过3支烟。他问医生,为什么现在又可以了?医生说,相比于抑郁症,抽烟喝酒的伤害更小一些。心情对人体的影响,胜过肉体的伤害。工作十多年,我几乎都是裸辞。找工作也基本不内推,走网上投简历。每次也都找的挺好,下一家总能解决上一家的问题。当然,新一家也会有新的问题,但每次都是迈上一个新台阶。干这行,能进步就好。


不委屈自己的内心,也不委屈自己的能力。一切都有解决方案,未知的变化胜过已知的不变。


期望我现在的公司越来越好。也祝原来的公司蒸蒸日上。


另外,我相信白胖子以后肯定比我混得好,甚至我相信他去另一家公司依然能混得风生水起,但是这并不影响我坚守我自己内心的信念。


这就是各有各的道。我老领导经常召集众兄弟给他帮忙,我们多数也是友情出演。他经常感慨,自己也狠不下心去干一些坑蒙拐骗的活,即便他知道别人干项目就是“要么坑客户,要么坑自己”,他也依然选择自己吃点亏。不然他也能混的风生水起。但是,也正是因为他这么做,他才能召集起来我们这一众兄弟。我们本就不是耍心机的人,因此才愿意向他靠拢。他要变了,虽然我们也很一般,那他身边该换人了。


都有各自的活法,无所谓好坏之分。


作者:TF男孩
来源:juejin.cn/post/7496709980680863744
收起阅读 »

阮一峰推荐to-unocss,尤雨溪点赞

web
最近投稿了阮一峰周刊 to-unocss 的网站。 这个网站可以直接将 style 转换成 UnoCss 语法, 他内部使用了 transform-to-unocss 的库,可以直接将 SFC 页面转换成 UnoCss SFC,这个特性,它能带来的收益就是,...
继续阅读 »

image.png


最近投稿了阮一峰周刊 to-unocss 的网站。


image.png
这个网站可以直接将 style 转换成 UnoCss 语法, 他内部使用了 transform-to-unocss 的库,可以直接将 SFC 页面转换成 UnoCss SFC,这个特性,它能带来的收益就是,可以将你原本 sasslessclassinline-style 转换成 UnoCss 然后原子化这样 class 能够最大限度被复用,对于 inline-style 被转换后还能带来性能收益,其他转换后能极大程度上减少打包后 css 体积, 当然 transform-to-unocss 底层还有一个 ]transform-to-unocss-core 的支持,能够将一长串的 style 输入,编译成 UnoCss 语法的输出,他已经用在用户量不错的 figma 插件的 fubukicss-tool 之中,当然处理 UnoCss,我同样做了 Tailwind 的一套,如果你是 UnoCss 新手,你一定要试试这个网站 to-unocss ,如果你是 vscode 用户可以按住 Unot 这个插件,他提供了 vscode hover inline-style 提示转换和整个 SFC page 转换的强大能力


视频演示: http://www.bilibili.com/video/BV1HY…


作者:Simon_He
来源:juejin.cn/post/7499251236128342067
收起阅读 »

WC!一键微信聊天记录MCP!老板监工神器!

作者:前端开发爱好者 上班经常摸鱼的小伙伴们要注意了,老板们的监工神器来了! ChatLog 是一款开源的聊天记录分析工具,能够帮助用户高效解析、可视化和挖掘聊天数据中的关键信息。 它支持从本地数据库文件获取微信、QQ、Telegram 等主流聊天工具的聊天...
继续阅读 »

作者:前端开发爱好者


上班经常摸鱼的小伙伴们要注意了,老板们的监工神器来了!


ChatLog 是一款开源的聊天记录分析工具,能够帮助用户高效解析可视化挖掘聊天数据中的关键信息。


图片


它支持从本地数据库文件获取微信QQTelegram 等主流聊天工具的聊天记录,并通过智能分析数据可视化等功能,帮助用户更好地管理和利用这些数据。


无论是个人社交分析团队协作优化,还是商业场景的客户对话洞察,ChatLog 都能提供强大的支持。


ChatLog 核心特点



  • 智能分析:自动提取聊天记录中的高频词情感倾向活跃时段等关键指标,帮助用户快速了解聊天内容的核心信息。

  • 多平台支持:兼容微信QQTelegram 等主流聊天工具导出的数据格式,满足不同用户的需求。

  • 数据可视化:生成词云时序活跃图关系网络图等,直观呈现聊天特征,让复杂的数据一目了然。

  • 隐私保护本地化处理数据,无需上传云端,确保信息安全。用户可以放心使用,无需担心隐私泄露。

  • 自动化报告:一键生成分析报告,支持导出 PDF/Excel,便于分享与存档。


如何快速使用


安装 ChatLog


ChatLog 提供了两种方式安装,一种是直接下载预编译版直接安装,另外一种就是下载源码方式安装



go install github.com/sjzar/chatlog@latest

对于大部分人来说,直接使用 预编译版 就能满足大部分需求,除非你需要做二次开发的可以下载源码本地运行


图片


执行 ChatLog 启动 Terminal UI 界面。


图片


使用 ↑ ↓ 键选择菜单项,按 Enter 确认选择,按 Esc 返回上级菜单,按 Ctrl+C退出程序。


MCP 集成指南


运行 ChatLog,完成数据解密并开启 HTTP 服务


以 ChatWise 使用 MCP SSE 为例,


在 URL 中填写 http://127.0.0.1:5030/sse,并勾选 动执行工具,点击 查看工具 即可检查连接 chatlog 是否正常:


图片


测试功能是否正常:


图片


更多 MCP 集成指南 查看https://github.com/sjzar/chatlog/blob/main/docs/mcp.md


“牛马”开始慌了?


在日常工作中,微信等工具已成为“牛马”日常沟通的重要工具。


海量的聊天记录中蕴含着大量有价值的信息,却往往难以高效利用。


图片


ChatLog 的出现,无疑为“牛马”提供了一个强大的工具,可以更高效地管理和分析微信聊天记录。


老板会不会用这个工具来分析我们的聊天记录?


随着 ChatLog 这样的工具出现,不少 “牛马” 开始担心:


老板会不会用这个工具来分析我们的聊天记录?以后摸鱼的时间是不是都没了?


图片


但实际上,ChatLog 的出现更多是为了帮助我们更好地管理自己的聊天记录,提高工作效率。


它可以帮助我们快速总结群聊内容,提取聊天记录中有用的信息,从而更好地完成工作任务。


例如:



  • 快速查找聊天内容:通过关键词搜索,快速定位到重要的聊天记录。

  • 每周工作总结:自动生成聊天记录的总结报告,帮助我们快速回顾本周的工作重点。

  • 提取关键信息:从大量聊天记录中提取关键信息,比如客户的需求、项目进展等,方便我们进行后续的工作安排。


总之,ChatLog 是一款功能强大的开源聊天记录分析工具。


它不仅支持多种主流聊天工具的数据格式,还提供了智能分析、数据可视化和自动化报告生成等功能。



作者:独立开阀者_FwtCoder
来源:juejin.cn/post/7496033842748571698
收起阅读 »

聊一下MCP,希望能让各位清醒一点吧🧐

最近正在忙着准备AI产品示例项目的数据,但是有好几群友问了MCP的问题,先花点时间给大家安排一下MCP。作为一个带队落地AI应用的真实玩家,是怎么看待MCP的。 先说观点:MCP不错,但它仅仅是个协议而已,很多科普文章中,提到的更多都是愿景,而不是落地的场景。...
继续阅读 »

最近正在忙着准备AI产品示例项目的数据,但是有好几群友问了MCP的问题,先花点时间给大家安排一下MCP。作为一个带队落地AI应用的真实玩家,是怎么看待MCP的。


先说观点:MCP不错,但它仅仅是个协议而已,很多科普文章中,提到的更多都是愿景,而不是落地的场景。


本文不再重新陈述MCP的基本概念,而是旨在能让大家了解的是MCP 有什么用?怎么用?要不要用?


我准备了一份MCP实现的核心代码,只保留必要的内容,五分钟就能看明白MCP回事。


先上代码,让我们看看实现MCP最核心的部分我们都干了些什么东西。顺便让大家看看MCP到底和Function call是个什么关系



此处只贴用于讲解的代码,其他代码基本都是逻辑处理与调用。也可关注公众号:【华洛AI转型纪实】,发送mcpdemo,来获取完整代码。



MCP代码核心逻辑


我们在本地运行的MCP,所以使用的是Stdio模式的客户端和服务端。也就是:StdioServerTransportStdioClientTransport


先看打满日志的demo运行起来起来后,我们获得的信息:


mcpdemo.jpg

我们的服务端写了两个简单的工具,加法减法


服务端启动成功之后,客户端成功的从服务端获取到了这两个工具。


我们发起了一个问题:计算1+1


接下来做的事情就是MCP的客户端核心三步逻辑:



  1. 客户端调用AI的function call能力,由AI决定是否使用工具,使用哪个工具。

  2. 客户端把确定要使用的工具和参数发送回服务端,由服务端实现API调用并返回结果。

  3. 客户端根据结果,再次调用AI,由AI进行回答。


我们一边看代码一边说里面的问题:


第一步调用AI,决定使用工具


客户端代码:


  const response = await this.openai.chat.completions.create({
model: model,
messages,
tools: this.tools, // ! 重点看这里,this.tools是服务端返回的工具列表
});

看到了么?这里用的还是Function call! 谣言一:MCP和Function call没关系,MCP就可以让大家调用工具,终结了。MCP就是用的function call的能力来实现的工具调用。当然我们也可以不用Function call,我们就直接用提示词判断,也是可以的。


这里要说的是:MCP就是个协议。并没有给大模型带来任何新的能力,也没有某些人说的MCP提升了Function call的能力,以后不用Function call了,用MCP就够了这种话,千万不要被误导。


MCP并没有让大模型的工具调用能力提升


在真实的生产环境中,目前Function call主要的问题有:



  • 工具调用准确性不够。
    真正的生成环境可能不是三个五个工具,而是三十个五十个。工具之间的界限不够清晰的话,就会存在模型判断不准确的情况。

  • 参数提取准确性不够。
    特别是当一个工具必填加选填的参数达到十个以上的时候,面对复杂问题,参数的提取准确率会下降。

  • 多意图的识别。

    用户的一个问题涉及到多个工具时,目前没有能够稳定提取的模型。


第二步把工具和参数发回服务端,由服务端调用API


客户端代码:


const result = await this.mcp.callTool({
name: toolName,
arguments: toolArgs,
});

服务端的代码:


server.tool(
"加法",
"计算数字相加",
{
"a": z.number().describe("加法的第一个数字"),
"b": z.number().describe("加法的第二个数字"),
},
async ({ a, b, c }) => {
console.error(`服务端: 收到加法API,计算${a}${b}两个数的和。模型API发送`)
// 这里模拟API的发送和使用
let data = a + b
return {
content: [
{
type: "text",
text: a + '+' + b + '的结果是:' + data,
},
],
};
},
);

发现问题了么? API是要有MCP服务器提供者调用的。要花钱的朋友!


每一台MCP服务器背后都是要成本的,收费产品进行MCP服务器的支持还说的过去,不收费的产品全靠爱发电。更不要说,谁敢在生成环境接一个不收费的私人的小服务器?


百度地图核心API全面兼容MCP了,百度地图是收费的,进行多场景的支持是很正常的行为。


来看看百炼吧,阿里的百炼目前推出了MCP的功能,支持在百炼上部署MCP server。


也是要花钱的朋友~,三方API调用费用另算。


blfy.jpg

阿里的魔塔社区提供了大量的MCP,可以看到有一些大厂的服务在,当然有收费的有免费的,各位可以尝试


mota.jpg

第三步客户端根据结果,再次调用AI,由AI进行回答。


客户端代码:


messages.push({
role: "user",
content: result.content,
});

const aiResponse = await this.openai.chat.completions.create({
model: model,
messages: messages,
});

从服务端返回的结果,添加到messages中,配合提示词由大模型进行回复即可。


这一步属于正常的流程,没什么好说的。


那么问题是:我们使用MCP来实现,和我们自己实现这套流程有什么区别么?我们为什么要用MCP呢?


当初群里朋友第一次提到MCP的时候,我去看了一眼文档,给了这样的结论:


大厂为了抢生态做的事情,给落地的流程中定义了一些概念,多了脑力负担,流程和自己实现没区别。


对于工具的使用,自己实现和用MCP实现有什么区别么?


自己实现的流程和逻辑是这样的:



  1. 我们的提示词工程师写好每个工具的提示词

  2. 我们的后端工程师写好模型的调用,使用的是前面写好的提示词

  3. 提供接口给前端,等待前端调用

  4. 前端调用传入query,后端通过AI获取了工具

  5. 通过工具配置调用API,拿到数据交给AI,流式返回用户。


MCP的逻辑是这样的:



  1. 我们的提示词工程师写好每个工具的提示词

  2. 我们后端工程师分别写好MCP服务端、MCP客户端

  3. MCP客户端提供个接口给前端,等待前端调用

  4. 前端调用传入query,MCP客户端调用AI,获取了工具。

  5. 客户端把确定要使用的工具和参数发送会服务端,由服务端实现API调用并返回结果。

  6. 客户端根据结果,再次调用AI,由AI进行回答,流式返回用户。


看吧,本质上是没有区别的。


什么?你说MCP服务端,如果日后需要与其他企业进行合作,可以方便的让对方的MCP客户端调用?
我们的客户端也可以很方便的接入别人的MCP服务端。


不好意思,不用MCP也可以,因为Function call的参数格式已经确定了,这里原本存在差异性就极小。而且MCP也并没有解决这个差异性。还是需要客户端进行修改的。


MCP真正的意义


现在还是诸神混战时期,整个AI产品的上下游所有的点,都具有极高的不确定性。


MCP给出了一个技术标准化的协议,是大家共建AI的愿景中的一环,潜力是有的。


但是Anthropic真的只是在乎这个协议么?前面的内容我们也看到了,MCP和我们自己实现的流程几乎是一样的。但是为什么还要提出MCP呢?


为了生态控制权和行业话语权。


MCP它表面上是一个开放的协议,旨在解决AI模型与外部工具集成的碎片化问题,但其实他就是Anthropic对未来AI生态主导权的竞争。


未来MCP如果真的作为一个标准的协议成为大家的共识,围绕这个协议,甚至每家模型的工具调用格式都将被统一,此时Anthropic在委员会里的位置呢?不言而喻啊。


结语


最后把我的策略分享给大家吧:


打算在圈子里玩的部分,就和大家用一样的,不在圈子里玩的,其实自己团队实现也是OK的。


我这边更多的是自己团队实现的,而且在这个实现过程中大家对模型应用、AI产品的理解不断地在提升。


希望各位读者也多进行尝试,这样未来面对新出的各路牛鬼蛇神时大家才能有更多的判断力。


共勉吧!



☺️你好,我是华洛,如果你对程序员转型AI产品负责人感兴趣,请给我点个赞。


你可以在这里联系我👉http://www.yuque.com/hualuo-fztn…


已入驻公众号【华洛AI转型纪实】,欢迎大家围观,后续会分享大量最近三年来的经验和踩过的坑。



实战专栏


# 从0到1打造企业级AI售前机器人——实战指南一:根据产品需求和定位进行agent流程设计🧐


# 从0到1打造企业级AI售前机器人——实战指南二:RAG工程落地之数据处理篇🧐


# 实战派!百万PV的AI产品如何搭建RAG系统?


# 团队落地AI产品的全流程


# 30行代码,随时进行云端大模型能力评估!


# 5000字长文,AI时代下程序员的巨大优势!


作者:华洛
来源:juejin.cn/post/7492271537010671635
收起阅读 »

2025 跨平台框架更新和发布对比,这是你没看过的全新版本

web
2025 年可以说又是一个跨平台的元年,其中不妨有「鸿蒙 Next」 平台刺激的原因,也有大厂技术积累“达到瓶颈”的可能,又或者“开猿截流、降本增笑”的趋势的影响,2025 年上半年确实让跨平台框架又成为最活跃的时刻,例如: Flutter Platform...
继续阅读 »

2025 年可以说又是一个跨平台的元年,其中不妨有「鸿蒙 Next」 平台刺激的原因,也有大厂技术积累“达到瓶颈”的可能,又或者“开猿截流、降本增笑”的趋势的影响,2025 年上半年确实让跨平台框架又成为最活跃的时刻,例如:



而本篇也是基于上面的内容,对比当前它们的情况和未来可能,帮助你在选择框架时更好理解它们的特点和差异。



就算你不用,也许面试的时候就糊弄上了



Flutter


首先 Flutter 大家应该已经很熟悉了,作为在「自绘领域」坚持了这么多年的跨平台框架,相信也不需要再过多的介绍,因为是「自绘」和 「AOT 模式」,让 Flutter 在「平台统一性」和「性能」上都有不错的表现。



开发过程过程中的 hotload 的支持程度也很不错。



而自 2025 以来的一些更新也给 Flutter 带来了新的可能,比如 Flutter Platform 和 UI 线程合并 ,简单来说就是以前 Dart main Thread 和 Platform UI Thread 是分别跑在独立线程,它们的就交互和数据都需要经过 Channel 。



而合并之后,Dart main 和 Platform UI 在 Engine 启动完成后会合并到一个线程,此时 Dart 和平台原生语言就支持通过同步的方式去进行调用,也为 Dart 和 Kotlin/Java,Swift/OC 直接同步互操作在 Framework 提供了进一步基础支持。



当然也带来一些新的问题,具体可见线程合并的相关文章。



另外在当下,其实 Flutter 的核心竞争力是 Impeller ,因为跨平台框架不是系统“亲儿子”,又是自绘方案,那么在性能优化上,特别 iOS 平台,就不得不提到着色器预热或者提前编译。



传统 Skia 需要把「绘制命令」编译成可在 GPU 执行代码的过程,一般叫做着色器编译, Skia 需要「动态编译」着色器,但是 Skia 的着色器「生成/编译」与「帧工作」是按顺序处理,如果这时候着色器编译速度不够快,就可能会出现掉帧(Jank)的情况,这个我们也常叫做「着色器卡顿」



而 Impeller 正是这个背景的产物,简单说,App 所需的所有着色器都在 Flutter 引擎构建时进行离线编译,而不是在应用运行时编译


image-20250515102018153


这其实才是目前是 Flutter 的核心竞争力,不同于 Skia 需要考虑多场景和平台通用性,需要支持各种灵活的额着色器场景,Impeller 专注于 Flutter ,所以它可以提供更好的专注支持和问题修复,更多可见:着色器预热?为什么 Flutter 需要?



当然 Skia 也是 Google 项目,对于着色器场景也有 Graphite 后端在推进支持,它也在内部也是基于 Impeller 为原型去做的改进,所以未来 Skia 也可以支持部分场景的提前编译。



而在鸿蒙平台,华为针对 Flutter 在鸿蒙的适配,在华为官方过去的分享里,也支持了 Flutter引擎Impeller鸿蒙化,详细可见:b23.tv/KKNDAQB


甚至,Flutter 在类游戏场景支持也挺不错,如果配合 rive 的状态机和自适应,甚至可以开发出很多出乎意料的效果,而官方也有 Flutter 的游戏 SDK 或者 Flame 第三方游戏包支持:



最后,那么 Flutter 的局限性是什么呢?其实也挺多的,例如:



  • 文字排版能力不如原生

  • PC平台推进交给了 Canonical 团队负责,虽然有多窗口雏形,但是推进慢

  • 不支持官方热更新,shorebird 国内稳定性一般

  • 内存占用基本最高

  • Web 只支持 wasm 路线

  • 鸿蒙版本落后主版本太多

  • 不支持小程序,虽然有第三方实现,但是力度不大

  • ····



所以,Flutter 适合你的场景吗?



React Native


如果你很久没了解过 RN ,那么 2025 年的 RN 会超乎你的想象,可以说 Skia 和 WebGPU 给了它更多的可能。


img


RN 的核心之一就是对齐 Web 开发体验,其中最重要的就是 0.76 之后 New Architecture 成了默认框架,例如 Fabric, TurboModules, JSI 等能力解决了各种历史遗留的性能瓶颈,比如:



  • JSI 让 RN 可以切换 JS 引擎,比如 Chakrav8Hermes ,同时允许 JS 和 Native 线程之间的同步相互执行

  • 全新的 Fabric 取代了原本的 UI Manager,支持 React 的并发渲染能力,特别是现在的新架构支持 React 18 及更高版本中提供的并发渲染功能,对齐 React 最新版本,比如 Suspense & Transitions:

  • Hermes JS 引擎预编译的优化字节码,优化 GC 实现等

  • TurboModules 按需加载插件

  • ····


另外现在新版 RN 也支持热重载,同时可以更快对齐新 React 特性,例如 React 19 的 Actions、改进的异步处理等 。


而另一个支持就是 RN 在 Skia 和 WebGPU 的探索和支持,使用 Skia 和 WebGPU 不是说 RN 想要变成自绘,而是在比如「动画」和「图像处理」等场景增加了强力补充,比如:



React Native Skia Video 模块,实现了原生纹理(iOS Metal, Android OpenGL)到 React Native Skia 的直接传输,优化了内存和渲染速度,可以被用于视频帧提取、集成和导出等,生态中还有 React Native Vision Camera 和 React Native Video (v7) 等支持 Skia 的模块:



还有是 React Native 开始引入 WebGPU 支持,其效果将确保与 Web 端的 WebGPU API 完全一致,允许开发者直接复制代码示例的同时,实现与 Web Canvas API 对称的 RN Canvas API



最后,WebGPU 的引入还可以让 React Native 开发者能够利用 ThreeJS 生态,直接引入已有的 3D 库,这让 React Native 的能力进一步对齐了 Web :



最后,RN 也是有华为推进的鸿蒙适配,会采用 XComponent 对接到 ArkUI 的后端接口进行渲染,详细可见:鸿蒙版 React Native 正式开源


而在 PC 领域 RN 也有一定支持,比如微软提供的 windows 和 macOS 支持,社区提供的 web 和 Linux 支持,只是占有并不高,一般忽略。


而在小程序领域,有京东的 Taro 这样的大厂开源支持,整体在平台兼容上还算不错。



当然,RN 最大的优势还在于成熟的 code-push 热更新支持。



那么使用 RN 有什么局限性呢?最直观的肯定是平台 UI 的一致性和样式约束,这个是 OEM 框架的场景局限,而对于其他的,目前存在:



  • 第三方库在新旧框架支持上的风险

  • RN 版本升级风险,这个相信大家深有体会

  • 平台 API 兼容复杂度较高

  • 0.77 之后才支持 Google Play 的 16 KB 要求

  • 可用性集中在 Android 和 iOS ,鸿蒙适配和维度成本更高

  • 小程序能力支持和客户端存在一定割裂

  • ····


事实上, RN 是 Cordova 之后我接触的第一个真正意义上的跨平台框架,从我知道它到现在应该有十年了,那么你会因为它的新架构和 WebGPU 能力而选择 RN 么?


更多可见:



Compose Multiplatform


Compose Multiplatform(CMP) 近期的热度应该来自 Compose Multiplatform iOS 稳定版发布 ,作为第二个使用 Skia 的自绘框架,除了 Web 还在推进之外, CMP 基本完成了它的跨平台稳定之路。




Compose Multiplatform(CMP) 是 UI,Kotlin Multiplatform (KMP) 是语言基础。



CMP 使用 Skia 绘制 UI ,甚至在 Android 上它和传统 View 体系的 UI 也不在一个渲染树,并且 CMP 通过 Skiko (Skia for Kotlin) 这套 Kotlin 绑定库,进而抹平了不同架构(Kotlin Native,Kotlin JVM ,Kotlin JS,Kotlin wasm)调用 skia 的差异。


所以 CMP 的优势也来自于此,它可以通过 skia 做到不同平台的 UI 一致性,并且在 Android 依赖于系统 skia ,所以它的 apk 体积也相对较小,而在 PC 平台得益于 JVM 的成熟度,CMP 目前也做到了一定的可用程度。


其中和 Android JVM 模式不同的是,Kotlin 在 iOS 平台使用的是 Kotlin/Native ,Kotlin/Native 是 KMP 在 iOS 支持的关键能力,它负责将 Kotlin 代码直接编译为目标平台的机器码或 LLVM 中间表示 (IR),最终为 iOS 生成一个标准 .framework ,这也是为什么 Compose iOS 能实现接近原生的性能。



实现鸿蒙支持目前主流方式也是 Kotlin/Native ,不得不说 Kotlin 最强大的核心价值不是它的语法糖,而是它的编译器,当然也有使用 Kotlin/JS 适配鸿蒙的方案。



所以 CMP 最大的优势其实是 Kotlin ,Kotlin 的编译器很强大,支持各种编译过程和产物,可以让 KMP 能够灵活适配到各种平台,并且 Kotlin 语法的优势也让使用它的开发者忠诚度很高。


不过遗憾的是,目前 CMP 鸿蒙平台的适配上都不是 Jetbrains 提供的方案,华为暂时也没有 CMP 的适配计划,目前已知的 CMP/KMP 适配基本是大厂自己倒腾的方案,有基于 KN 的 llvm 方案,也有基于 Kotlin/JS 的低成本方案,只是大家的路线也各不相同。



在小程序领域同样如此。



另外现在 CMP 开发模式下的 hot reload 已经可以使用 ,不过暂时只支持 desktop,原理大概是只支持 jvm 模式。


而在社区上,klibs.io 的发布也补全了 Compose Multiplatform 在跨平台最后一步,这也是 Compose iOS 能正式发布的另外一个原因:



那么聊到这里,CMP 面临的局限性也很明显:



  • 鸿蒙适配成本略高,没有官方支持,低成本可能会选择 Kotlin/JS,为了性能的高成本可能会考虑 KN,但是 KN 在 iOS 和鸿蒙的 llvm 版本同步适配也是一个需要衡量的成本

  • 小程序领域需要第三方支持

  • iOS 平台可能面临的着色器等问题暂无方案,也许未来等待 Skia 的 Graphite 后端

  • 在 Android JVM 模式和 iOS 的 KN 模式下,第三方包适配的难度略高

  • hotload 暂时只支持 PC

  • 桌面内存占用问题-

  • 没有官方热更新条件

  • kjs、kn、kjvm、kwasm 之间的第三方包兼容支持问题

  • ····


相信 2025 年开始,CMP 会是 Android 原生开发者在跨平台的首选之一,毕竟 Kotlin 生态不需要额外学习 Dart 或者 JS 体系,那么你会选择 CMP 吗?


Kuikly


Kuikly 其实也算是 KMP 体系的跨平台框架,只是腾讯在做它的时候还没 CMP ,所以一开始 Kuikly 是通过 KMM 进行实现,而后在 UI 层通过自己的方案完成跨平台



这其实就是 Kuikly 和 CMP 最大的不同,底层都是 KMP 方案,但是在绘制上 Kuikly 采用的是类 RN 的方式,目前 Kuikly 主要是在 KMP 的基础上实现的自研 DSL 来构建 UI ,比如 iOS 平台的 UI 能力就是 UIkit ,而大家更熟悉的 Compose 支持,目前还处于开发过程中:




SwiftUI 和 Compose 无法直接和 Kuikly 一起使用,但是 Kuikly 可以在 DSL 语法和 UI 组件属性对齐两者的写法,变成一个类 Compose 和 SwiftUI 的 UI 框架,也就是 Compose DSL 大概就是让 Kuikly 更像 Compose ,而不是直接适配 Compose



那么,Kuikly 和 RN 之间又什么区别?


第一,Kuikly 支持 Kotlin/JS 和 Kotlin/Native 两种模式,也就是它可以支持性能很高的 Native 模式


第二,Kuikly 实现了自己的一套「薄原生层」,Kuikly 使用“非常薄”的原生层,该原生层只暴露最基本和无逻辑的 UI 组件(原子组件),也就是 Kuikly 在 UI 上只用了最基本的原生层 UI ,真正的 UI 逻辑主要在共享的 Kotlin 代码来实现:



通过将 UI 逻辑抽象到共享的 Kotlin 层,减少平台特定 UI 差异或行为差异的可能性,「薄原生层」充当一致的渲染目标,确保 Kotlin 定义的 UI 元素在所有平台上都以类似的方式显示。




也就是说,Kuikly 虽然会依赖原生平台的控件,但是大部分控件的实现都已经被「提升」到 Kuikly 自己的 Kotlin 共享层,目前 Kuikly 实现了 60% UI 组件的纯 Kotlin 组合封装实现,不需要 Native 提供原子控件



另外 Kuikly 表示后续会支持全平台小程序,这也是优势之一。



最后,Kuikly 还在动态化热更新场景, 可以和自己腾讯的热更新管理平台无缝集成,这也是优势之一。


那么 Kuikly 存在什么局限性?首先就是动态化场景只支持 Kotlin/JS,而可动态化类型部分:



  • 不可直接依赖平台能力

  • 不可使用多线程和协程

  • 不可依赖内置部分


其他的还有:



  • UI 不是 CMP ,使用的是类 RN 方式,所谓需要稍微额外理解成本

  • 不支持 PC 平台

  • 基于原生 OEM,虽然有原子控件,但是还是存在部分不一致情况

  • 在原有 App 集成 Kuikly ,只能把它简单当作如系统 webview 的概念来使用



另外,腾讯还有另外一个基于 CMP 切适配鸿蒙的跨平台框架,只是何时开源还尚不明确



那么,你会为了小程序和鸿蒙而选择 Kuikly 吗?


更多可见:腾讯 Kuikly 正式开源


Lynx


如果说 Kuikly 是一个面向客户端的全平台框架,那么 Lynx 就是一个完全面向 Web 前端的跨平台全家桶



目前 Lynx 开源的首个支持框架就是基于 React 的 ReactLynx,当然官方也表示Lynx 并不局限于 React,所以不排除后续还有 VueLynx 等其他框架支持,而 Lynx 作为核心引擎支持,其实并不绑定任何特定前端框架,只是当前你能用的暂时只有 ReactLynx :



而在实现上,源代码中的标签,会在运行时被 Lynx 引擎解析,翻译成用于渲染的 Element,嵌套的 Element 会组成的一棵树,从而构建出UI界面:



所以从这里看,初步开源的 Lynx 是一个类 RN 框架,不过从官方的介绍“选择在移动和桌面端达到像素级一致的自渲染” ,可以看出来宣传中可以切换到自渲染,虽然暂时还没看到。


而对于 Lynx 主要的技术特点在于:



  • 「双线程架构」,思路类似 react-native-reanimated ,JavaScript 代码会在「主线程」和「后台线程」两个线程上同时运行,并且两个线程使用了不同的 JavaScript 引擎作为其运行时:

  • 另外特点就是 PrimJS ,一个基于 QuickJS 深度定制和优化的 JavaScript 引擎,主要有模板解释器(利用栈缓存和寄存器优化)、与 Lynx 对象模型高效集成的对象模型(减少数据通信开销)、垃圾回收机制(非 QuickJS 的引用计数 RC,以提升性能和内存分析能力)、完整实现了 Chrome DevTools Protocol (CDP) 以支持 Chrome 调试器等

  • “Embedder API” 支持直接与原生 API 交互 ,提供多平台支持


所以从 Lynx 的宏观目标来看,它即支持类 RN 实现,又有自绘计划,同时除了 React 模式,后期还适配 Vue、Svelte 等框架,可以说是完全针对 Web 开发而存在的跨平台架构。



另外支持平台也足够,Android、iOS、鸿蒙、Web、PC、小程序都在支持列表里。



最后,Lynx 对“即时首帧渲染 (IFR)”和“丝滑流畅”交互体验有先天优势,开发双线程模型及主线程脚本 (MTS) 让 Lynx 的启动和第一帧渲染速度还挺不错,比如:



  • Lynx 主线程负责处理直接处理屏幕像素渲染的任务,包括:执行主线程脚本、处理布局和渲染图形等等,比如负责渲染初始界面和应用后续的 UI 更新,让用户能尽快看到第一屏内容

  • Lynx 的后台线程会运行完整的 React 运行时,处理的任务不直接影响屏幕像素的显示,包括在后台运行的脚本和任务(生命周期和其他副作用),它们与主线程分开运行,这样可以让主线程专注于处理用户交互和渲染,从而提升整体性能


而在多平台上,Lynx 是自主开发的渲染后端支持 Windows、tvOS、MacOS 和 HarmonyOS ,但是不确实是否支持 Linux:



那 Lynx 有什么局限性?首先肯定是它非常年轻,虽然它的饼很大,但是对应社区、生态系统、第三方库等都还需要时间成长。



所以官方也建议 Lynx 最初可能更适合作为模块嵌入到现有的原生应用中,用于构建特定视图或功能,而非从零开始构建一个完整的独立应用



其次就是对 Web 前端开发友好,对客户端而言学习成本较高,并且按照目前的开源情况,除了 Android、iOS 和 Web 的类 RN 实现外,其他平台的支持和自绘能力尚不明确:


=


最后,Lynx 的开发环境最好选 macOS,关于 Windows 和 Linux 平台目前工具链兼容性还需要打磨


那么,总结下来,Lynx 应该会是前端开发的菜,那你觉得 Lynx 是你的选择么?


更多可见:字节跨平台框架 Lynx 开源


uni-app x


说到 uni-app 大家第一印象肯定还是小程序,而虽然 uni-app 也可以打包客户端 app,甚至有基于 weex 的 nvue 支持,但是其效果只能说是“一言难尽”,而这里要聊的 uni-app x ,其实就是 DCloud 在跨平台这两年的新尝试。


具体来说,就是 uni-app 不再是运行在 jscore 的跨平台框架,它是“基于 Web 技术栈开发,运行时编译为原生代码”的模式,相信这种模式大家应该也不陌生了,简单说就是:js(uts) 代码在打包时会直接编译成原生代码:


目标平台uts 编译后的原生语言
AndroidKotlin
iOSSwift
鸿蒙ArkTS
Web / 小程序JavaScript

甚至极端一点说,uni-app x 可以不需要单独写插件去调用平台 API,你可以直接在 uts 代码里引用平台原生 API ,因为你的代码本质上也是会被编译成原生代码,所以 uts ≈ native code ,只是使用时需要配置上对应的条件编译(如 APP-ANDROIDAPP-IOS )支持:


import Context from "android.content.Context";
import BatteryManager from "android.os.BatteryManager";

import { GetBatteryInfo, GetBatteryInfoOptions, GetBatteryInfoSuccess, GetBatteryInfoResult, GetBatteryInfoSync } from '../interface.uts'
import IntentFilter from 'android.content.IntentFilter';
import Intent from 'android.content.Intent';

import { GetBatteryInfoFailImpl } from '../unierror';

/**
* 获取电量
*/

export const getBatteryInfo : GetBatteryInfo = function (options : GetBatteryInfoOptions) {
 const context = UTSAndroid.getAppContext();
 if (context != null) {
   const manager = context.getSystemService(
     Context.BATTERY_SERVICE
  ) as BatteryManager;
   const level = manager.getIntProperty(
     BatteryManager.BATTERY_PROPERTY_CAPACITY
  );

   let ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
   let batteryStatus = context.registerReceiver(null, ifilter);
   let status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
   let isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;

   const res : GetBatteryInfoSuccess = {
     errMsg: 'getBatteryInfo:ok',
     level,
     isCharging: isCharging
  }
   options.success?.(res)
   options.complete?.(res)
} else {
   let res = new GetBatteryInfoFailImpl(1001);
   options.fail?.(res)
   options.complete?.(res)
}
}



比如上方代码,通过 import BatteryManager from "android.os.BatteryManager" 可以直接导入使用 Android 的 BatteryManager 对象。


可以看到,在 uni-app x 你是可以“代码混写”的,所以与传统的 uni-app 不同,uni-app 依赖于定制 TypeScript 的 uts 和 uvue 编译器:



  • uts 和 ts 有相同的语法规范,并支持绝大部分 ES6 API ,在编译时会把内置的如ArrayDateJSONMapMathString 等内置对象转为 Kotlin、Swift、ArkTS 的对象等,所以也不需要有 uts 之类的虚拟机,另外 uts 编译器在处理特定平台时,还会调用相应平台的原生编译器,例如 Kotlin 编译器和 Swift 编译器

  • uvue 编译器基于 Vite 构建,并对它进行了扩展,大部分特性(如条件编译)和配置项(如环境变量)与 uni-app 的 Vue3 编译器保持一致,并且支持 less、sass、ccss 等 CSS 预处理器,例如 uvue 的核心会将开发者使用 Vue 语法和 CSS 编写的页面,编译并渲染为 ArkUI


而在 UI 上,目前除了编译为 ArkUI 之外,Android 和 iOS 其实都是编译成原生体系,目前看在 Android 应该是编译为传统 View 体系而不是 Compose ,而在 iOS 应该也是 UIKit ,按照官方的说法,就是性能和原生相当


所以从这点看,uni-app x 是一个类 RN 的编译时框架,所以,它的局限性问题也很明显,因为它的优势在于编译器转译得到原生性能,但是它的劣势也是在于转译



  • 不同平台翻译成本较高,并不支持完整的语言,阉割是必须的,API 必然需要为了转译器而做删减,翻译后的细节对齐于优化会是最大的挑战

  • iOS 平台还有一些骚操作,保留了可选 js 老模式和新 swift 模式,核心是因为插件生态,官方表示 js 模式可以大幅降低插件生态的建设难度, 插件作者只需要特殊适配 Android 版本,在iOS和Web端仍使用 ts/js 库,可以快速把 uni-app/web 的生态迁移到 uni-app x

  • 生态支持割裂,uni-app 和 uni-app x 插件并不通用

  • 不支持 PC

  • HBuilderX IDE

  • ·····


那么,你觉得 uni-app x 会是你跨平台选择之一么?


更多可见:uni-app x 正式支持鸿蒙


最后


最后,我们简单做个总结:


框架 (Framework)开发语言渲染方式特点缺点支持平台维护企业
FlutterDart自绘,Impeller自绘,多平台统一,未来支持 dart 和平台语言直接交互,Impeller 提供竞争力,甚至支持游戏场景占用内存大,文本场景略弱,Impeller 还需要继续打磨android、iOS、Web、Windows、macOS、Linux、鸿蒙(华为社区提供)Google
React NativeJS 体系原生 OEM + Skia/WebGPU 支持新架构提供性能优化,对齐 Web,引入 skia 和 webGPU 补充,code-push 热更新UI 一致性和新旧架构的第三方支持android、iOS、鸿蒙(华为社区提供),额外京东 Taro 支持小程序,web、windows、macOS、Linux 第三方支持Facebook
Compose MultiplatformKotlin体系Skia 自绘Kotlin 体系,skia 自绘,多平台统一,支持 kn、kjs、kwasm 、kjvm 多种模式KN  JVM、JS、Wasm 生态需要整合,没有着色器预编方案android、iOS、Web、Windows、macOS、LinuxJetbrains
KuiklyKotlin体系原生 OEM ,「薄原生层」基于 KMP 的类 RN 方案,在动态化有优势小部分 UI 一致性场景,UI 与 CMP 脱轨android、iOS、Web、鸿蒙、小程序腾讯
LynxJS 体系原生 OEM,未来也有自绘对齐 Web 开发首选,秒开优化,规划丰富非常早期 ,生态发展中,客户端不友好android、iOS、Web、Windows、macOS、鸿蒙、小程序字节
uni-app xuts原生 OEM,直接翻译为原生语言支持混写 uts 和原生代码,直接翻译为原生生态插件割裂,UI 一致性问题,翻译 API 长期兼容成本android、iOS、Web、鸿蒙、小程序DCloud

什么,你居然看完了?事实上我写完都懒得查错别字了,因为真的太长了。


作者:恋猫de小郭
来源:juejin.cn/post/7505578411492474915
收起阅读 »

前端何时能出个"秦始皇"一统天下?我是真学不动啦!

web
前端何时能出个"秦始皇"一统天下?我是真学不动啦! 引言 前端开发的世界,就像历史上的战国时期一样,各种框架、库、工具层出不穷,形成了一个百花齐放但也令人眼花缭乱的局面。 而且就因为百家争鸣,导致各种鄙视链出现 比如 React 和 Vue 互喷 v:你re...
继续阅读 »

前端何时能出个"秦始皇"一统天下?我是真学不动啦!


引言


前端开发的世界,就像历史上的战国时期一样,各种框架、库、工具层出不穷,形成了一个百花齐放但也令人眼花缭乱的局面。




而且就因为百家争鸣,导致各种鄙视链出现


比如 React 和 Vue 互喷


v:你react 这么难用,不如我vue 简单


r:你一点都不灵活,我想咋用咋用


v:你useEffect 心智负担太重,一点都好用


r:啥心智负担,那是你太笨了,我就喜欢这种什么都掌握在自己手里的感觉


v:你内部更是混乱,一个状态管理就那么多种 啥redux、mobx、recoil。。。。不像我们一个pinia 走天下


r:你管我 我想用哪个用哪个,你还说我,你内部对一个 用ref还是用reactive 都吵得不可开交!


......


2.jpeg


1. 框架之争



  • React: 由Facebook维护的一个用于构建用户界面的JavaScript库。其设计理念是通过组件化的方式简化复杂的UI开发。




  • Vue.js: 一种渐进式JavaScript框架,非常适合用来构建单页应用。Vue的核心库只关注视图层,易于上手。




  • Angular: Google支持的一个开源Web应用框架,适用于大型企业级项目。它提供了一个全面的解决方案来创建动态Web应用程序。




  • Solid.js: 一个专注于性能和简单性的声明式UI库,采用细粒度的响应式系统,提供了极高的运行效率。




  • Svelte: 一种新兴的前端框架,通过在编译时将组件转换为高效的原生代码,从而避免了运行时开销。




  • Ember.js: 一个旨在帮助开发者构建可扩展的Web应用的框架,尤其适合大型团队协作。







2. 样式处理满花齐放


样式处理方面可以进一步细分,包括CSS预处理器、CSS-in-JS、Utility-First CSS框架以及CSS Modules等。



  • CSS预处理器



    • Sass: 提供变量、嵌套规则等高级功能,极大地提高了CSS代码的可维护性。


    • Less: 另一种流行的CSS预处理器,支持类似的功能但语法稍有不同。


    • Stylus: 一款灵活且功能强大的CSS预处理器,允许省略括号和分号等符号,使代码更加简洁。




  • CSS-in-JS




  • 原子化css



    • Tailwind CSS: 一种实用优先的CSS框架,让你可以通过低级实用程序类构建定制设计。




    • UnoCSS: 新一代的原子化CSS引擎,旨在提供极致的性能和灵活性。




    • Windi CSS: 一个基于Tailwind CSS的即时按需CSS框架,提供了更快的开发体验。




    • GitHub Stars: 约6.5k(截至2025年4月)






3. 构建工具五花八门


构建工具是现代前端开发不可或缺的一部分,它们负责将源代码转换为生产环境可用的形式,并优化性能。



  • Webpack: 一个模块打包工具,广泛用于复杂的前端项目中。它支持多种文件类型的处理,并具有强大的插件生态。




  • Vite: 由Vue.js作者尤雨溪开发的下一代前端构建工具,以其极快的冷启动速度和热更新闻名。




  • Rollup: 一个专注于JavaScript库的打包工具,特别适合构建小型库或框架。




  • Rspack: 一个基于Rust实现的高性能构建工具,兼容Webpack配置,旨在提供更快的构建速度。




  • esbuild: 一个用Go语言编写的极速打包工具,专为现代JavaScript项目设计。




  • Turbopack: 由Next.js团队推出的下一代构建工具,号称比Webpack快700倍。




  • Rolldown: 一个基于Rust的Rollup替代方案,旨在提供更快的构建速度和更高的性能。





对比分析:



  • Webpack 是目前最成熟的构建工具,生态系统庞大,但配置复杂度较高。

  • Vite 凭借其快速的开发体验迅速崛起,尤其在中小型项目中表现优异。

  • Rollup 更适合轻量级项目或库的构建,虽然社区规模较小,但在特定场景下非常高效。

  • Rspackesbuild 利用高性能语言(如Rust和Go)实现了极快的构建速度,适合对性能要求较高的项目。

  • Turbopack 是新兴工具,主打极速构建,未来可能成为Webpack的有力竞争者。

  • Rolldown 提供了另一种基于Rust的高速构建解决方案,特别针对Rollup用户群体。




4. 包管理工具逐步更新





5. 状态管理百家争鸣


状态管理是前端开发中的重要组成部分,它帮助开发者有效地管理应用的状态变化。





6. JavaScript运行时环境都有好几种


JavaScript运行时环境是现代前端和后端开发的核心部分,它决定了代码如何被解析和执行。以下是几种主流的JavaScript运行时环境:



  • Node.js:



    • Node.js 是一个基于Chrome V8引擎的JavaScript运行时,广泛用于构建服务器端应用、命令行工具以及全栈开发。

    • 它拥有庞大的生态系统,npm作为其默认包管理器,已经成为全球最大的软件注册表。

    • 官网: nodejs.org/

    • GitHub: github.com/nodejs/node

    • GitHub Stars: 约111k(截至2025年4月)



  • Deno:



    • Deno 是由Node.js的原作者Ryan Dahl创建的一个现代化JavaScript/TypeScript运行时,旨在解决Node.js的一些设计缺陷。

    • 它内置了对TypeScript的支持,并提供了更安全的权限模型(如文件系统访问需要显式授权)。

    • Deno还集成了标准库,无需依赖第三方模块即可完成许多常见任务。

    • 官网: deno.land/

    • GitHub: github.com/denoland/de…

    • GitHub Stars: 约103k(截至2025年4月)



  • Bun:



    • Bun 是一个新兴的JavaScript运行时,旨在提供更快的性能和更高效的开发体验。

    • 它不仅可以用作运行时环境,还可以替代npm、Yarn等包管理工具,同时支持ES Modules和CommonJS。

    • Bun的目标是成为Node.js和Deno的强大竞争者,特别适合高性能需求的场景。

    • 官网: bun.sh/

    • GitHub: github.com/oven-sh/bun

    • GitHub Stars: 约77.5k(截至2025年4月)




对比分析:



  • Node.js 是目前最成熟且广泛应用的JavaScript运行时,尤其在企业级项目中占据主导地位。

  • Deno 提供了更现代化的设计理念,特别是在安全性、TypeScript支持和内置工具方面表现突出。

  • Bun 是一个新兴的选手,凭借其极速的性能和多功能性迅速吸引了开发者关注,未来潜力巨大。




7. 跨平台开发


随着移动设备和多终端生态的普及,跨平台开发成为现代应用开发的重要方向。以下是几种主流的跨平台开发工具和技术:



  • React Native:



    • React Native 是由Facebook推出的一个基于React的跨平台移动应用开发框架,允许开发者使用JavaScript和React构建原生性能的iOS和Android应用。

    • 它提供了丰富的社区支持和插件生态,适合需要快速迭代的项目。

    • 官网: reactnative.dev/

    • GitHub: github.com/facebook/re…

    • GitHub Stars: 约122k(截至2025年4月)



  • Flutter:



    • Flutter 是由Google开发的一个开源UI框架,使用Dart语言构建高性能的跨平台应用。

    • 它通过自绘引擎渲染UI,提供了一致的用户体验,并支持Web、iOS、Android以及桌面端开发。

    • 官网: flutter.dev/

    • GitHub: github.com/flutter/flu…

    • GitHub Stars: 约170k(截至2025年4月)



  • Electron:



    • Electron 是一个用于构建跨平台桌面应用的框架,基于Node.js和Chromium,广泛应用于桌面端应用开发。

    • 它允许开发者使用Web技术(HTML、CSS、JavaScript)构建功能强大的桌面应用,但可能会导致较大的应用体积。

    • 官网: http://www.electronjs.org/

    • GitHub: github.com/electron/el…

    • GitHub Stars: 约116k(截至2025年4月)



  • Tauri:



    • Tauri 是一个轻量级的跨平台桌面应用框架,旨在替代Electron,提供更小的应用体积和更高的安全性。

    • 它利用系统的原生Webview来渲染UI,同时支持Rust作为后端语言,从而实现更高的性能。

    • 官网: tauri.app/

    • GitHub: github.com/tauri-apps/…

    • GitHub Stars: 约91.5k(截至2025年4月)



  • Capacitor:



    • Capacitor 是由Ionic团队推出的一个跨平台工具,允许开发者将Web应用封装为原生应用。

    • 它支持iOS、Android和Web,并提供了丰富的插件生态,方便调用原生设备功能。

    • 官网: capacitorjs.com/

    • GitHub: github.com/ionic-team/…

    • GitHub Stars: 约13.1k(截至2025年4月)



  • UniApp:



    • UniApp 是一个基于 Vue.js 的跨平台开发框架,能够将代码编译到多个平台,包括微信小程序、H5、iOS、Android以及其他小程序(如支付宝小程序、百度小程序等)。

    • 它的优势在于一次编写,多端运行,特别适合需要覆盖多个小程序平台的项目。

    • 官网: uniapp.dcloud.io/

    • GitHub: github.com/dcloudio/un…

    • GitHub Stars: 约40.6k(截至2025年4月)




对比分析:



  • React NativeFlutter 是移动端跨平台开发的两大主流选择,分别适合熟悉JavaScript和Dart的开发者。

  • Electron 是桌面端跨平台开发的经典解决方案,虽然体积较大,但易于上手。

  • Tauri 提供了更轻量化的桌面端开发方案,适合对性能和安全性有更高要求的项目。

  • Capacitor 则是一个灵活的工具,特别适合将现有的Web应用快速迁移到移动端。

  • UniApp 非常适合需要覆盖多种小程序平台的项目,尤其在国内的小程序生态中表现出色。




结论


你看我这还是只是列举了一部分,都这么多了,学前端的是真的命苦啊,真心学不动了。


1.jpeg


而且最近 尤雨溪宣布成立 VoidZero 说是一代JavaScript工具链,能够统一前端 开发构建工具,如果真能做到,真是一件令人振奋的事情,希望尤雨溪能做到跟 spring 一样统一java 天下 把前端的天下给统一了,大家觉得有可能么?


作者:前端摸鱼杭小哥
来源:juejin.cn/post/7493420166878822450
收起阅读 »

脱裤子放屁 - 你们讨厌这样的页面吗?

web
前言 平时在逛掘金和少数派等网站的时候,经常有跳转外链的场景,此时基本都会被中转到一个官方提供的提示页面。 掘金: 知乎: 少数派: 这种官方脱裤子放屁的行为实在令人恼火。是、是、是、我当然知道这么做有很多冠冕堂皇的理由,比如: 防止钓鱼攻击 增强用户...
继续阅读 »

前言


平时在逛掘金和少数派等网站的时候,经常有跳转外链的场景,此时基本都会被中转到一个官方提供的提示页面。


掘金:


site-juejin.png


知乎:


site-zhihu.png


少数派:


site-sspai.png


这种官方脱裤子放屁的行为实在令人恼火。是、是、是、我当然知道这么做有很多冠冕堂皇的理由,比如:



  • 防止钓鱼攻击

  • 增强用户意识

  • 品牌保护

  • 遵守法律法规

  • 控制流量去向


(以上5点是 AI 告诉我的理由)


但是作为混迹多年的互联网用户,什么链接可以点,什么最好不要点(悄悄的点) 我还是具备判断能力的。


互联网的本质就是自由穿梭,一个 A 标签就可以让你在整个互联网翱翔,现在你每次起飞的时候都被摁住强迫你阅读一次免责声明,多少是有点恼火的。


解决方案


这些中转站的实现逻辑基本都是将目标地址挂在中转地址的target 参数后面,在中转站做免责声明,然后点击继续跳转才跳到目标网站。


掘金:


https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fcn%2Fdesign%2Fhuman-interface-guidelines%2Fapp-icons%23macOS/


少数派:
https://sspai.com/link?target=https%3A%2F%2Fgeoguess.games%2F


知乎:
https://link.zhihu.com/?target=https%3A//asciidoctor.org/


所以我们就可以写一个浏览器插件,在这些网站中,找出命中外链的 A 标签,替换掉它的 href 属性(只保留 target 后面的真实目标地址)。


核心函数:



function findByTarget() {
if (!hostnames.includes(location.hostname)) return;
const linkKeyword = "?target=";
const aLinks = document.querySelectorAll(
`a[href*="${linkKeyword}"]:not([data-redirect-skipper])`
);
if (!aLinks) return;
aLinks.forEach((a) => {
const href = a.href;
const targetIndex = href.indexOf(linkKeyword);
if (targetIndex !== -1) {
const newHref = href.substring(targetIndex + linkKeyword.length);
a.href = decodeURIComponent(newHref);
a.setAttribute("data-redirect-skipper", "true");
}
});
}

为此我创建了一个项目仓库 redirect-skipper ,并且将该浏览器插件发布在谷歌商店了 安装地址


安装并启用这个浏览器插件之后,在这些网站中点击外链就不会看到中转页面了,而是直接跳转到目标网站。


因为我目前明确需要修改的就是这几个网站,如果大家愿意使用这个插件,且有其他网站需要添加到替换列表的,可以给 redirect-skipper 仓库 提PR。


如果需要添加的网站的转换规则是和 findByTarget 一致的,那么仅需更新 sites.json 文件即可。


如果需要添加的网站的转换规则是独立的,那么需要更新插件代码,合并之后,由我向谷歌商店发起更新。


为了后期可以灵活更新配置(谷歌商店审核太慢了),我默认将插件应用于所有网站,然后在代码里通过 hostname 来判断是否真的需要执行。


{
"$schema": "https://json.schemastore.org/chrome-manifest.json",
"name": "redirect-skipper",
"manifest_version": 3,
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["./scripts/redirect-skipper.js"],
"run_at": "document_end"
}
],
}

在当前仓库里维护一份 sites.json 的配置表,格式如下:


{
"description": "远程配置可以开启 Redirect-Skipper 插件的网站 (因为谷歌商店审核太慢了,否则无需通过远程配置,增加复杂性)",
"sites": [
{
"hostname": "juejin.cn",
"title": "掘金"
},
{
"hostname": "sspai.com",
"title": "少数派"
},
{
"hostname": "www.zhihu.com",
"title": "知乎"
}
]
}

这样插件在拉取到这份数据的时候,就可以根据这边描述的网站配置,决定是否执行具体代码。


插件完整代码:


function replaceALinks() {
findByTarget();
}

function observerDocument() {
const mb = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
if (mutation.addedNodes.length) {
replaceALinks();
}
}
}
});
mb.observe(document, { childList: true, subtree: true });
}

// 监听路由等事件
["hashchange", "popstate", "load"].forEach((event) => {
window.addEventListener(event, async () => {
replaceALinks();
if (event === "load") {
observerDocument();
await updateHostnames();
replaceALinks(); // 更新完数据后再执行一次
}
});
});

let hostnames = ["juejin.cn", "sspai.com", "www.zhihu.com"];

function updateHostnames() {
return fetch(
"https://raw.githubusercontent.com/dogodo-cc/redirect-skipper/master/sites.json"
)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok");
})
.then((data) => {
// 如果拉到了远程数据,就用远程的
hostnames = data.sites.map((site) => {
return site.hostname;
});
})
.catch((error) => {
console.error(error);
});
}

// 符合 '?target=' 格式的链接
// https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fcn%2Fdesign%2Fhuman-interface-guidelines%2Fapp-icons%23macOS/
// https://sspai.com/link?target=https%3A%2F%2Fgeoguess.games%2F
// https://link.zhihu.com/?target=https%3A//asciidoctor.org/

function findByTarget() {
if (!hostnames.includes(location.hostname)) return;
const linkKeyword = "?target=";
const aLinks = document.querySelectorAll(
`a[href*="${linkKeyword}"]:not([data-redirect-skipper])`
);
if (!aLinks) return;
aLinks.forEach((a) => {
const href = a.href;
const targetIndex = href.indexOf(linkKeyword);
if (targetIndex !== -1) {
const newHref = href.substring(targetIndex + linkKeyword.length);
a.href = decodeURIComponent(newHref);
a.setAttribute("data-redirect-skipper", "true");
}
});
}


更详细的流程可以查看 redirect-skipper 仓库地址


标题历史



  • 浏览器插件之《跳过第三方链接的提示中转页》


作者:dogodo
来源:juejin.cn/post/7495977411273490447
收起阅读 »

理想正式官宣开源!杀疯了!

最近,新能源汽车制造商「理想汽车」面向业界搞了一个大动作,相信不少同学也看到了,那就是: 正式宣布开源「理想星环OS」操作系统,并且欢迎各位开发者参与验证开源组件的功能和性能。 作为一名开发者和理想车主,说实话第一眼看到这个信息时还是挺意外的,万万没想到,如今...
继续阅读 »

最近,新能源汽车制造商「理想汽车」面向业界搞了一个大动作,相信不少同学也看到了,那就是:


正式宣布开源「理想星环OS」操作系统,并且欢迎各位开发者参与验证开源组件的功能和性能。


作为一名开发者和理想车主,说实话第一眼看到这个信息时还是挺意外的,万万没想到,如今汽车制造商也开始玩开源这一套了。



「理想星环OS」是理想汽车历时3年所研发的汽车操作系统,在车辆中担任“中央指挥官”这一角色,向下管理车辆硬件,向上支撑应用软件。



具体来说,「理想星环OS」包含如下四个组成部分,用于高效调度全车资源并确保汽车功能稳定运行。



  • 辅助驾驶OS(大脑):用于处理复杂的思维过程,以确保辅助驾驶又快又好地工作。

  • 智能车控OS(小脑):用于控制车辆“肢体”,快速执行各种车辆基础控制命令。

  • 通信中间件(神经系统):负责车内各个模块(如刹车、屏幕、雷达)间的高效可靠通信。

  • 信息安全系统(免疫系统):负责数据加密保护以及身份认证和权限管控等信息安全的保障。


星环OS主要组成部分


早在今年3月份的时候,理想汽车CEO李想就曾在2025中关村论坛年会上宣布过,理想汽车自研整车操作系统 ——“理想星环OS”将会开源,而这一承诺终于在最近开始逐步向外兑现。



按照理想官方发布的开源计划时间轴来看,「理想星环OS」的开源将会分为三个阶段来逐步落地。



  • 第一阶段主要是开源星环OS 0.1.0版本,包含安全实时RTOS以及通信总线Lite。

  • 第二阶段开源星环OS 1.0.0版本,包含完整的智能车控系统以及智能驾驶系统基础能力,时间节点为今年的6月30号左右。

  • 第三阶段开源则将会包括完整的智能驾驶系统以及虚拟化引擎,时间节点定在了2025年的7月后。



并且理想承诺,星环OS将会采用宽松型的Apache License,既不会通过开源来收取费用,也不会干涉代码的使用方式,更不会控制使用者的数据。


按照官方的说法,第一阶段的开源目前已经正式兑现,代码已经托管于国内的Gitee平台之上。


出于好奇,我也特地去Gitee平台上搜了一下。


果然,理想汽车已经在Gitee平台上创建了一个名为「HaloOS」的开源组织,里面包含了一阶段开源相关的多个代码仓库和文档仓库。



具体来看,目前的开源代码主要是 智能车控OS(VCOS)通信总线lite(VBSlite) 两大部分,并且其开源仓库划分得非常细,文档是文档,代码是代码,配置是配置,示例是示例。



文档仓库我们以智能车控OS(VCOS)文档为例,其专门搞了一个文档仓库和详细文档说明,并且附有详细的快速跳转链接,大家可以感受一下这个文档仓库的组织风格,还是非常便于开发者使用的。



docs
├── OVERVIEW.md # 项目概述
├── README.md # 文档结构简介(即本文)
├── _static/image # 文档中用到的图片资源
├── api_reference # API参考文档
├── configuration_reference # 配置项参考文档
├── key_technical # 关键技术说明
├── porting # 芯片移植文档
├── quick_start # 快速入门指南
└── user_manual # 开发者手册与详细说明
├── components # 功能组件使用说明
├── kernel # 内核模块文档
└── studio # Studio集成开发环境相关文档

而代码仓库这一块,我们以通信总线lite(VBSlite)工程核心组件之一的MVBS代码仓库为例,仓库说明里给出了详细的代码架构组织,大家也可以感受一下。


mvbs
├── README.md # 这个是MVBS仓库的readme
├── build.mk # 用于构建的makefile文件
├── CMakeLists.txt # cmake编译脚本
├── posix_aux # 为linux和windows平台提供扩展支持库
├── include
│ ├── mcdr # 序列化接口
│ ├── mvbs # MVBS头文件集合
│ │ ├── adapter # 适配层头文件
│ │ ├── core # MVBS内部核心的实体定义和操作
│ │ ├── diag # 诊断相关的头文件
│ │ ├── rte # RTE接口文件
│ │ ├── rtps # RTPS协议元素定义文件
│ │ ├── sections # 用于支持内存layout
│ │ └── utils # 常用的工具文件
│ └── rpc # RPC头文件
└── src
├── adapter # 适配层实现
│ ├── auto # 基于VCOS 适配层的参考实现
│ └── posix # 基于POSIX提供的适配层实现
├── core
│ ├── diag # 诊断工具的实现
│ ├── discovery # 实体发现协议的实现
│ ├── entities # MVBS内部实体的实现
│ ├── include # 提供给MVBS内部的头文件
│ ├── messages # 报文组装的实现
│ ├── mvbs # MVBS内部接口层的实现
│ ├── netio # 网络接口的封装实现
│ ├── qos # E2E和WLP的实现
│ ├── storages # CacheChange和History的实现
│ ├── transport # Transport的实现
│ └── utils # 常用工具的实现
├── mcdr # 序列化库的实现
├── rpc # RPC的实现
└── rte # RTE接口的实现

再看一下具体代码,函数和代码组织都比较宽松,是我个人比较喜欢的风格,另外关键步骤或关键字段设有代码注释,阅读起来也便于理解。



并且仓库里还给出了非常详细的快速入门开发者手册,内容我看了一下,内容甚至从安装 git-repo 工具开始,确实给得非常详细。



追了其中几个比较核心的代码仓库后我们会发现,这几个核心项目源码都是基于C语言来实现的,这也再次说明了 C 语言在某些关键系统中不可撼动的核心地位



大家感兴趣的话也可以上去学习学习相关的代码,研究通了以后想进新能源智能车企做核心系统研发那不就是分分钟的事情了。



众所周知,这两年新能源智能汽车领域的竞争也进入到白热化阶段了,各家新能源车企都在不断地进行产品优化和技术摸高,这个趋势在未来很长一段时间内想必还会继续保持。


按照理想官方的说法,此次开源的主要目的是促进行业合作,旨在破解行业“重复造轮子”的困局,同时通过生态的共建来实现车企之间、车企与其他厂商之间的互利共赢,最终普惠到每个用户。


当然不管他们怎么去说,作为一名开发者我们都清晰地知道,开源的背后其实也是生态的建设和博弈,说实话这一步,理想在新能源车企阵营里走得还是非常超前的。


最近这两年,我自己一直都挺关注新能源汽车市场的,线下也试驾和体验过诸多品牌的新能源汽车产品,也切实感受到了这几年技术和产品的飞速迭代。希望国产智能新能源汽车能持续崛起,为用户带来更多技术普惠和感动人心的好产品。



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



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

待业的两个月,让我觉得独立开发者才是职场的归宿。

前言 今年2月中旬左右因为公司运营的问题,具体原因是缺少版权。毕竟现在的大环境有些刚性的产品,没有版权轻易入场即使可以赚钱,那么这个钱终于不是自己的。 在接到待业通知的时候,既不意外也不慌张。 不意外,是因为工作几年之后都是老江湖,公司能不能支棱起来都是很明...
继续阅读 »

前言


今年2月中旬左右因为公司运营的问题,具体原因是缺少版权。毕竟现在的大环境有些刚性的产品,没有版权轻易入场即使可以赚钱,那么这个钱终于不是自己的。


在接到待业通知的时候,既不意外也不慌张。



  • 不意外,是因为工作几年之后都是老江湖,公司能不能支棱起来都是很明显的状态。不论多大的公司,什么时候开始人力扛把子,抓考勤、搞制度,距离凉不远了

  • 不慌张,是因为从3年前就已经开始准备副业了。最近两年更是经常搞到凌晨1~2点才睡,接到待业通知的时候,被动收益已经是薪资的1.5~2倍的水平了。毕竟,给人打工终究不过是为人做嫁衣,寄人篱下罢了。


未雨绸缪


找方向


都知道短视频是红利,所以基本上短视频已经玩过了。某音现在还有1800多粉丝,头条、UC、百度家号,基本上累计粉丝也有几百个,也写出过几篇10w+的爆款文章


wechat_2025-04-21_170709_618.png


wechat_2025-04-21_170535_331.png


后来发现这个赛道并不好做,1是花费的时间和收入不成比例,2是竞品太多。想脱颖而出难上加难。


尝试过发现,还是得用自己的专业知识变现。1是轻车熟路,2是清楚获量和变现模式,相当于短视频的红海不如给自己打工写自己的App。


经常自我反思,做了那么多产品,应付了那么多需求,有多少是真的为了服务用户?有多少不过是为了应付KPI?有多少不过是满足产品经理的梦?,所以很早之前就开始研究自营产品。 第一年基本上是0收入,勉强把开发账号的688赚回来了。


后来认识了一些志同道合同僚,有想法、有技术,所以一拍即合!王侯将相宁有种乎?


对于一款成功的产品,无法满足三个核心词之一。高频、刚需,痛点,三者得一者得天下!


高频:常见的比如社交、游戏(大型网游、小型休闲)、影视、音频等。


刚需:常见的比如教育、办公打卡、宝宝起名等。


痛点:常见的比如电子衣橱、月子餐、自媒体工具


能落地


这里的能落地从两个方面阐述,1是从产品方向,2是从人的方向。如果是单枪匹马打天下,那大可不用考虑第二点。


产品维度


无论从什么角度切入独立开发者,都要想清楚产品给谁用?赚谁的钱?怎么赚?很多项目出师未捷身先死,大多数并没有找到自己的用户群体,更不用谈赚钱这件事了。


简单来说:准备好了再出发,而不是头脑发热想一出是一出。


人的维度


第一点:谋事在人成事在天,为什么要把人提出来?因为很多常见的合作都需要基于对项目的认可,如果不认可自己正在做点事情,那和打工的心态一样,混日子得过且过。这种很容易消极怠工,也容易半路腰斩


第二点:要有专业的技术能力,确保能为合作保驾护航。因为只有这样才能保障产品正常推进,保证项目平稳落地。


与君共勉


当品尝过自由的味道,大抵很难再适应被束缚的生活。 认清自己的方向和定位,找到适合方向和机遇,毕竟一辈子能够遇到的机遇屈指可数。毕竟现在的大环境,想打工都是一件奢侈的事情,更不要想提高福利待遇了,不过是痴人说梦罢了。


职场终究不过是赚钱赎身,换取早日恢复自由自身的过渡!那些在职场内卷的同行们,没必要,除非你的老板有胖东来的福利待遇。


遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!


作者:iOS研究院
来源:juejin.cn/post/7495570300123267084
收起阅读 »

一年一度开发者盛会,鲲鹏昇腾开发者大会2025即将开幕

5月23-24日,鲲鹏昇腾开发者大会2025(KADC2025)将在北京中关村国际创新中心举办。作为华为面向开发者一年一度的技术盛典,本次大会以“心怀挚爱,共绽光芒”为主题,旨在打造开放、共享、学习交流的平台,分享计算产业趋势、前沿技术、行业创新案例和优秀开发...
继续阅读 »

5月23-24日,鲲鹏昇腾开发者大会2025(KADC2025)将在北京中关村国际创新中心举办。作为华为面向开发者一年一度的技术盛典,本次大会以“心怀挚爱,共绽光芒”为主题,旨在打造开放、共享、学习交流的平台,分享计算产业趋势、前沿技术、行业创新案例和优秀开发者实践等,促进AI应用创新,加速行业数智化。

图片1.png

AI技术迅猛发展,行业智能化全面提速AI平台和工具的易用性、灵活性和扩展性是AI开发、部署和运营的重要基础。今年的大会,将传统大型现场发布会变为技术深度讲解和代码实操,从会前的鲲鹏昇腾创享周,到会中的动手体验,到会后的技术公开课,让广大开发者从认知、理解到完全掌握,学会使用鲲鹏、昇腾的工具和平台,加速AI的创新。

鲲鹏昇腾创享周(5月19-22日),4天12场直播开启线上狂欢

华为始终坚持打造开放的计算产业生态,深耕根技术,打造好用易用的开发工具和平台,加速创新者创新。5月19-22日将举办鲲鹏昇腾创享周,4天12场的线上直播,一批华为资深研发专家为开发者聚焦当前AI热点话题,讲解鲲鹏、昇腾的硬核技术,解密代码背后的故事,帮助开发者了解鲲鹏昇腾的技术,加速构建创新的产品和应用。

在通用计算领域,鲲鹏将带来话题,如AI Agent、RAG解决方案、如何通过CPU实现推理加速等。在AI计算领域,昇腾将带来话题,如超节点架构如何实现超低时延推理;如何提升训练和推理的效果,强化学习、推理加速;vLLM等主流生态支持昇腾;大规模专家并行、MLA融合算子和低时延通信优化算法等。

鲲鹏昇腾开发者大会(5月23-24日),相聚北京共话计算

鲲鹏昇腾开发者大会现场两天,将给予开发者现场实操,动手体验,以及与业界专家面对面交流的机会。

两天的大会现场包括2场峰会,40多场技术分论坛,2000多平米展区和开发者动手体验区。届时现场将带来最新的技术、新软件和工具,展示联合创新的成果和行业应用创新,设置Codelabs环节,以加深开发者对技术知识的理解,学会对工具、平台的使用。

京城五月,诚邀您参加鲲鹏昇腾开发者大会2025,共话计算,畅聊AI,心怀挚爱,共绽光芒。

鲲鹏社区:https://www.hikunpeng.com/kunpeng-2025KADC

昇腾社区:https://www.hiascend.com/ascend-2025KADC

收起阅读 »

驭电·新境 长安马自达MAZDA EZ-60全球首秀

4月23日,长安马自达MAZDA EZ-60(以下称EZ-60)在2025上海国际车展正式迎来全球首秀。作为长安马自达全面转型新能源时代的战略级之作,EZ-60以全面进化的「魂动」美学设计、超愉悦AI智能座舱、电感「人马一体」驾控、全球顶级的主被动安全性能,重...
继续阅读 »

4月23日,长安马自达MAZDA EZ-60(以下称EZ-60)在2025上海国际车展正式迎来全球首秀。作为长安马自达全面转型新能源时代的战略级之作,EZ-60以全面进化的「魂动」美学设计、超愉悦AI智能座舱、电感「人马一体」驾控、全球顶级的主被动安全性能,重新定义电动时代的智电豪华出行新体验。即日起,国内消费者可通过长安马自达官网、「悦马星空」官方APP/小程序,以及授权经销商预订新车,订金10元,每天膨胀,膨胀不封顶,直至新车交付,膨胀金直接抵扣车款。

[MAZDA EZ-60全球首发]

自宣布中国市场电动化战略以来,马自达汽车株式会社社长毛笼胜弘展现出对中国市场前所未有的高度关注。本次车展,毛笼胜弘再次莅临长安马自达展台并揭开了EZ-60全球首秀的面纱。他表示:“EZ-60是一款融合了汽车与身俱来的纯正魅力和为锐意进取的中国用户量身定制的先进技术的跨界SUV。是集马自达设计精髓的现代派优雅造型、悦享驾驶日常的「人马一体」驾控,以及无缝连接车内车外生活的智能座舱的‘集大成者’。我相信 EZ-60能以马自达独有的行驶性能、操控驾趣和智能科技,给中国用户带去极具品质感的出行体验”。

[马自达汽车株式会社社长 毛笼胜弘]

近未来,新魂动,9风道设计重塑中型SUV普适之美

「魂动」设计问世15年以来,始终以持续进化之姿引领全球汽车造型美学。EZ-60的外观设计延续了马自达「魂动」的设计理念,同时融入电动化时代的科技感与现代感。家族式DNA的前脸设计极具未来感和辨识度,双段式腰线和精工打造的曲面造型从车头贯穿C柱至车尾,极具速度感和张力感。

EZ-60在SUV设计领域首次搭载了9风道设计。第一,降低了风阻系数,整体风阻优化60.8 conuts,让续航里程提升41公里;其次,降低了行驶风噪,车内语音清晰度提升了8%;第三,在高速行驶时,风道可以为后轴提供最高705N的下压力,让操控更加灵敏,更加平稳。独特的D柱风道设计,形成了EZ-60独特的视觉符号,进一步强化了车身的动感姿态。全新的紫色车漆,初见惊艳,再见倾心。犹如星芒初绽的暮空,真实又梦幻,神秘又尊贵,将电动车型的未来感与新生优雅融汇于一身。

超愉悦AI智能座舱,全球首发裸眼3D HUD,全球首发联发科技MT8676 4nm芯片

座舱是用户与车辆情感连接的核心场景。在座舱设计上,EZ-60充分融入了科技感和悬浮感。首先,新车搭载了行业唯一超150英寸的6联科技大屏,大尺寸、多数量的屏幕为用户带来舒适、开阔、无死角的信息视野。其中,EZ-60首发同级最大、机身最薄、边框最窄的超画幅26.45英寸5K视网膜一体式大屏,它拥有同级最薄的9.5mm机身,同级最窄的6.4mm边框。灰态亮度小于0.5尼特,让画面更立体,观影沉浸感更强。32:9相当于两个16:9的画面,可以全面适配视频资源的播放要求。自适应调光技术,能根据环境光动态调节色温与亮度,做到烈日下不反光,隧道内不刺眼。

为打造更具科技沉浸感的座舱体验,EZ-60在全球首发联发科技MT8676 4nm芯片,宣告智能座舱进入4nm时代。相比传统7nm芯片,晶体管密度提升约1.6-1.8倍,同功耗下算力提升约25-40%,,响应速度更快、连网更快、操作无卡顿、切屏更丝滑。

科技造就美好出行生活。EZ-60全球首发裸眼3D HUD,最大画幅100英寸,利用双目成像原理,可以实现眼球随动,3D导航让道路虚拟标志与真实道路无缝重合,安全驾驶更轻松。在驻车模式下,巨幅3D HUD还能化身裸眼3D影院,当外出露营时,超爽大片一次看个够。此外,电子外后视镜和流媒体内后视镜的组合,让视野提升30%,雨天晴天都能用,黑夜白天都清晰。

在AI智能化领域,EZ-60打造了人—车—环境跨模态的人机交互AI框架,基于大模型对用车场景的剖析理解和数据的精心训练,革新了人机交互方式。为让用户无压力、无门槛使用海量的功能,EZ-60提供了AI助手,用户无需了解车辆有多少功能,只要想用,超过 1700 项功能可随时通过AI助手调用,实时呈现。出行、吃喝玩乐、聊天、车控等等需求,语音指令可一语直达,功能无折叠。

双零重力座椅,23个高品质扬声器,营造奢侈出行享受

在内饰设计上,EZ-60采用了流水式的光栅,极具豪华感,搭配白、紫、金三色的轻奢撞色设计,更显内饰高级品味。长达2902mm的轴距为EZ-60带来了开阔的驾乘空间。新车前排配备了同级少有的人体工学双零重力座椅,能够实现双120°平躺。8种按摩模式,8个按摩点位,有效疏解疲劳。主驾16向电动调节,副驾14向电动调节。前排座椅根据马自达“人马一体”坐姿进行开发,拥有出色的侧翼包裹感与腰部支撑感,转向时,驾驶者的动态位移减少60%,坐得稳,才能开得稳。EZ-60还提供全车通风加热功能。真正做到在主驾,你可以享受驾驭的快乐,在副驾,你可享受躺赢的人生。

同时,EZ-60搭载了同级领先的23个高品质扬声器,其中,高音扬声器5个,中音扬声器5个,低音扬声器4个,低音炮1个,天空全频扬声器4个,主副驾头枕扬声器4个。支持杜比全景声,以前所未有的层次感、清晰度,让你尽享全方位萦绕的沉浸式影音体验。此外,空间校准技术和声音补偿技术,让每一个座位都是“甜点位”。

延续马自达顶级安全基因,主被动安全登峰造极

EZ-60 延续了马自达在安全领域的核心DNA。发布会上,长安马自达汽车有限公司执行副总裁邓智涛向参展嘉宾展示了“史上最硬的车展邀请函”——它以2,000MPa的热成型钢制成,取自EZ-60的车身,历经527个昼夜研发,包含13项核心专利技术,在达到 2,073MPa抗拉强度的同时,依然能保持8.2%的均匀延伸率,强度提升40%,重量减轻19%,这块特殊的邀请函象征着长安马自达在安全领域追求极致的坚定信念。

[长安马自达汽车有限公司执行副总裁 邓智涛]

如果说安全是最大的豪华,马自达就从入门款开始。在被动安全领域,EZ-60 全系标配9气囊,包括远端气囊,总容积高达370L,保护面积高达26,500cm²,同级领先。7横5纵的高强度铠甲笼式结构,高强钢占比达到86.5%,其中,前防撞梁和车门防撞梁采用了2,000MPa热成型钢,车顶横梁采用了1,470Mpa的世界最高强度冷轧钢。在主动安全领域,EZ-60全系标配L2级组合驾驶辅助系统,超过30项行泊一体辅助功能,覆盖用户200+使用场景,满足用户的高频需求。AEB刹停速度提升至80KM/h,支持横穿、夜间、通用障碍物等复杂场景,场景覆盖与性能表现处于同级领先水平,后续提升性能后能够继续保持行业领先。

对于用户最关注的电池安全。长安马自达选择了中国市场头部电池供应商,从电池、电芯的材料安全到多达8级的安全防护,构筑起了坚实的电池安全底层逻辑,提前满足2026年电动车最严安全令。

安全,也是马自达实现驾乘愉悦的核心前提。作为一个取悦驾驶者的品牌,马自达希望用户出行不仅仅是距离的改变,而是愉悦的出发、愉悦的到达。EZ-60首次搭载连续可变阻尼悬架,以毫秒级的响应智能匹配不同路况与驾驶意图,配合马自达独家调校功底,实现车辆纵向和横向加速度的顺滑过渡,实现操控与舒适的极致平衡。有操控基因的品牌才能打造出有操控乐趣的产品。

作为行业首个亮相即开启预订的车型,EZ-60推出了“点拾成金”的预订政策:订金10元,每天膨胀,膨胀不封顶,直至新车交付,膨胀金直接抵扣车款。即日起,国内消费者可通过长安马自达官网、「悦马星空」官方APP/小程序,以及授权经销商预订新车,尊享智电豪华出行新时代。

收起阅读 »

BOE(京东方)发布行业首个可持续发展品牌“ONE”:以开放创新科技之力守护人类未来

2025年4月23日,中国北京——恰逢ESG(环境、社会和治理)理念提出20周年与《巴黎协定》签署10周年,BOE(京东方)正式发布中国显示行业首个可持续发展品牌“ONE”(Open Next Earth),以“开放包容(Open)、创新引领(Next)、永续...
继续阅读 »

2025年4月23日,中国北京——恰逢ESG(环境、社会和治理)理念提出20周年与《巴黎协定》签署10周年,BOE(京东方)正式发布中国显示行业首个可持续发展品牌“ONE”(Open Next Earth),以“开放包容(Open)、创新引领(Next)、永续生态(Earth)”为三大品牌内涵,深刻诠释了“以开放创新科技之力守护人类未来”的核心理念。该品牌源自BOE(京东方)宏伟愿景——成为地球上最受人尊敬的伟大企业,凝聚了公司三十年来“用心改变生活”的初心,更是其“屏之物联”战略升维的结晶。这一发布标志着BOE(京东方)从“科技创新引领者”向“可持续生态构建者”的蜕变,彰显了BOE(京东方)引领全球显示产业实现永续增长的坚定决心,更为中国科技企业在未来全球化浪潮中践行ESG理念提供了示范和借鉴。

BOE(京东方)董事长陈炎顺在发布会上表示,可持续发展是企业竞争力的新维度,不是选择题,而是企业生存的必答题。BOE(京东方)发布“ONE”可持续发展品牌,不仅是内涵和标识的升级,更是中国科技企业向全球递交的“价值宣言”,BOE(京东方)始终相信技术的温度,始终敬畏自然的力量,始终坚守长期主义,这不仅是BOE(京东方)今天发布ONE品牌的承诺,也是我们共同的期盼。通过这一品牌的发布,BOE(京东方)希望与全球伙伴携手,共同推动显示产业的绿色升级,走向更加绿色、创新、和谐的未来。

活动现场,BOE(京东方)CEO冯强发布了全新可持续发展品牌,并对品牌的由来、标识和内涵进行了深入诠释。冯强表示,BOE(京东方)三十年初心不改,将可持续发展逐渐升维成集团的核心战略,并在2024年建立了治理层、管理层和执行层的三层可持续发展组织架构,确保公司运营的每一个环节都与可持续发展理念紧密契合。BOE(京东方)“成为地球上最受人尊敬的伟大企业”的伟大愿景不仅代表技术的领先或市场的领先,更在于我们如何用科技服务社会,实现商业与生态和谐共生,构建可复制的可持续升维样本。

BOE(京东方)可持续发展品牌标识解读:

BOE(京东方)可持续发展品牌以BOE经典“O”标为设计原点,在传承集团母品牌基因的同时,融入莫比乌斯环的哲学意象,构建出立体循环系统,象征“以终为始”的东方智慧,寓意技术创新与生态建设的永续共生。渐变绿色环带勾勒出地球轮廓,隐喻自然循环系统,呼应“开放包容-创新引领-永续生态”的品牌内涵,展现科技与自然的和谐边界。环体层叠的线条设计突出纵深感,宛如“未来之眼”,展现京东方的前瞻性眼光,传递携手全球伙伴共建可持续发展共同体的宣言。

BOE(京东方)可持续发展品牌三大品牌内涵(Open Next Earth):

Open(开放包容):构建全球责任共同体

“Open”代表开放包容,是BOE(京东方)可持续发展的根本态度和全球化布局的重要理念。BOE(京东方)以开放包容的姿态,与全球5000家合作伙伴携手共进,构建融合共生的产业生态,致力于通过技术共享与协同发展,推动人类社会的不断进步。

自2021年始,BOE(京东方)携手中国乡村发展基金会开展“照亮成长路”公益项目,累计在偏远地区建设智慧教室达126间,超过63,500名师生受益;BOE(京东方)还携手故宫博物院开展“百堂故宫传统文化公益课”项目,为 26 所学校,2万余名学生送去 400 余场线上公益课程。2023年,BOE(京东方)携手联合国开发计划署(UNDP)开展“屏水相逢”水资源保护项目,以“一水三村”的形式助推周边村落节水低碳可持续发展模式的建立,促进可持续发展示范村的建设推广。2024年,BOE(京东方)举办主题为“屏之物联 链动未来”的全球供应伙伴大会,搭建开放包容、互利共赢的多方交流平台,倡导行业加快绿色转型及生态构建,携手供应伙伴积极搭建绿色体系,推动全球产业链不断优化升级。

Next(创新引领):以未来视野重塑全新技术价值

“Next”代表创新引领,是BOE(京东方)对未来的深刻洞察和持续创新。公司秉持“对技术的尊重和对创新的坚持”,以前瞻性眼光布局未来,推动显示技术的突破,并以“屏之物联”战略重新定义技术价值,实现可持续发展的无限可能。

BOE(京东方)每年将营收的7% 左右投入到研发中,截至2024年,BOE(京东方)累计自主专利申请已超10万件,以强大技术实力巩固全球竞争优势。在“屏之物联”战略指导下,BOE(京东方)依托半导体显示、物联网创新、传感器件三大技术策源地,积极与国内外科研院所、顶尖高校、知名企业、生态链合作伙伴开展广泛合作,截至2024年底,已与170家国内外机构达成合作,累计落地合作项目百余项。BOE(京东方)还持续布局人工智能领域,形成“AI+制造”、“AI+产品”、“AI+运营”三大核心应用业务格局。同时,BOE(京东方)高度重视对内外部下一代科技人才的培育与储备,通过与高校、科研机构建立深度合作关系、成立联合实验室等,为年轻员工、学子搭建广阔的成长平台。这种对人才的培育不仅是对企业的赋能,更是对整个行业可持续发展的贡献。

Earth(永续生态):定义产业与自然共生新范式

“Earth” 寓意永续生态,体现了BOE(京东方)对地球环境的尊重与承诺。BOE(京东方)致力于构建和谐共生的命运共同体,通过绿色技术创新和循环经济发展,定义产业与自然共生新范式,展现科技与自然的和谐边界,推动技术、生态与人文的永续共生。

BOE(京东方)承诺在2050年全集团实现碳中和目标,通过“绿色管理、绿色产品、绿色制造、绿色循环、绿色投资、绿色行动”六大核心路径,构建了覆盖全价值链的低碳体系。截至目前,BOE(京东方)已拥有18家 “国家级绿色工厂”,其中包括显示行业唯一1家“灯塔工厂”,2家“零碳工厂”,1家“国家级无废工厂”,4家“国家级绿色供应链管理企业”,8家工厂加入科学碳目标倡议组织(SBTi)。BOE(京东方)还通过将双碳管理纳入审核标准,组织240多家供应商完成碳盘查,策划搭建供应商碳管理平台,引领整个供应链向绿色低碳方向发展。在绿色技术产品方面,BOE(京东方)推出低功耗氧化物技术、LTPO技术、绿色护眼技术,这些技术全面应用到产品中,在行业内遥遥领先。此外,BOE(京东方)还鼓励员工积极参与“绿色行动”计划,员工全年参与达132万次,减排量约等于种植14441棵树木。

发布会上还举办了主题为“来自地球的一封信”的圆桌论坛,由阳光媒体集团董事长、资深媒体人杨澜女士主持,并特别邀请国家气候战略中心战略规划部主任柴麒敏、北京绿色交易所副董事长梅德文及京东方能源科技董事长马亮进行现场对话,围绕绿色能源、低碳转型及长期主义实践展开探讨,共同探索气候、自然、能源与经济的共生模式。现场嘉宾的精彩分享获得了与会者一致认可,凸显了各方努力以技术开放打破边界,用生态协作定义未来,共同推动变革的重要性。在活动最后,BOE(京东方)还携手到场嘉宾共同发起了“ONE计划”,诚邀全球伙伴加入,并通过三大“破”、“立”举措,即破“零和博弈”,立“共生价值”;破“技术壁垒”,立“开源生态”;破“能源消耗”,立“零碳范式”,携手共创可持续发展的美好未来。

2021 年,BOE(京东方)率先推出中国显示行业首个技术品牌,开启技术与品牌双轮驱动模式,引领行业发展新潮流。2025 年,面对全球 ESG 浪潮,BOE(京东方)再次率先推出中国显示行业首个可持续发展品牌。这不仅标志着 BOE(京东方)引领行业全面迈向可持续发展的升维之路,也推动科技企业 ESG 实践进入 3.0 时代,即从单点技术突破转向系统生态构建,从合规披露转向价值创造。BOE(京东方)可持续发展品牌的发布不仅为企业的全球化布局和多元化业务拓展奠定基础,更为中国企业在全球化竞争中提供了重要借鉴。

关于BOE(京东方):

京东方科技集团股份有限公司(BOE)是全球领先的物联网创新企业,为信息交互和人类健康提供智慧端口产品和专业服务。作为全球半导体显示产业的龙头企业,BOE(京东方)带领中国显示产业破局“少屏”困境,实现了从0到1的跨越。如今,全球每四个智能终端就有一块显示屏来自BOE(京东方)。在“屏之物联”战略引领下,BOE(京东方)凭借“1+4+N+生态链”业务架构,将超高清液晶显示、柔性显示、MLED、微显示等领先技术广泛应用于交通、金融、艺术、零售、教育、办公、医疗等多元场景,赋能千行百业。目前,BOE(京东方)的子公司遍布全球近20个国家和地区,拥有超过5000家全球生态合作伙伴。更多详情可访问BOE(京东方)官网:https://www.boe.com.cn


收起阅读 »

长安马自达全球车型MAZDA 6e启航欧洲,全球化战略迈入新里程

4月22日,上海外高桥码头,长安马自达首批发往欧洲市场的纯电旗舰轿车MAZDA 6e正式装船启航。此次发运标志着MAZDA 6e在欧洲市场进入交付倒计时阶段,长安马自达“双百翻番”战略计划逐步落地,中国“合资智造”正加速赋能马自达全球电动化布局,传递着中国新能...
继续阅读 »

4月22日,上海外高桥码头,长安马自达首批发往欧洲市场的纯电旗舰轿车MAZDA 6e正式装船启航。此次发运标志着MAZDA 6e在欧洲市场进入交付倒计时阶段,长安马自达“双百翻番”战略计划逐步落地,中国“合资智造”正加速赋能马自达全球电动化布局,传递着中国新能源产业的技术自信。长安马自达汽车有限公司管理层和临港片区管委会代表、物流合作伙伴出席装船仪式,共同见证这一里程碑时刻。

上午10时,外高桥码头海风轻拂,首批600辆MAZDA 6e整齐列队,与停泊在蓝天碧海间的巨型滚装运输船交相辉映。随着发运按钮的正式启动,首辆MAZDA 6e平稳驶入船舱,现场响起热烈掌声。这批车辆预计将于5月抵达比利时港口,并于今年夏天交付至欧洲多国经销商。MAZDA 6e的到来,将为欧洲市场客户带来全新的电动旗舰轿车选择,并将进一步丰富马自达欧洲市场的产品阵容。

自今年1月10日首次亮相2025比利时布鲁塞尔车展以来,MAZDA 6e的全球化进程在不断加速。MAZDA 6e是以MAZDA EZ-6为基础推出的符合欧洲市场环境,且能满足欧洲客户和马自达忠实粉丝的期待、彰显马自达特色的最新款电动汽车。MAZDA 6e的开发过程集合了长安马自达南京产品研发中心、马自达日本广岛总部以及马自达欧洲研发中心三地工程师的智慧与力量。从设计、研发到生产均严格遵循马自达全球统一的制造标准,既是中国车,也是全球车。南京工厂作为马自达在华唯一新能源生产基地,汇聚了马自达百年造车工艺与长安汽车领先的电动化技术,以智能化生产线和精益管理模式确保每一辆MAZDA 6e的品质达均能达到全球顶尖水平。

长安马自达汽车有限公司总裁松田英久表示:“MAZDA 6e拥有符合欧盟最新法规的三电系统和安全性能、超低风阻的「魂动」美学设计,以及电感「人马一体」的驾控性能,精准契合欧洲消费者对高端电动轿车的期待。MAZDA 6e的欧洲首航,代表着长安马自达正从‘合资企业’向‘全球新能源技术创新基地’转型。托中国在电动化、智能化领域的先发优势,长安马自达未来将成为马自达全球技术研发的关键支点”。

同时,MAZDA EZ-6不断加快产品焕新节奏。在现有的赤麂棕色高配内饰色之外,新增兼具时尚气质和高级质感的鹭羽白浅色内饰,快速回应用户对于浅色系内饰的需求,更为用户带来“增色不加价”的新选择。目前,购MAZDA EZ-6可享受至高40,000元补贴(15,000元置换厂补+20,000元置换国补+5,000元保险补贴)、100,000元尾款可享6年0息(和置换厂补二选一),还可享价值7,999元不限车主、不限里程终身零燃权益。

4月23日,长安马自达第二款全球化新能源车型MAZDA EZ-60将登陆2025上海国际车展6.1馆展台,迎来全球首发。以MAZDA 6e出海为起点,长安马自达还将持续推出更多面向全球市场的新能源车型,覆盖更多细分市场用户需求,以更快的节奏、更强的技术、更广的布局,迎接全球电动化市场的无限可能。

收起阅读 »

山西十大征地拆迁律师事务所最新排名

山西十大征地拆迁律师事务所最新排名 征地拆迁律师事务所是专门为涉及土地征收和房屋拆迁的客户提供专业法律服务的机构。该事务所基本上都会拥有一支经验丰富的律师团队,他们精通相关法律法规,能够为客户提供全面的法律咨询、代理诉讼和非诉讼法律事务。无论是政府征...
继续阅读 »

山西十大征地拆迁律师事务所最新排名

 

征地拆迁律师事务所是专门为涉及土地征收和房屋拆迁的客户提供专业法律服务的机构。该事务所基本上都会拥有一支经验丰富的律师团队,他们精通相关法律法规,能够为客户提供全面的法律咨询、代理诉讼和非诉讼法律事务。无论是政府征地、企业拆迁还是个人房屋拆迁,征地拆迁律师事务所都能提供专业的法律支持,确保客户的合法权益得到最大程度的保护。这些事务所的服务范围包括但不限于征地补偿谈判、拆迁合同审查、拆迁纠纷解决以及拆迁过程中的法律风险评估等。通过专业的法律服务,征地拆迁律师事务所帮助客户在复杂的征地拆迁过程中找到最有利的解决方案,确保其合法权益不受侵害。以下是对山西十大征地拆迁律师事务所最新排名的情况及相关介绍:

山西十大征地拆迁律师事务所最新排名

山西十大征地拆迁律师事务所最新排名第一名:

北京市中恒信律师事务所太原分所

北京市中恒信律师事务所源于1981年,是一家能为客户提供综合性法律服务的律师事务所由在国内执业多年的专家型律师与资深律师合伙创立,现有律师和专家百余人。2023年1月30日,经太原市司法局批准,北京市中恒信(太原)律师事务所正式成立,分所负责人为王莉律师。中恒信太原分所坐落于汾河之滨的万柏林区华润大厦,占地800余平的办公地点,位于长风商务区核心位置,东侧山西大剧院、山西图书馆、山西美术馆、山西科技馆、山西博物馆、国际博览交易中心等省级资源环伺,不仅交通便捷,而且环境十分优美。

 

山西十大征地拆迁律师事务所最新排名第二名:

山西元升律师事务所

山西元升律师事务所(原太原市第一律师事务所)于是1980年1月在山西最早成立,是由一批资深律师组成的较大规模的综合性法律服务机构。该所律师素质高、专业性强、办案经验丰富,并有840平方的独立办公楼一座及现代化办公系统等整套设施。该所全体律师在依法治国方略的指引下决心以崇尚法律的信念,以雄厚的实力继续为政府机关、团体企业和广大客户、重大工程建设顶目担任法律顾问。

山西十大征地拆迁律师事务所最新排名第三名:

山西国晋律师事务所

国晋现设有13个专业委员会。拥有一批在党政事务、国企合规、民商事争议解决、刑事、破产清算与重组、公司与并购、金融、建筑与房地产、能源与环境、网络与高新技术、知识产权、执行事务、涉外事务等业务专业领域的卓越领军人才,有一大批律师担任省市律师协会、法学会等机构的专业骨干力量,有能力、有实力为客户提供综合性、全方位、整体化的高端法律服务。

山西十大征地拆迁律师事务所最新排名第四名:

山西鼎信泽律师事务所

山西鼎信泽律师事务所是经山西省司法厅批准的合伙制专业律师执业机构,成立于2004年10月。该所以“忠于法律、维护正义、勤勉尽责、务实高效”为宗旨,以“为客户提供服务,为社会创造价值”的执业理念,为国内外客户提供了优质、高效的法律服务。

山西十大征地拆迁律师事务所最新排名第五名:

山西东奥律师事务所

山西东奥律师事务所成立于2004年6月,现有法律服务人员48名,是山西省规模较大的综合性律师事务所。业务范围涵盖刑事诉讼、民商事诉讼、婚姻家事、法律顾问、企业改制、房地产与建筑、矿产与自然资源、兼并收购、破产重整等传统业务,近年来在投融资、新三板、IPO等领域也有所发展。

山西十大征地拆迁律师事务所最新排名第六名:

山西艾伦律师事务所

山西艾伦律师事务所在全国优秀律师王志萍带领下于2008年4月正式成立,2016年12月,为适应经济社会发展和律师业发展需要,艾伦律师事务所由个人所改制为合伙所。经过数年的稳健发展,艾伦所吸引和培养了近40名律师,在政府法律服务、公司、企业、金融等专业法律服务领域业绩卓著、经验丰富,完全具备承接政府法律服务及大型专项法律服务能力与条件。

山西十大征地拆迁律师事务所最新排名第七名:

山西华炬律师事务所

山西华炬律师事务所(以下简称“华炬”)前身始创于1993年,是一家实施特殊普通合伙制的综合性区域大所。现有党员近二百名,在华炬党委的领导下,华炬在管理上实行合伙人会议及所主任领导下的常务委员会及其专门委员会、监督委员会、行政运营中心、业务部和分所的治理结构和工作制度。律所内部管理规范,团总支、工会等组织全面建立,形成了较强的团队凝聚力。

山西十大征地拆迁律师事务所最新排名第八名:

山西中吕律师事务所

中吕律师事务所创立于2000年,中吕律师事务所专业领域覆盖传统能源、新能源、矿产资源、生态环境、税务、金融债券、新基建与基础设施工程、并购重组、破产重整、国际贸易、行政法与政府法律顾问、企业合规管理、刑事与民事诉讼、商事诉讼与仲裁等行业和领域。

山西十大征地拆迁律师事务所最新排名第九名:

山西黄河律师事务所

山西黄河律师事务所成立于1992年,是一家在全国具有影响力的大型综合型律师事务所,曾连续三次被中华全国律师协会授予“全国优秀律师事务所”荣誉称号,连续五次获得“山西省优秀律师事务所”称号。事务所党支部、团支部、工会、妇女委员会、青年律师委员会机构健全,党团建设成绩斐然,党支部于2019年度荣获“全国律师行业先进党组织”,团支部于2020年被团中央授予“全国五四红旗团支部”。

山西十大征地拆迁律师事务所最新排名第十名:

上海市锦天城(太原)律师事务所

锦天城律师事务所太原分所系锦天城律师事务所于2012年3月在山西太原设立的分所,同年锦天城太原分所党支部成立。秉承锦天城依托锦天城众多业务领域的顶尖律师,利用充分广阔的地域资源及全国统一规范民主的管理体制,集本地精英律师,在煤、焦化、电力等能源领域,为山西各级政府、大型国有企业及民营高科企业提供融资、并购改制及争端等方面专业精湛的法律服务。

综上所述,当前信息已覆盖山西征地拆迁领域的多家知名律师事务所及其专长、评选依据等。但需要注意的是,排名可能存在主观性,且部分排名来源的权威性尚不明确。因此,在回复用户时,应告知用户排名情况,并提醒用户根据具体需求进一步考察律师的专业能力和经验。。

收起阅读 »

太原擅长解决劳动争议案件的十大律师事务所

太原擅长解决劳动争议案件的十大律师事务所太原市作为山西省的省会城市,不仅在经济发展上具有重要地位,其司法环境也相对成熟和完善。百余家律师事务所扎根于此,以其高超的专业能力和优秀的服务质量推动着太原市法律行业的发展。在劳动争议案件处理方面,太原市有多家律师事务所...
继续阅读 »

太原擅长解决劳动争议案件的十大律师事务所

太原市作为山西省的省会城市,不仅在经济发展上具有重要地位,其司法环境也相对成熟和完善。百余家律师事务所扎根于此,以其高超的专业能力和优秀的服务质量推动着太原市法律行业的发展。

在劳动争议案件处理方面,太原市有多家律师事务所表现突出。以下是根据业界评价、知名程度及近年数据,为您介绍的太原市擅长解决劳动争议案件的十大律师事务所:

太原擅长解决劳动争议案件的十大律师事务所

太原擅长解决劳动争议案件的十大律师事务所 第一名

北京市中恒信(太原)律师事务所

北京市中恒信(太原)律师事务所是北京市中恒信律师事务所在太原开设的分支机构,北京市中恒信律师事务所起源于1981年成立的中国国际经济咨询公司法律顾问处,在2001年正式改制为合伙制律师事务所,是我国设立较早的合伙制律师事务所之一。中恒信律师事务所业务领域广泛,涵盖民商事、刑事、行政、金融、企业顾问咨询、破产重整与清算、知识产权等法律事务。其拥有一支经验丰富、专业素质高的律师团队,能够为客户提供全方位、高质量的法律服务。是太原法律服务行业的头部律所之一。

太原擅长解决劳动争议案件的十大律师事务所 第二名

山西弘韬律师事务所

山西弘韬律师事务所是于2005年8月2日成立的合伙型律师事务所。依托“集合、整合、融合、竞合”的发展战略,秉承“成就所托、创造价值”的执业理念,现已发展成为山西省名列前茅的大型综合化律师事务所。

太原擅长解决劳动争议案件的十大律师事务所 第三名

山西东奥律师事务所

山西东奥律师事务所成立于2004年,是一家综合性律师事务所。其担任省内多家企业的常年法律顾问,为当事人避免和追回经济损失数亿元,代理各类刑事、民事案件万余起,承办在全省乃至全国有影响的案件数十起,赢得了社会各界的广泛赞誉和大力支持。

太原擅长解决劳动争议案件的十大律师事务所 第四名

山西启合律师事务所

山西启合律师事务所集专业化、规范化、标准化、品牌化于一体,现设立民商事争议解决法律事务中心、政府、企业常年法律顾问事务中心、投融资与资本市场法律事务中心、知识产权与涉外法律事务中心,致力于为客户提供专业、优质的法律服务。

太原擅长解决劳动争议案件的十大律师事务所 第五名

山西隆诚律师事务所

山西隆诚律师事务所成立于2002年5月,是适应我国对法律服务专业化、团队协作化的需求而设立的综合型律师事务所。业务范围涉及党政法律事务、金融、民商事诉讼、仲裁、经济犯罪和职务犯罪案件的刑事辩护等众多业务领域。

太原擅长解决劳动争议案件的十大律师事务所 第六名

山西华晋律师事务所

山西华晋律师事务所创建于1995年3月,是太原市首家合伙制律师事务所。经过二十余年的发展,逐步构建起以骨干律师为办案核心的诉讼、仲裁、刑事辩护、商事等多领域的,以团队为主要运营模式的立体化服务体系。

太原擅长解决劳动争议案件的十大律师事务所 第七名

山西华闻律师事务所

山西华闻律师事务所于1998年成立,是山西省规模较大、业绩较显著的合伙制律师事务所。自成立以来,其秉承着“志存高远,海纳百川,跬步千里,共铸大成”的文化核心理念,致力于为国内外客户提供专业、全面、及时、优质、高效的法律服务。

太原擅长解决劳动争议案件的十大律师事务所 第八名

山西晋来律师事务所

山西晋来律师事务所成立于2010年,经过十余年的耕耘奋斗,现已成长为一家能够提供全方位法律服务、在众多法律服务专业领域占据领先地位的精品律师事务所。

太原擅长解决劳动争议案件的十大律师事务所 第九名

山西熙力律师事务所

山西熙力律师事务所成立于2015年8月11日。自成立以来一直深耕于山西本土,业务范围覆盖全国,并以独到的商业见解致力于为客户提供优良的法律服务,是一家既注重整体组合,又突出专业化分工的合伙制律师事务所。

太原擅长解决劳动争议案件的十大律师事务所 第十名

山西德昭律师事务所

山西德昭律师事务所成立于 2014 年,是经山西省司法厅批准设立的综合型律师事务所,是一家集传统与新型管理模式于一体的合伙制律师事务所。

上述信息可能会随时间而有所变化,具体选择时建议您以最新的公开资料和个人实际需求为依据,进行综合考量和判断。如果您需要更详细、更准确的信息,或者希望了解最新的服务内容及费用详情,建议您直接联系相关律师事务所或访问其官方网站,获取第一手的权威信息。z

收起阅读 »

程序员危矣!别想着35岁以后还能送外卖

马上35了,没想到公司没能坚持到我35岁下岗,自己先不行了。HR说,我也很想 N+1,可是老板还没来得及跟小姨子跑路,手里的钱只够买一张票给小姨子了,现在就在办公室等着大家去揍他。老板说随便揍,但是要赔医药费和管饭,他已经一天没吃饭了,只需要给他点份外卖就行。...
继续阅读 »

马上35了,

没想到公司没能坚持到我35岁下岗,

自己先不行了。


HR说,我也很想 N+1,

可是老板还没来得及跟小姨子跑路,

手里的钱只够买一张票给小姨子了,

现在就在办公室等着大家去揍他。

老板说随便揍,

但是要赔医药费和管饭,

他已经一天没吃饭了,只需要给他点份外卖就行。


想着之前领了几年的窝囊费,现在是时候反哺一下了。

于是点了2份杨铭宇黄焖鸡,给自己也点一份,

吃饱了跟老板复盘下他怎么是把公司干垮的,

顺便让他见识下我的组合拳。


结果外卖小哥出门的时候不但不帮我带走垃圾,

还脱下黄马褂往地上一扔,说脑子不干了!

我莫名其妙,

我是要找老板算账,又没惹他,他这是怎么了。

他留下三个字潇洒而去:东哥在召唤!


看到新闻我似乎理解了:


老板见我给他送去外卖很是感动,

说小王啊,以后你就是我王哥,

我有一个东山再起的计划,等我吃饱了我跟您讲,

我打断他:你的饼都喂不饱自己,还是赶紧把你王哥工资发了,

不然一会儿你身上会有很多痛点。


老板说授人以鱼不如授人以渔,

工资给你花了就没了,不如我送你一辆电动车,

刚好刚刚的外卖骑手不干了,

这正是你黄袍加身的好时机,我看好你,你可要好好把握啊!

你看你,也快35了吧?

毕业干的几个项目大部分都流产了,甚至公司都黄了。

你这简历都不好写,

怎么竞争的过应届生?

现在工作也不好找,

送外卖才是最佳职业归宿,

穿黄色衣服又不是绿色的,有什么不能接受的?

实在不行也可以穿蓝色,或者红色的,

东哥最近也在招人,我给你内推下?

好歹也算进了大厂你说是不是!

今天你请我吃黄焖鸡,兄弟我非常感动,

我要送你一辆电动车,

让你以后天天能送黄焖鸡……


等等,

我不是来揍他的吗?

怎么又被PUA了?


不过话说回来,

大龄程序员找不到工作怎么办?

外卖骑手能不能干?

东哥的召唤是不是一个机会?


经过我不依赖 deepseek 的深度思考,

得出以下结论:

程序员危矣,

外卖骑手只是商业大佬开疆拓土的马前卒。


表面上是维护外卖骑手的权益,

实际上骑手只是商业竞争中的一枚棋子。


未来低空经济下的外卖行业并没有骑手参与的环节,

无人机派送成本微乎其微,甚至一路上还能给企业带点广告收入。


如今各位互联网大哥争夺的骑手,

短短几年又会成为被抛弃的对象。

别人真正要的不是骑手,

而且分食外卖蛋糕的刀叉。


商家处境也好不到哪里去,

当年轻人都习惯了预制菜,

廉价机器人都可以胜任预制菜的加热,

无人机派送,全程无人工参与,又卫生又快。


谁说京东只能送快递了?

投入的无人机要充分利用起来啊,

超市闪送、外卖到家,对它来说无缝接入啊。

只是你点外卖不会去他那里下单这让东哥很头疼,

占据市场后,降本增效的第一步就是针对骑手,

其次就是商家,为什么商家可以用预支包官方不能呢?

官网机器人餐厅将是又一次降本增效的尝试。


当所有的成本都将被压缩为电力成本,

这收益可能不比超级电商少吧,

何况收益模式可以随时定制:

无人机空中文字广告、

取餐点对点视频广告、

用户充值黄金会员免看一分钟广告。


几年后,

曾经的外卖小哥放下手中的砖,

吃着从无人机上取下午餐,

会发现还是熟悉的互联网味道,

而自己曾经就是这外卖商业帝国最底层的一块砖……

收起阅读 »

【Fiddler】Fiddler抓包工具(详细讲解)_抓包工具fiddler

抓包工具使用指南 序章 Fiddler 是一款功能强大的抓包工具,能够截获、重发、编辑和转存网络传输中的数据包,同时也常用于网络安全检测。它的功能丰富,但在学习过程中可能会遇到一些隐藏的小功能,容易遗忘。因此,本文总结了 Fiddler 的常用功能,并结合 S...
继续阅读 »

抓包工具使用指南


序章


Fiddler 是一款功能强大的抓包工具,能够截获、重发、编辑和转存网络传输中的数据包,同时也常用于网络安全检测。它的功能丰富,但在学习过程中可能会遇到一些隐藏的小功能,容易遗忘。因此,本文总结了 Fiddler 的常用功能,并结合 SniffMaster 抓包大师的特点,帮助大家更好地掌握抓包工具的使用。




1. Fiddler 抓包简介


Fiddler 通过改写 HTTP 代理来监控和截取数据包。当 Fiddler 启动时,它会自动设置浏览器的代理,关闭时则会还原代理设置,非常方便。


1.1 字段说明


Fiddler 抓取的数据包会显示在列表中,以下是各字段的含义:


名称含义
#抓取 HTTP 请求的顺序,从 1 开始递增
ResultHTTP 状态码
Protocol请求使用的协议(如 HTTP/HTTPS/FTP 等)
Host请求地址的主机名
URL请求资源的位置
Body请求的大小
Caching请求的缓存过期时间或缓存控制值
Content-Type请求响应的类型
Process发送此请求的进程 ID
Comments用户为此会话添加的备注
Custom用户设置的自定义值

1.2 Statistics 请求性能数据分析


点击任意请求,可以在右侧查看该请求的性能数据和分析结果。


1.3 Inspectors 查看数据内容


Inspectors 用于查看会话的请求和响应内容,上半部分显示请求内容,下半部分显示响应内容。


1.4 AutoResponder 拦截指定请求


AutoResponder 允许拦截符合特定规则的请求,并返回本地资源或 Fiddler 资源,从而替代服务器响应。例如,可以将关键字 "baidu" 与本地图片绑定,访问百度时会被劫持并显示该图片。


1.5 Composer 自定义请求发送


Composer 允许自定义请求并发送到服务器。可以手动创建新请求,或从会话表中拖拽现有请求进行修改。


1.6 Filters 请求过滤规则


Filters 用于过滤请求,避免无关请求干扰。常用的过滤条件包括 Zone(内网或互联网)和 Host(指定域名)。


1.7 Timeline 请求响应时间


Timeline 显示指定内容从服务器传输到客户端的时间,帮助分析请求的响应速度。




2. Fiddler 设置解密 HTTPS 数据


Fiddler 可以通过伪造 CA 证书来解密 HTTPS 数据包。具体步骤如下:



  1. 打开 Fiddler,点击 Tools -> Fiddler Options -> HTTPS

  2. 勾选 Decrypt HTTPS Traffic

  3. 点击 OK 保存设置。




3. 抓取移动端数据包


3.1 设置代理



  1. 打开 Fiddler,点击 Tools -> Fiddler Options -> Connections

  2. 设置代理端口为 8888,并勾选 Allow remote computers to connect

  3. 在手机端连接与电脑相同的 WiFi,并设置代理 IP 和端口。


3.2 安装证书



  1. 在手机浏览器中访问 http://<电脑IP>:8888,下载 Fiddler 根证书。

  2. 安装证书并信任。


3.3 抓取数据包


配置完成后,手机访问应用时,Fiddler 会截取到数据包。




4. Fiddler 内置命令与断点


Fiddler 提供了命令行功能,方便快速操作。常用命令包括:


命令功能示例
?匹配包含指定字符串的请求?google
匹配请求大小大于指定值的请求>1000
<匹配请求大小小于指定值的请求<100
=匹配指定 HTTP 返回码的请求=200
@匹配指定域名的请求@http://www.baidu.com
select匹配指定响应类型的请求select image
cls清空当前所有请求cls
dump将所有请求打包成 saz 文件dump
start开始监听请求start
stop停止监听请求stop

断点功能


Fiddler 的断点功能可以截获请求并暂停发送,方便修改请求内容。常用断点命令包括:



  • bpafter:中断包含指定字符串的请求。

  • bpu:中断响应。

  • bps:中断指定状态码的请求。

  • bpv:中断指定 HTTP 方法的请求。




5. SniffMaster 抓包大师


SniffMaster 是一款跨平台抓包工具,支持 Android、iOS 和 PC 端抓包。与 Fiddler 相比,SniffMaster 具有以下优势:



  • 自动生成证书:无需手动配置 HTTPS 解密。

  • 多设备支持:支持同时抓取多个设备的数据包。

  • 智能过滤:按协议、域名等条件快速筛选数据。

  • 可视化界面:提供更直观的数据分析和展示。


5.1 SniffMaster 使用场景



  • 移动端抓包:支持 Android 和 iOS 设备,自动配置代理和证书。

  • HTTPS 解密:内置 HTTPS 解密功能,无需手动安装证书。

  • 多平台支持:支持 Windows、macOS 和 Linux 系统。




总结


Fiddler 和 SniffMaster 都是强大的抓包工具,适用于不同的场景。Fiddler 适合需要深度定制和高级功能的用户,而 SniffMaster 则更适合新手和需要快速抓包的用户。无论是开发调试还是网络安全检测,这两款工具都能提供极大的帮助。


作者:用户958221080790
来源:juejin.cn/post/7481463851298635827
收起阅读 »

前端实现画中画超简单,让网页飞出浏览器

web
Document Picture-in-Picture 介绍     今天,我来介绍一个非常酷的前端功能:文档画中画 (Document Picture-in-Picture, 本文简称 PiP)。你有没有想过,网页上的任何内容能悬浮在桌面上?😏 🎬 视频流媒...
继续阅读 »

Document Picture-in-Picture 介绍


    今天,我来介绍一个非常酷的前端功能:文档画中画 (Document Picture-in-Picture, 本文简称 PiP)。你有没有想过,网页上的任何内容能悬浮在桌面上?😏


🎬 视频流媒体的画中画功能


        你可能已经在视频平台(如腾讯视频哔哩哔哩等网页)见过这种效果:视频播放时,可以点击画中画后。无论你切换页面,它都始终显示在屏幕的最上层,非常适合上班偷偷看电视💻


pip.gif


        在今天的教程中,不仅仅是视频,我将教你如何将任何 HTML 内容放入画中画模式,无论是动态内容、文本、图片,还是纯炫酷的 div,统统都能“飞”起来。✨


        一个如此有趣的功能,在网上却很少有详细的教程来介绍这个功能的使用。于是我决定写一篇详细的教程来教大家如何实现画中画 (建议收藏)😁


体验网址:Treasure-Navigationpip08.gif


github地址




📖 Document Picture-in-Picture 详细教程


🛠 HTML 基本代码结构


    首先,我们随便写一个简单的 HTML 页面,后续的 JS 和样式都会基于它实现。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Picture-in-Picture API 示例</title>
<style>
#pipContent {
width: 600px;
height: 300px;
background: pink;
font-size: 20px;
}
</style>
</head>

<body>
<div id="container">
<div id="pipContent">这是一个将要放入画中画的 div 元素!</div>
<button id="clickBtn">切换画中画</button>
</div>
<script>
// 在这里写你的 JavaScript 代码
</script>
</body>
</html>



1️. 请求 PiP 窗口


    PiP 的核心方法是 window.documentPictureInPicture.requestWindow。它是一个 异步方法,返回一个新创建的 window 对象。

    PIP 窗口可以将其看作一个新的网页,但它始终悬浮在屏幕上方。


document.getElementById("clickBtn").addEventListener("click", async function () {
// 获取将要放入 PiP 窗口的 DOM 元素
const pipContent = document.getElementById("pipContent");
// 请求创建一个 PiP 窗口
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200, // 设置窗口的宽度
height: 300 // 设置窗口的高度
});

// 将原始元素添加到 PiP 窗口中
pipWindow.document.body.appendChild(pipContent);
});

演示:


pip01.gif


👏 现在,我们已经成功创建了一个画中画窗口!
这段代码展示了如何将网页中的元素放入一个新的画中画窗口,并让它悬浮在最上面。非常简单吧


关闭PIP窗口


可以直接点右上角关闭PIP窗口,如果我们想在代码中实现关闭,直接调用window上的api就可以了


window.documentPictureInPicture.window.close();



2️. 检查是否支持 PiP 功能


    一切不能兼容浏览器的功能介绍都是耍流氓,我们需要检查浏览器是否支持PIIP功能
实际就是检查documentPictureInPicture属性是否存在于window上 🔧


if ('documentPictureInPicture' in window) {
console.log("🚀 浏览器支持 PiP 功能!");
} else {
console.warn("⚠️ 当前浏览器不支持 PiP 功能,更新浏览器或者换台电脑吧!");
}

    如果是只需要将视频实现画中画功能,视频画中画 (Picture-in-Picture) 的兼容性会好一点,但是它只能将<video>元素放入画中画窗口。它与本文介绍的 文档画中画(Document Picture-in-Picture) 使用方法也是十分相似的。


image.png


image.png




3️. 设置 PiP 样式


    我们会发现刚刚创建的画中画没有样式,一点都不美观。那是因为我们只放入了dom元素,没有添加css样式。


3.1. 全局样式同步


假设网页中的所有样式如下:


<head>
<style>
#pipContent {
width: 600px;
height: 300px;
background: pink;
font-size: 20px;
}
</style>
<link rel="stylesheet" type="text/css" href="https://abc.css">
</head>

为了方便,我们可以直接把之前的网页的css样式全部赋值给画中画


// 1. document.styleSheets获取所有的css样式信息
[...document.styleSheets].forEach((styleSheet) => {
try {
// 转成字符串方便赋值
const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
// 创建style标签
const style = document.createElement('style');
// 设置为之前页面中的css信息
style.textContent = cssRules;
console.log('style', style);
// 把style标签放到画中画的<head><head/>标签中
pipWindow.document.head.appendChild(style);
} catch (e) {
// 通过 link 引入样式,如果有跨域,访问styleSheet.cssRules时会报错。没有跨域则不会报错
const link = document.createElement('link');
/**
* rel = stylesheet 导入样式表
* type: 对应的格式
* media: 媒体查询(如 screen and (max-width: 600px))
* href: 外部样式表的 URL
*/

link.rel = 'stylesheet';
link.type = styleSheet.type;
link.media = styleSheet.media;
link.href = styleSheet.href ?? '';
console.log('error: link', link);
pipWindow.document.head.appendChild(link);
}
});

演示:

image.png




3.2. 使用 link 引入外部 CSS 文件


向其他普通html文件一样,可以通过link标签引入特定css文件:


创建 pip.css 文件:


#pipContent {
width: 600px;
height: 300px;
background: skyblue;
}

js引用:


// 其他不变
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = './pip.css'; // 引入外部 CSS 文件
pipWindow.document.head.appendChild(link);
pipWindow.document.body.appendChild(pipContent);

演示:

pip02.gif


3.3. 媒体查询的支持


可以设置媒体查询 @media (display-mode: picture-in-picture)。在普通页面中会自动忽略样式,在画中画模式会自动渲染样式


<style>
#pipContent {
width: 600px;
height: 300px;
background: pink;
font-size: 20px;
}

<!-- 普通网页中会忽略 -->
@media (display-mode: picture-in-picture) {
#pipContent {
background: lightgreen;
}
}
</style>

在普通页面中显示为粉色,在画中画自动变为浅绿色


演示:

pip03.gif




4️. 监听进入和退出 PiP 模式的事件


我们还可以为 PiP 窗口 添加事件监听,监控画中画模式的 进入退出。这样,你就可以在用户操作时,做出相应的反馈,比如显示提示或执行其他操作。


// 进入 PIP 事件
documentPictureInPicture.addEventListener("enter", (event) => {
console.log("已进入 PIP 窗口");
});

const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200,
height: 300
});
// 退出 PIP 事件
pipWindow.addEventListener("pagehide", (event) => {
console.log("已退出 PIP 窗口");
});

演示


pip04.gif




5️. 监听 PiP 焦点和失焦事件


const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200,
height: 300
});

pipWindow.addEventListener('focus', () => {
console.log("PiP 窗口进入了焦点状态");
});

pipWindow.addEventListener('blur', () => {
console.log("PiP 窗口失去了焦点");
});

演示


pip05.gif




6. 克隆节点画中画


我们会发现我们把原始元素传入到PIP窗口后,原来窗口中的元素就不见了。

我们可以把原始元素克隆后再传入给PIP窗口,这样原始窗口中的元素就不会消失了


const pipContent = document.getElementById("pipContent");
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200,
height: 300
});
// 核心代码:pipContent.cloneNode(true)
pipWindow.document.body.appendChild(pipContent.cloneNode(true));

演示


pip07.gif


PIP 完整示例代码


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Picture-in-Picture API 示例</title>
<style>
#pipContent {
width: 600px;
height: 300px;
background: pink;
font-size: 20px;
}
</style>
</head>
<body>
<div id="container">
<div id="pipContent">这是一个将要放入画中画的 div 元素!</div>
<button id="clickBtn">切换画中画</button>
</div>

<script>
// 检查是否支持 PiP 功能
if ('documentPictureInPicture' in window) {
console.log("🚀 浏览器支持 PiP 功能!");
} else {
console.warn("⚠️ 当前浏览器不支持 PiP 功能,更新浏览器或者换台电脑吧!");
}

// 请求 PiP 窗口
document.getElementById("clickBtn").addEventListener("click", async function () {
const pipContent = document.getElementById("pipContent");

// 请求创建一个 PiP 窗口
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200, // 设置窗口的宽度
height: 300 // 设置窗口的高度
});

// 将原始元素克隆并添加到 PiP 窗口中
pipWindow.document.body.appendChild(pipContent.cloneNode(true));

// 设置 PiP 样式同步
[...document.styleSheets].forEach((styleSheet) => {
try {
const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
const style = document.createElement('style');
style.textContent = cssRules;
pipWindow.document.head.appendChild(style);
} catch (e) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = styleSheet.type;
link.media = styleSheet.media;
link.href = styleSheet.href ?? '';
pipWindow.document.head.appendChild(link);
}
});

// 监听进入和退出 PiP 模式的事件
pipWindow.addEventListener("pagehide", (event) => {
console.log("已退出 PIP 窗口");
});

pipWindow.addEventListener('focus', () => {
console.log("PiP 窗口进入了焦点状态");
});

pipWindow.addEventListener('blur', () => {
console.log("PiP 窗口失去了焦点");
});
});

// 关闭 PiP 窗口
// pipWindow.close(); // 可以手动调用关闭窗口
</script>
</body>
</html>




总结


🎉 你现在已经掌握了如何使用 Document Picture-in-Picture API 来悬浮任意 HTML 内容!
希望能带来更灵活的交互体验。✨


如果你有什么问题,或者对 PiP 功能有更多的想法,欢迎在评论区与我讨论!👇📬


作者:前端金熊
来源:juejin.cn/post/7441954981342036006
收起阅读 »

还在每次都写判断?试试惰性函数,让你的代码更聪明!

web
什么是惰性函数? 先来看个例子 function addEvent(el, type, handler) { if (window.addEventListener) { el.addEventListener(type, handler, fal...
继续阅读 »

什么是惰性函数?


先来看个例子


function addEvent(el, type, handler) {
if (window.addEventListener) {
el.addEventListener(type, handler, false);
} else {
el.attachEvent('on' + type, handler);
}
}

上面这段代码中,每次调用 addEvent 都会进行一遍判断。实际上,我们并不需要每次都进行判断,只需要执行一次就够了,当然,我们也可以存个全局的flag来记录,但是,有更好的办法了


function addEvent(el, type, handler) {
if (window.addEventListener) {
addEvent = function(el, type, handler) {
el.addEventListener(type, handler, false);
}
} else {
addEvent = function(el, type, handler) {
el.attachEvent('on' + type, handler);
}
}
return addEvent(el, type, handler); // 调用新的函数
}

这就是惰性函数:第一次执行时会根据条件覆盖自身,后续调用直接执行更新后的逻辑


惰性函数的实现方式


惰性函数一般情况下有两种实现方式


在函数内部重写自身


这种实现方式就是上面我们介绍的那样


function foo() {
console.log('初始化...');
foo = function() {
console.log('后续逻辑');
}
}

大多数情况下,这种实现方式都可以覆盖


用函数表达式赋值


const foo = (function() {
if (someCondition) {
return function() { console.log('A'); }
} else {
return function() { console.log('B'); }
}
})();

这种情况适用于模块或者立即执行的情况,其实就是用闭包做了个封装


惰性函数的应用场景


兼容性判断


我们在做适配的时候,很多时候需要进行浏览器特性的判断,比如之前提到的事件绑定


性能优化


其实惰性函数说起来很像单例,他的原理就是只执行一次,那么如果想要避免一些重复操作,尤其是重复初始化,就可以想一下是不是可以用惰性函数来处理,比如Canvas渲染引擎,加载某些外部依赖、判断登录状态等等


注意事项



  1. 写好注释,一定要写好注释,因为函数在执行后会变化,不写注释如果除了一些问题,可能后面维护的人会骂街,会大大增加你的不可替代性,咳咳,千万不要这么操作,一定要写好注释

  2. 不适合频繁修改逻辑和复杂上下文的场景,会增加复杂度


一句话总结:能判断一次就不要判断两次,惰性函数让你的代码更聪明


作者:那小孩儿
来源:juejin.cn/post/7490850417976508428
收起阅读 »

Electron 应用太重?试试 PakePlus 轻装上阵

web
Electron 作为将 Web 技术带入桌面应用领域的先驱框架,让无数开发者能够使用熟悉的 HTML、CSS 和 JavaScript 构建跨平台应用。然而,随着应用规模的扩大,Electron 应用的性能问题逐渐显现——内存占用高、启动速度慢、安装包体积庞...
继续阅读 »


Electron 作为将 Web 技术带入桌面应用领域的先驱框架,让无数开发者能够使用熟悉的 HTML、CSS 和 JavaScript 构建跨平台应用。然而,随着应用规模的扩大,Electron 应用的性能问题逐渐显现——内存占用高、启动速度慢、安装包体积庞大,这些都成为了用户体验的绊脚石。不过,现在有了 PakePlus,这些烦恼都将迎刃而解。


PakePlus官网文档:PakePlus


PakePlus开源地址:github.com/Sjj1024/Pak…



首先要轻


以一款基于 Electron 的文档编辑应用为例,在使用 PakePlus 优化前,安装包大小达 200MB,启动时间超过 10 秒。但是使用PakePlus重新打包之后,安装包大小控制在5M左右,缩小了将近40倍!启动时间也做到了2秒以内!这就是PakePLus的魅力所在。


开发者反馈:"迁移过程出乎意料的顺利,大部分代码无需修改,性能提升却立竿见影。"




其次都是其次



  • 🚀 基于 Rust Tauri,PakePlus 比基于 JS 的框架更轻量、更快。

  • 📦 内置丰富功能包——支持快捷方式、沉浸式窗口、极简自定义。

  • 👻 PakePlus 只是一个极简的软件,用 Tauri 替代旧的打包方式,支持跨平台桌面,将很快支持手机端。

  • 🤗 PakePlus 易于操作使用,只需一个 GitHub Token,即可获得桌面应用。

  • 🌹 不需要在本地安装任何复杂的依赖环境,使用 Github Action 云端自动打包。

  • 🧑‍🤝‍🧑 支持国际化,对全球用户都非常友好,并且会自动跟随你的电脑系统语言。

  • 💡 支持自定义 js 注入。你可以编写自己的 js 代码注入到页面中。

  • 🎨 ui 界面更美观更友好对新手更实用,使用更舒适,支持中文名称打包。

  • 📡 支持网页端直接使用,但是客户端功能更强大,更推荐客户端。

  • 🔐 数据安全,你的 token 仅保存在你本地,不会上传服务器,你的项目也都在你自己的 git 中安全存储。

  • 🍀 支持静态文件打包,将 Vue/React 等项目编译后的 dist 目录或者 index.html 丢进来即可成为客户端,何必是网站。

  • 🐞 支持 debug 调试模式,无论是预览阶段还是发布阶段,都可以找到 bug 并消灭 bug



使用场景


你有一个网站,想把它立刻变成跨平台桌面应用和手机APP,立刻高大尚。

你有一个 Vue/React 等项目,不想购买服务器,想把它打包成桌面应用。

你的 Cocos 游戏是不是想要跨平台客户端运行?完全没有问题。

你的 Unity 项目是不是想要跨平台打包为客户端?也完全没有问题。

隐藏你的网站地址,不被随意传播和使用,防止爬虫程序获取你的网站内容。

公司内网平台,不想让别人知道你的网站地址,只允许通过你的客户端访问。

想把某个网站变成自己的客户端,实现自定义功能,比如注入 js 实现自动化操作。

网站广告太多?想把它隐藏起来,用无所不能的 js 来屏蔽它们吧。

需要使用 tauri2 打包,但是依赖环境太复杂,本地电脑硬盘不够用,就用 PakePlus



热门包



PakePLus 支持 arm 和 inter 架构的安装包,流行的程序安装包仅仅包含了 mac 的 arm(M 芯片)版本 和 windows 的 Inter(x64)版本 和 Linux 的 x64 版本,如果需要更多架构的安装包,请使用 PakePlus 单独编译自己需要的安装包。热门包的下载地址请到官方文档下载体验



常见问题


mac提示:应用已随坏



这是因为没有给苹果给钱,所以苹果会拒绝你的应用。


解决办法:


Mac 用户可能在安装时看到“应用已损坏”的警告。 请点击“取消”,然后运行以下命令,输入电脑密码后,再重新打开应用:(这是由于应用需要官方签名,才能避免安装后弹出“应用已损坏”的提示,但官方签名的费用每年 99 美元...因此,需要手动绕过签名以正常使用)


sudo xattr -r -d com.apple.quarantine /Applications/PakePlus.app

 当你打包应用时,Mac 用户可能在安装时看到“应用已损坏”的警告。 请点击“取消”,然后运行以下命令,再重新打开应用:


sudo xattr -r -d com.apple.quarantine /Applications/你的软件名称.app



作者:1024小神
来源:juejin.cn/post/7490876486292389914
收起阅读 »

只需一行代码,任意网页秒变可编辑!

web
大家好,我是石小石! 在我们日常工作中,可能会遇到截图页面的场景,有时页面有些内容不符合要求,我们可能需要进行一些数值或内容的修改。如果你会PS,修改内容难度不高,如果你是前端,打开控制台也能通过修改dom的方式进行简单的文字修改。 今天,我就来分享一个冷门...
继续阅读 »

大家好,我是石小石!




在我们日常工作中,可能会遇到截图页面的场景,有时页面有些内容不符合要求,我们可能需要进行一些数值或内容的修改。如果你会PS,修改内容难度不高,如果你是前端,打开控制台也能通过修改dom的方式进行简单的文字修改。


今天,我就来分享一个冷门又实用的前端技巧 —— 只需一行 JavaScript 代码,让任何网页瞬间变成可编辑的! 先看看效果:


甚至,还可以插入图片等媒体内容



如何实现


很难想象,这么炫酷的功能,居然只需要在控制台输入一条指令:


document.designMode = "on";

打开浏览器控制台(F12),复制粘贴这行代码,回车即可。



如果你想关闭此功能,输入document.designMode = "off"即可。


Document:designMode 属性


MDN是这样介绍的:


document.designMode 控制整个文档是否可编辑。有效值为 "on""off"。根据规范,该属性默认为 "off"。Firefox 遵循这一标准。早期版本的 Chrome 和 IE 默认为 "inherit"。从 Chrome 43 开始,默认为 "off" 并不再支持 "inherit"。在 IE6-10 中,该值为大写。


兼容性方面,基本上所有浏览器都是支持的。



借助次API,我们也能实现Iframe嵌套页面的编辑:


iframeNode.contentDocument.designMode = "on";

关联API


与designMode关联的API其实还有contentEditable和execCommand(已弃用,但部分浏览器还可以使用)。


contentEditabledesignMode功能类似,不过contentEditable可以使特定的 DOM 元素变为可编辑,而designMode只能使整个文档可编辑。


特性contentEditabledocument.designMode
作用范围可以使单个元素可编辑可以使整个文档可编辑
启用方式设置属性为 truefalse设置 document.designMode = "on"
适用场景用于指定某些元素,如 <div>, <span>用于让整个页面变为可编辑
兼容性现代浏览器都支持现代浏览器都支持,部分老旧浏览器可能不支持

document.execCommand() 方法允许我们在网页中对内容进行格式化、编辑或操作。它主要用于操作网页上的可编辑内容(如 <textarea> 或通过设置 contentEditabledesignMode 属性为 "true" 的元素),例如加粗文本、插入链接、调整字体样式等。由于它已经被W3C弃用,所以本文也不再介绍了。


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

“新E代弯道王”MAZDA EZ-6鹭羽白内饰焕新

今日,“新E代弯道王”MAZDA EZ-6(以下称EZ-6)宣布鹭羽白内饰焕新,现在购车可享补贴后9.98万起。新车在现有的赤麂棕色高配内饰色之外,新增兼具时尚气质和高级质感的鹭羽白浅色内饰,不仅快速回应了部分用户对于浅色系内饰的需求,更为用户带来“增色不加价...
继续阅读 »

今日,“新E代弯道王”MAZDA EZ-6(以下称EZ-6)宣布鹭羽白内饰焕新,现在购车可享补贴后9.98万起。新车在现有的赤麂棕色高配内饰色之外,新增兼具时尚气质和高级质感的鹭羽白浅色内饰,不仅快速回应了部分用户对于浅色系内饰的需求,更为用户带来“增色不加价”的新选择。

EZ-6自推出补贴后9.98万起售的超高智价比购车模式以来,市场热度持续攀升。在春日出游季到来之际,长安马自达精准捕捉用户对浅色高质感内饰的喜好,将纤细轻柔,丝般细腻,又蓬松似云的鹭羽白色融入座椅工艺,与那些追求色泽明快简约大气的用户相得益彰,彰显出他们对高品质生活的高雅品味。

EZ-6的座椅采用了和MAZDA CX-90相同的菱形衍缝工艺,包裹性极强。Nappa真皮工艺,经鞣制后软度大幅提升,冬暖夏凉的亲肤感让身体一秒沦陷,配合10向电动调节,3档座椅通风&加热,能够满足各种身材驾驶者对理想坐姿的需求和温度需求。此外,EZ-6内饰材质均通过EPD环保产品声明、VEGAN「素食」产品、OEKO-TEX Standard 100婴儿级生态产品三大权威认证,打造让用户安全、安心更健康的乘坐体验。

作为合资B级电动轿车市场唯一同时提供增程和纯电动力选择的车型,EZ-6满足了用户全场景、全工况的出行需求。线性流畅的加速、自信安心的刹车、舒适愉悦的过弯、精准稳定的转向、迅捷的车身响应,EZ-6在电动化时代,依然能够为用户带来「人马一体」的驾乘愉悦。

目前,购EZ-6全系可享至高40,000元补贴,包括至高20,000元置换国补+15,000元置换/增购厂补+5,000元保险补贴;选择金融购车的用户可享100,000元尾款6年0息(和置换厂补二选一),在安全领域,长安马自达再次送出价值7,999元不限车主、不限里程终身零燃权益,彻底消除用户的后顾之忧无论是你的第一辆车之选,还是家庭之选,都能享受高品质的新能源出行乐趣。现在,登录长安马自达悦马星空」APP或小程序预约试驾,或亲临全国授权经销商门店试驾,即可解锁EZ-6全场景驾控乐趣

收起阅读 »

e签宝携华为鸿蒙打造全国首个"智能签署江南范式"。

3月31日鸿蒙生态在钱塘江畔绽放新活力。“HDD·浙江春日鸿蒙生态伙伴论坛”汇聚政企学研近200家单位,中国工程院院士陈纯、浙江省经信厅副厅长王忠民等嘉宾,与曹操出行、丁香园、e签宝等企业代表共同见证浙江鸿蒙生态建设新里程。目前全省已有政务、金融、教育等领域近...
继续阅读 »

3月31日鸿蒙生态在钱塘江畔绽放新活力。“HDD·浙江春日鸿蒙生态伙伴论坛”汇聚政企学研近200家单位,中国工程院院士陈纯、浙江省经信厅副厅长王忠民等嘉宾,与曹操出行、丁香园、e签宝等企业代表共同见证浙江鸿蒙生态建设新里程。目前全省已有政务、金融、教育等领域近千家机构完成鸿蒙应用适配,正构建智能物联时代的“江南范式”。

001.jpg

论坛现场,e签宝与来自政务、金融、教育等领域的众多机构企业一道,共同展示了鸿蒙生态的创新成果与价值,发布了专为鸿蒙系统的应用适配。

e签宝通过深度适配鸿蒙系统,实现了与华为生态的无缝对接,为用户带来了更加流畅、便捷、安全的电子签名与合同管理体验。

002.jpg

值得一提的是,2024年12月26日,在华为智慧办公生态峰会上,e签宝在武汉与华为鸿蒙完成了战略合作协议的签署。

作为全国领先的电子签名服务商,e签宝一直致力于推动电子签名技术的普及与应用,为政府、企业及个人用户提供高效、便捷、安全的电子签名服务。此次与鸿蒙的合作,是e签宝在拓展技术生态、提升服务品质方面的重要一步。通过鸿蒙生态的分布式架构与多设备协同能力,e签宝App得以在多场景下实现更加智能化的应用体验,为用户带来前所未有的便捷与高效。论坛期间,e签宝代表与浙江大学、华为等单位的嘉宾进行了深入交流与探讨。大家一致认为,随着数字经济的不断发展与普及,电子签名等数字化服务将成为未来发展的重要趋势。而鸿蒙作为华为在智能物联时代的重要布局,将为e签宝等合作伙伴提供更加广阔的技术舞台与市场空间。

003.jpg

浙江省作为全国首个国家信息经济示范区,将开源生态建设列为软件产业核心战略,一直以来都高度重视数字经济产业的发展。未来,e签宝将继续秉承“让签署更便捷、让信任更简单”的使命,携手鸿蒙生态及更多合作伙伴,依托浙江人才与产业优势,加速千行万业应用适配进程,共同推动数字经济的繁荣发展,为全国产业升级贡献浙江智慧。


收起阅读 »

为什么把私钥写在代码里是一个致命错误

为什么把私钥写在代码里是一个致命错误 在技术社区经常能看到一些开发者分享的教训,前几天就有人发帖讲述一位Java开发者因同事将私钥直接硬编码在代码里而感到愤怒的事情。这种情况虽然听起来可笑,但在开发团队中却相当常见,尤其对于经验不足的程序员来说。 为什么把私钥...
继续阅读 »

为什么把私钥写在代码里是一个致命错误


在技术社区经常能看到一些开发者分享的教训,前几天就有人发帖讲述一位Java开发者因同事将私钥直接硬编码在代码里而感到愤怒的事情。这种情况虽然听起来可笑,但在开发团队中却相当常见,尤其对于经验不足的程序员来说。


为什么把私钥写在代码里如此危险?


1. 代码会被分享和同步


代码通常会提交到Git或SVN等版本控制系统中。一旦私钥被提交,团队中的每个人都能看到这些敏感信息。即使后来删除了私钥,在历史记录中依然可以找到。有开发者就分享过真实案例:团队成员意外将AWS密钥提交到GitHub,结果第二天账单暴增数千元——有人利用泄露的密钥进行了挖矿活动。


2. 违反安全和职责分离原则


在规范的开发流程中,密钥管理和代码开发应该严格分离。通常由运维团队负责密钥管理,而开发人员则不需要(也不应该)直接接触生产环境的密钥。这是基本的安全实践。


3. 环境迁移的噩梦


当应用从开发环境迁移到测试环境,再到生产环境时,如果密钥硬编码在代码中,每次环境切换都需要修改代码并重新编译。这不仅效率低下,还容易出错。


正确的做法


业内已有多种成熟的解决方案:



  • 使用环境变量存储敏感信息

  • 采用专门的配置文件(确保加入.gitignore)

  • 使用AWS KMS、HashiCorp Vault等专业密钥管理系统

  • 在CI/CD流程中动态注入密钥


有开发团队就曾经花费两周时间清理代码中的硬编码密钥。其中甚至发现了一个已离职员工留下的"临时"数据库密码,注释中写着"临时用,下周改掉"——然而那个"下周"已经过去五年了。


作为专业开发者,应当始终保持良好的安全习惯。将私钥硬编码进代码,就像把家门钥匙贴在门上一样不可理喻。


这个教训值得所有软件工程师引以为戒。


作者:Asthenian
来源:juejin.cn/post/7489043337290203163
收起阅读 »

老板花一万大洋让我写的艺术工作室官网?! HeroSection 再度来袭!(Three.js)

web
引言.我不是鸽子大王!! 哈喽大家好!距离我上次发文已经过去半个月了,差点又变回了那只熟悉的“老鸽子”。不行,我不能堕落!我还没有将 Web3D 推广到普罗大众,还没有让更多人感受到三维图形的魅力 (其实是还没有捞着米)。怀着这样的心情,我决定重新振作,继续为...
继续阅读 »

引言.我不是鸽子大王!!


哈喽大家好!距离我上次发文已经过去半个月了,差点又变回了那只熟悉的“老鸽子”。不行,我不能堕落!我还没有将 Web3D 推广到普罗大众,还没有让更多人感受到三维图形的魅力 (其实是还没有捞着米)。怀着这样的心情,我决定重新振作,继续为大家带来更多关于 Three.jsShader 的进阶内容。


0.前置条件


欢迎阅读本篇文章!在深入探讨 Three.jsShader (GLSL) 的进阶内容之前,确保您已经具备以下基础知识:



  1. Three.js 基础:您需要熟悉 Three.js 的基本概念和使用方法,包括场景(Scene)、相机(Camera)、渲染器(Renderer)、几何体(Geometry)、材质(Material)和网格(Mesh)等核心组件。如果您还不熟悉这些内容,建议先学习 Three.js 的入门教程。

  2. Shader 语法:本文涉及 GLSL(OpenGL Shading Language)的编写,因此您需要了解 GLSL 的基本语法,包括顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)的编写,以及如何在 Three.js 中使用自定义着色器。


1. Hero Section 概览



Hero Section 是网页设计中的一个术语,通常指页面顶部的一个大型横幅区域。但对于开发人员而言,这个概念可以更直观地理解为用户在访问网站的瞬间所感受到的视觉冲击,或者促使用户停留在该网站的关键原因因素



话说这天老何接到了一个私活


chat.png


起始钱不钱的无所谓!主要是想挑战一下自己(不是)。最后的成品如图所示 (互动方式为鼠标滑动 + 鼠标点击 GIF 压缩太多了内容了,实际要好看很多)。


01.gif


PC端在线预览地址: fluid-light.vercel.app


Debug调试界面: fluid-light.vercel.app/#debug


源码地址: github.com/hexianWeb/f…


2.基础场景搭建


首先我来为你解读一下这个场景里面有什么,他非常简单。只有一个可以接受光照影响的平面几何体以及数个点光源构成,仅此而已。


让我去掉后处理以及一些页面文本元素展示给你看


fluidLight04.gif


构建这样的一个基础场景不难。


2.1 构建平面几何体


让我们先来解决平面几何体


值得注意的是,为了让显示效果更好,我使用了正交相机并让平面覆盖整个视窗大小


    this.geometry = new THREE.PlaneGeometry(2 * 屏幕宽高比, 2);

然后构建相应的物理材质,可以去 polyhaven 下载一些自己喜欢的texture 并下载下来。


Snipaste_2025-03-05_14-26-01.png
根据右边的分类选择纹理大类细分,随后选择想要下载的纹理点击下载。


因为我们本质是需要 Displacement Texture置换贴图 & Normal Texture 法线贴图


所以不需要太在意这个纹理是作用在什么物件上面的


Snipaste_2025-03-05_14-30-09.png


随后将纹理导入后赋予材质相应的属性,并对部分参数进行调整。通常直接赋予displacementMapThreejs 中显示平面的凹凸会特别明显。所以记得通过


displacementScale来调整相应的大小。


    this.material = new THREE.MeshPhysicalMaterial({
color: '#121423',
metalness: 0.59,
roughness: 0.41,
displacementMap: 下载的纹理贴图,
displacementScale: 0.1,
normalMap: 下载的法线贴图,
normalScale: new THREE.Vector2(0.68, 0.75),
side: THREE.FrontSide
});

最后将物体加入场景即可


    this.mesh = new THREE.Mesh(this.geometry, this.material);
scene.add(this.mesh);

(tips:MeshStandardMaterialMeshPhysicalMaterial 适合需要真实感光照和复杂物理特性的场景,但性能消耗较高。如果您的电脑出现卡顿可以选择消耗较少性能的物理材质)


2.2 灯光加入战场


在本案例中,高级感的来源之一就是灯光的变换。如果您足够细心,可能会注意到一些更微妙的细节:场景中的灯光并不是简单地从 A Color 切换到 B Color,而是同时存在多个光源,并且它们的强度也在动态变化。这种设计使得场景的光影效果更加丰富和立体。


03.gif


如果场景中只有一个光源,效果可能会显得单调。而本案例中,灯光的变化呈现出一种层次感:中间是白色,周围还有一层类似年轮的光圈,最后逐渐扩散为纯色背景。这种效果的关键在于同一时间场景中存在多个点光源。虽然多个光源会显著增加性能消耗,但为了实现唯美的视觉效果,这是值得的。


让我们逐步分析灯光是如何实现的。


1. 封装创建点光源的函数


为了简化代码并提高复用性,我们可以先封装一个创建点光源的函数。这个函数会返回一个包含光源对象和目标颜色的对象。


  createPointLight(intensity) {
const light = new THREE.PointLight(
0xff_ff_ff,
intensity,
100,
Math.random() * 10
);
light.position.copy(this.lightPosition); //所有的光源都同步在一个位置
return {
object: light,
targetColor: new THREE.Color()
};
}

2. 生成多个点光源


接下来,我们可以调用这个函数生成多个点光源,并将它们添加到场景中。


this.colors = [
new THREE.Color('orange'),
new THREE.Color('red'),
new THREE.Color('red'),
new THREE.Color('orange'),
new THREE.Color('lightblue'),
new THREE.Color('green'),
new THREE.Color('blue'),
new THREE.Color('blue')
];
this.lights = [
this.createPointLight(2),
this.createPointLight(3),
this.createPointLight(2.5),
this.createPointLight(10),
this.createPointLight(2),
this.createPointLight(3),
];

// 初始化灯光颜色
const numberLights = this.lights.length;
for (let index = 0; index < numberLights; index++) {
const colorIndex = Math.min(index, this.colors.length - 1);
this.lights[index].object.color.copy(this.colors[colorIndex]);
}

for (const light of this.lights) this.scene.add(light.object);

3. 动态调整光源强度


在场景中,所有光源同时存在,但它们的强度会有所不同。每次由光照强度为 10 的光源担任场景的主色。当用户点击场景时,灯光会像上楼梯或者传送带一样逐步切换,即由新的点光源担任场景主色。


aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
8 8"b, "Ya
8 8 "b, "Ya
8 aa=D光源=a8, "b, "Ya
8 8"b, "Ya "8""""""8
8 8 "
b, "Ya 8 8
8 a=C光源=8, "
b, "Ya8 8
8 8"
b, "Ya "8""""""" 8
8 8 "
b, "Ya 8 8
8 a=B光源=8, "
b, "Ya8 8
8 8"
b, "Ya "8""""""" 8
8 8 "
b, "Ya 8 8
8=A光源=, "
b, "Ya8 8
8"
b, "Ya "8""""""" 8
8 "
b, "Ya 8 8
8, "
b, "Ya8 8
"
Ya "8""""""" 8
"Ya 8 8
"
Ya8 8
"""""""""""""""""""""""""""""""""""""

让我们看看代码是如何实现的吧


    window.addEventListener('click', () => {
// 打乱颜色数组(看个人喜好)
this.colors = [...this.colors.sort(() => Math.random() - 0.5)];
// 标记开始颜色过渡
this.colorTransition = true;

// 为每个灯光设置目标颜色
const numberLights = this.lights.length;
for (let index = 0; index < numberLights; index++) {
const colorIndex = Math.min(index, this.colors.length - 1);
this.lights[index].targetColor = this.colors[colorIndex].clone();
}
});

然后再Render函数中以easeing方式更新颜色


  update() {
// 只在需要时更新颜色
if (this.colorTransition) {
const numberLights = this.lights.length;
const baseSmooth = 0.25;
const smoothIncrement = 0.05;

let allTransitioned = true; // 检查所有颜色是否已完成过渡

for (let index = 0; index < numberLights; index++) {
const smoothTime = baseSmooth + index * smoothIncrement;

// 使用目标颜色进行平滑过渡
const currentColor = this.lights[index].object.color;
const targetColor = this.lights[index].targetColor;

this.dampC(currentColor, targetColor, smoothTime, delta);

// 检查是否还在过渡
if (!this.isColorClose(currentColor, targetColor)) {
allTransitioned = false;
}
}

// 如果所有颜色都已完成过渡,停止更新
if (allTransitioned) {
this.colorTransition = false;
}
}
}

03.gif


3.后处理完善场景


在完成了场景的基本构建之后,我们已经实现了大约 80% 的内容。即使现在加上 UI,效果也不会太差。不过,为了让场景更具视觉冲击力和艺术感,我们可以通过后处理(Post Processing)技术来进一步提升质感。


04.gif


使用 UnrealBloomPassFilmPass


在本文中,我们将使用 UnrealBloomPass(辉光效果)和 FilmPass(电影滤镜)来增强场景的视觉效果。以下是具体的实现步骤:



  1. 引入后处理库:首先,我们需要引入 Three.js 的后处理库 EffectComposer 以及相关的 Pass 类。

  2. 创建 EffectComposerEffectComposer 是后处理的核心类,用于管理和执行各种后处理效果。

  3. 添加 RenderPassRenderPass 用于将场景渲染到后处理管道中。

  4. 添加 UnrealBloomPassUnrealBloomPass 用于实现辉光效果,可以使场景中的亮部区域产生光晕。

  5. 添加 FilmPassFilmPass 用于模拟电影胶片的效果,增加颗粒感和复古风格。


这里的具体参数需要看个人品味进行调试。同款参数可以从这里看我的源码。具体路径位于src\js\world\effect.js


    this.composer = new EffectComposer(this.renderer);
this.composer.addPass(this.renderPass);
this.composer.addPass(this.bloomPass);
this.composer.addPass(this.filmPass);

此时页面的质感是不是一下就上来了呢?


05.gif


最后我们需要添加最关键的一部,就是画面扭曲。


这里我们需要用到 ThreejsShaderPass,让我们来创建一个初始的ShaderPass,仅将 EffectComposer 的读取缓冲区的图像内容复制到其写入缓冲区,而不应用任何效果。


具体内容你可以从 Threejs 后处理中了解到更多


import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';

const BaseShader = {

name: 'BaseShader',

uniforms: {

'tDiffuse': { value: null },
'opacity': { value: 1.0 }

},

vertexShader: /* glsl */`

varying vec2 vUv;

void main() {

vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

}`
,

fragmentShader: /* glsl */`

uniform float opacity;

uniform sampler2D tDiffuse;

varying vec2 vUv;

void main() {

vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;


}`


};

const BasePass = new ShaderPass( BaseShader );

此时画面不会有任何变化


让我们对uv进行简单操纵,让其读取tDiffuse时可以发生扭曲


      vec2 uv = vUv;
uv.y += sin(uv.x * frequency + offset) * amplitude;
gl_FragColor = texture2D(tDiffuse, uv);

最后得到效果


06.gif


4.最后一些话


技术的未来与前端迁移


随着 AI 技术的快速发展,各类技术的门槛正在大幅降低,以往被视为高门槛的 3D 技术也不例外。与此同时,过去困扰开发者的数字资产构建成本问题,也正在被最新的 3D generation 技术所攻克。这意味着,在不久的将来,前端开发将迎来一次技术迁移,开发者需要掌握更新颖的交互方式和更出色的视觉效果。


为什么选择 Three.js


Three.js 作为最流行的 WebGL 库之一,不仅简化了三维图形的开发流程,还提供了丰富的功能和强大的扩展性。无论是创建复杂的 3D 场景,还是实现炫酷的视觉效果,Three.js 都能帮助开发者快速实现目标。


本专栏的愿景


本专栏的愿景是通过分享 Three.js 的中高级应用和实战技巧,帮助开发者更好地将 3D 技术应用到实际项目中,打造令人印象深刻的 Hero Section。我们希望通过本专栏的内容,能够激发开发者的创造力,推动 Web3D 技术的普及和应用。


加入社区,共同成长


如果您对 Threejs 这个 3D 图像框架很感兴趣,或者您也深信未来国内会涌现越来越多 3D 设计风格的网站,欢迎加入 ice 图形学社区。这里是国内 Web 图形学最全的知识库,致力于打造一个全新的图形学生态体系!您可以在认证达人里找到我这个 Threejs 爱好者和其他大佬。


此外,如果您很喜欢 Threejs 又在烦恼其原生开发的繁琐,那么我诚邀您尝试 TresjsTvTjs, 他们都是基于 VueThreejs 框架。 TvTjs 也为您提供了大量的可使用案例,并且拥有较为活跃的开发社区,在这里你能碰到志同道合的朋友一起做开源!


5.下期预告


未来科技?机器人概念官网来袭 !!!


08.gif


6. 往期回顾


2025 年了,我不允许有前端不会用 Trae 让页面 Hero Section 变得高级!!!(Threejs)


2024年了,前端人是时候给予页面一点 Hero Section 魔法了!!! (Three.js)


2023 年了,还有前端人不知道 commit 规范 ?


作者:何贤
来源:juejin.cn/post/7478403990141796352
收起阅读 »

前端如何优雅通知用户刷新页面?

web
前言老板:新的需求不是上线了嘛,怎么用户看到的还是老的页面呀窝囊废:让用户刷新一下页面,或者清一下缓存老板:那我得告诉用户,刷新一下页面,或者清一下缓存,才能看到新的页面呀,感觉用户体验不好啊,不能直接刷新页面嘛?窝囊废:可以解决(OS:一点改的必要没有,用户...
继续阅读 »

前言

老板:新的需求不是上线了嘛,怎么用户看到的还是老的页面呀
窝囊废:让用户刷新一下页面,或者清一下缓存
老板:那我得告诉用户,刷新一下页面,或者清一下缓存,才能看到新的页面呀,感觉用户体验不好啊,不能直接刷新页面嘛?
窝囊废:可以解决(OS:一点改的必要没有,用户全是大聪明)

产品介绍

c端需要经常进行一些文案调整,一些老版的文字字眼可能会导致一些舆论问题,所以就需要更新之后刷新页面,让用户看到新的页面。

思考问题为什么产生

项目是基于vue的spa应用,通过nginx代理静态资源,配置了index.html协商缓存,js、css等静态文件Cache-Control,按正常前端重新部署后, 用户重新访问系统,已经是最新的页面。

但是绝大部份用户都是访问页面之后一直停留在此页面,这时候前端部署后,用户就无法看到新的页面,需要用户刷新页面。

产生问题

  • 如果后端接口有更新,前端重新部署后,用户访问老的页面,可能会导致接口报错。
  • 如果前端部署后,用户访问老的页面,可能无法看到新的页面,需要用户刷新页面,用户体验不好。
  • 出现线上bug,修复完后,用户依旧访问老的页面,仍会遇到bug。

解决方案

  1. 前后端配合解决
  • WebSocket
  • SSE(Server-Send-Event)
  1. 纯前端方案 以下示例均以vite+vue3为例;
  • 轮询html Etag/Last-Modified

在App.vue中添加如下代码

const oldHtmlEtag = ref();
const timer = ref();
const getHtmlEtag = async () => {
const { protocol, host } = window.location;
const res = await fetch(`${protocol}//${host}`, {
headers: {
"Cache-Control": "no-cache",
},
});
return res.headers.get("Etag");
};

oldHtmlEtag.value = await getHtmlEtag();
clearInterval(timer.value);
timer.value = setInterval(async () => {
const newHtmlEtag = await getHtmlEtag();
console.log("---new---", newHtmlEtag);
if (newHtmlEtag !== oldHtmlEtag.value) {
Modal.destroyAll();
Modal.confirm({
title: "检测到新版本,是否更新?",
content: "新版本内容:",
okText: "更新",
cancelText: "取消",
onOk: () => {
window.location.reload();
},
});
}
}, 30000);
  • versionData.json

自定义plugin,项目根目录创建/plugins/vitePluginCheckVersion.ts

import path from "path";
import fs from "fs";
export function checkVersion(version: string) {
return {
name: "vite-plugin-check-version",
buildStart() {
const now = new Date().getTime();
const version = {
version: now,
};
const versionPath = path.join(__dirname, "../public/versionData.json");
fs.writeFileSync(versionPath, JSON.stringify(version), "utf8", (err) => {
if (err) {
console.log("写入失败");
} else {
console.log("写入成功");
}
});
},
};
}

在vite.config.ts中引入插件

import { checkVersion } from "./plugins/vitePluginCheckVersion";
plugins: [
vue(),
checkVersion(),
]

在App.vue中添加如下代码

const timer = ref()
const checkUpdate = async () => {
let res = await fetch('/versionData.json', {
headers: {
'Cache-Control': 'no-cache',
},
}).then((r) => r.json())
if (!localStorage.getItem('demo_version')) {
localStorage.setItem('demo_version', res.version)
} else {
if (res.version !== localStorage.getItem('demo_version')) {
localStorage.setItem('demo_version', res.version)
Modal.confirm({
title: '检测到新版本,是否更新?',
content: '新版本内容:' + res.content,
okText: '更新',
cancelText: '取消',
onOk: () => {
window.location.reload()
},
})
}
}
}

onMounted(()=>{
clearInterval(timer.value)
timer.value = setInterval(async () => {
checkUpdate()
}, 30000)
})

Use

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { webUpdateNotice } from '@plugin-web-update-notification/vite'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
webUpdateNotice({
logVersion: true,
}),
]
})

作者:李暖阳啊
来源:juejin.cn/post/7439905609312403483

收起阅读 »

部署项目,console.log为什么要去掉?

web
console.log的弊端 1. 影响性能(轻微但可优化) console.log 会占用 内存 和 CPU 资源,尤其是在循环或高频触发的地方(如 mousemove 事件)。 虽然现代浏览器优化了 console,但大量日志仍可能导致 轻微性能下降。 2...
继续阅读 »

console.log的弊端


1. 影响性能(轻微但可优化)


console.log 会占用 内存 和 CPU 资源,尤其是在循环或高频触发的地方(如 mousemove 事件)。
虽然现代浏览器优化了 console,但大量日志仍可能导致 轻微性能下降


2. 暴露敏感信息(安全风险)


可能会 泄露 API 接口、Token、用户数据 等敏感信息,容易被恶意利用。


3. 干扰调试(影响开发者体验)


生产环境日志过多,可能会 掩盖真正的错误信息,增加调试难度。
开发者可能会误以为某些 console.log 是 预期行为,而忽略真正的 Bug。


4. 增加代码体积(影响加载速度)


即使 console.log 本身很小,但 大量日志 会增加打包后的文件体积,影响 首屏加载速度


解决方案:移除生产环境的 console.log


1. 使用 Babel 插件


在 babel.config.js 中配置:


module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
]
,
plugins: [
['@babel/plugin-proposal-optional-chaining']
,
...process.env.NODE_ENV === 'production' ? [['transform-remove-console', { exclude: ['info', 'error', 'warn'] }]] : []
]
}

特点



  • 不影响源码,仅在生产环境生效,开发环境保留完整 console

  • 配置简单直接,适合快速实现基本需求。

  • 依赖 Babel 插件


2. 使用 Terser 压缩时移除(Webpack/Vite 默认支持)


在 vite.config.js 或 webpack.config.js 中配置:


module.exports = {
chainWebpack: (config) => {
config.optimization.minimizer("terser").tap((args) => {
args[0].terserOptions.compress = {
...args[0].terserOptions.compress,
drop_console: true, // 移除所有 console
pure_funcs: ["console.log"], // 只移除 console.log,保留其他
};
return args;
});
},
};

特点



  • 不影响源码,仅在生产环境生效,开发环境保留完整 console

  • 避免 Babel 插件兼容性问题

  • 需要额外配置


3. 自定义 console 包装函数(按需控制)


// utils/logger.js
const logger = {
log: (...args) => {
if (process.env.NODE_ENV !== "production") {
console.log("[LOG]", ...args);
}
},
warn: (...args) => {
console.warn("[WARN]", ...args);
},
error: (...args) => {
console.error("[ERROR]", ...args);
},
};

export default logger;

使用方式


import logger from "./utils/logger";

logger.log("Debug info"); // 生产环境自动不打印
logger.error("Critical error"); // 始终打印

特点



  • 可以精细控制日志,可控性强,可以自定义日志级别。

  • 不影响 console.warn 和 console.error

  • 需要手动替换 console.log


作者:用户2612458340161
来源:juejin.cn/post/7485938326336766003
收起阅读 »

只写后台管理的前端要怎么提升自己

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。 写优雅的代码 一道面试题 大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单...
继续阅读 »

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。


写优雅的代码


一道面试题


大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。


原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb,而我要展示成 KBMB 等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):


function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;

while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024;
unitIndex++;
}

return `${kb.toFixed(2)} ${units[unitIndex]}`;
}

而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:


function formatSizeUnits(kb) {
var result = '';

if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}

return result;
}

虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。


如何提升代码质量


想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。


还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。


还是上面的问题,看看 GPT 给的答案


// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。

/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/

function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);

// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}

// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);

// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}

// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB

还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)


我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。


学会封装


一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?


你说,没时间,没必要,复制粘贴反而更快。


那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。


而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。


关注业务


对于前端业务重要吗?


相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。


但是就我找工作的经验,业务非常重要!


如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。


一场面试


还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。



  • 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”

  • 我:“好嘞!”


等到面试的时候:



  • 前端ld:“你知道xxx吗?(业务名词)”

  • 我:“我……”

  • 前端ld:“那xxxx呢?(业务名词)”

  • 我:“不……”

  • 前端ld:“那xxxxx呢??(业务名词)”

  • 我:“造……”


然后我就挂了………………


如何了解业务



  1. 每次接需求的时候,都要了解需求背景,并主动去理解


    我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么 cluster controller topic broker partition…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。


  2. 每次做完一个需求,都需要了解结果


    有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?


  3. 理解需求,并主动去优化


    产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?


    产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。


    其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。



关注源码


说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。


除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。


那说什么,后台管理就这些啊?!


如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?


可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点


至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?



讲一下 Axios 源码中,拦截器是怎么实现的?


Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。


在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含 fulfilledrejected 函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。


以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:


class InterceptorManager {
constructor() {
this.handlers = []; // 存储拦截器的数组
}

use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1; // 返回拦截器的ID
}

eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 移除拦截器
}
}

forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
}
}

在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过 forEach 方法将拦截器中的 fulfilledrejected 函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。


axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的 .then.catch 执行之前,插入自定义的逻辑。


请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。



前端基建


当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。


技术选型


技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表单配置化方案,比如Formily?


对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……


image.png

不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)


Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。


React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。


总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。


开发规范


这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlintstylelintprettiercommitlint 等。


前端监控


干了这么多年前端,前端监控我是……一点没做过。


image.png

前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。


对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。


对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerrorwindow.addEventListener('unhandledrejection', ...) 去分别捕获同步和异步错误,然后通过错误信息和 sourceMap 来定位到源码。


对于性能监控,我们可以通过 window.performancePerformanceObserver 等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。


最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon 还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。


CI/CD


持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。


场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。


这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline 、 Stage 和 Job 分别是什么,怎么配置,如何在不同环境配置不同工作流等。


了解技术动态


这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。


比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。


还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……


虽然不可能学完每一项新技术,但是可以多去了解下。


总结


写了这么多,可能有人会问,如果能回到过去,你会怎么做。


啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。


image.png

作者:我不吃饼干
来源:juejin.cn/post/7360528073631318027
收起阅读 »

双Token无感刷新方案

提醒一下 双Token机制并没有从根本上解决安全性的问题,本文章只是提供一个思路,具体是否选择请大家仔细斟酌考虑,笔者水平有限,非常抱歉对你造成不好的体验。 token有效期设置问题 最近在做用户认证模块的后端功能开发,之前就有一个问题困扰了我好久,就是如何设...
继续阅读 »

提醒一下


双Token机制并没有从根本上解决安全性的问题,本文章只是提供一个思路,具体是否选择请大家仔细斟酌考虑,笔者水平有限,非常抱歉对你造成不好的体验。


token有效期设置问题


最近在做用户认证模块的后端功能开发,之前就有一个问题困扰了我好久,就是如何设置token的过期时间,前端在申请后端登录接口成功之后,会返回一个token值,存储在用户端本地,用户要访问后端的其他接口必须通过请求头带上这个token值,但是这个token的有效期应该设置为多少?



  1. 如果设置的太短,比如1小时,那么用户一小时之后。再访问其他接口,需要再次重新登录,对用户的体验极差

  2. 如果设置为一个星期,那么在这个时间内







      • 一旦token泄露,攻击者可长期冒充用户身份,直到token过期,服务端无法限制其访问用户数据

      • 虽然可以依赖黑名单机制,但会增加系统复杂度,还要进行系统监测

      • 如果在这段时间恶意用户利用未过期的条款持续调用后端API将会导致资源耗尽或产生巨额费用






所以有没有两者都兼顾的方案呢?


双token无感刷新方案


传统的token方案要么频繁要求用户重新登录,要么面临长期有效的安全风险


但是双token无感刷新机制,通过组合设计,在保证安全性的情况下,实现无感知的认证续期


核心设计



  1. access_token:访问令牌,有效期一般设置为15~30分钟,主要用于对后端请求API的交互

  2. refresh_token:刷新令牌,一般设置为一个星期到一个月,主要用于获取新的access_token


大致的执行流程如下


用户登录之后,后端返回access_tokenrefresh_token响应给前端,前端将两个token存储在用户本地



在用户端发起前端请求,访问后端接口,在请求头中携带上access_token



前端会对access_token的过期时间进行检测,当access_token过期前一分钟,前端通过refresh_token向后端发起请求,后端判断refresh_token是否有效,有效则重新获取新的access_token,返回给前端替换掉之前的access_token存储在用户本地,无效则要求用户重新认证



这样的话对于用户而言token的刷新是无感知的,不会影响用户体验,只有当refresh_token失效之后,才需要用户重新进行登录认证,同时,后端可以通过对用户refresh_token的管理来限制用户对后端接口的请求,大大提高了安全性


有了这个思路,写代码就简单了


@Service
public class LoginServiceImpl implements LoginService {

@Autowired
private JwtUtils jwtUtils;

// token过期时间
private static final Integer TOKEN_EXPIRE_DAYS =5;
// token续期时间

private static final Integer TOKEN_RENEWAL_MINUTE =15;

@Override
public boolean verify(String refresh_token) {
Long uid = jwtUtils.getUidOrNull(refresh_token);
if (Objects.isNull(uid)) {
return false;
}
String key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN,uid);
String realToken = RedisUtils.getStr(key);
return Objects.equals(refresh_token, realToken);
}

@Override
public void renewalTokenIfNecessary(String refresh_token) {
Long uid = jwtUtils.getUidOrNull(refresh_token);
if (Objects.isNull(uid)) {
return;
}
String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);
long expireSeconds = RedisUtils.getExpire(refresh_key, TimeUnit.SECONDS);
if (expireSeconds == -2) { // key不存在,refresh_token已过期
return;
}
String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);
RedisUtils.expire(access_key, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
}

@Override
@Transactional(rollbackFor = Exception.class)
@RedissonLock(key = "#uid")
public LoginTokenResponse login(Long uid) {
String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);
String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);
String refresh_token = RedisUtils.getStr(refresh_key);
String access_token;
if (StrUtil.isNotBlank(refresh_token)) { //刷新令牌不为空
access_token = jwtUtils.createToken(uid);
RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
return LoginTokenResponse.builder()
.refresh_token(refresh_token).access_token(access_token)
.build();
}
refresh_token = jwtUtils.createToken(uid);
RedisUtils.set(refresh_key, refresh_token, TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);
access_token = jwtUtils.createToken(uid);
RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
return LoginTokenResponse.builder()
.refresh_token(refresh_token).access_token(access_token)
.build();
}
}}

注意事项



  1. 安全存储Refresh Token时,优先使用HttpOnly+Secure Cookie而非LocalStorage

  2. 在颁发新Access Token时,重置旧Token的生存周期(滑动过期)而非简单续期

  3. 针对高敏感操作(如支付、改密),建议强制二次认证以突破Token机制的限制


安全问题


双Token机制并没有从根本上解决安全性的问题,它只是尝试通过改进设计,优化用户体验,全面的安全策略需要多层防护,分别针对不同类型的威胁和风险,而不仅仅依赖于Token的管理方式或数量


安全是一个持续对抗的过程,关键在于提高攻击者的成本,而非追求绝对防御。


"完美的认证方案不存在,但聪明的权衡永远存在。"


本笔者水平有限,望各位海涵


如果文章中有不对的地方,欢迎大家指正。


作者:昔年种柳
来源:juejin.cn/post/7486782063422717962
收起阅读 »

程序员,你使用过灰度发布吗?

大家好呀,我是猿java。 在分布式系统中,我们经常听到灰度发布这个词,那么,什么是灰度发布?为什么需要灰度发布?如何实现灰度发布?这篇文章,我们来聊一聊。 1. 什么是灰度发布? 简单来说,灰度发布也叫做渐进式发布或金丝雀发布,它是一种逐步将新版本应用到生产...
继续阅读 »

大家好呀,我是猿java


在分布式系统中,我们经常听到灰度发布这个词,那么,什么是灰度发布?为什么需要灰度发布?如何实现灰度发布?这篇文章,我们来聊一聊。


1. 什么是灰度发布?


简单来说,灰度发布也叫做渐进式发布金丝雀发布,它是一种逐步将新版本应用到生产环境中的策略。相比于一次性全量发布,灰度发布可以让我们在小范围内先行测试新功能,监控其表现,再决定是否全面推开。这样做的好处是显而易见的:



  1. 降低风险:新版本如果存在 bug,只影响少部分用户,减少了对整体用户体验的冲击。

  2. 快速回滚:在小范围内发现问题,可以更快地回到旧版本。

  3. 收集反馈:可以在真实环境中收集用户反馈,优化新功能。


2. 原理解析


要理解灰度发布,我们需要先了解一下它的基本流程:



  1. 准备阶段:在生产环境中保留旧版本,同时引入新版本。

  2. 小范围发布:将新版本先部署到一小部分用户,例如1%-10%。

  3. 监控与评估:监控新版本的性能和稳定性,收集用户反馈。

  4. 逐步扩展:如果一切正常,将新版本逐步推广到更多用户。

  5. 全面切换:当确认新版本稳定后,全面替换旧版本。


在这个过程中,关键在于如何切分流量,确保新旧版本平稳过渡。常见的切分方式包括:



  • 基于用户ID:根据用户的唯一标识,将部分用户指向新版本。

  • 基于地域:先在特定地区进行发布,观察效果后再扩展到其他地区。

  • 基于设备:例如,先在Android或iOS用户中进行发布。


3. 示例演示


为了更好地理解灰度发布,接下来,我们通过一个简单的 Java示例来演示基本的灰度发布策略。假设我们有一个简单的 Web应用,有两个版本的登录接口/login/v1/login/v2,我们希望将百分之十的流量引导到v2,其余流量继续使用v1


3.1 第一步:引入灰度策略


我们可以通过拦截器(Interceptor)来实现流量的切分。以下是一个基于Spring Boot的简单实现:


import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Random;

@Component
public class GrayReleaseInterceptor implements HandlerInterceptor {

private static final double GRAY_RELEASE_PERCENT = 0.1; // 10% 流量

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if ("/login".equals(uri)) {
if (isGrayRelease()) {
// 重定向到新版本接口
response.sendRedirect("/login/v2");
return false;
} else {
// 使用旧版本接口
response.sendRedirect("/login/v1");
return false;
}
}
return true;
}

private boolean isGrayRelease() {
Random random = new Random();
return random.nextDouble() < GRAY_RELEASE_PERCENT;
}
}

3.2 第二步:配置拦截器


在Spring Boot中,我们需要将拦截器注册到应用中:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private GrayReleaseInterceptor grayReleaseInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(grayReleaseInterceptor).addPathPatterns("/login");
}
}

3.3 第三步:实现不同版本的登录接口


import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/login")
public class LoginController {

@GetMapping("/v1")
public String loginV1(@RequestParam String username, @RequestParam String password) {
// 旧版本登录逻辑
return "登录成功 - v1";
}

@GetMapping("/v2")
public String loginV2(@RequestParam String username, @RequestParam String password) {
// 新版本登录逻辑
return "登录成功 - v2";
}
}

在上面三个步骤之后,我们就实现了登录接口地灰度发布:



  • 当用户访问/login时,拦截器会根据设定的灰度比例(10%)决定请求被重定向到/login/v1还是/login/v2

  • 大部分用户会体验旧版本接口,少部分用户会体验新版本接口。


3.4 灰度发布优化


上述示例,我们只是一个简化的灰度发布实现,实际生产环境中,我们可能需要更精细的灰度策略,例如:



  1. 基于用户属性:不仅仅是随机切分,可以根据用户的地理位置、设备类型等更复杂的条件。

  2. 动态配置:通过配置中心动态调整灰度比例,无需重启应用。

  3. 监控与告警:集成监控系统,实时监控新版本的性能指标,异常时自动回滚。

  4. A/B 测试:结合A/B测试,进一步优化用户体验和功能效果。


grayscale-release.png


4. 为什么需要灰度发布?


在实际工作中,为什么我们要使用灰度发布?这里我们总结了几个重要的原因。


4.1 降低发布风险


每次发布新版本,尤其是功能性更新或架构调整,都会伴随着一定的风险。即使经过了充分的测试,实际生产环境中仍可能出现意想不到的问题。灰度发布通过将新版本逐步推向部分用户,可以有效降低全量发布可能带来的风险。


举个例子,假设你上线了一个全新的支付功能,直接面向所有用户开放。如果这个功能存在严重 bug,可能导致大量用户无法完成支付,甚至影响公司声誉。而如果采用灰度发布,先让10%的用户体验新功能,发现问题后只需影响少部分用户,修复起来也更为迅速和容易。


4.2 快速回滚


在传统的全量发布中,一旦发现问题,回滚到旧版本可能需要耗费大量时间和精力,尤其是在高并发系统中,数据状态的同步与恢复更是复杂。而灰度发布由于新版本只覆盖部分流量,问题定位和回滚变得更加简单和快速。


比如说,你在灰度发布阶段发现新版本的某个功能在某些特定条件下会导致系统崩溃,立即可以停止向新用户推送这个版本,甚至只针对受影响的用户进行回滚操作,而不用影响全部用户的正常使用。


4.3 实时监控与反馈


灰度发布让你有机会在真实的生产环境中监控新版本的表现,并收集用户的反馈。这些数据对于评估新功能的实际效果至关重要,有助于做出更明智的决策。


举个具体的场景,你新增了一个推荐算法,希望提升用户的点击率。在灰度发布阶段,你可以监控新算法带来的点击率变化、服务器负载情况等指标,确保新算法确实带来了预期的效果,而不是引入了新的问题。


4.4 提升用户体验


通过灰度发布,你可以在推出新功能时,逐步优化用户体验。先让一部分用户体验新功能,收集他们的使用反馈,根据反馈不断改进,最终推出一个更成熟、更符合用户需求的版本。


举个例子,你开发了一项新的用户界面设计,直接全量发布可能会让一部分用户感到不适应或不满意。灰度发布允许你先让一部分用户体验新界面,收集他们的意见,进行必要的调整,再逐步扩大使用范围,确保最终发布的版本能获得更多用户的认可和喜爱。


4.5 支持A/B测试


灰度发布是实现A/B测试的基础。通过将用户随机分配到不同的版本,你可以比较不同版本的表现,选择最优方案进行全面推行。这对于优化产品功能和提升用户体验具有重要意义。


比如说,你想测试两个不同的推荐算法,看哪个能带来更高的转化率。通过灰度发布,将用户随机分配到使用算法A和算法B的版本,比较它们的表现,最终选择效果更好的算法进行全面部署。


4.6 应对复杂的业务需求


在一些复杂的业务场景中,全量发布可能无法满足灵活的需求,比如分阶段推出新功能、针对不同用户群体进行差异化体验等。灰度发布提供了更高的灵活性和可控性,能够更好地适应多变的业务需求。


例如,你正在开发一个面向企业用户的新功能,希望先让部分高价值客户试用,收集他们的反馈后再决定是否全面推广。灰度发布让这一过程变得更加顺畅和可控。


5. 总结


本文,我们详细地分析了灰度发布,它是一种强大而灵活的部署策略,能有效降低新版本上线带来的风险,提高系统的稳定性和用户体验。作为Java开发者,掌握灰度发布的原理和实现方法,不仅能提升我们的技术能力,还能为团队的项目成功保驾护航。


对于灰度发布,如果你有更多的问题或想法,欢迎随时交流!


6. 学习交流


如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7488321730764603402
收起阅读 »

因网速太慢我把20M+的字体压缩到了几KB

web
于水增 故事背景 事情起源于之前做的海报编辑器,自己调试时无意中发现字体渲染好慢,第一反应就是网怎么变慢了,断网了?仔细一看才发现,淦!这几个字体资源咋这么大,难怪网速变慢了呢😁😁。 图片中的海报包含6种字体,其中最大的字体文件超过20M,而最长的网络加载...
继续阅读 »

于水增



故事背景


事情起源于之前做的海报编辑器,自己调试时无意中发现字体渲染好慢,第一反应就是网怎么变慢了,断网了?仔细一看才发现,淦!这几个字体资源咋这么大,难怪网速变慢了呢😁😁。



图片中的海报包含6种字体,其中最大的字体文件超过20M,而最长的网络加载时长已接近20s。所以海报实际效果图展示耗时太久,很影响用户体验。那就趁此机会跟大家聊聊 字体 这件小事。


字体文件为什么那么大?


🙋 DeepSeek同学来回答下大家:


这里所说的大体积的字体资源多数是指中文主要原因下边两点



  • 中文字符数量庞大,英文仅 26 个字母 + 符号,中文(全字符集)包含 70,000+ 字符

  • 字形结构复杂,字体文件需为每个字符存储独立的矢量轮廓数据,而汉字笔画复杂,每个字符需存储数百个控制点坐标(例如「龍」字的轮廓点数量可能是「A」的 10 倍以上)


总结下来就是咱们不光汉字多,书法也是五花八门,它是真小不了。如果你硬要压缩,我们只能从第一点入手,将字符数量进行缩减,比如保留 1000 个常用汉字。


web网站中常见字体格式



由于我司物料部门提供的为TTF格式,所以这里通过 思源黑体 给一个直观的对比:



  • TTF 文件:16.9 MB

  • WOFF2 文件:7.4 MB(压缩率约 60%)


两者为什么会差这么多,其实WOFF2 只是在 TTF/OTF 基础上添加了压缩和 Web 专用元数据,且WOFF2支持增量解码,也就是边下载边解析,文本可更快显示(即使字体未完全加载,不过有待考证)。


TTF有办法优化吗?


回归问题本身


首先来简单回顾下我们自定义的字体是如何在浏览器中完成渲染的


一般情况下我们对字体文件的引用方式为下边三种



  • 通过绝对路径来引用,这种就是将字体文件打包在工程内,所以带来的结果就是工程打包文件体积太大


@font-face {
font-family: 'xxx';
src: url('../../assets/fonts.woff2')
}


  • 第二种就是 CDN 中存放的字体文件,一般是通过这种方式来减少工程的编译后体积


@font-face {
font-family: 'xxx';
src: url('https://xxx.woff2')
}


  • 通过 FontFace 构造一个字体对象


前两种一般是在浏览器构建 CSSOM 时,当遇到**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(243, 243, 243);">url()</font>** 引用时会发起资源请求。第三种则是通过 js 来控制字体的加载流程,所以归根结底就是字体文件太大,导致网络资源下载速度慢,我们只能从优化字体大小的方向入手


确定解决方向


下面汇总下查到的具体几个优化方案,诸如提高网络传输效率,增加缓存之类的就不讲了,能够立竿见影的主要下边这两个方案


方案方法/原理适用场景
字体子集化通过工具将字体文件进行提取(支持动态),返回指定的字符集的字体文件,其根本就是减少单次资源请求的体积,需要服务端支持这个方案是所有优化场景的基础
按需加载通过设置 unicode-range 属性,浏览器在进行css样式计算时候,会根据页面中的字符与设置的字符范围进行比对,匹配上会加载对应的字体文件前提是资源已经被子集化,比较适用多语言切换的场景

简单来说,字体子集化可单独食用,按需加载则必须要将字体前置子集化。才能完美实现按需加载。就我的这个项目而言,动态子集化方案不要太完美,毕竟一张海报本身就没几个字儿!所以我们这次将抛弃 CDN,通过动态的将服务本地中的字体资源子集化来实现字体的压缩效果。



这里我们使用python中的一个字体工具库 fontTools 来实现一个动态子集化,类似于 Google Fonts 的实现。核心思路就是将字符传给服务端,通过工具将传入的字符在本地字体文件中提取并返回给客户端,通过fontTools 还可以将TTF格式转化为和Web更搭的WOFF2格式。实现细节如下述代码所示


@app.route('/font/<font_name>', methods=['GET'])
def get_font_subset(font_name):
# 获取本地字体文件路径
font_path = os.path.join(FONTS_DIR, f"{font_name}.ttf")
# 获取子集字符
chars = request.args.get('text', '')
# 字体文件格式
format = request.args.get('format', 'woff2').lower()

# 处理字符,去重
unique_chars = ''.join(sorted(set(chars)))
try:
# 配置子集化选项
options = Options()
options.flavor = format if format in {'woff', 'woff2'} else
options.desubroutinize = True # 增强兼容性
subsetter = Subsetter(options=options)

# 加载字体并生成子集
font = TTFont(font_path)
subsetter.populate(text=unique_chars)
subsetter.subset(font)

# 保存为指定格式
buffer = io.BytesIO()
font.save(buffer)
buffer.seek(0)

# 确定MIME类型
mime_type = {
'woff2': 'font/woff2',
'woff': 'font/woff',
}[format]

# 创建响应并设置
response = Response(buffer.read(), mimetype=mime_type)
# 其他设置...
return response

except Exception as e:
# 子集化失败...


前端代码中增加了一些字符提取的工作,我本身就是通过 FontFace Api 来请求字体资源的,所以我仅需将资源链接替换为子集化字体的接口就可以了,下面代码来描述字体的加载过程


// ...其他逻辑
Toast.loading('字体加载中')
// 遍历海报中的字体对象
[...new Set(fontFamilies)].forEach((fontName) => {
// 在字体库中找到对应字体详细信息
const obj = fontLibrary.find((el) => el?.value === fontName) ?? {};

if (obj.value && obj.src) {
// 处理海报中提取的文案集合
const text = textMap[obj.value].join('');
// 构建字体对象
const font = new FontFace(
obj.value,
`url(http://127.0.0.1:5000/font/${obj.value}?text=${text}&format=woff2)`
);
// 加载字体
font.load();
// 添加到文档字体集中
document.fonts.add(font);
}
});
// 文档所有字体加载完毕后返回成功的 Promise
return document.fonts.ready.finally(() => Toast.destory());

好了,刷新下浏览器,来看看最终的效果:



这这 真立竿见影(主要是基数大😁😁),最终得到的结果就是,实际 22.4M 的字体文件,子集化后缩减到 3.6KB。实际效果图生成的时间由 20s+ 缩减到毫秒级(300ms 以内)。这下就无惧网速了吧!


结语


总的来说,优化字体加载的方案有很多,我们需要结合自己的实际业务场景来进行选型,字体子集化确实是一种高效且实用的优化手段,更多的实践思路可以参考下 Google fonts


作者:古茗前端团队
来源:juejin.cn/post/7490337281866317836
收起阅读 »

面试官:前端倒计时有误差怎么解决

web
前言 去年遇到的一个问题,也是非常经典的面试题了。能聊的东西还蛮多的 倒计时为啥不准 一个最简单的常用倒计时: const [count, setCount] = useState(0) let total = 10 // 倒计时10s const coun...
继续阅读 »

前言


去年遇到的一个问题,也是非常经典的面试题了。能聊的东西还蛮多的


倒计时为啥不准


一个最简单的常用倒计时:


const [count, setCount] = useState(0)
let total = 10 // 倒计时10s
const countDown = ()=>{
if(total > 0){
setCount(total)
total--
setTimeout(countDown ,1000)
}
}

image.png


稍微有几毫秒的误差,但是问题不大。
原因:JavaScript是单线程,setTimeout 的回调函数会被放入事件队列,既然要排队,就可能被前面的任务阻塞导致延迟 。且任务本身从call stack中拿出来执行也要耗时。所以有1000变1002也合理。就算setTimeout的第二个参数设为0,也会有至少有4ms的延迟。


如果切换了浏览器tab,或者最小化了浏览器,那误差就会变得大了。


image.png
倒计时10s,实际时间却经过了15s,误差相当大了。(不失为一种穿越时间去到未来的方法)


原因:当页面处于后台时,浏览器会降低定时器的执行频率以节省资源,导致 setTimeout 的延迟增加。切回来后又正常了


目标:解决切换后台导致的倒计时不准问题


解决方案1


监听 visibilitychange 事件,在切回tab时修正。


页面从后台离开或者切回来,都能触发visibilitychange事件。只需在document.visibilityState === 'visible'时去修正时间,删掉旧的计时器,设置正确的计时,计算下一次触发的差值,然后创建新的计时器。



// 监听页面切换
useEffect(() => {

const handleVisibilityChange = () => {
console.log('Page is visible:', document.visibilityState);
if(document.visibilityState === 'visible'){
updateCount()
}
};

// 添加事件监听器
document.addEventListener('visibilitychange', handleVisibilityChange);

// 清理函数:移除事件监听器
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
// 修正倒计时
const updateCount = ()=>{
clearTimeout(timer) // 清除
const nowStamp = Date.now()
const pastTime = nowStamp - firstStamp

const remainTime = CountSeconds * 1000 - pastTime

if(remainTime > 0){
setCount(Math.floor(remainTime/1000))
total = Math.floor(remainTime/1000)
timer = setTimeout(countDown,remainTime%1000)
}else{
setCount(0)
console.log('最后时间:',new Date().toLocaleString(),'总共耗时:', nowStamp-firstStamp)
}

}

特点:会跳过一些时刻计数,可能会错过一些关键节点上事件触发。如果长时间离开,误差变大,实际时间结束,倒计时仍在,激活页面时才结束。


image.png


解决方案2


修改回调函数,自带修正逻辑,每次执行时都去修正


    // 每次都修正倒计时
const countDown = ()=>{
const nowDate = new Date()
const nowStamp = nowDate.getTime()
firstStamp = firstStamp || nowStamp
lastStamp = lastStamp || nowStamp

const nextTime = firstStamp + (CountSeconds-total) * 1000
const gap = nextTime - nowStamp ;

// 如果当前时间超过了下一次应该执行的时间,就修正时间
if(gap < 1){
clearTimeout(timer)
if(total == 0){
setCount(0)
console.log('最后时间:',nowDate.toLocaleString(),'总共耗时:', nowStamp-firstStamp)
}else{
console.log('left',total, 'time:',nowDate.toLocaleString(),'间隔:',nowStamp-lastStamp)
lastStamp = nowStamp
setCount(total)
total--
countDown()
}
}else{
timer = setTimeout(countDown,gap)
}
}

结果:


image.png


特性:每个倒计时时刻都触发,最后更新更精准。(顺便一提,edge浏览器后台状态timeout间隔最低是1000)


解决方案3


上面的都依赖Date模块,改本地时间就会爆炸,一切都乱套了。(可以用performance.now 来缺相对值判断时间)


有没有方案让时钟像邓紫棋一样一直倒数的



有的,就是用web worker,单独的线程去计时,不会受切tab影响


let intervalId;
let count = 0;
self.onmessage = function (event) {
const data = event.data; // 接收主线程传递的数据
console.log('Worker received:', data);
count = data;
intervalId = setInterval(countDown,1000); // 这里用了interval
};

function countDown() {
count--
self.postMessage(count); // 将结果发送回主线程
if (count == 0) {
clearInterval(intervalId);
}
}

const [worker, setWorker] = useState(null);

// 初始化 Web Worker
useEffect(() => {
const myWorker = new Worker(new URL('./worker.js', import.meta.url));
// 监听 Worker 时钟 返回的消息
myWorker.onmessage = (event) => {
// console.log('Main thread received:', event.data);
const left = event.data
const nowDate = new Date()
const nowStamp = nowDate.getTime()
if(left > 0){
const gap = nowStamp - lastStamp
console.log('left',left, 'time:',nowDate.toLocaleString(),'间隔:',gap)
lastStamp = nowStamp
setCount(left)
}else{
setCount(0)
console.log('最后时间:',nowDate.toLocaleString(),'总共耗时:', nowStamp-firstStamp)
}
};
setWorker(myWorker);
// 清理函数:关闭 Worker
return () => {
myWorker.terminate();
};
}, []);


缺点:worker的缺点 ;优点:精准计时


总结:


方案1 大修正


方案2 小修正


方案3 无修正


三种方式来使倒计时更准确


作者:水下黑化已放电
来源:juejin.cn/post/7478687361737768986
收起阅读 »