注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

盘点下web常见的攻击方式 --- XSS篇

web
前言 Web攻击(WebAttack)是针对用户上网行为或网站服务器等设备进行攻击的行为,如植入恶意代码,修改网站权限,获取网站用户隐私信息等等。 常见的Web攻击方式有以下几种 XSS (Cross Site Scripting) 跨站脚本攻击 CSRF(...
继续阅读 »

前言


Web攻击(WebAttack)是针对用户上网行为或网站服务器等设备进行攻击的行为,如植入恶意代码,修改网站权限,获取网站用户隐私信息等等。


常见的Web攻击方式有以下几种



  • XSS (Cross Site Scripting) 跨站脚本攻击

  • CSRF(Cross-site request forgery)跨站请求伪造

  • SQL注入攻击


本文主要讲解XSS方面。


XSS是什么


XSS,跨站脚本攻击,允许攻击者将恶意代码植入到提供给其它用户使用的页面中。 它涉及到三方,即攻击者、客户端与Web应用。XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以借助合法用户的身份信息与网站进行交互。


XSS 有哪些类型


根据攻击的来源,XSS攻击可以分成:



  • 存储型

  • 反射型

  • DOM 型


存储型XSS


存储型XSS的攻击步骤:



  1. 攻击者将恶意代码提交到目标网站的数据库中

  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器

  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行

  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作


这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。


反射型XSS


反射型XSS的攻击步骤:



  1. 攻击者构造出特殊的URL,其中包含恶意代码

  2. 用户打开带有恶意代码的URL 时,网站服务端将恶意代码从URL中取出,拼接在HTML中返回给浏览器

  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行

  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作


区别:



  • 存储型XSS的恶意代码存在数据库里,反射型XSS的恶意代码存在URL里。


反射型XSS漏洞常见于通过URL传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。


DOMXSS


DOMXSS的攻击步骤:



  1. 攻击者构造出特殊的URL,其中包含恶意代码

  2. 用户打开带有恶意代码的URL

  3. 用户浏览器接收到响应后解析执行,前端JavaScript取出URL中的恶意代码并执行

  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作


区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。


如何对XSS攻击进行预防呢?


通过前面介绍,看到XSS攻击的两方面:



  • 攻击者提交恶意代码

  • 浏览器执行恶意代码


针对这两个方面就可以得出几条预防措施:



  1. 输入验证与过滤:

  2. 输出编码:

  3. 使用安全框架和工具:

  4. 实施内容安全策略(CSP):


1.输入验证与过滤:


确保对所有用户输入的数据进行严格验证和过滤,包括表单提交、URL 参数、Cookie 等。使用白名单过滤机制,只允许特定的字符和标签通过,过滤掉所有潜在的恶意代码。这样可以防止攻击者向应用程序提交恶意脚本。


2.输出编码:


在将用户数据输出到 HTML 页面时,使用适当的编码方式对数据进行转义,确保浏览器不会将其解析为可执行的脚本。常用的编码方式包括 HTML 实体编码(例如将 < 转换为 &lt;)和 JavaScript 编码(例如将 ' 转换为 ')。这样可以防止恶意脚本在用户浏览器中执行。


3.使用安全框架和工具:


利用现有的安全框架和工具来帮助检测和防御 XSS 攻击。例如,可以使用 Web 应用程序防火墙(WAF)来检测恶意请求,并且可以配置特定的规则来防止 XSS 攻击。还可以使用专门的 XSS 过滤器来检测和过滤潜在的 XSS 攻击载荷。


4.实施内容安全策略(CSP):


内容安全策略(Content Security Policy,CSP)是一种通过 HTTP 头部来控制页面加载资源的策略,可以有效减轻 XSS 攻击的风险。通过 CSP,可以限制页面加载的资源来源,包括脚本、样式表、图片等,从而防止恶意脚本的执行。


作者:笨鸟更要先飞
来源:juejin.cn/post/7350143110495846450
收起阅读 »

只写后台管理的前端要怎么提升自己

web
本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。 写优雅的代码 一道面试题 大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单...
继续阅读 »

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。


写优雅的代码


一道面试题


大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。


原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是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
收起阅读 »

trae 深度体验:使用trae完美开发微信小程序

web
trae 深度体验:使用trae完美开发微信小程序 安装 trae 安装 trae 教程和使用文档大家可以参考官方文档,很详细。使用过 vscode 的用户几乎可以无缝切换过来。官方文档:docs.trae.ai/docs/what-i… 目前只支持 mac...
继续阅读 »

trae 深度体验:使用trae完美开发微信小程序



安装 trae


安装 trae 教程和使用文档大家可以参考官方文档,很详细。使用过 vscode 的用户几乎可以无缝切换过来。官方文档:docs.trae.ai/docs/what-i…


目前只支持 mac 系统,windows 预计 2 月份上线。


如果遇到下面的错误,请科学上网解决;


9d570441458a5014cd84fe035457eddc.jpg


trae 项目实战:开发微信小程序


插件安装


要想在 trae 中完美体验小程序开发首先需要安装必要的两个插件WXML微信小程序开发工具


WXML:微信小程序 .wxml 文件代码高亮,标签、属性的智能补全(同时支持原生小程序、mpvue 和 wepy 框架,并提供 code snippets)


微信小程序开发工具:提供小程序预览、打包上传、代码补全、语法高亮、项目模版等功能


安装 “wxml”插件

按照 vscode、trae 的插件安装方式安装就可以顺利安装:


CleanShot 2025-01-23 at 10.39.54


安装 “微信小程序开发工具”插件

这个工具安装有一些曲折,按照 vscode 的使用习惯,首先在插件市场按名称搜索,结果大出意料,没有😄。


image-20250123105253075


不知道是哪里出现了问题,按照官方文档指引去下载。


image-20250123105506806


打开官方的网址 docs.trae.ai/docs/manage…, 全是英文,没关系,使用豆包 APP 打开网页,让豆包总结网页内容就行 😄:


image-20250123110057035


文档中提到了两种方式:



  • 从 Trae 的插件市场中安装(没搜索到微信小程序开发工具插件,此路不通😭)

  • 把插件下载到本地,使用本地安装的方式。看下面动图:


CleanShot 2025-01-23 at 11.05.14


右下角提示,直接安装失败!此路也不行。作为一个程序员折腾是我的本能,看看 trae 的 AI 能力能不能提供帮助。


顺便遇到个 bug:


image-20250123111111794


插件安装失败后,图中的两个按钮点击了都没有任何反应,只能重启 trae 才能解决。



  • 求助 trae 的 AI


    使用快捷键 command + U 打开右侧边栏,输入要问的问题:


    image-20250123112424432



看到上图,这个插件我们已经安装,在 trae chat 中给到的建议是里面有 "小程序开发助手"插件,但是没有提到如何安装。


更换模型,在 chat 的对话框右侧点击切换模型,使用 gpt-4o,来解决插件安装的问题:


image-20250123112853201


多次尝试后,回答还是一如既往的固执。


在AI 给到的回复当中有个插件的命令,不过这个命令适合 vscode。image-20250123113145456


点击运行按钮试试,此时 trae 会自动打开 terminal,直接执行命令


image-20250123113559819


提示安装成功,但是给 vscode 安装了。继续提问:


image-20250123114016037


嗯,还是 vscode 命令,不过也没关系,更换为 trae 就行了:


trae --install-extension /Users/oo7/Downloads/crazyurus.miniprogram-vscode-extension-1.5.1.vsix


等待命令执行完毕:


image-20250123114209616


安装成功。


至此两个插件就安装完毕,可以做小程序的开发了。


小结

在trae中安装用于微信小程序开发的“WXML”和“微信小程序开发工具”插件,过程各有不同:



  • “WXML”插件:按照vscode、trae常规的插件安装方式即可顺利安装。

  • “微信小程序开发工具”插件:在trae插件市场和vscode插件市场均搜索不到,通过从官方文档下载插件本地安装失败,求助trae的AI起初未得到有效解决,最终通过将适用于vscode的安装命令修改为适用于trae的命令trae --install-extension /xxxx/crazyurus.miniprogram-vscode-extension-1.5.1.vsix ,成功安装。

  • 安装完成两个插件后,即可进行小程序开发。 同时,安装插件失败时存在点击重试和关闭按钮无反应的bug,需重启trae解决。

  • 点击 chat 区域的 run 按钮一定要检测命令的安全性(不然遇到非法的命令直接运行结果很严重),同时也建议trae 只复制命令到终端即可。


小程序项目开发


在 trae 中开发小程序,还需要下载微信开发者工具,也许有人会问既然有了微信开发者工具为什么还要使用 trae?



  • 微信开发者工具编写代码远远没有使用 trae 写代码快,bug 多,没有 AI。

  • trae 功能插件丰富、UI nice、拥有免费的 AI👍。

  • 微信开发者工具不能少,微信开发者工具有实时渲染,代码检测、性能分析、一键上传代码等微信小程序必须的功能。


使用 微信开发者工具打开你的项目,并点击打开模拟器和分离窗口,如下图:


image-20250123134947333


然后打开 trae 编辑器,在你的桌面布局中配置如下排列方式:


image-20250123135201768


这样我们就可以实现一边写代码一边调试效果的目地。


编写页面


代码编写

我已经有这样一个页面,不过界面太难看了,使用 Trae 来调试他:


image-20250123140535900


页面 wxml 代码 :


 <!--pages/tools/index.wxml-->
 <navigation-bar
     title="{{pageTitle}}"
     back="{{false}}"
 >
 </navigation-bar>
 <scroll-view
     type="custom"
     scroll-y
     enable-flex="{{false}}"
     scroll-with-animation="{{true}}"
     enable-back-to-top="{{true}}"
     enable-passive="{{true}}"
     show-scrollbar="{{false}}"
     refresher-default-style="white"
     bounces="{{true}}"
     fast-deceleration="{{true}}"
     lower-threshold="{{50}}"
     style="width: 100%; height: 100%;"
 >
     <sticky-section>
         <view class="toolbox" wx:if="{{tools.length > 0}}">
             <view class="item" wx:for="{{toolList}}">
                 <navigator open-type="navigate" hover-class url="{{item.url}}">
                     <image src="{{item.imageUrl}}" fade-in="{{true}}" mode="widthFix"></image>
                     <text class="title">{{item.title}}</text>
                     <view  class="description">
                         <text><span class="iconfont icon-Fire-fill-red"></span>{{100}}</text>
                         <text class="description_right">去创作 <span class="iconfont icon-ChevronRight" style="font-size: 12px;"></span></text>
                     </view>
                 </navigator>
             </view>
         </view>
     </sticky-section>
 </scroll-view>

界面样式实在太丑了,对 .description 进行样式修改。在 index.wxss 文件中,选中 .description 的样式,在悬浮工具条中点击添加到对话,然后我们在对话区域输入我们的修改要求,trae 进行回答。然后点击应用按钮,可以直接把新的代码插入到源文件对应的行。并且 trae 还很贴心的显示了新旧代码的区分。


CleanShot 2025-01-23 at 14.23.05


最后完成页面的修改,看效果:


image-20250123145057241


index.wxss


 @import '../../asseat/iconfont.wxss';
 ​
 .toolbox {
     display: flex;
     flex-direction: row;
     flex-wrap: wrap;
     justify-content: space-around;
 }
 ​
 .toolbox .item {
     display: flex;
     flex-direction: row;
     flex-wrap: wrap;
     justify-content: space-around;
     width: 45%;
     background-color: white;
     margin-bottom: 20px;
     box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
 }
 ​
 .toolbox .item image{
     /* height: 50px;*/
     /* max-width: 100px; */
     width: 100%;
     overflow: hidden;
     /* border-radius: 5px; */
     border-top-left-radius: 5px;
     border-top-right-radius: 5px;
 }
 ​
 .toolbox .item .title {
     line-height: 40px;
     font-size: 15px;
     /* white-space: normal; */
     align-items: center;
     width: 100%;
     padding-left: 10px;
     font-weight: 400;
     text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3)
 }
 ​
 .description {
     display: flex;
     flex-direction: row; /* 修改为列布局 */
     flex-wrap: nowrap;
 }
 .description .iconfont{
     font-size: 12px;
 }
 .description text {
     display: inline;
     line-height: 20px;
     font-size: 12px;
     width: 100%;
     padding-left: 10px;
     font-weight: 400;
     /* text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3) */
  }
  .description text:first-child{
      color: red;
  }
 ​
 .description .description_right{
     font-size: 12px;
     text-align: right;
     width: 95%;
     display: inline-block;
     margin-right: 5px;
     color: #3cc51f;
 }

如果我们需要回溯代码的历史记录,我们可以选中代码,然后在工具条中选择编辑即可查看。


image-20250123145436290


再来看动图,效果杠杠的🐂:


CleanShot 2025-01-23 at 15.20.41


使用设计图转换为小程序代码

首先我们准备一个页面的设计图


image-20250123161559306


然后使用快捷键 command+U打开右侧的chat 区域,把设计图粘贴进去,并进行对话。输入对话内容:把上图的样式布局转换为微信小程序的代码。看下面动图:


CleanShot 2025-01-23 at 16.13.00


这样会生成对应微信小程序的3个文件: index.wxml、index.wxss、index.js ,然后我们使用应用按钮,将代码插入到对应的文件即可。看最后的效果:


image-20250123161916511


看着效果还行,如果在使用过程中效果不是很好,可以多尝试几次。


小结

1、我们在编写代码过程中与AI 助手聊天,可以指定Trae中的内容(例如代码、文件、文件夹和工作区)作为AI助手阅读和理解的上下文。这 可确保AI助手的响应更符合您的需求。


image-20250123162808495


大家在使用AI的过程中,普遍感觉就是AI不能代替程序员,写出来的代码基础就不能用,原因就是一般的 AI 无法理解用户的工程文件结构黑内容,更无法知道你文件之间、代码直接的关系。trae 做到了,通过项目、文件、代码直接的逻辑生成的答案更贴合实际情况,所以效果会更好些。


2、将图片直接转换为代码依赖强大的多模态模型,大大减低了程序员的工作量。不需要依赖任何内容,将生成的内容稍微修改就可以直接使用, good job 👍。


代码管理


trae 无缝集成了 git 的代码管理功能,我们只需要点点按钮就可以了。可以通过下面的两种方式激活代码管理:



  • 如果当前打开的文件夹没有 Git 仓库,请单击初始化仓库以为其初始化一个仓库。初始化完成后,源代码控制将被启用。

  • 单击发布到 GitHub直接将此文件夹发布到 GitHub 仓库。发布后,可以访问源代码控制。


image-20250123164831862


Trae配置


熟悉 vscode 的用户,对于配置 Trae 也很简单,使用快捷键 command+, 打开设置项:


根据自己的喜好配置即可。


image-20250123165241398


总结



  • 安装 Trae:可参考官方文档进行安装,使用过 VS Code 的用户能无缝切换。

  • 插件安装


    WXML 插件:按常规方式顺利安装,可实现代码高亮、智能补全等功能。


    微信小程序开发工具插件:在市场搜索无果,本地安装失败。最终将适用于 VS Code 的命令修改后成功安装。安装失败时存在按钮无响应的 Bug,需重启 Trae 解决。


    Trae 的插件市场有部分插件是无法搜索到(具体原因未知),遇到无法安装的插件建议使用离线安装的方式,使用命令安装,


  • 小程序项目开发


    结合工具:同时使用微信开发者工具和 Trae,微信开发者工具于实时渲染等,Trae用于高效代码编写和利用 AI 功能。


    代码编写:可选中代码向 Trae 的 AI 提出修改要求,直接将新代码插入源文件,还能查看代码历史记录。



    • 设计图转换代码:依赖多模态的能力,可以在 chat 区域,粘贴设计图并对话,可生成小程序代码文件,效果不佳时可多次尝试。

    • 代码管理:无缝集成 Git 功能,可通过初始化仓库或发布到 GitHub 激活源代码控制。

    • 配置 Trae:熟悉 VS Code 的用户可使用快捷键打开设置项进行个性化配置。




作者:demo007x
来源:juejin.cn/post/7462947628474171403
收起阅读 »

后端:没空,先自己 mock 去

web
前言后端开发忙,不给你接口?后端抱怨你在测试过程中,频繁的给脏数据?后端修个接口很慢没法测试?有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!真这么丝滑?请看我的使用方式:当后端接口无法满足要求,且不能及时更改时。例如后端返回{ ...
继续阅读 »

前言

后端开发忙,不给你接口?

后端抱怨你在测试过程中,频繁的给脏数据?

后端修个接口很慢没法测试?

image.png

有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!

真这么丝滑?

请看我的使用方式:

当后端接口无法满足要求,且不能及时更改时。例如后端返回

{
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
}
}
],
}

但我此时希望增加一个 user_type 来确定页面的展示。

那我就直接起一个文件:user.js,把刚才的响应 copy 过来,并追加改动

myMock('/api/v1/user', 'post', () => {
return {
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
"user_type": "admin",
}
}
],
}
});

如此一来,这个请求就被无缝替换为了我们的 mock,可以随便测试了。

如何接入 mockjs

有的同学就要问了,主播主播,你的 mockjs 确实很方便,怎么接入比较好呀。别急,我们一步一步来

  1. 安装 mockjs
pnpm i mockjs

如果是使用 ts 的同学,可能需要额外安装 type 类型包:@types/mockjs

  1. 新建一个 mock 文件夹,在 mock/index.ts 放入基本路径
// 各种 mock 的文件,视条件而定,我这里有俩文件就引入了俩
import './login/user.js';
import './model/model.js';

并且在你的项目入口 ts 中引入 mock/index.ts

import './mock/index'; // 引入 mock 配置
  1. 导出一个 myMock 方法,并追加一个 baseUrl 方便直接联动你的 axios
import { ENV_TEST } from '@/api/config/interceptor';
import Mock from 'mockjs';

export const myMock = (
path: string,
method: 'get' | 'post',
callback: (options: any) => any
) => {
Mock.mock(`${ENV_TEST}${path}`, method, callback);
};

如此一来,你就可以在 mock 文件夹下去搞了,比如:

我想新增一个服务模块的各类接口的 mock,那么我就新增一个 service 文件夹,在其下增加一个 index.ts,并对对应路径进行 mock

myMock('/api/v1/service', 'get', () => {
return {
code: 0,
msg: 'hello service',
data: null,
};
});

另外,别忘了在 mock/index.ts 引入文件

不显示在 network 中?

需要说明的是,这样走 mock 是不会触发真正的请求的,相当于 xhr 直接被 mock 拦截了下来并给了你返回值。所以你无法在 network 中看到你的请求。

这是个痛点,目前比较好的解决方案还是起一个单独的服务来 mock。但这样也就意味着,需要另起一个项目来单独做 mock,太不优雅了。

有没有什么办法,既可以走上述简单的mock,又可以在需要的时候起一个服务来查看 network,并且不需要额外维护两套配置呢?

有的兄弟,有的。

import express from 'express';
import bodyParser from 'body-parser';
import Mock from 'mockjs';
import './login/user.js';
import './model/model.js';
import { ENV_TEST } from './utils/index.js';

const app = express();
const port = 3010;

// 使用中间件处理请求体和CORS
app.use(bodyParser.json());

// 设置CORS头部
app.use(( _ , res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});

// 设置Mock路由的函数
const setupMockRoutes = () => {
const mockApis = Mock._mocked || {};

// 遍历每个Mock API,并生成对应的路由
Object.keys(mockApis).forEach((key) => {
const { rurl, rtype, template } = mockApis[key];
const route = rurl.replace(ENV_TEST, ''); // 去掉环境前缀

// 根据请求类型(GET, POST, 等)设置路由
app[rtype.toLowerCase()](route, (req, res) => {
const data =
typeof template === 'function' ? template(req.body || {}) : template;
res.json(Mock.mock(data)); // 返回模拟数据
});
});
};

// 设置Mock API路由
setupMockRoutes();

// 启动服务器
app.listen(port, () => {
process.env.NODE_ENV = 'mock'; // 设置环境变量
console.log(`Mock 服务已启动,访问地址: http://localhost:${port}`);
});

直接在 mock 文件夹下追加这个启动文件,当你需要看 network 的时候,将环境切换为 mock 环境即可。本质是利用了 Mock._mocked 可以拿到所有注册项,并用 express 起了一个后端服务响应这些注册项来实现的。

在拥有了这个能力的基础上,我们就可以调整我们的命令

  "scripts": {
"dev": "cross-env NODE_ENV=test vite",
"mock": "cross-env NODE_ENV=mock vite & node ./src/mock/app.js"
},

顺便贴一下我的 env 配置:

export const ENV_TEST = 'https://api-ai.com/fuxi';
export const ENV_MOCK = 'http://localhost:3010/';

let baseURL: string = ENV_TEST;

console.log('目前环境为:' + process.env.NODE_ENV);
switch (process.env.NODE_ENV) {
case 'mock':
baseURL = ENV_MOCK;
break;
case 'test':
baseURL = ENV_TEST;
break;
case 'production':
break;
default:
baseURL = ENV_TEST;
break;
}

export { baseURL };

这样一来,如果你需要看 network ,就 pnpm mock,如果不需要,就直接 pnpm dev,完全不需要其他心智负担。

三个字:

image.png

参数相关

具体的 api 可查阅:github.com/nuysoft/Moc… 相关的文章也非常多,就不展开说明了。

如果这篇文章对你有帮助,不妨点个赞吧~


作者:imoo
来源:juejin.cn/post/7460091261762125865

收起阅读 »

小程序开发体验差,你试过 PageSpy 了吗?

web
做过小程序的人都知道,小程序的开发体验很糟糕,其中一个很难受的点就是调试。虽然在电脑上开发可以用调试工具,但众所周知很多问题得到真机上才能暴露出来,而小程序真机调试用的 vconsole 又很鸡肋,屏幕小,输入难,还是阉割版(阉割了网络、存储功能,不知道出于什...
继续阅读 »

做过小程序的人都知道,小程序的开发体验很糟糕,其中一个很难受的点就是调试。虽然在电脑上开发可以用调试工具,但众所周知很多问题得到真机上才能暴露出来,而小程序真机调试用的 vconsole 又很鸡肋,屏幕小,输入难,还是阉割版(阉割了网络、存储功能,不知道出于什么考虑)。另一个缺陷是,无论是开发工具还是 vconsole,你都只能在「本机」上运行,测试同学要是离你很远的话,想喊你看个 bug,只能截图。


今天介绍一个神奇的工具,全方位的提升小程序的调试体验。


PageSpy 简介


官网:http://www.pagespy.org/


github:github.com/HuolalaTech…


# 相见恨晚的前端开发利器-PageSpy


PageSpy 是由货拉拉大前端开源的一款用于远程调试 Web 的工具,它可以针对远程页面提供类似 Chrome DevTools 的调试体验,无论网页实际运行在哪里。除了实时调试,它还支持离线录制,将已经发生的用户操作和历史日志录制下来随时回放。


除了 web 平台,它还把同样的调试功能带到了小程序上。我们来看看使用 PageSpy 调试小程序有什么不一样的体验。


部署和接入


PageSpy 分为服务端、调试端网页和客户端 SDK,官方文档有详细的部署和接入说明,这里不再赘述:


部署指南:http://www.pagespy.org/#/docs/depl…


小程序的 SDK 以 npm 包的方式提供:


import PageSpy from '@huolala-tech/page-spy-wechat';

const $pageSpy = new PageSpy({
api: "<your-pagespy-host>",
})

详细参考:http://www.pagespy.org/#/docs/mini…


在线调试


针对小程序,目前 PageSpy 内置了四个模块:输出,网络,存储,系统。


1. 输出



1. 大屏看日志


比手机小屏上的 vconsole 爽多了,而且不受设备限制,无论小程序运行在什么设备上,都能通过调试端网页远程看到运行情况。


2. 远程执行代码


vconsole 输入很难受,而 PC 键盘输入的效率就很高,PageSpy 打破了小程序无法执行远程代码的限制。这一功能需要安装插件 @huolala-tech/page-spy-plugin-mp-eval 来支持。不过需要注意上线的时候要去掉,小程序对远程执行代码审查很严格,把该插件带到线上去的话很可能审核不通过。


3. 运行上下文


PageSpy 的远程执行代码和你自己写的代码运行在 「同一个上下文」。这有什么意义呢?


你可以自己试一下:


例如你在你的代码里为全局变量加一个字段:wx.a = 123,在 vconsole 里,你是获取不到这个变量的,反之亦然。


甚至 getCurrentPages 和 getApp 也不能用:



冷知识:小程序的 vconsole 和你的代码 不在一个上下文!


vconsole 是把用户环境的日志通过代理打印到了自己的上下文,又把 wx.xxx 之类的 api 代理到用户上下文去执行。微信似乎只想把它当成一个查看日志的窗口,而不希望用户利用它随意执行代码。


PageSpy 就不会有这个问题,它和你的代码运行在同一个上下文空间,可以直接和你的代码进行交互。


2. 网络


微信小程序自带的 vconsole 阉割了网络模块,所以在真机调试时看不到网络请求日志,非常的不方便。


来看 PageSpy 的网络面板:




和 Chrome 很像。通过 wx.request 发起的请求都可以记录到,而图片、字体之类的资源类请求还看不到,目前来说已经能带来很大帮助了。


3. 存储


小程序的 vconsole 同样也没有 storage 面板🤦🏻,只提供了一个清除 storage 的按钮,令人费解。



来看 PageSpy 的存储面板:



PageSpy 的 web 版 SDK 有 localStorage,sessionStorage,cookie,indexedDB 等多种存储方式,小程序原生只有一个 storage。不过未来倒是可能支持小程序的「本地临时文件」。


4. 系统


系统面板就是把你调用 wx.getSystemInfo、wx.getSetting 等系统 API 能获取到的信息,在这里更清晰、直观的列了出来。例如用户说他某个功能不起效,你看一下这里,可能就知道是不是因为他的系统版本过低,或者某个权限没开导致的。



用户授权信息:



5. 页面呢 ??


如果你用过 web 版的 PageSpy,会发现小程序版的比 web 版的少了一个「页面」模块。因为小程序本身的限制,没有办法拿到页面的 dom 结构,也就没法像 web 一样远程调试界面,这是目前唯一输给 vconsole 的点。也许未来发明了什么黑科技,或者官方良心发现放出一些接口,这个功能才得以实现。


离线录制


PageSpy 不仅支持实时调试,还支持离线录制。假如你在调试小程序的时候发现了一个问题而恰巧又没有连上实时调试,或者你想把某次操作记录存下来慢慢研究或者分享给其他人,就可以用到这个功能。


首先安装插件 @huolala-tech/page-spy-plugin-mp-data-harbor


import PageSpy from '@huolala-tech/page-spy-wechat';
// 引入离线录制插件
import DataHarborPlugin from '@huolala-tech/page-spy-plugin-mp-data-harbor';
// 注册插件
const harbor = new DataHarborPlugin(config);
PageSpy.registerPlugin(harbor);
// 实例化 pageSpy
const $pageSpy = new PageSpy();

添加了该插件之后,小程序的一切日志就会被离线的记录在内存中,之后你可以在需要的时候,调用 $pageSpy.showPanel()方法呼出一个弹窗,就可以将刚刚记录的日志传到 PageSpy 后台:



在 PageSpy 的调试端,进入「日志回放」页面,就可以看到刚刚上传的日志:




兼容性


小程序有那么多平台,每家都有差异,PageSpy 都支持吗?


是的,PageSpy 目前支持绝大部分市面上的小程序类型:微信、支付宝、抖音、百度、mpaas... 官方给出了4个小程序平台的包:


如果是用原生框架写的小程序,目前官方针对使用量较大的微信和支付宝提供了专门的原生 SDK:


@huolala-tech/page-spy-wechat


@huolala-tech/page-spy-alipay


如今很多小程序使用的是 uniapp 或 taro 之类的跨端框架,官方也提供了相应的 SDK:


@huolala-tech/page-spy-uniapp


@huolala-tech/page-spy-taro


如果你要开发抖音、百度、钉钉之类的冷门平台小程序,只要 uniapp 或者 taro 支持,那就可以用上 PageSpy。


除此之外,uniapp 编译的原生 APP,React Native,甚至鸿蒙应用,它都支持,全平台制霸了属于是。


扩展性


插件系统


前文提到的很多功能都依赖于插件,实际上 PageSpy 的所有功能模块都是通过插件实现的,输出、网络、存储、系统这些是内置插件,不需额外配置,而远程执行代码、离线日志是可选的插件。


除此之外你还可以开发自定义的插件,PageSpy 的核心功能是在客户端和调试端之间建立了一个双向的消息通道,通过自定义插件,你可以利用这条通道做你想做的任何事情。例如观测埋点上报,远程执行指令,甚至通过它远程聊天,发送语音视频消息,也不是不可能。


插件文档:


http://www.pagespy.org/#/docs/plug…


开源贡献


最后,不要忘了 PageSpy 是个开源软件,通过插件实现不了的,还可以贡献代码:


github:github.com/HuolalaTech…


作者:qkang
来源:juejin.cn/post/7461626575207825434
收起阅读 »

2025年微信授权登录全方案

web
经常做小程序的小伙伴都曾遇到过这个问题,那就是小程序授权登录,官方文档经常更新,API不时更新迭代,对于基础库版本等等,都有着既定要求。 按照以往方式再去做,发现行不通,折腾一上午,整个人沉默了,原来是官方又进行了大改...... 对于小程序登录的问题,借着2...
继续阅读 »

经常做小程序的小伙伴都曾遇到过这个问题,那就是小程序授权登录官方文档经常更新API不时更新迭代对于基础库版本等等,都有着既定要求。


按照以往方式再去做,发现行不通,折腾一上午,整个人沉默了,原来是官方又进行了大改......


对于小程序登录的问题,借着2025开年之际,给宝子们做一期盘点,来一期《小程序授权登录复盘全攻略》,日常做需求时,大家尽管放心食用,话不多说,直接开始正题!


拜托拜托:点赞收藏是我更新的最大动力!!!


目前为止,微信小程序登录授权,有三种方式,第一种是无感登录,第二种是手机号登录,第三种是用户信息授权登录


比较经典的就是飞猪小程序和顺丰小程序,如果你不知道怎么做,可以去看看它们的样式,一目了然


比如下面这个登录首页面,就属于经典的手机号登陆


QQ图片20250110103439_副本.png


点击快速登录,小程序会迅速调用用户的手机号授权


QQ图片20250110103433_副本.png


再放一个案例,那就是用户信息授权登录,顾名思义,该登陆主要为了获取用户信息(姓名、性别、地址、昵称等等),用于给个人中心模块做铺垫,图中人名我这边和谐掉,因为也是我前两天刚做完的业务。


Snipaste_2025-01-10_10-43-33_副本.png


至于无感登录,这里就不演示了,因为无感无感,顾名思义就是没有感觉,用户是看不出来授权的,所以直接讲方法就好!


顺便放上小程序开发文档:微信小程序官方文档


1.无感登录


首先无感登录是最简单的,步骤只有两步,第一步是前端调用官方文档API——wx.login,拿到登陆凭证code,通过wx.request()发起网络请求,随即传给后端。


第二步,后端那边利用code + appid + appsecret这三个数值,调用微信的auth.code2Session接口,拿到用户唯一标识openid 和 会话密钥session_key,随即定义token,将之与openid和session_key关联,最后再返回给前端。


前端拿到token,就很简单了,按照正常操作即可,比如拿token设置请求头、存入vuex、pinia等等,顺理成章直接写即可,大家都能明白。


总结一点:无感登录,说白了,就是拿小程序token的过程,够直白了吧!


至于有人会问,appid和appsecret是什么?看下图即可!(都在你的小程序后台里)


微信小程序开放平台:官方传送门


Snipaste_2025-01-10_11-02-01_副本.png


具体讲一下方法,顺便把代码附在下面,大家可以直接移植!


首先要注意的是,调用wx.login的时候,你的小程序基础库版本,不能低于2.3.1


image.png


可以在你的原生小程序工具查看


image.png


其次再看看wx.login的传参+返回值,重点关注success和fail,一个是成功回调,一个是失败回调


image.png


返回值是code,有时效限制,这里要注意的是,前端的appId,要和后端的appId一致。


有的人拿不同的appId去调用接口,最后会导致500报错!


image.png


代码示例:(用uniapp的,将wx.login替换为uni.login即可),example.com/onLogin 这个是事例网址,需要替换为你们后端的接口,主要用来获取token。


wx.login({
success (res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'https://example.com/onLogin',
data: {
code: res.code
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})

2.手机号登陆


需要注意的是,个人账号,无法使用手机号登录功能,并且该功能是收费的。


标准单价每次组件调用成功,收0.03元,每个小程序账号将有1000次体验额度,该1000次的体验额度为正式版、体验版和开发版小程序共用,超额后,体验版和开发版小程序调用同正式版小程序一样,均收费。


这一要说明一点的是,相信很多人在网上都看到类似encryptedData、iv获取手机号的方法,25年为止,微信又改版了,手机号登录的流程又得到了简化。(前提是使用付费服务)


流程为:调用bindgetphonenumber,返还code,这个code是限时+一次性的,服务器只需要拿着这个code去和微信换手机号就可以了


传送门一:官方手机组件


传送门二获取手机号最新方法
`


<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"></button>

`
这里要注意一点,如果你用的是uniapp,那么bindgetphonenumber需要换为@getphonenumber


还是通过wx.login拿code,然后调用这个接口,具体要和后端商量,前端的工作并不多,调用而已


image.png


参数需要这几个


image.png


与此同时,返回值手机号就来了


image.png


所以手机号登录没那么复杂,重点是需要付费,不付费的话,让用户自行输入表单,也行,看具体业务实现方式


3.用户信息授权登录


对于用户授权登录的问题,那么就绕不过wx.getUserInfo和wx.getUserProfile的历史渊源了。


早期的小程序开发,大家都是通过wx.getUserInfo拿到用户头像昵称,结果2021年4月,微信社区改版,导致getUserInfo不再有授权流程,开发者只能获取到匿名信息。


比如名字,大家都叫做“微信用户”,而头像,接口返回的都是统一灰色头像。


可这样就带来一个问题,那就是不同用户,昵称头像都一样,完全不方面管理,所以wx.getUserProfile接口应运而生!


这一有一个行为,大家要注意,wx.getUserInfo获取用户信息,不会有底部弹窗,而wx.getUserProfile则会出现下方的底部弹窗(样式看开头),根据你的需求自行选择。


再到2022年10月,微信社区又改版了,就连wx.getUserProfile这个接口,也不给开发者权限了,用户名+头像,全部变成了统一的“微信用户”+灰色头像。


所以如果你实在想获取用户信息,那么利用组件,让用户自行填写,是不错的选择。
`


  getUserProfile(e) {
wx.getUserProfile({
desc: '用于完善会员资料',
success: (res) => {
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
}

`


作者:西红柿炖前男友
来源:juejin.cn/post/7457926197163687970
收起阅读 »

慎重!小公司到底该不该自己封装组件库?

web
前端开发与组件库 注:全文所说的小公司特指:资源不足、技术能力不足的团队 在一些小公司的项目开发中,我们可能常常听到这样的想法: 我们公司的需求比较特殊,用现有的组件库不够灵活,不如自己封装一套! 现有的组件库样式不符合我们的产品需求,我们需要统一风格和...
继续阅读 »

前端开发与组件库



注:全文所说的小公司特指:资源不足、技术能力不足的团队



在一些小公司的项目开发中,我们可能常常听到这样的想法:



  • 我们公司的需求比较特殊,用现有的组件库不够灵活,不如自己封装一套!

  • 现有的组件库样式不符合我们的产品需求,我们需要统一风格和功能,不如自己开发一套组件库吧!


以前我会很天真的支持这样的想法,现在,我会给提出者一个大嘴巴子!看似高瞻远瞩,实则全是陷阱,甚至可能成为整个团队的噩梦。


一个loading组件引起的生产事故


我先讲一个我们公司因为组件库导致的财产损失生产事故!


之前,我们业务有实现过一个表格,由于接口非常快,我们并没有增加loading样式。



代码实现也非常简单:


<template>
<section class="table-section">
<m-table :columns="columns" :data-source="tableData">
</m-table>
</section>

</template>
<script setup>
const tableData = ref([]);
const columns = []

const getTableData = async () => {
// ...接口调用逻辑
queryManageList()
};

// 获取表格数据
getTableData();

onMounted(() => {
// 动态设置表头数据
columns = []
});

</script>


m-table是我们公司的内部表格组件,上面的代码在生产稳定运行。随着数据的增多,接口有些慢了,于是客户希望加个loading。



我们公司的Loading组件模仿自Elemnet Plus,api的调用也非常相似




参考文档,代码的更改也就非常容易


<template>
<section class="table-section">
<m-table :columns="columns" :data-source="tableData">
</m-table>
</section>

</template>
<script setup>
const tableData = ref([]);
const columns = []

const getTableData = async () => {
loadingInstance = Loading('.table-section');
// ...接口调用逻辑
await queryManageList()
loadingInstance.destroy
};

// 获取表格数据
getTableData();

onMounted(() => {
// 动态设置表头数据
columns = []
});

</script>


代码看着严丝合缝,十分完美,然而,部署生产后,发现columns直接没了!


经过线上排查,发现loadingInstance = Loading('.table-section')这段代码直接替换了section标签内部的所有dom元素,造成表格的vue实例出现异常,onMounted钩子根本没有执行!



反观Element PLUS,人家直接是在section标签下生成的遮罩层,就不会存在这个问题!



小公司开发的组件,由于开发者技术参差不齐,很容易出现线上问题啊!这种问题在我们的日常开发中非常常见,害人啊!


为什么小公司不要轻易封装组件库


通过上面的案例,可以看出:小公司的开发人员技术参差不齐,组件库的质量也就无法得到保证。当然,技术还不是主要原因,毕竟技术是可以提升的,但下面的几个问题才是真要命的!


资源不足:人力和时间的双重消耗


封装组件库并非单纯的开发任务,它需要大量的人力和时间投入。对小公司而言,团队往往规模有限,开发资源紧张。



  • 开发人员:为了封装一个组件库,原本负责业务开发的人员必须抽出精力进行组件封装工作,业务开发的进度被迫拖延。

  • 时间成本:开发一个组件库不仅仅是写几个按钮或者表单,还涉及到设计体系、文档编写、单元测试、性能优化和浏览器兼容性处理等,这是一项长期工程。


就拿我们公司举例,我们一遍要写业务代码,一遍要维护组件,非常消耗时间!



就这,公司还不断地给我们加任务,把我们当牛马,直接开启996



加班费没有我们就忍了,996一次不够,还梅开二度



业务开发都没时间,还维护组件库,这不是自己坑自己么? 小公司没钱没实力,再别开发组件库了,来来回回坑自己人!


维护成本高:一时造轮子,一世修轮子


自己封装组件库容易,但长期维护它却很困难。随着项目的迭代和需求的变化,组件库也需要不断更新和优化。



  • 需求增加: 业务需求多样化导致组件库功能膨胀,原本简单的组件变得复杂不堪。

  • Bug 修复: 自己封装的组件库缺乏大规模使用的验证,隐藏的 Bug 往往在上线后爆发,修复工作耗费大量时间。

  • 兼容性问题: 浏览器兼容、新技术支持(如 Vue 3、React 18)的适配工作更是让人头疼。


我们的组件库更新非常频繁,因为随着产品的迭代,要增加很多新功能。有时候,为了使用组件的新功能或者样式,我们不得不升级组件版本。然而,有一次升级后,组件库内部存在严重bug,导致我们原有的许多界面崩溃,造成了重大生产事故!



这种组件升级导致的页面问题时常发生,加了一个功能,导致一个隐藏bug,后续为了解决这个隐藏bug,又引入其他bug,最终一个小功能要发好几个组件版本才能解决。我已经无力吐槽,害人啊!


而且,由于组件的功能不完善,经常要花费非常多的沟通成本



技术负债:短期便利,长期拖累


自建组件库在开发初期可能感觉很“顺手”,但随着项目规模扩大,组件库的缺陷会逐渐显现,成为团队的技术负债。



  • 缺乏标准化: 自建组件库的规范不够完善,不同开发者在实现同一功能时可能写出风格完全不同的代码。

  • 文档不足: 由于时间和人力限制,自建组件库的文档往往不完善,后期新成员加入时难以上手。

  • 升级困难: 自建组件库的每次升级都可能影响到现有业务,增加维护和测试成本。


员工离职风险:组件库成孤岛


小公司人员流动较为频繁。


如果负责组件库开发的员工离职,组件库很可能会变成“孤岛”,无人维护,直接影响到项目的可持续性。


经济形式不好,我们公司近几年也裁了不少员,导致一些组件直接没人维护了,直接影响项目进度。


所以,资金、时间不充足,咱小厂还是别学大厂维护组件库了,要钱没钱,要时间没时间,来来会会坑的都是自己!


总结


封装组件库对小公司来说是一个高风险、高成本、低收益的选择。本人建议如下:



  1. 优先选择成熟的开源组件库: 如 Ant Design、Element Plus 等,它们功能完善且生态丰富,能够快速适配业务需求!

  2. 定制而非重造: 如果开源组件库无法完全满足需求,可以在其基础上进行二次封装(各位leader注意看,组件库基本都支持样式定制的!) ,而不是从零开始构建。

  3. 聚焦业务: 小公司开发团队的首要任务是满足业务需求,组件库的开发应该是锦上添花,而非拖慢进度的负担。


各位Leader注意,技术的最终目的是为业务服务!别为了凸显自己牛逼,强行开发维护一个组件库,最终只会害人害,搬起石头砸自己的脚!


小公司不要让组件库拖住脚步,把资源投入到更有价值的地方,才是发展的正确道路。



注:以上所有言论只针对公司的产品、项目而言!对于个人,你怎么玩都行,玩的越花越好,毕竟以后出去都是面试的资本!





啥也不是,散会!


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

写给我前端同事,从事一年多应该要怎么成长的路线

web
写给我前端同事,从事一年多前端应该要怎么成长的路线我知道在很多中大型公司,其实有好多领导前辈、以及师傅会给那些校招生,以及应届生规划一定的学习成长路线,可能还会有定期的大佬分享。有这么一个领路人、环境在其实成长是飞速的。我入职了一家新单位,这家单位的没有太多规...
继续阅读 »

写给我前端同事,从事一年多前端应该要怎么成长的路线

我知道在很多中大型公司,其实有好多领导前辈、以及师傅会给那些校招生,以及应届生规划一定的学习成长路线,可能还会有定期的大佬分享。有这么一个领路人、环境在其实成长是飞速的。

我入职了一家新单位,这家单位的没有太多规范,没有太多的组件封装积累,还会考核每周的代码量,我发现有些阶段代码量(测试阶段、需求阶段等)不够的时候大家都是往项目中塞没用的代码,还有些同学会复制公共组件的代码进自己的模块充代码,技术栈使用的是vue3 + js + ant-design-vue,一大部分人做的都是项目

苏洋同学(化名)工作了2年左右,换过2家公司,第一家是个小公司也是在写一些后台管理的功能,这是他的第二家公司入职这家公司也有一年左右时间了。他逻辑,工作态度都没问题,也算是个积极上进的零零后好青年。他当前项目中的代码开发没问题,项目中多是一些业务表单,工作流之类的东西,有用户权限控制。有的时候他会请教我一些问题我俩也就从问题聊了聊技术,并且我把我的博客分享给了他,他说他有空会看看学习下。

我跟他从我的博客文章中聊了下他可能需要怎么做来获得成长,当然现在这个环境下我不能跟他说你多学一点可能工资就会高一些,假如在3年前我一定会告诉他,你学完这些其实可以换个公司去试试一定会比你现在的要高一点。可能学习完假如被迫换工作(裁员)了,机会会大点吧

大佬请绕路,我可能给的建议并不是最好的,但是我觉得对他来说现阶段是最使用的

针对他的成长经历规划

他的js基础可能没那么好,像一些数据处理上是有些问题,那么我建议他:

  1. 重新学习下js针对数组,字符串等API,像字符串的cancat、includes、indexOf、lastIndexOf、endsWith、startsWith等等,像数组的forEach、map、filter、reduce、find、findIndex、some、every等等,他说他有些好像没有使用过。 学习了解用法,并且写一样的源码。例如:
'123'.endsWith('3'); // true

export const _endsWith = (source: string, target: string) => {
const len = target.length;
const substrValue = source.substr(source.length - len, len);

return substrValue === target;
};

_endsWith('123456', '56'); // true
  • 要有一定的理解,对深拷贝、浅拷贝有一定的理解。
  • 宏任务微任务以及事件执行的理解。
  • 防抖节流有一定的理解
  • this有一定的理解并写出applycallbind的实现。
  • 对类型判断instanceoftypeofObject.prototype.toString.call等方法有理解。
  • 对对象方法的使用Object.keys、Object.values、Object.entries、Object.assign等等
  1. 去看下lodash的源码,例如:throttle、debounce、cloneDeep、groupBy、get、difference等等一些常用的方法函数要会写源码,最好自己写一遍。
  2. 对正则表达式能对有一定的理解,并且写出一些常用的正则。
  3. CSS 中对主题适配能有一定的理解,例如使用 less 和 Scss变量做主题适配,以及使用2套样式,或者使用css全局变量做主题适配。能区分出这几种的不同点

如果能把js的以上都能掌握了,那么你的基础算是扎实的了,差的可能就是工作经验以及深入了解一些框架的源码了。

  1. 这个时候可以学习下代码规范了,其实vue的话可以看看element ui组件代码的规范,组件的设计以及源码具体。至少能实现message组件以及按钮组件
  2. 学习下设计模式,例如:单例模式策略模式代理模式发布订阅模式等等。
  3. 可以多看看怎么写防御式编程,让你的代码更加健壮(这也就是为啥项目中bug多的问题,代码写的还不够严谨)
  4. 可以去学习下TS,可能不用去特别做类型体操,基本的泛型能用,例如:ArrayRecordPartialPickOmitExcludeExtract等等。
  5. 如果你对vuereact想深入研究,可以对比着使用看看它们之前的差异,有自己的认识。
  6. webpack的配置,对打包优化有理解,对loader和plugin有理解,对打包工具有使用过。
  7. 了解下npm,对npm 发布等命令有一定的理解,并且尝试自己发布一个包。
  8. git提交规范有理解,并且使用。可以深入了解下git规范定义以及拦截。
  9. 对 nginx 有一定的了解,并且使用。因为现在好多项目都是多页应用了,nginx就显得很重要了。
  10. echarts 是图表库,可以学习下他的那些简单图表怎么使用canvas画出来的。

恭喜,假如你上面都能学会,我觉得你很了不起了,至少算是中级前端工程师

  1. 制定公司代码规范eslintgit 提交规范等等
  2. git CI制定工作流是很重要的,可以学习下。
  3. ...
  4. ...
  5. ...

结语

其实如果从事这个行业,可以把上面当作一个学习清单,然后按照顺序学习,这些都是必须且要学会的。然后把学习到的东西都记录下来。多总结,多思考,


作者:三原
来源:juejin.cn/post/7448899248475684899

收起阅读 »

看完前端各种风骚操作,我眼睛被亮瞎了!

web
一、实现一个快速评分组件 const getRate = rate => "★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate); console.log(getRate(0)); // ☆☆☆☆☆ console.log(get...
继续阅读 »

一、实现一个快速评分组件


const getRate = rate => "★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate);
console.log(getRate(0)); // ☆☆☆☆☆
console.log(getRate(1)); // ★☆☆☆☆
console.log(getRate(2)); // ★★☆☆☆
console.log(getRate(3)); // ★★★☆☆
console.log(getRate(4)); // ★★★★☆
console.log(getRate(5)); // ★★★★★

这个都不用多解释了,简直写的太妙了!


二、巧用位运算


用位运算可以实现很多功能,比如乘2、除2(或者2的倍数),向下取整这些计算操作,而且性能很高!


let num = 3.14159;
console.log(~~ num); // 向下取整,输出3
console.log(2 >> 1); // >>表示右移运算符,除2,输出1
console.log(2 << 1); // <<表示左3移运算符,乘2,输出4

并且,利用~符,即按位取反运算符(NOT operator),还可以和字符串的indeOf方法配合使用。


const str = 'acdafadfa'
if (~str.indexOf('ac')) {
console.log('包含')
}

其实原理很简单,举几个例子大家就明白了:



  • ~-1的结果是0

  • ~0的结果是-1

  • ~1的结果是-2~2的结果是-3


三、漂亮随机码


const str = Math.random().toString(36).substring(2, 10)
console.log(str); // 随机输出8位随机码

这个在要为每个用户生成一个随机码的时候特别好用,具体随机码多少位可以自己控制,如果要的随机码位数特别长,可以把这个函数多调用一次,然后把结果进行字符串拼接。


四、史上最NB的报错处理


try {
const str = '';
str.map(); // Uncaught TypeError: str.map is not a function
} catch(e) {
window.open("https://stackoverflow.com/search?q=js+" + e.message);
}

这应该是史上最NB的报错处理了,一般来说,抛出错误时应该打印日志并上报,这里直接带着报错信息给人家重定向到stackoverflow去了,顺便stackoverflow搜索了下这个报错,直接搜一波错误解决方案,而且这个网站是全英文的,顺便还能学一波英语,对于开发者来说,这简直太妙了!不过记得上线的时候,记得在跳转前加一个if(process.env.NODE_ENV === 'development'),不然上到线上一旦报错可就惨了!


五、倒序排序的简写


const arr = [1, 2, 3, 4, 5];
for (let i = arr.length - 1; i >= 0; i--) {
console.log(arr[i]);
}

可简写为:


const arr = [1, 2, 3, 4, 5];
for(let i = arr.length; i--;) {
console.log(arr[i]);
}

代码解释:


先来回顾下for循环的书写结构,即for (初始化表达式; 条件表达式; 递增表达式),初始化表达式只会执行一次,而条件表达式和递增表达式在每次循环时都会执行一次,而正好这个倒序循环的终止执行条件为i==0,所以就可以把条件表达式递增表达式合而为一了,主打的就是一个简洁。


六、在控制台输出一个键盘图形


console.log((_=>[..."`1234567890-=~~QWERTYUIOP[]\\~ASDFGHJKL;'~~ZXCVBNM,./~"].map(x=>(o+=`/${b='_'.repeat(w=x<y?2:' 667699'[x=["BS","TAB","CAPS","ENTER"][p++]||'SHIFT',p])}\\|`,m+=y+(x+'    ').slice(0,w)+y+y,n+=y+b+y+y,l+=' __'+b)[73]&&(k.push(l,m,n,o),l='',m=n=o=y),m=n=o=y='|',p=l=k=[])&&k.join`
`
)())

这段代码会在浏览器控制台中打印出一个键盘图形,不得不说写出这段代码的人真的太有才了!



以上就是我总结的一些前端代码的风骚操作,大家有没有更风骚的操作呢?欢迎大家留言分享!


作者:程序员小寒
来源:juejin.cn/post/7453414571563925542
收起阅读 »

没想到学会这个canvas库,竟能做这么多项目

web
大家好,我是一名前端工程师,也是开源图片编辑器vue-fabric-editor项目的作者,2024年5月从北京辞职,我便开始了自己的轻创业之路,接触了不同的客户和业务场景,回顾这半年,没想到学会fabric.js这个Canvas库,竟能做这么多项目。 如果你...
继续阅读 »

大家好,我是一名前端工程师,也是开源图片编辑器vue-fabric-editor项目的作者,2024年5月从北京辞职,我便开始了自己的轻创业之路,接触了不同的客户和业务场景,回顾这半年,没想到学会fabric.js这个Canvas库,竟能做这么多项目。


如果你打算学习一个Canvas库或者做图片设计、定制设计相关的工具,我建议你学习一下fabric.js 这个库,它非常强大,可以做出很多有意思的项目,希望我的项目经历能给你的技术选型做一些参考



1预览图.png


项目经历


从北京回老家邯郸后,我陆续做了很多项目,包括正件照设计、锦旗/铭牌定制工具、Shopify定制插件、批量生成图片、手机版图片设计工具、服装设计、电商工具等,这些项目都离不开fabric.js这个库。回顾这段经历,让我深刻体会到它的强大和广泛应用


图片设计


图片设计是我接触的第一个主要应用领域。项目最初源于一个小红书成语卡片设计工具的构想,随后逐步扩展到更广泛的设计场景,包括小红书封面、公众号头图、营销海报以及电商图片等多种自媒体内容制作。


这类应用的核心功能在于自定义画布尺寸和元素排版,得益于fabric.js的原生支持,实现起来相对简单。我们主要工作是开发直观的属性编辑面板,使用户能够便捷地调整所选元素的文字和图片属性。


当然如果做的完善一些,还需要历史记录标尺辅助线对齐快捷键等,这些功能fabric.js并没有包含,需要我们自己实现,这些功能可以参考vue-fabric-editor 项目,它已经实现了这些功能。


还有很多细节的功能,比如组合保存、字体特效、图层拖拽、图片滤镜等,这些功能我们做的比较完善了。


2 功能展示.gif


定制设计工具


图片设计的场景相对通用,没有太多定制化的需求。而定制类的设计工具则需要针对特定场景深度开发,比如正件照、锦旗/铭牌设计、相册设计等,每个场景有不同的定制功能


正件照设计工具的核心在于自动化的处理。主要工作量集中在尺寸的匹配,确保图片能自动调整到最佳大小。同时,需要提供人物图片的裁剪功能,让用户能便捷地进行换装、切换正件尺寸、更换背景等操作。


3.png


4.png


锦旗与铭牌设计则更注重文字内容的自动排版。系统需要根据用户输入的抬头、落款、赠言等内容,自动计算最优的文字间距和整体布局,确保作品的美观性。特别是铭牌设计,还需要实现曲线文字功能,让文字能够优雅地沿着弧形排布。


6.jpeg


5.jpeg


相册设计工具的重点是提供灵活的画布裁剪功能。用户可以使用各种预设的形状模板来裁剪图片,需要确保裁剪后的图片既美观又协调,最终生成精美的画册作品,交互上方便用户拖拽图片快速放入裁剪区域。


7相册功能.gif


电商工具


电商场景比图片设计更垂直,除了普通的平面设计,例如店铺装修、商品主图、详情图的设计,另外还需要对商品进行换尺寸、抠图、换背景、去水印、涂抹消除、超清放大等操作,这些对图片处理的要求更高一些。


涂抹.gif


9.png


批量生成


批量算是一个比较刚需的功能,比如电商的主图,很多需要根据不同产品到图片和价格来批量加边框和文字,以及节庆价格折扣等,来生成商品主图,结合图片和表格可以快速生成,减少设计师的重复工作量。


12.png


另一部分是偏打印的场景,比如批量制作一些商品的二维码条形码,用在超市价签、电子价签、一物一码、服装标签等场景,根据数据表格来批量生成。


11.png


这种项目主要的工作量在交互上,如何将画布中的文字和图片元素与表格中的数据一一对应,并批量生成,另外会有一些细节,比如条形码的尺寸、图片的尺寸如何与画布中的尺寸比例进行匹配,这些细节需要我们自己实现。


上边的方式是通过表格来批量生成图片,还有一种是根据 API来批量生成图片,很多场景其实没有编辑页面,只希望能够通过一个 API,传入模板和数据,直接生成图片,fabric.js 支持在nodejs 中使用,我们要做的就是根据模板和数据拼接 JSON,然后通过fabric.js 在后端生成图片,然后返回给前端,性能很好,实际测试 2 核 2G 的机器,每张图片在 100ms 左右。


很多营销内容和知识卡片、证书、奖状也可以通过批量生成图片API来实现。


10.gif


当然,还有一些更复杂的场景,比如不同的数据匹配不同的模板,不同的组件展示不同的形式等,包括错别字检测、翻译等,我们也为客户做了很多定制化的匹配规则。


13.png


服装/商品定制


服装/商品定制是让用户在设计平台上上传图片,然后将图片贴图到对应的商品模板上,实现让用户快速预览设计效果的需求


这种场景一般会分为 2 类,一类是是针对 C 端用户,需要的是简单、直观,能够让用户上传一张图片,简单调整一下位置就能确认效果快速下单。


14.gif


我在这篇文章里做了详细介绍:《fabric.js 实现服装/商品定制预览效果》


另一类是针对小 B 端的用户,他们对设计细节有更高的要求,比如领子、口袋、袖子等,不同的位置进行不同的元素贴图,最后将这些元素组合成一个完整的服装效果图,最后需要生成预览图片,在电商平台售卖,完成设计后,还要将不同区域的图片进行存储,提供给生产厂家,厂家快速进行生产。


比如抱枕、手机壳、T恤、卫衣、帽子、鞋子、包包等,都可以通过类似服装设计的功能来实现。


15.gif


很多开发者会提出疑问,是否需要介入 3D 的开发呢?
我们也和很多客户沟通过,从业务的角度看,他回答是:3D 的运营成本太高。他们做的都是小商品,SKU 很多很杂,如果每上一个商品就要进行 3D 建模,周期长并且成本高,他们更希望的是通过 2D 的图片来实现,而且 2D 完全能够满足让用户快速预览确认效果的需求,所以 2D 的服装设计工具就成为了他们的首选。


包装设计


包装设计是让用户在设计平台上,上传自己的图片,然后将图片贴图都包装模板上,主要的场景是生成定制场景,比如纸箱、纸袋、纸盒、纸杯、纸质包装等,这些场景需要根据不同的尺寸、形状、材质、颜色等进行定制化设计,最后生成预览图片


16.png


因为设计到不同的形状和切面,而且大部分是大批量定制生产,所以对细节比较谨慎,另外包装规格相对比较固定,所有用3D模型来实现就比较符合


另外,在确定设计效果后,需要导出刀版图,提供给生产厂家,厂家根据刀版图进行生产,所以需要将设计图导出为刀版图,这个功能 fabric.js 也支持,可以导出为 SVG 格式直接生产使用。


17.gif


AI结合


在AI 大火的阶段,就不得不提 AI 的场景了,无论在自媒体内容、电商、商品、服装设计的场景,都有 AI 介入的影子,举个例子,通过 AI生成内容来批量生成营销内容图片,通过 AI 来对电商图片进行换背景和图片翻译,通过 AI 生成印花图案来制作服装,通过 AI 来生成纹理图来生成纸盒包装,太多太多的 AI 的应用场景,也是客户真金白银定制开发的功能


展望2025


从图片设计的场景来看,我们的产品已经很成熟了,也算是主力产品了,未来会持续迭代和优化,让体验更好,功能更强大,把细节做的更完善,例如支持打印、视频生成等功能。


从定制设计工具的场景来看,我们积累了不同商品定制设计的经验,从技术和产品到角度看,我们还可以抽象出更好的解决方案,让客户能够更高效、低成本的接入,提供给他们的客户使用,快速实现设计生产的打通。


2024 到 2025 ,从在家办公一个人轻创业,搬到了我们的办公室,期待未来越来创造更多价值。


18.png


总结


半年的时间,这些项目的需求fabric.js都帮我们实现了,所以如果你对Canvas感兴趣,我的亲身经历告诉你,学习fabric.js是一个不错的选择


另外,对我来说更重要的是,客户教会了我们很多业务知识,这些才是宝贵的业务知识和行业经验,一定要心存敬畏,保持空杯,只有这样我们才能做好在线设计工具解决方案。


这篇文章也算是我从 2024年离职出来到现在的一个年终总结了,希望我们踩过的坑和积累的经验都变成有价值的服务,作为基石在2025年服务更多客户,文章内容供大家一些参考,期待你的批评指正,一起成长,祝大家 2025年大展宏图。


给我们的开源项目一个Star吧:github.com/ikuaitu/vue… 😄。


作者:愚坤秦少卫
来源:juejin.cn/post/7459286862839054373
收起阅读 »

Timesheet.js - 轻松打造炫酷时间表

web
Timesheet.js - 轻松打造炫酷时间表 前言 在现代网页设计中,时间表是一个常见的元素,用于展示项目进度、历史事件、个人经历等信息。 然而,创建一个既美观又功能强大的时间表并非易事。 幸运的是,Timesheet.js 这款神奇的 JavaScri...
继续阅读 »

Timesheet.js - 轻松打造炫酷时间表


前言



在现代网页设计中,时间表是一个常见的元素,用于展示项目进度、历史事件、个人经历等信息。


然而,创建一个既美观又功能强大的时间表并非易事。


幸运的是,Timesheet.js 这款神奇的 JavaScript 开源时间表库为我们提供了一个简洁而强大的解决方案。


本文将详细介绍 Timesheet.js 的特点、使用方法,并通过一个真实的使用案例来展示其强大功能。


介绍


Timesheet.js 是一个轻量级的 JavaScript 库,专门用于创建基于 HTML5CSS3 的时间表。


它无需依赖任何外部框架,如 jQueryAngular.js,即可快速生成美观的时间表布局。


Timesheet.js 的优势在于其简洁性和用户友好性,仅需几行 JavaScript 代码即可实现功能,同时提供了丰富的自定义选项,允许开发者根据需求进行样式调整。


核心特性


无依赖:不依赖任何外部 JavaScript 框架,减少了项目复杂性和加载时间。
易于使用:通过简单的 JavaScript 代码即可创建时间表,易于上手。
高度可定制:提供了丰富的 CSS 类,方便开发者自定义时间表的外观。
响应式设计:支持移动设备,确保在不同屏幕尺寸上都能良好显示。


官方资源


官网:sbstjn.github.io/timesheet.j…


GitHub 仓库:github.com/sbstjn/time…


使用案例


假设我们要为一个在线教育平台创建一个展示学生学习历程的时间表。


这个时间表将展示学生从入学到毕业的各个阶段,包括参加的课程、获得的证书等信息。


步骤 1:引入库文件


首先,在 HTML 文件中引入 Timesheet.jsCSSJavaScript 文件。


<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/timesheet.js/dist/timesheet.min.css" />
<div id="student-timeline">div>
<script src="https://cdn.jsdelivr.net/npm/timesheet.js/dist/timesheet.min.js">script>

步骤 2:准备数据


接下来,准备时间表所需的数据。


在这个案例中,我们将展示一个学生从 2018 年入学到 2022 年毕业的学习历程。


const studentTimelineData = [
['09/2018', '06/2019', '入学 & 基础课程学习', 'default'],
['09/2019', '06/2020', '专业课程学习', 'ipsum'],
['07/2020', '01/2021', '暑期实习', 'dolor'],
['09/2020', '06/2021', '高级课程学习', 'lorem'],
['07/2021', '01/2022', '毕业设计', 'default'],
['06/2022', '09/2022', '毕业 & 就业', 'ipsum']
];

步骤 3:初始化 Timesheet.js


最后,使用 Timesheet.js 初始化时间表,并传入准备好的数据。




完整代码


将上述代码整合到一个 HTML 文件中,即可创建出一个展示学生学习历程的时间表。


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生学习历程时间表title>
<link rel="stylesheet" href="./timesheet.js/dist/timesheet.min.css" />
head>
<body>
<div id="student-timeline">div>
<script src="./timesheet.js/dist/timesheet.min.js">script>
<script>
const studentTimelineData = [
[
'09/2018', '06/2019', '入学 & 基础课程学习', 'default'],
[
'09/2019', '06/2020', '专业课程学习', 'ipsum'],
[
'07/2020', '01/2021', '暑期实习', 'dolor'],
[
'09/2020', '06/2021', '高级课程学习', 'lorem'],
[
'07/2021', '01/2022', '毕业设计', 'default'],
[
'06/2022', '09/2022', '毕业 & 就业', 'ipsum']
];
const timesheet = new Timesheet('student-timeline', 2018, 2022, studentTimelineData);
script>
body>
html>

效果如下



总结


Timesheet.js 是一个非常实用的 JavaScript 时间表库,它以简洁的代码和强大的功能为开发者提供了一个创建时间表的便捷工具。


通过本文的介绍和使用案例,相信你已经对 Timesheet.js 有了基础的了解。


无论是在个人项目还是企业应用中,Timesheet.js 都能帮助你快速创建出美观且功能强大的时间表,提升用户体验。


如果你对 Timesheet.js 感兴趣,不妨尝试在自己的项目中使用它,探索更多可能。


作者:我码玄黄
来源:juejin.cn/post/7461233603431890980
收起阅读 »

前端同时联调多个后端

web
前言 最近公司项目有时需要和多个后端同时对接(😔小公司一对N),于是对公司项目基础配置文件做了些修改,最终达到能对于多个后端同时启动多个前端服务,而不是每次都需要和A同学对接完,代理地址再换成B同学然后重新启动项目 个人经验0.5年,菜鸡前端一枚,第一次写文章...
继续阅读 »

前言


最近公司项目有时需要和多个后端同时对接(😔小公司一对N),于是对公司项目基础配置文件做了些修改,最终达到能对于多个后端同时启动多个前端服务,而不是每次都需要和A同学对接完,代理地址再换成B同学然后重新启动项目


个人经验0.5年,菜鸡前端一枚,第一次写文章,只是对个人工作简要记录😂!!!


公司项目有vue脚手架搭建的也有vite搭建的,下面让我们分两种方式来修改配置文件


vue-cli方式【webpack】


1. 个人习惯把proxy单独抽离出来放到.env.development


# 启动端口号
VUE_PORT = 8000

# 代理配置
# A同学
VUE_PROXY_A = [["/api","http://localhost:3001"]]

# B同学
VUE_PROXY_B = [["/api","http://localhost:3002"]]

2. 使用cross-env来加载不同的代理


npm i -D cross-env

重新编写下script


image.png


3. 读取环境变量


vueCli内部dotenv已经加载到process.env,我们再做一层包裹,之前配置的proxy,这种其实是字符串,需要处理


const { VUE_PROXY, VUE_PORT } = require("./constant.js")

// Read all environment variable configuration files to process.env
function wrapperEnv(envConf) {
const ret = {}
const SERVER_NAME = process.env.NODE_ENV_PROXY || VUE_PROXY

for (const envName of Object.keys(envConf)) {
if (!envName.startsWith('VUE')) {
continue
}
let realName = envConf[envName].replace(/\\n/g, '\n')
realName = realName === 'true' ? true : realName === 'false' ? false : realName

if (envName === VUE_PORT) {
realName = Number(realName)
}
if (envName === SERVER_NAME && realName) {
try {
realName = JSON.parse(realName.replace(/'/g, '"'))
} catch (error) {
realName = ''
}
}
ret[envName === SERVER_NAME ? VUE_PROXY : envName] = realName
if (typeof realName === 'string') {
process.env[envName] = realName
} else if (typeof realName === 'object') {
process.env[envName] = JSON.stringify(realName)
}
}
return ret
}

module.exports = {
wrapperEnv
}

这样我们就可以拿到所有的环境变量,并且proxy是数组,而不是字符串


4. 生成proxy


/**
* Used to parse the .env.development proxy configuration
*/


const httpsRE = /^https:\/\//

/**
* Generate proxy
* @param list
*/

function createProxy(list = []) {
const ret = {}
for (const [prefix, target] of list) {
const isHttps = httpsRE.test(target)

// https://webpack.docschina.org/configuration/dev-server/#devserverproxy
ret[prefix] = {
target: target,
changeOrigin: true,
ws: true,
pathRewrite: { [`^${prefix}`]: '' },
// https is require secure=false
...(isHttps ? { secure: false } : {}),
}
}
return ret
}

module.exports = {
createProxy,
}

5. 修改vue.config.js


const { defineConfig } = require('@vue/cli-service')
const { wrapperEnv } = require('build/vue/util')
const { createProxy } = require('./build/vue/proxy')

const {
VUE_PORT,
VUE_PROXY
} = wrapperEnv(process.env)

module.exports = defineConfig({
transpileDependencies: true,
// webpack-dev-server 相关配置
devServer: {
host: '0.0.0.0',
port: VUE_PORT,
open: false,
overlay: {
logging: 'info',
errors: true,
warnings: true
},
proxy: createProxy(VUE_PROXY),
disableHostCheck: true
},
})

6. 使用mock模拟两个后端服务


A同学使用3001端口
B同学使用3002端口


image.png


7. 测试是否达到效果


同样我们前端也起两个8000和8001


image.png


接下来看下8000端口请求


image.png
再看下8001请求


image.png


vite


结语


以上只写了webpack不过vite和这也差不多!!!


作者:向天卷飞机
来源:juejin.cn/post/7456266020379541531
收起阅读 »

原来微信小游戏用的技术就是web, 有想法的直接可以做

web
12月玩了2个微信小游戏, 发现都是在玩数值, 其实就是同一个游戏场景, 于是想自己写一个试试.然后看了微信小游戏文档, 推荐 cocos creator, 学了下发现 web 开发者那是根本不用学.自己写了2个demo, 于是分享给大家.cocos crea...
继续阅读 »

12月玩了2个微信小游戏, 发现都是在玩数值, 其实就是同一个游戏场景, 于是想自己写一个试试.

然后看了微信小游戏文档, 推荐 cocos creator, 学了下发现 web 开发者那是根本不用学.

自己写了2个demo, 于是分享给大家.

cocos creator

cocos creator 是个游戏引擎, 他推荐使用 vscode 和 chrome, 并且 ts 是唯一支持的脚本语言.

他的预览就是打开chrome的一个网页, 主体是个canvas, 这个场景下cc可能就是一系列资源的执行器.

重要的是他可以打包到微信小游戏, 也是微信小游戏推荐的框架.

也就是我可以用 ts 写小程序了.

其实也就是个 html5 的小游戏, 而 cc 包装了h5小游戏要手动写的requestAnimationFrame执行器, 提供了更方便的编辑器, 包装了一些游戏开发要用到的概念.

网页开发和游戏开发的区别

显然网页开发和游戏开发是不同的, 来稍作分析.

游戏元素

网页元素一般由div布局, 终端的节点一般是文字, 或者输入框.

游戏元素看起来容易一些, 因为没有输入. 手机小游戏只有通过点击来传达一些指令.

游戏元素也有布局, 但没网页 bfc, flex 这些复杂的东西, 全部绝对定位, 也有z轴.

再细看游戏元素, 其实每个元素就是个图片.

简单总结, 游戏的所有元素就是图片, 通过设置x, y, z的数值来定位. 比网页开发容易得多.

游戏交互

网页的功能主要是2个部分: 输入和展示.

所以网页的交互也就是改变参数后刷新列表.

我们来分析游戏的交互, 也分为2个部分: 改变位置与结算.

随着游戏的开始和玩家的点击, 其实就是元素的位置发生改变而已.

我们只要通过脚本控制元素的位置. 这些位置和具体游戏场景相关, cc 也会提供常用工具库.

另外一个是结算, 判断分数高低, 或者数组比较, 最多通过位置计算碰撞, 来判断游戏结果.

可以看到这些计算都是在脚本中进行的, 也都是比较简单的数据结构或者数学公式.

在游戏场景外, 一些菜单, 设置的界面就和网页差不多了.

cc 系统介绍

我看了一个视频, 自己写了2个demo, 简单总结下 cc 的系统.

总的来说, cc 像是个低代码平台.

编辑器界面

编辑器就是典型的低代码.

  • 场景界面. 就是把元素拖拖拽拽的地方.
  • 资源列表. 放代码和图片的地方, 就是网页开发的src目录. 资源的类型值得下文展开.
  • 节点层级. 在编辑场景的时候, 场景通常是有多个节点的, 节点之间有层级关系便于维护, 所以有个界面展示.
  • 节点属性. 在场景界面里选中节点, 肯定是可以编辑这个节点的属性的, 大小/位置什么的.

这些元素一看就是低代码了, 应该是低代码借鉴了这些游戏引擎的.

这些面板都是可以拖动位置, 或者合并成tab的, 很方便.

资源类型介绍

上面说到资源, 资源类型还挺多的. 这里介绍一点我用到的.

  • ts文件, 图片文件.

脚本文件和图片文件都是用来拖到节点里, 和节点绑定的.

  • 场景.

应该是 cc 的核心了. 从文件看来, 就是个 json. 所以拖拖拽拽的结果就是修改 json. 然后通过 json schema 执行渲染或打包.

场景是由节点组成的. 在场景里新建节点并嵌套, 来构建游戏场景.

节点的种类是很多的, 可以插入图片变为元素, 也可以绑定脚本, 作为一个"虚拟节点", 只是为了维护方便.

场景有必须的节点是 canvas 和 camera.

  • prefab.

可以理解为"组件". 在场景中编辑了一些节点, 如果觉得可以复用, 直接把整个节点拖到资源列表里, 就会产生一个 prefab. 使用的时候拖动这个 prefab 到场景, 就会产生一个实例了.

更多的应该是用脚本批量创建.

  • 动画.

其实和ts文件与图片文件一样, 是关联到节点上的. 但他是 cc 特有的, 可以在 cc 里编辑动画内容, 可以对各个属性做帧动画, 也可以导入动画软件做的动画.

开发流程

我写了2个算能跑的项目, 来说说开发的过程.

  1. 资源目录下新建一些文件夹: scripts, imgs, animation, scene.
  2. 主要开发就是编辑场景. 在场景里添加节点, 然后给节点贴图, 从"资源列表"把资源拖到"节点属性面板"就好了, 容易.

我的节点很简单, 就是玩家角色, 和背景.

  1. 建立个空节点, 写游戏逻辑. 具体操作是新建个 ts 文件, 然后拖到这个节点属性上.
  2. 游戏逻辑需要操作的内容, 包括动画, 都以"拖动"的方式关联到"节点属性面板"上.

这样就写好一个游戏了.

游戏逻辑开发是和 html5 游戏一样的, 最后一小节我再赘述下吧.

游戏逻辑编写

游戏逻辑在 ts 的脚本文件中编写.

所有新建的 ts 文件都会有一个初始模板. 内容是export class XXX extends cc.Component {}.

这个类有2个生命周期方法. start()update().

update()方法的参数deltatime是离上一帧的时间, 不了解的去看下 h5 游戏的执行就好了.

游戏逻辑一定涉及到元素, 只要在脚本文件里声明一个属性, 就能在节点属性面板上看到一个属性.

把这个属性需要控制的元素拖过去就行.

然后元素节点也可以绑定脚本. 这个脚本可以通过this.node提供的 api 来操作元素的位置.

元素节点一般会绑定动画, 也需要把动画声明在属性里, 然后从资源列表把动画拖动到自己的节点属性面板上, 就可以在脚本里调用动画了.

我现在理解的层级是这样的:

  • 总脚本gameControl写在单独节点里. 写游戏逻辑与结算判断.
  • 会动的元素, 自己绑定节点, 写一些方法供总脚本调用.
  • 编辑一些动画, 供上一步"会动的元素"调用. 一般是和元素位置的移动同时调用的.

贴一些代码

这里分享个具体的demo代码. demo内容很简单, 按方向键角色就会在地图上走路.

走路的时候会播放一个帧动画, 是从微信表情里导出的20个png.

脚本文件只有2个. 一个是gameControl游戏控制, 只做了监听键盘事件, 并调用player脚本的对应方法.

另一个player脚本写了对应的方法, 改变一些参数, 在update()方法根据参数来设置角色的位置.

gameControl.ts

import { _decorator, Component, Node, input, Input, EventKeyboard } from 'cc'
const { ccclass, property } = _decorator

import { player } from './player'

@ccclass('gameControl')
export class gameControl extends Component {
@property(player)
public player: player = null

start() {
input.on(Input.EventType.KEY_DOWN, (event) => {
switch (event.keyCode) {
case 37:
this.player.left()
break
case 38:
this.player.up()
break
case 39:
this.player.right()
break
case 40:
this.player.down()
break

}
})
}

update(deltaTime: number) {

}
}

player.ts

import { _decorator, Component, Node, Animation, tween, Vec3, math } from 'cc'
const { ccclass, property } = _decorator

@ccclass('player')
export class player extends Component {

@property(Animation)
anim: Animation = null

@property(Node)
lulu: Node = null

private direction = new Vec3(1, 0, 0)
private isMoving = false
private movePeriod = 0

start() {

}

update(deltaTime: number) {
if (this.isMoving) {
if (this.movePeriod < 1) {
let target = this.node.position
Vec3.add(target, this.node.position, this.direction)
this.node.setPosition(target)
this.movePeriod += deltaTime
} else {
this.isMoving = false
}
}
}

left() {
if (!this.isMoving) {
this.lulu.setRotationFromEuler(0, 0, 180)
this.direction = new Vec3(-1, 0, 0)
this.startMove()
}
}

right() {
if (!this.isMoving) {
this.lulu.setRotationFromEuler(0, 0, 0)
this.direction = new Vec3(1, 0, 0)
this.startMove()
}
}

up() {
if (!this.isMoving) {
this.lulu.setRotationFromEuler(0, 0, 90)
this.direction = new Vec3(0, 1, 0)
this.startMove()
}
}

down() {
if (!this.isMoving) {
this.lulu.setRotationFromEuler(0, 0, 270)
this.direction = new Vec3(0, -1, 0)
this.startMove()
}
}

startMove() {
this.anim.play()
this.isMoving = true
this.movePeriod = 0
}
}

另外做的demo是跟着cocos creator 文档的2d游戏做的. 有兴趣的也可以跟我一样, 先照着这个做一遍, 再自己新建个空项目自己操作.

最后

我认为 cocos creator 对 web 开发者来说真的是非常好上手了.

我认为小游戏的设计分为2个吧. 核心游戏场景, 与, 游戏运营.

其实核心游戏场景都不复杂的, 那怎么能让玩家一直玩呢.

其实就是策划运营, 操作一些数据, 让每次玩同一个场景, 看到不同的数字, 和不同的皮肤.

用户就会为了这些数字(pay for ability), 和皮肤(pay for love)来付费了.

我认为游戏脚本不难. 难在2点:

  1. 游戏的完整度, 需要美术和动画, 程序只能控制角色的位置, 加上动画才让人有操作角色的感觉. 精美的游戏场景也能让玩家觉得真实.
  2. 策划: 数值系统, 货币系统, 奖励系统, 活动这些. 让玩家重复玩同一个场景几百遍还觉得自己在成长, 真是牛逼.


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

海康摄像头 web 对接

web
真的烦躁,一个月拿着死工资,每天写着增删改查,不知道以后能做什么,有时候真的想离职,进广东电子厂.... 这段时间,XXXX 要加一个海康监控,哎。 苦命开发 从官网下载下来的web包 \webs\codebase 目录中有,第一个是插件,必须安装的, 后面...
继续阅读 »

真的烦躁,一个月拿着死工资,每天写着增删改查,不知道以后能做什么,有时候真的想离职,进广东电子厂....


这段时间,XXXX 要加一个海康监控,哎。


苦命开发


从官网下载下来的web包


image.png


\webs\codebase 目录中有,第一个是插件,必须安装的, 后面两个JS文件是开发必要的。还要一个 JQ的,它内部使用了jq


image.png


初始化插件


引入了提供的jS后,就可以开始牛马了。。。。


首先注册插件,并检查更新


因为我这是4个摄像头,所以窗口是 2 * 2


WebVideoCtrl.I_InitPlugin 是用于初始化插件,成功回调是cbInitPluginComplete


WebVideoCtrl.I_CheckPluginVersion 用于检查更新。


在自动登录这里,我准备了数组,包含登录端口密码等信息,建议每一个之后都要等1秒。多个账号登录,插件只加载一次即可。


init() {
// 这里的代码会在文档完全加载后执行
WebVideoCtrl.I_InitPlugin({
iWndowType: 2, // 设置分屏类型为 2*2,显示 4 个窗口
bWndFull: true, // 支持单窗口双击全屏
bDebugMode: true, // 关闭调试模式
cbInitPluginComplete: async () => {
console.log("插件初始化完成")
try {
// 加载插件
await WebVideoCtrl.I_InsertOBJECTPlugin("divPlugin")
// 检查插件是否最新
const bFlag = await WebVideoCtrl.I_CheckPluginVersion()
if (bFlag) {
alert("检测到新的插件版本,双击开发包目录里的HCWebSDKPlugin.exe升级!")
}

for (const item of this.channel) {
// 自动登陆
this.clickLogin(item)
await new Promise(resolve => setTimeout(resolve, 1000))
}
} catch {
alert("插件初始化失败,请确认是否已安装插件;如果未安装,请双击开发包目录里的HCWebSDKPlugin.exe安装!")
}
},
iTopHeight: 0 // 插件窗口的最高高度
})
}

实现登录


WebVideoCtrl.I_Login 是登录接口



  • 参数1:ip地址

  • 参数2:1 是http,2 是https

  • 参数3:端口

  • 参数4:平台账户

  • 参数5:平台密码


// 登陆
clickLogin(item) {
WebVideoCtrl.I_Login(item.ip, 1, item.port, 'admin', 'admin123', {
timeout: 3000,
success: () => {
console.log('登陆成功')
setTimeout(() => {
setTimeout(() => {
this.getChannelInfo(item)
}, 1000)
}, 10)
},
error: (oError) => {
if (this.ERROR_CODE_LOGIN_REPEATLOGIN === oError.errorCode) {
console.log('已登录过!')
} else {
console.log(" 登录失败!", oError.errorCode, oError.errorMsg)

}
}
})
}

获取通道信息


getChannelInfo 函数需要传递一个当前控制摄像头的信息对象。


模拟通道接口:WebVideoCtrl.I_GetAnalogChannelInfo


这里会使用 jq的一些方法,会对获取的xml元素进行遍历,并将获取的信息,加入到数组集合中,进行预览视频。



  • id:获取的通道号是预览的必要字段。

  • 数字通道:支持高清甚至超高清分辨率,如 1080P、2K、4K 等,但是对网络要求较高

  • 零通道:无法播放,坏掉了。

  • 模拟通道:成本小,实时性高。


// 初始化通道
getChannelInfo(item) {
// 模拟通道
WebVideoCtrl.I_GetAnalogChannelInfo(item.ip, {
success: (xmlDoc) => {
const oChannels = $(xmlDoc).find('VideoInputChannel')
$.each(oChannels, (i, channelObj) => {
let id = $(channelObj).find('id').eq(0).text(),
name = $(channelObj).find('name').eq(0).text()
if ("" === name) {
name = "Camera " + (i < 9 ? "0" + (i + 1) : (i + 1))
}
const ch = this.channel.find(arr => arr.ip === item.ip)
ch.channelId = id
ch.name = name
})
console.log(item.ip + '获取模拟通道成功!')

},
error: function (oError) {
console.log(ip + '获取模拟通道失败!', oError.errorCode, oError.errorMsg)

}
})
// 数字通道
WebVideoCtrl.I_GetDigitalChannelInfo(item.ip, {
success: function () {
// console.log(item.ip + '获取数字通道成功!')
},
error: function (oError) {
// console.log(item.ip + '获取数字通道失败!', oError.errorCode, oError.errorMsg)
}
})
// 零通道
WebVideoCtrl.I_GetZeroChannelInfo(item.ip, {
success: function () {
// console.log(item.ip + '获取零通道成功!')
},
error: function (oError) {
// console.log(item.ip + '获取零通道失败!', oError.errorCode, oError.errorMsg)
}
})
// 直接预览
this.clickStartRealPlay(item)
}

预览窗口


clickStartRealPlay 函数需要传递一个当前控制摄像头的信息对象。


WebVideoCtrl.I_GetWindowStatus 可以获取窗口的状态,比如传递 0 ,可以查看 第一个窗口的状态。返回值如果不是null,表示在播放了。


WebVideoCtrl.I_Stop 用于关闭当前播放的窗口,参数 iWndIndex 用于控制关闭的那个窗口,默认会根据当前选中的窗口。


WebVideoCtrl.I_StartRealPlay 预览视频



  • 参数一:ip地址 + 下划线 + 端口,拼接的字符串,比如:'192.168.1.101_80'

  • 参数二:是码流,1 主码流,2 子码流

  • 参数三:是前面通过通道获取的通道ID

  • 参数四:默认是false,表示是否播放零通道

  • 参数五:RTSP端口号


// 预览窗口
clickStartRealPlay(item) {
const ips = item.ip + '_' + item.port
// 获取窗口的状态
const oWndInfo = WebVideoCtrl.I_GetWindowStatus(item.g_iWndIndex)
const iRtspPort = ''
const iChannelID = item.channelId
const bZeroChannel = item.zeroType
const szInfo = ''

const startRealPlay = function () {
WebVideoCtrl.I_StartRealPlay(ips, {
iWndIndex: item.g_iWndIndex,
iStreamType: 1,
iChannelID: iChannelID,
bZeroChannel: bZeroChannel,
iPort: iRtspPort,
success: function () {
console.log(ips + '开始预览成功!')
},
error: function (oError) {
console.log(ips + " 开始预览失败!", oError.errorCode, oError.errorMsg)
}
})
}

if (oWndInfo != null) { // 已经在播放了,先停止
WebVideoCtrl.I_Stop({
success: function () {
startRealPlay()
}
})
} else {
startRealPlay()
}
}

摄像头功能控制


接口:WebVideoCtrl.I_PTZControl



  • 参数一:操作类型(1-上,2-下,3-左,4-右,5-左上,6-左下,7-右上,8-右下,9-自转,10-调焦+, 11-调焦-, 12-F聚焦+, 13-聚焦-, 14-光圈+, 15-光圈-

  • 参数二:true 停止,false 启动

  • 参数三:对象:iWndIndex 窗口号,默认为当前选中窗口,iPTZSpeed 云台速度,默认为4


<div class="jiu" :style="{display: isOpen ? 'flex': 'none'}">
<div class="remote-control">
<el-tooltip content="向左上转动" placement="top-start" effect="light">
<div class="button top-left" @mousedown="mouseDownPTZControl(5, false)"
@mouseup="mouseDownPTZControl(1, true)">
</div>
</el-tooltip>
<el-tooltip content="向上转动" placement="top-start" effect="light">
<div class="button" @mousedown="mouseDownPTZControl(1, false)"
@mouseup="mouseDownPTZControl(1, true)">

<i class="iconfont icon-shangjiantou1"></i>
</div>
</el-tooltip>
<el-tooltip content="向右上转动" placement="top-start" effect="light">
<div class="button top-right" @mousedown="mouseDownPTZControl(7, false)"
@mouseup="mouseDownPTZControl(1, true)">
</div>
</el-tooltip>
<el-tooltip content="向左转动" effect="light">
<div class="button" @mousedown="mouseDownPTZControl(3, false)"
@mouseup="mouseDownPTZControl(1, true)">

<i class="iconfont icon-zuojiantou"></i>
</div>
</el-tooltip>
<el-tooltip content="开启自动旋转" effect="light">
<div class="button center" @click="mouseDownPTZControl(9, false)">
<i class="iconfont icon-zidongxuanzhuan"></i>
</div>
</el-tooltip>
<el-tooltip content="向右转动" effect="light">
<div class="button" @mousedown="mouseDownPTZControl(4, false)"
@mouseup="mouseDownPTZControl(1, true)">

<i class="iconfont icon-youjiantou"></i>
</div>
</el-tooltip>
<el-tooltip content="向左下转动" effect="light">
<div class="button bottom-left" @mousedown="mouseDownPTZControl(6, false)"
@mouseup="mouseDownPTZControl(1, true)">
</div>
</el-tooltip>
<el-tooltip content="向下转动" effect="light">
<div class="button" @mousedown="mouseDownPTZControl(2, false)"
@mouseup="mouseDownPTZControl(1, true)">

<i class="iconfont icon-xiajiantou1"></i>
</div>
</el-tooltip>
<el-tooltip content="向右下转动" effect="light">
<div class="button bottom-right" @mousedown="mouseDownPTZControl(8, false)"
@mouseup="mouseDownPTZControl(1, true)">
</div>
</el-tooltip>
</div>
</div>
<!-- 下方操作按钮 -->
<div class="div-group" :style="{display: isOpen ? 'block': 'none'}">
<div style="display: flex; justify-content:space-around;">
<el-button-group>
<el-tooltip content="焦距变大" placement="top" effect="light">
<div class="btn-groups" @mousedown="mouseDownPTZControl(10, false)" @mouseup="mouseDownPTZControl(11, true)">
<i class="iconfont icon-fangdajing-jia"></i>
</div>
</el-tooltip>
<el-tooltip content="焦距变小" placement="top" effect="light">
<div class="btn-groups" @mousedown="mouseDownPTZControl(11, false)" @mouseup="mouseDownPTZControl(11, true)">
<i class="iconfont icon-fangdajing-jian"></i>
</div>
</el-tooltip>
</el-button-group>
<el-button-group>
<el-tooltip content="焦点前调" placement="top" effect="light">
<div class="btn-groups" @mousedown="mouseDownPTZControl(12, false)" @mouseup="mouseDownPTZControl(12, true)">
<i class="iconfont icon-jiaodianqiantiao"></i>
</div>
</el-tooltip>
<el-tooltip content="焦点后调" placement="top" effect="light">
<div class="btn-groups" @mousedown="mouseDownPTZControl(13, false)" @mouseup="mouseDownPTZControl(12, true)">
<i class="iconfont icon-jiaodianhoutiao"></i>
</div>
</el-tooltip>
</el-button-group>
<!-- <el-button-group>
<el-tooltip content="光圈扩大" placement="top" effect="light">
<el-button>
<i class="iconfont icon-guangquankuoda"></i>
</el-button>
</el-tooltip>
<el-tooltip content="光圈缩小" placement="top" effect="light">
<el-button>
<i class="iconfont icon-guangquansuoxiao"></i>
</el-button>
</el-tooltip> -->

</el-button-group>
</div>
</div>

mouseDownPTZControl(iPTZIndex, selection) {
// 获取窗口状态
const oWndInfo = WebVideoCtrl.I_GetWindowStatus(this.item.g_iWndIndex)
if (oWndInfo == null) {
return
}

// 如果是零通道,直接返回
if (this.item.zeroType) {
return
}
let iPTZSpeed = selection ? 0 : 4
// 表示开启了自动
if (9 === iPTZIndex && this.g_bPTZAuto) {
// 将速度置为 0
iPTZSpeed = 0

} else {
this.g_bPTZAuto = false
}

// 控制云平台
WebVideoCtrl.I_PTZControl(iPTZIndex, selection, {
iWndIndex: this.item.g_iWndIndex, iPTZSpeed,
success: (xmlDoc) => {
if (9 == iPTZIndex) {
this.g_bPTZAuto = !this.g_bPTZAuto
}
},
error: function (oError) {
console.log(oWndInfo.szDeviceIdentify + " 开启云台失败!", oError.errorCode, oError.errorMsg)
}
})

}

到此就结束了,海康这个还不错,就是没有vue webpack的包,在webpack 的环境下,是会报错的。


作者:哪里的破水瓶
来源:juejin.cn/post/7449644683330240549
收起阅读 »

我:偷偷告诉你,我们项目里的进度条,全都是假的!🤣 产品:???😲

web
扯皮 最近接到了一个需求:前端点击按钮触发某个任务并开启轮询获取任务进度,直至 100% 任务完成后给予用户提示 这个业务场景还挺常见的,但是突然上周后端联系到我说现在的效果有点差,之前都是小任务那进度条展示还挺不错的,现在有了一些大任务且会存在排队阻塞的情况...
继续阅读 »

扯皮


最近接到了一个需求:前端点击按钮触发某个任务并开启轮询获取任务进度,直至 100% 任务完成后给予用户提示


这个业务场景还挺常见的,但是突然上周后端联系到我说现在的效果有点差,之前都是小任务那进度条展示还挺不错的,现在有了一些大任务且会存在排队阻塞的情况,就导致视图上经常卡 0% 排队,用户体验太差了,问能不能在刚开始的时候做个假进度先让进度条跑起来😮


因此就有了这篇文章,简单做一下技术调研以及在项目中的应用


正文


其实假进度条也不难做,无非是轮询的时候我们自己做一个随机的自增,让它卡到 99% 等待后端真实进度完成后再结束


只不过还是想调研一下看看市面上有没有一些成熟的方案并去扒一下它们的源码🤓


NProgress


首先当我听到这里的需求后第一时间想到的就是它:rstacruz/nprogress: For slim progress bars like on YouTube, Medium, etc


记得大学期间做的一些中后台系统基本都少不了路由跳转时的顶部进度条加载,那时候就有了解到 NProgress,它的使用方式也很简单,完全手控:NProgress: slim progress bars in JavaScript,去文档里玩一下就知道了


视图呈现的效果就是如果你不手动结束那它就会一直缓慢前进卡死 99% ,挺符合我们这里的需求,可以去扒一下它内部进度计算相关的逻辑


NProgress 的内容实际上比较少,源码拉下来可以看到主要都在这一个 JS 文件里了:


image.png


需要注意的是我们看的是这个版本:rstacruz/nprogress at v0.2.0,master 分支与 npm 安装的 0.2.0 内部实现还是有些差别的


我们这里不关注它的样式相关计算,主要来看看对进度的控制,直奔 start 方法:


image.png


还是比较清晰的,这里的 status 就是内部维护的进度值,默认为 null,所以会执行 NProgress.set,我们再来看看 set 方法:


image.png


set 方法里有一大堆设置动画样式逻辑都被我剪掉了,关于进度相关的只有这些。相当于利用 clamp 来做一个夹层,因为初始进来的 n 为 null,所以经过处理后进度变为 0.08


再回到 start 的逻辑,其中 work 就是内部轮询控制进度自增的方法了,初始配置 trickle 为 true 代表自动开启进度自增,由于进度条在 set 方法中已经设置为 0.08,所以走到后面的 NProgress.trickle 逻辑


image.png


看来这里就是进度控制的核心逻辑了, trickle 里主要调用了 inc,在 trickle 中给 inc 传递了一个参数:Math.random() * Settings.trickleRate,显然这里范围是:0 <= n < 0.02


而在 inc 中,如果传递的 amount 有值的话那就每次以该值进行自增,同时又使用 clamp 将最大进度卡在 0.994


最后再调用 set 方法,set 里才是更新进度和视图进度条的方法,涉及到进度更新时都需要回到这里


当然 NProgress.inc 也可以手动调用,还对未传参做了兼容处理:



amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95)



即根据当前进度 n 计算剩余进度,再随机生成自增值


再来看 done 方法,它就比较诡异了:


image.png


按理来说直接将进度设置为 1 就行,但它以链式调用 inc 再调用 set,相当于调用了两次 set


而这里 inc 传参又没什么规律性,推测是为了 set 中的样式处理,感兴趣的可以去看看那部分逻辑,还挺多的...😶


一句话总结一下 NProgress 的进度计算逻辑:随机值自增,最大值限制


但是因为 NProgress 与进度条样式强绑定,我们肯定是没法直接用的


fake-progress


至于 fake-progress 则是我在调研期间直接搜关键词搜出来的😶:piercus/fake-progress: Fake a progress bar using an exponential progress function


很明显看介绍就是干这个事的,而且还十分专业,引入数学函数展示假进度条效果,具有说服力:


image.png


所以我们项目中其实就是用的这个包,只不过和 NProgress 类似,两个包都比较老了,瞟一眼源码发现都是老 ES5 了🤐


因为我们项目中用的是 React,这里给出 React 的 demo 吧,为了编写方便使用了几个 ahooks 里的 hook:


image.png


其实使用方法上与 NProgress 都类似,不过两者都属于通用的工具库不依赖其他框架,所以像视图渲染都需要自己手动来做


注意实例化中的传参 timeConstant,某种意义上来讲这个值就相当于“进度增长的速率”,但也不完全等价,我们来看看源码


因为不涉及到样式,fake-progress 源码更简单,核心就在这里:


image.png


下方的数学公式就是介绍图中展示的,只能说刚看到这部分内容是真的是死去的数学只是突然又开始攻击我😅,写了那么多函数,数学函数是啥都快忘了


我们来简单分析一下这个函数 1 - Math.exp(-1 * x),exp(x)= exe^xexe^x 的图像长这样,高中的时候见的太多了:


image.png


那假如这里改成 exp(-x) 呢?有那味了,以前应该是有一个类似的公式 f(x)f(x)f(x)f(-x) 图像效果是关于 y 轴对称,好像是有些特殊的不符合这个规律?🤔反正大部分都是满足的


image.png


OK,那我们继续进行转换,看看 -exp(-x) 的效果


同样有个公式 f(x)f(x)f(x)-f(x) 图像效果是关于 x 轴对称:


image.png


初见端倪,不知道你们有没有注意 -exp(-x) 最终呈现的图像是无限接近于 x 轴的,也就是 0:


image.png


那有了🤓,假如我再给它加个 1 呢?它不就无限接近于 1 了,即 -exp(-x) + 1,这其实就是 fake-progress 里公式的由来:
image.png


但你会发现如果 x 按 1 递增就很快进度就接近于 1 了,所以有了 timeConstant 配置项来控制 x 的增长,回看这个公式:1 - Math.exp(-1 * this._time / this.timeConstant)


this._time 是一直在增长的,而 this.timeConstant 作为分母如果被设置为一个较大的值,那可想而知进度增长会巨慢


所以 fake-progress 的核心原理是借助数学函数,以函数值无限接近于 1 来实现假进度条,但是这种实现有一个 bug,可以看我提的这个 issues,不过看这个包的更新时间感觉作者也不会管了😅:


bug: progress may reach 100% · Issue #7 · piercus/fake-progress


image.png


useFakeProgress


虽然我们现在项目中使用的是 fake-progress,但是个人感觉用起来十分鸡肋,而且上面的 bug 也需要自己手动兼容,因此萌生出自己封装一个 hook 的想法,让它更符合业务场景


首先我们确定一下进度计算方案,这里我毫不犹豫选择的是 NProgress 随机值增长方案,为什么?因为方便用户自定义


而且 NProgress 相比于 fake-progress 有一个巨大优势:手动 set 进度后仍然保持进度正常自动递增


这点在 fake-progress 中实现是比较困难的,因为你无法保证手动 set 的进度是在这个函数曲线上,相当于给出函数 y 值反推 x 值,根据反推的 x 值再进行递增,想想都麻烦


确定好方案后我们来看下入参吧,参考 NProgress 我定义了这几个配置项:


image.png


这里我简单解释一下 rerender 和 amount 配置:


实际上在封装这个 hook 的时候我一直在纠结这里的 progress 到底是 state 还是 ref,因为大多数场景下 hook 内部通过轮询定时器更新进度,而真实业务代码中也会开启定时器去轮询监听业务接口的


所以如果写死为 state,那这个场景 hook 内部的每次更新 render 是没必要的,但是假如用户又想只是使用假进度展示,没有后端业务接口呢?


思来想去其实完全可以放权给用户进行配置,因为 state = ref + update,统一使用 ref,用户配置 rerender 时我们在每次更新时 update 即可


至于 amount 我是希望放权给用户进行自定义递增值,你可以配置成一个固定值也可以配置成随机值,更可以像 NProgress master 分支下这样根据当前进度来控制自增,反正以函数参数的形式能够拿到当前的 progress:


image.png


至于实现细节就不再讲述了,实际上就是轮询定时器没什么复杂的东西,直接上源码了:


import { useRef, useState } from "react";

interface Options {
minimun?: number;
maximum?: number;
speed?: number;
rerender?: boolean;
amount?: (progress: number) => number;
formatter?: (progress: number) => string;
onProgress?: (progress: number) => void;
onFinish?: () => void;
}

export function useFakeProgress(options?: Options): [
{ current: string },
{
inc: (amount?: number) => void;
set: (progress: number) => void;
start: () => void;
stop: () => void;
done: () => void;
reset: () => void;
get: () => number;
}
] {
const {
minimun = 0.08,
maximum = 0.99,
speed = 800,
rerender = false,
amount = (p: number) => (1 - p) * clamp(Math.random() * p, minimun, maximum),
formatter = (p: number) => `${p}`,
onProgress,
onFinish,
} = options || {};

const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const progressRef = useRef(0);
const progressDataRef = useRef(""); // formatter 后结果
const [, update] = useState({});

const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);

const setProgress = (p: number) => {
progressRef.current = p;
progressDataRef.current = formatter(p);
onProgress?.(p);
if (rerender) update({});
};

const work = () => {
const p = clamp(progressRef.current + amount(progressRef.current), minimun, maximum);
setProgress(p);
};

const start = () => {
function pollingWork() {
work();
timerRef.current = setTimeout(pollingWork, speed);
}

if (!timerRef.current) pollingWork();
};

const set = (p: number) => {
setProgress(clamp(p, minimun, maximum));
};

const inc = (add?: number) => {
set(progressRef.current + (add || amount(progressRef.current)));
};

const stop = () => {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = null;
};

const reset = () => {
stop();
setProgress(0);
};

const done = () => {
stop();
setProgress(1);
onFinish?.();
};

return [
progressDataRef,
{
start,
stop,
set,
inc,
done,
reset,
get: () => progressRef.current,
},
];
}

这里需要补充一个细节,在返回值里使用的是 progressDataRef 是 formatter 后的结果为 string 类型,如果用户想要获取原 number 的 progress,可以使用最下面提供的 get 方法拿 progressRef 值


一个 demo 看看效果,感觉还可以:
image.png


fake-progress.gif


当然由于直接返回了 ref,为了防止用户篡改可以再上一层代理劫持,我们就省略了


这也算一个工具偏业务的 hook,可以根据自己的业务来进行定制,这里很多细节都没有补充只是一个示例罢了🤪


End


以上就是这篇文章的内容,记得上班之前还在想哪有那么多业务场景需要封装自定义 hook,现在发现真的是各种奇葩需求都可以封装,也算是丰富自己武器库了...


作者:討厭吃香菜
来源:juejin.cn/post/7449307011710894080
收起阅读 »

2025年,前端开发为什么一定要学习Rust?

web
引言Rust语言是一门现代系统编程语言,由Mozilla Research于2009年开始开发,Mozilla Research 是 Mozilla 基金会旗下的一个研究部门,专注于推动开放网络和创新技术的发展,Rust语言正是在 Mozilla Resear...
继续阅读 »

引言

Rust语言是一门现代系统编程语言,由Mozilla Research于2009年开始开发,Mozilla Research 是 Mozilla 基金会旗下的一个研究部门,专注于推动开放网络和创新技术的发展,Rust语言正是在 Mozilla Research 中孕育并发展的。

Rust 最早是 Mozilla 雇员 Graydon Hoare 的个人项目,在2006年开始了Rust语言的初步设计,Mozilla 随后投入资源,支持Rust的发展,并最终于2010年公开这个项目,2015年发布1.0版本。

以下引用自Rust 语言圣经

大家可能疑惑 Rust 为啥用了这么久才到 1.0 版本?与之相比,Go 语言 2009 年发布,却在 2012 年仅用 3 年就发布了 1.0 版本[^1]。

● 首先,Rust 语言特性较为复杂,所以需要全盘考虑的问题非常多;

● 其次,Rust 当时的参与者太多,七嘴八舌的声音很多,众口难调,而 Rust 开发团队又非常重视社区的意见;

● 最后,一旦 1.0 快速发布,那绝大部分语言特性就无法再被修改,对于有完美强迫症的 Rust 开发者团队来说,某种程度上的不完美是不可接受的。

因此,Rust 语言用了足足 6 年时间,才发布了尽善尽美的 1.0 版本。

大家知道 Rust 的作者到底因为何事才痛下决心开发一门新的语言吗?

说来挺有趣,在 2006 年的某天,作者工作到精疲力尽后,本想回公寓享受下生活,结果发现电梯的程序出 Bug 崩溃了,要知道在国外,修理工可不像在中国那样随时待岗,还要知道,他家在 20 多楼!

最后,他选择了妥协,去酒店待几天等待电梯的修理。

当然,一般人可能就这样算了,毕竟忍几天就过去了嘛。但是这名伟大的程序员显然也不是一般人,他面对害他流离失所的电梯拿起了屠龙宝刀 - Rust。

自此,劈开一个全新的编程世界。

深入了解Rust

为什么要创建Rust这门语言?

在 Rust 出现之前,系统级编程领域主要由 C 和 C++ 统治。虽然这两种语言在性能方面表现出色,但它们也存在一些固有的缺陷,促使了 Rust 的诞生。

什么是系统级编程语言?

简单来说,系统级编程语言用于开发操作系统、驱动程序、嵌入式系统、游戏引擎、数据库等对性能和硬件控制要求极高的软件。

有以下特性:

  • 硬件访问: 系统级语言需要能够直接访问硬件资源,直接操作硬件
  • 高性能: 系统级程序通常需要直接操作硬件,对性能要求非常高。因此,系统级语言通常具有高效的内存管理机制和优化的编译器,以生成高效的机器码。
  • 较强的类型系统和编译时检查:为了尽早发现潜在的错误,系统级语言通常具有较强的类型系统和编译时检查机制,以提高代码的可靠性和安全性。
  • 并发和并行: 现代计算机系统通常具有多核处理器,系统级程序需要能够有效地利用多核资源,实现并发和并行执行,以提高性能。
  • 内存控制: 系统级编程需要对内存进行精细的控制,包括内存分配、释放、布局等。一些系统级语言允许开发者直接操作内存地址,以实现更高的灵活性和效率

有哪些系统级编程语言?

  • C/C++

    无GC,性能高,内存不安全

  • Rust

    无GC,性能高,内存安全

  • Go

    有GC,性能不如Rust,安全性不如Rust。

  • Assembly Language(汇编语言)

    性能高,开发效率低

  • zig

    无GC,性能高,安全性不如Rust,发展初期

C/C++ 的缺陷

  • 内存安全问题: C/C++ 允许开发者手动管理内存,这虽然提供了灵活性,但也容易导致各种内存安全问题,如:
    • 空指针(Null Pointers): 访问未初始化的指针或空指针会导致程序崩溃。
    • 野指针(Wild Pointers): 指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。。
    • 悬垂指针(Dangling Pointers): 指针指向曾经存在的对象,但该对象已经被释放,再次访问该指针会导致未定义行为,悬垂指针是野指针的一种。
    • 双重释放(Double Free): 释放同一块内存两次,导致崩溃或不可预测的行为。
    • 内存泄漏(Memory Leaks): 分配的内存没有被及时释放,导致内存占用不断增加,最终可能导致系统崩溃。
    • 缓冲区溢出(Buffer Overflows): 向缓冲区写入超出其容量的数据,可能覆盖相邻的内存区域,导致程序崩溃或安全漏洞。
  • 并发安全问题: C/C++ 的并发编程容易引入数据竞争(Data Races)等问题,导致程序行为不确定,难以调试和维护。
  • 缺乏现代化的语言特性: C/C++ 的语法相对陈旧,缺乏一些现代化的语言特性,如模式匹配、类型推断等,使得代码编写和维护相对繁琐。

Rust 的创建正是为了解决 C/C++ 等语言的这些不足,同时保留其高性能的优点。具体来说,Rust 的设计目标是:

  • 解决内存安全问题: Rust 通过所有权系统、借用检查器等机制,在编译时就杜绝了空指针、野指针、数据竞争等内存安全问题。
  • 提供安全的并发编程: Rust 的所有权系统和类型系统也对并发安全提供了保障,使得开发者可以更容易地编写安全的并发程序。
  • 提供现代化的语言特性: Rust 引入了模式匹配、类型推断、trait 等现代化的语言特性,提高了代码的简洁性、可读性和可维护性。
  • 保持高性能: Rust 的设计理念是“零成本抽象”,即提供高级的抽象能力,但不会带来额外的运行时开销,且无需垃圾回收器等运行时机制,从而避免了额外的性能开销,媲美 C/C++。

简而言之,因为还缺一门无 GC 且无需手动内存管理、性能高、工程性强、语言级安全性、广泛适用性的语言,而 Rust 就是这样的语言。

为什么选择Rust语言?

  • 保证安全、内存占用小的同时能提供和C/C++ 一样的性能
  • 广泛的适用性,系统编程、网络服务、命令行工具、WebAssembly等场景都能应用
  • 生态渐渐完善,有大量的库和框架,完整的工程化开发工具链,强大的包管理
  • 社区非常活跃和友好,文档全面

Rust不是闭门造车的语言,能看出来设计者是做过大量不同语言的项目的人,Rust从解决实际问题出发,借鉴和融合其他语言的优点,又能够创新地提出所有权和生命周期,这个强大的能力带来了0开销的内存安全和线程安全。

凡事有利必有弊,Rust是站在前人的肩膀上,它集百家之长,借鉴了其他语言的许多优秀特性,如npm的包管理、go的channel来进行并发通信、Haskell的trait(类似java的接口),还有元组、泛型、枚举,闭包、智能指针这些特性并非rust原创,但Rust确实把这些优点全部吸收了进来,而没有做过度的设计和发散,让有一些其他语言基础的人还能够减轻一些上手成本。

即使这样,Rust依然是在一众语言里学习曲线最陡峭的语言之一,此外,Rust为了提高运行时性能必然是会牺牲一些编译时的效率的。

但是,这丝毫不会影响Rust成为一门伟大的语言,如果有一门语言可以改变你的编程思维方式,倒逼你进行更好的代码设计,让你在初学过程中连连发出“原来是这样啊”的感叹,那么它一定是Rust。

Rust 核心设计理念对前端有哪些启示

Rust的很多设计理念都可以在前端领域中或多或少地找到影子,让你明白前端某些技术为什么要这么设计,以及为什么不那么设计。

安全性设计

1. 类型安全

  • 静态类型检查:

    Rust 提供了一个非常强大的类型系统,确保了类型安全。在编译时,Rust 会强制要求所有变量和函数都有明确的类型声明。这使得很多潜在的错误能够在编译时被捕获,避免了运行时出现类型错误。

    反观JavaScript,作为js开发者,那是太有发言权了,由于js是动态类型语言,所以很多错误只能在运行时被发现,跑着跑着可能就出一个线上bug,这是多少前端开发者的痛啊。

    当在写了一段时间Rust后,我们就会明白TypeScript为什么会火了,以及TS为什么是必要的,TS不能解决所有问题,但能解决大部分低级问题。

    如果你不用TS,提高代码健壮性也是有方法的,只不过心智负担更重。参照文章接口一异常你的页面就直接崩溃了?

  • 不可变性和可变性:

    在 Rust 中,变量默认是不可变的,只有显式声明为可变 (mut) 才能修改。这种设计减少了错误发生的概率,因为不可变数据是线程安全的,不会在多个地方被修改。

    在JS中也有类似的设计,联想到ES6的const和let,const 只能保证变量引用不可变,但如果引用的是对象或数组,内容依然可以改变。可谓是相当鸡肋。

2. 内存安全

Rust 提供了一种独特的所有权系统来自动管理内存,避免了许多传统语言中常见的内存错误,如内存泄漏、悬垂指针和双重释放。

Rust 的内存安全设计包括以下几个方面:

  • 所有权系统:Rust 中的所有权系统确保每个资源只有一个所有者,而所有权可以转移。一旦所有权转移,原所有者无法再访问或修改该资源,资源离开作用域时会自动释放,这就避免了双重释放和内存泄漏的问题。
  • 借用检查器:Rust 的借用检查器确保在同一时间只能存在不可变借用或一个可变借用。这避免了并发情况下的内存冲突。
  • 生命周期:Rust 的生命周期系统确保引用的有效性,在编译时检查引用的生命周期与持有它的资源的生命周期是否匹配,从而防止悬空引用和野指针。

反观JavaScript 中的内存问题:

  • 内存泄漏(Memory Leak): 内存泄漏是指程序无法释放不再使用的内存,导致内存资源被浪费。在 JavaScript 中,由于垃圾回收机制,内存泄漏通常发生在以下几种情况:

    • 全局变量:

      全局变量是在全局作用域中声明的,它们在程序执行期间存在,直到程序结束时才会被销毁。因此,无论这些全局变量是否仍在使用,它们都将保持存在,无法被垃圾回收器回收。

        // 全局变量
      var globalVar = { name: 'example' };

      // 该对象即使没有被引用,仍然会存在,直到页面关闭

      在浏览器中,全局变量被视为 window 对象的属性,在 Node.js 中,则是 global 对象。

      垃圾回收器一般不会回收全局变量,原因之一是全局变量的清理通常意味着整个应用程序的关闭或重载。如果要强制回收全局变量,会导致额外的复杂性和性能开销。因此,大多数 JavaScript 引擎(如 V8)选择让全局变量一直存活。

      全局变量不仅会导致内存泄漏还有容易被意外覆盖的风险,尽量使用模块化、闭包等方式来避免将变量暴露到全局作用域中。

      同理,全局变量也会导致无法准确的进行treeshaking优化,因为全局变量是有副作用的。

    • 闭包(Closures)

      闭包可能会保持对外部函数作用域变量的引用,从而防止这些变量被回收。

      function createClosure() {
      let largeObject = new Array(1000000).fill('Memory leak');

      // 返回一个函数,访问 largeObject
      return function() {
      console.log(largeObject[0]);
      };
      }

      const closure = createClosure();

      // 使用完闭包后,显式清除引用
      closure = null; // 删除对闭包的引用,垃圾回收器可以回收 largeObject

      largeObject 是一个占用大量内存的对象。当我们调用 createClosure 时,它返回一个内部函数 closure,这个内部函数会引用 largeObject。尽管 largeObject 的生命周期在 createClosure 执行完之后结束,但由于 closure 仍然持有对 largeObject 的引用,这个对象就无法被垃圾回收器回收,从而导致内存泄漏。

      闭包本身不会引起内存泄漏,但如果闭包捕获了外部函数的引用,且这些引用长时间未清除,就可能导致内存泄漏。

  • 事件监听器

    如果没有正确移除事件监听器,可能导致无法释放关联的内存。

    如果我们为 DOM 元素注册了事件监听器,但没有在适当的时候移除它们,尤其是在元素被删除或不再需要时,事件监听器会一直保持对 DOM 元素的引用,从而防止垃圾回收。

    "my-element">Click me!


    <script>
    let element = document.getElementById('my-element');

    // 给 DOM 元素添加事件监听器
    function handleClick() {
    console.log('Element clicked');
    }

    element.
    addEventListener('click', handleClick);

    // 假设我们从 DOM 中移除了该元素
    document.body.removeChild(element);

    // 但是我们没有移除事件监听器,事件监听器仍然持有对该元素的引用
    // 因此该元素无法被垃圾回收
    script>

    需要手动清除事件监听器

    element.removeEventListener('click', handleClick);  // 移除事件监听器
    element = null; // 清除对 DOM 元素的引用
  • DOM 元素引用

    如果 DOM 元素的引用在不再需要时没有清除,垃圾回收机制也无法回收它们。

    当我们通过 DOM 操作获取并引用一个 DOM 元素时,如果该元素的引用没有及时清除,即使该元素已经被移除或不再需要,它也不会被垃圾回收,从而导致内存泄漏。

    "my-element">Hello, World!


    <script>
    // 获取 DOM 元素并保存引用
    let element = document.getElementById('my-element');

    // 动态移除该元素
    document.body.removeChild(element);

    // 但是我们没有清除 element 引用
    // 这个引用仍然指向已经从 DOM 树中移除的元素
    // 此时垃圾回收器无法回收这个元素,因为引用仍然存在
    script>

    可以使用 element = null 来清除引用,但这个操作需要手动执行,容易忘记。

v8的垃圾回收器

V8 中的GC采用标记清除法进行垃圾回收。主要流程如下:

  • 标记:从根对象开始,遍历所有的对象引用,被引用的对象标记为活动对象,没有被引用的对象(待清理)标记为垃圾数据。
  • 垃圾清理:将所有垃圾数据清理掉

在我们的开发过程中,如果我们想要让垃圾回收器回收某一对象,就将对象的引用直接设置为 null

let a = {}; // {} 可访问,a 是其引用

a = null; // 引用设置为 null
// {} 将会被从内存里清理出去

但如果一个对象被多次引用时,例如作为另一对象的键、值或子元素时,将该对象引用设置为 null 时,该对象是不会被回收的

let a = {};
let arr = [a];

a = null;
console.log(arr)
// [{}]

因为a被arr引用,即使a不被使用了,也不会被释放,除非arr也被设置为null。

JS也考虑到了这一点,在ES6中推出了: WeakMap和WeakSet 。它对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用(对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为)。

let a = {};
let arr = new WeakSet();
arr.add(a);

a = null;
console.log(arr.has(a))
// false

即 arr 对a的引用是弱引用,如果a不用了,不会阻止垃圾回收。

以上代码可以在控制台自行尝试一下

即便JS给出了可以避免特定场景的内存泄漏的方案,但依然无法避免所有场景的内存泄漏,而且就算你熟谙内存泄漏的各种场景以及对应解决方案,百密也终有一疏,更何况实际开发中代码能跑起来我们就几乎不会考虑啥内存问题,而Rust则强制你一定要考虑内存安全,否则编译都不过。

3. 并发安全

Rust 的并发模型通过其所有权和借用规则确保了并发编程中的安全性。Rust 中的并发安全设计包括:

  • 数据竞争防止:Rust 中,数据竞争在编译时就能被发现。Rust 通过所有权规则确保要么有多个不可变借用,要么有一个可变借用,从而避免了并发时对共享数据的非法访问。
  • 线程安全:Rust 使用 Send 和 Sync 特性来标识哪些类型可以在线程之间传递或共享。Send 允许数据在不同线程之间传递,Sync 允许多个线程共享数据。
  • 锁机制:Rust 提供了 Mutex 和 RwLock 等机制来确保在多线程环境下对共享资源的安全访问。

JS是单线程,但是JS的单线程是基于事件循环的非阻塞的,所以可以通过异步来实现伪并发,竞态条件、数据竞争、数据共享等问题在JS中是很难发现的,甚至别人的代码修改了你的数据你都不知道,没有一定的开发经验积累,去排查由此产生地莫名其妙的bug是相当折磨人的。

当学习了Rust之后,你就会下意识地去考虑你所定义的数据的安全性,有没有不确定的执行顺序引发的问题?有没有可能被非预期的共享和修改?我改了这个对象会不会影响到其他部分的功能表现等等?从而去想办法将可能会发生的问题扼杀在摇篮里。例如在处理组件状态时,采用不可变数据结构和函数式编程模式可以减少出错的机会。实际上对于前端开发者来说,这类bug是相当常见的。

JS其实也是在不断努力解决其本身存在的各种问题的,例如不断升级的ES新特性,使用 use stric 开启严格模式,还有函数式编程范式的流行,以及各类框架都支持的状态管理等等,这些措施都是为了让JS代码能够更加健壮,弥补JS本身的一些不足。

不可变数据结构: 在React中,使用useState和useReducer来管理状态,避免直接修改状态对象。以及redux等状态管理库,还有像Immer这样的库来简化不可变数据的操作。 函数式编程: 在JavaScript中,封装有明确输入和输出的函数,或使用高阶函数(如map、filter、reduce)来处理数据,避免修改原始数据,从而保持代码的清晰性和可测试性。

无论是函数式编程,还是状态管理,都是为了减少每个动作的副作用,有明确的数据流,让代码更安全更加可维护,低耦合高内聚不是一句空话,是业界大佬们真正在不断去实践的。只是我们自己没有感知,而实际上JS这门语言自身的缺陷真的很多,用JS去开发很容易,但是用JS去开发出健壮又高性能的代码是很难的,这可能也是为什么前端框架和库百花齐放而又前仆后继的原因。

4. 错误处理

Rust没有传统意义上的异常机制。在许多编程语言中,错误通常会通过运行时抛出异常来传递,而Rust采用了一种完全不同的方式来处理错误。

Rust通过Result类型和Option类型来明确地处理错误和空值,Rust的错误处理是编译时检查的,必须显式处理Result或Option,如果忽略了错误处理,编译器会报错,确保错误处理不被遗漏。这种做法可以避免程序出现未处理的异常,增强程序的健壮性。

Result 类型:Rust使用Result类型来显式表示一个函数可能返回的两种状态:成功(Ok(T))或失败(Err(E))。这种方式要求函数调用者在编译时就明确考虑到错误的处理,而不是依赖于运行时的异常机制。

Result是一个枚举类型,定义如下:

enum Result {
Ok(T),
Err(E),
}

Option 类型:在处理可能的空值时,Rust使用Option类型,它表示一个值可能存在(Some(T))或不存在(),避免了空指针异常的问题。

enum Option {
Some(T),
,
}

在前端开发中,JavaScript和TypeScript也可以借鉴Rust的错误处理机制,明确地处理每一种错误情况,尤其是空值问题,没有一个前端开发能躲过 undefined 的摧残。

高性能设计

1. 零成本抽象

零成本抽象是指使用高级编程语言的抽象(如函数式编程的高阶函数、泛型、闭包等)时,不会引入额外的性能开销或运行时成本。换句话说,编写高抽象层的代码并不会影响程序的性能,编译器能够将抽象代码转化为与低级代码相同的高效机器码。

在 Rust 中,“零成本抽象”特别重要,因为 Rust 旨在提供与 C 和 C++ 等低级语言相似的性能,同时保持高层次的代码抽象和安全性。通过静态分析和优化,Rust 能够在编译时消除大多数抽象层的开销。

零成本抽象的三个原则:

  • 没有全局成本(No global cost): 一个零成本抽象不应该对不使用该功能的程序的性能产生负面影响。

    换句话说,零成本抽象应该只在使用时产生影响,在未使用时不会引入任何额外的开销。

  • 最佳性能(Optimal performance): 一个零成本的抽象应该编译成相当于底层指令编写的最佳实现。意味着它在使用时会以尽可能接近底层代码的方式运行,即它的性能应当与手写的低级实现相当。

    可以理解为,如果你想要用rust抽象某个高级能力,那么抽象完成的性能不能比用更原始写法实现的性能差,如果你想要抽象前端框架,那么就不能比直接操作DOM的JS原生写法性能差。

  • 改善开发者体验(Improves developer experience): 抽象的意义在于提供新的工具,由底层组件组装而成,让开发者更容易写出他们想要的代码,提高开发效率和代码可读性。

举几个例子

  1. Rust 的所有权系统(ownership system)和生命周期(lifetimes)。

当你写一个简单的程序,没有使用所有权系统的特性时,编译器会对这些特性进行优化,使得它们对程序的性能没有任何影响。只有当你使用这些特性时,编译器才会引入相关的检查和优化。

  1. 迭代器(Iterators) Rust 的迭代器是一种高效的抽象
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().map(|x| x * 2).sum();
println!("Sum: {}", sum);
}

在这个例子中,iter 返回一个迭代器,map 是一个高阶函数,它将一个闭包应用到每个元素。尽管你使用了高阶函数和迭代器,Rust 会在编译时优化这段代码,确保它与手写的 for 循环代码在性能上等效。

Rust 的迭代器通过惰性求值来避免不必要的中间计算。当你调用 .map() 和 .sum() 时,Rust 会合并这些操作为一个高效的迭代过程,不会在内存中创建不必要的临时集合,最终生成与手动实现同样高效的机器码。

而在JS中,几种高阶函数的使用则是有成本的,如map、foreach 等,在进行一些极端的数据量测试时,性能差异相比 for 循环就比较明显了,但是js引擎实际上也是做了很多的优化的,这点不用太过于担心,放心用就好了。

  1. 泛型(Generics) Rust 中的泛型是一个典型的零成本抽象。

使用泛型时,Rust 在编译时会根据类型替换(monomorphization单态化)生成具体的代码,在运行时没有任何额外的开销。

fn add(a: T, b: T) -> T
where
T: std::ops::Add,
{
a + b
}

在这个例子中,add 函数是一个泛型函数。Rust 编译器在编译时会为每种特定类型(例如 i32、f64 等)生成专门的机器码。

如果你调用 add(1, 2),Rust 会为 i32 类型生成专门的代码。同样,如果你调用 add(1.5, 2.5),Rust 会为 f64 类型生成专门的代码。泛型不会引入运行时的性能损失。编译后的代码与手写的具体类型版本在性能上是等价的。

  1. 错误处理

包括Rust的错误处理,由于Rust不使用异常机制,程序的控制流更加清晰,避免了异常捕获时的运行时性能开销。

对前端的启示

  • 抽象高级能力提升开发效率和体验: 前端框架(如 React 和 Vue)、TypeScript等都是属于抽象的高级能力。例如,React 的优化算法通过虚拟 DOM 的比对、最小化 DOM 更新,已实现类似的零成本抽象。除此之外还有很多事可以做。
  • 优化代码:减少抽象代码中不必要的计算,必要时惰性引入(前端叫懒加载)或计算,减少对不相关部分的隐形影响(前端叫副作用),利用好模块化开发和treeshaking等。
  • WebAssembly (Wasm): Rust 本身可以编译为 WebAssembly,这为前端性能优化开辟了新天地。借鉴 Rust 的这一特点,前端可以通过将性能瓶颈部分的代码(如图像处理、数据加密等)使用 Rust 编写并编译为 WebAssembly 运行,从而提升性能。

总结

社区很多人并不看好Rust甚至很激进地开喷,人确实是会有自己的舒适区的,当用熟了一样语言后,便不那么容易接受某一个自己不熟悉的语言更好,但是,尝试走出舒适区,真正地去接触Rust,一定会也能够感受到Rust的设计光辉。

要学习Rust,你需要先深入理解内存、堆栈、引用、变量作用域等这些其它高级语言往往不会深入接触的内容,Rust 会通过语法、编译器和 clippy 这些静态检查工具半帮助半强迫的让你成为更优秀的程序员,写出更好的代码。

Rust 程序只要能跑起来,那代码质量其实就是相当不错的,甚至不需要调试,能编译基本就没bug(当然是避免不了逻辑bug的)。正因为较高的质量下限,我们维护别人的代码时心智负担也比较小,能编译通过基本能保证不新增bug,把精力完全放在业务逻辑上就可以了。

而如果用javascript写程序,我们必须一边写一边调试,虽然写出能跑的程序极为简单,但要想没有bug,心智负担极高,js的程序员上限不封顶,但下限极低。而且review代码成本也很高,1000个人有1000种写法,review完改一改可能又不小心改出bug了。

JS对开发者要求较低,也是时代变了,条件好了,搁以前几百兆内存、单核cpu的时候,那时候的JS开发该有多痛苦啊。

社区流传着一个很奇怪的论调,“通过学习Rust,你能写出更好的xx语言的代码”。

学习Rust后,会潜移默化地影响你写其他语言代码时的思维方式,最直观的变化就是,对javascript中各类容易造成不安全不稳定的情况会更加敏感,所以,某种程度来看,Rust的价值可能并不在于用它写出多么优秀的代码,更重要的是它带给你的全面的方法论层面的提升。

我使用Rust做了比较丰富的尝试,包括用Rust写命令行工具、用Rust写 postcss 插件、用Rust写vite 插件、用Rust写WebAssembly在前端页面中使用,整体体验和效果还是非常棒的,WebAssembly的尝试可以查看文章Rust + wasm-pack + WebAssembly 实现Gitlab 代码统计,比JS快太多了,其他实践后面会陆续和大家分享,感兴趣的小伙伴可以关注收藏插个眼~

附:

Rust在前端领域的应用

  • SWC: 基于 Rust 的前端构建工具,可以理解为 Rust 版本的 Babel,但是性能有 10 倍提升。目前被 Next.js、Deno , Rspack等使用。
  • Tauri:Tauri 是目前最流行的 Electron 替代方案,通过使用 Rust 和 Webview2 成功解决了 Electron 的包体积大和内存占用高的问题。Atom 团队也是看到了 Tauri 的成功,才决定基于 Rust 去做 Zed 编辑器。
  • Parcel2:零配置构建工具,特点是快速编译和不需要配置,和 Vite、Webpack等打包比起来更加简单,而且是基于 Rust 开发
  • Biome: 旨在取代许多现有的 JavaScript 工具,集代码检测、打包、编译、测试等功能于一身。
  • Rspack: 基于 Rust 的高性能 Web 构建工具, 对标 Webpack, 兼容大部分Webpack api
  • Rocket: 可以帮助开发人员轻松编写安全的Web应用程序, 对标 Expressjs,性能卓越,具体参考 Web Frameworks Benchmark
  • Yew : 使用 Rust 开发 h5 页面,支持类 jsx 的语法,和 React 类似开发前端网页,打包产物是 wasm,挺有趣。
  • Napi-rs: 用 Rust 和 N-API 开发高性能 Node.js 扩展,可以替代之前用 C++ 开发的 Node.js 扩展,许多基于 Rust 语言开发的前端应用都结合这个库进行使用。
  • Rolldown: 基于 Rust 的 Rollup 的替代品。
  • 美国国防部准备征求一个把所有C代码翻译成Rust的软件。

作者:Pursue_LLL
来源:juejin.cn/post/7450021642377199643

收起阅读 »

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

web
今年大部分时间都是在编码上和写文章上,但是也不知道自己都学到了啥,那就写篇文章来盘点一下目前的技术栈吧,也作为下一年的参考目标,方便知道每一年都学了些啥。 我的技术栈 首先我先来对整体的技术做一个简单的介绍吧,然后后面再对当前的一些技术进行细分吧。 Reac...
继续阅读 »

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


20241223154451


我的技术栈


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


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


React


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


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


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


NextJs


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


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


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


Typescript


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


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


React Native



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



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


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


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


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


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


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


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


Nestjs


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



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



Prisma & mysql


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


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


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


Redis


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


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

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

import { isObject } from "@/utils";

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

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

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

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

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

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

return null;
}
}

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

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

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

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

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

return [];
}
}

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

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

前端工程化


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


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


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


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


20241223165138


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



  1. NextJs

  2. React Native

  3. prisma

  4. NestJs

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


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


总结


学无止境,任重道远。


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

App出现技术问题,这样的中国电信让用户糟心了

web
前言 最近在中国电信app上销户一张中国电信山西区的电话卡,一打开销户界面我就惊了 点开一看,写入的本地变量、cookie一览无遗。 查看数据 存在采集用户手机型号、来源等数据行为产生的cookie 最引人注目的就是 zhizhendata2015jss...
继续阅读 »

前言


image.png


最近在中国电信app上销户一张中国电信山西区的电话卡,一打开销户界面我就惊了


image-20250105103537705


点开一看,写入的本地变量、cookie一览无遗。


image.png


查看数据


存在采集用户手机型号、来源等数据行为产生的cookie


最引人注目的就是


zhizhendata2015jssdkcross={
"distinct_id": "MTkxOTZhYzk3YTAyMDItMDgxMjMwYmRlOTFhOWU4LTQ3NzE2ZTBmLTM2NzkyOC0xOTE5NmFjOTdhMWE5NQ==",
"
first_id": "",
"
props": {
  "
$latest_traffic_source_type": "直接流量",
  "
$latest_search_keyword": "未取到值_直接打开",
  "
$latest_referrer": "",
  "
_latest_utm_scha": "utm_ch-010001002009.utm_sch-hg_sy_pdkp-2-125971000001-10519100001.utm_af-1000000037.utm_as-0043300037.utm_sd1-default",
  "
_latest_utm_sd1": "app-充流量-本地推荐",
  "
_latest_utm_sd2": "",
  "
_latest_shopid": "189.WAP.llrb-2079",
  "
_latest_utm_ch": "hg_app",
  "
_latest_utm_sch": "hg_sy_pdkp_kw02",
  "
_latest_utm_as": "hg_19y15GBwxllb"
},
"
login_type": "",
"
utms": {
  "
shopid": "189.WAP.llrb-2079"
},
"
$device_id": "19196ac97a0202-081230bde91a9e8-47716e0f-367928-19196ac97a1a95"
}


简单解释一下各种参数


image.png
很显然 ,这是一个用户行为追踪工具,记录你从那个平台点进来(广告投放)、你手机是啥样的(用户画像)、你搜索了什么。可能还会有你住哪里之类的数据



  • 博主一下懵了,之前常听人说大数据时代没有什么秘密,还不以为然。今日一遇还真是阿


登录状态与服务取消cookie


SXH5_CANCEL_SERVICE_LOGINSTATUS=SXH5_CANCEL_SERVICE_a2d95a5a764d4744b1eb1e468b583287

SXH5_CANCEL_SERVICE_LOGINTYPE=fmknjikneolbnhclejikfnbggkmnookc



  • 这两没什么好说的,看不出来啥


查看持久化数据


image-20250105111122637


userInfo={"type":"object","data":{"userName":"","userId":"","userAddress":"","facePhoto":"","frontImage":"","phone":""}}

vConsole_switch_y=335

loginType=sjhocr

xhAccount={"type":"object","data":{"xhzkAccount":"15383404397","xhfkPhone":"","xhkdAccount":""}}

authorPlate=xh

vConsole_switch_x=92

orderId=SXSMRZH5XH202501042248308427503

__DC_STAT_UUID=17360018747207051970


  • 这段数据也没啥好看的,有一些手机号、订单编号、和证明是微信小程序的__DC_STAT_UUID 项


image-20250105111458145



  • 更多就不继续探索了,这里的填写号码获取验证码控制台正常输出。。。。


问题可能产生原因



  • 首先,得知道中国电信app现在的模式是怎样的。

  • 据博主个人观察,电信app页面虽然都一样,但每个地区各自为营。如果你使用福州的电信手机卡登录,那么是跳转到福州电信负责的页面,如下图


s



  • 所以本次app出现问题,是山西电信没有处理好app端


image.png



  • 山西电信微信小程序是没什么问题的,而他的app没有做好对接,要么版本不一样……


吐槽



  • 我有个朋友之前注销电信流量卡时,被告知要去号码归属地才能销户。。这归属地离他十万八千里,过去就为了销卡显然不划算。于是他去工信部12300(微信公众号 现改名为 电信用户投诉 )投诉才成功线上销卡。

  • 电信现在按省来处理业务,如果你电信卡丢了且忘记卡号、归属地,那只能通过线上投诉才能得知自己卡号、归属地,不然各省是无权查别省号码。

  • 我线上销户时,客服A要求先交40元月租才能销户,但这张卡我从未使用,为何会产生月租?联系客服B后,他让我提供身-份-证照片、委托书及手持委托书照片,最终未交钱完成注销。但不同客服的说法不一,且身份信息完全暴露给客服,让人不安。


作者:Qiuner
来源:juejin.cn/post/7456898384352362522
收起阅读 »

和后端大战三百回合后,卑微前端还是选择了自己写excel导出

web
前言 对于一个sass项目,或者一个中后台项目来说,导出excel表格应该是家常便饭,本来最简单的实现方式是后端去做表格,前端请求接口拿到一个地址下载就行了。但是,因为我们这个项目之前就是前端做表格,加上这个表格相对比较复杂需要合并行和列,后端不会加上又有别的...
继续阅读 »

前言


对于一个sass项目,或者一个中后台项目来说,导出excel表格应该是家常便饭,本来最简单的实现方式是后端去做表格,前端请求接口拿到一个地址下载就行了。但是,因为我们这个项目之前就是前端做表格,加上这个表格相对比较复杂需要合并行和列,后端不会加上又有别的项目堆积没有时间研究,所以就是后端提供数据,前端来做表格。


那里复杂


image-20241209095449488.png


可以看到,有二级标题,还有行的合并,如果仅仅是二级标题,倒是可以直接写死,但是行的合并是根据数据去计算该合并那些行的,再比如后面如果有三级标题,四级标题的需求呢?那不是又寄了,所以我选择将这个封装成一个方法,当然也是在网上找大佬们的解决方案实现的,东抄抄西抄抄就实现了我想要的功能。


传参


既然封装成一个方法,最好就是传入数据,表头,文件名后,就能自动下载一个excel表格,这才是封装的意义。代码并不是人,只能根据你设定好的路去走,所以数据的结构就显得很重要了,这个函数想要接收什么样的数据结构,要怎么去处理这些数据结构。


表头 header


表头接收一个数组,每一项有title,prop,children(如果有子级标题),title即为列名,prop为数据属性绑定名,children为子标题。


const header = [
{
'title': '券商(轉出方)',
'prop': 'orgName',
       'width': '100px'
  },
  {
       'title': '存入股票',
        'children': [
                {
                   'title': '存入股票名稱/代碼',
                   'prop': 'stockNameCode',
      'width': '100'
                },
                {
                   'title': '股票數量(股)',
                   'prop': 'stockNum',
                    'width': '100'
                },
                {
                   'title': '成本價(HKD)',
                   'prop': 'stockPrice',
                   'width': '100'
                }
          ]

  }
]

数据 dataSource


数据也是接收一个数组,但是这里需要做一个处理,因为每一项的children是一个数组,可能会有多个值,换句话来说,下面只有两条数据,分别是id为1和id为2,但实际上在excel表格中需要显示3行,所以需要处理一下。


const dataSource = [
  {
      id:1
      orgName:‘a’,
      children:[
      {
               stockNameCode:'A1',
               stockNum:'A2',
               stockPrice:'A3'
  },
          {
               stockNameCode:'B1',
               stockNum:'B2',
               stockPrice:'B3'
  },
]
  },
  {
      id:2
      orgName:'b',
      children:[
      {
               stockNameCode:'A1',
               stockNum:'A2',
               stockPrice:'A3'
  }
]  
  },
]

处理后的数据(也就是将children解构了,变成3条)


[
  {
      id:1
      orgName:‘a’,
      stockNameCode:'A1',
      stockNum:'A2',
      stockPrice:'A3'
  },
  {
      stockNameCode:'B1',
      stockNum:'B2',
      stockPrice:'B3'

  },
  {
      id:2
      orgName:‘b’,
  stockNameCode:'A1',
      stockNum:'A2',
      stockPrice:'A3'  
  }
]

sheetjs前置知识


对于我们前端生成excel,基本都是使用基于sheetjs封装的第三包,最经常使用的是xlsx,我这里因为对表格做了一些样式所以使用的xlsx-js-style,xlsx-js-style是提供了很多样式的,比如字体,居中,填充,具体大家可以去看官网。因为可能有些人是没做过excel的需求的,所以这里简单说一下生成excel的一种主流程。


import XLSX from 'xlsx-js-style'
// 需要一个二维数组
var aoa = [
  ["S", "h", "e", "e", "t", "J", "S"],
  [  1,   2,   ,   ,   5,   6,   7],
  [  2,   3,   ,   ,   6,   7,   8],
  [  3,   4,   ,   ,   7,   8,   9],
  [  4,   5,   6,   7,   8,   9,   0]
];
// 将二维数组转成工作表
var ws = XLSX.utils.aoa_to_sheet(aoa);
// 创建一个工作簿
var wb = XLSX.utils.book_new();
// 将工作表添加到工作簿
XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
// 生成excel
XLSX.writeFile(wb, "SheetJSExportAOA.xlsx");

导出的表格,这是官网的demo: xlsx.nodejs.cn/docs/api/ut…


image-20241210090608300.png


所以封装这个函数,主要流程也是和这个一样的,只不过我们要做的时候,将传入的参数处理成我们想要的二维数组,以及在这基础做一些合并,样式的操作,下面介绍了一些属性的作用,具体大家还是需要去官网查看的。


ws['!merges']


ws['!merges'] 是工作表对象 ws 的一个属性,用于存储工作表中的合并单元格信息,该属性的值是一个数组,其中每个元素都是一个对象,描述了一个合并单元格区域


// s是start e是end合并单元格区域的起始位置和结束位置,
// r是行 c是列
ws['!merges'] = [
{ s: { r: startRow, c: startCol }, e: { r: endRow, c: endCol } }
];

比如{ s: { r: 0, c: 0 }, e: { r: 0, c: 1 } } 表示合并从 A1(第 1 行第 1 列)到 B1(第 1 行第 2 列)的单元格。


ws['!ref']


ws['!ref'] 是工作表对象 ws 的一个属性,用于表示该工作表中数据的范围引用。这个范围引用是一个字符串,遵循 Excel 的单元格范围表示法,格式通常为 A1:B10,其中 A1 是范围的左上角单元格,B10 是范围的右下角单元格


ws['!cols']


ws['!cols'] 是工作表对象 ws 的一个属性,它用于存储工作表中列的相关信息,比如列的宽度、隐藏状态等


主函数


有了这些前置知识,相信你肯定是能看懂这个主函数的,我们先从主线上来看,不去研究这个函数做了什么,只需要看他得到了什么,某一个函数的细节我们后面会有介绍。



header 表头


dataSource 数据


fileName 文件名



import XLSX from 'xlsx-js-style'
function exportExcel (header, dataSource, fileName) {
 // 根据表头数组去计算行数和列数
 const {row: ROW, col: COL} = excelRoWCol(header)
 const aoa = []
 const mergeArr = []
 
 // 根据表头初始化aoa 二维数组
 for (let rowNum = 0; rowNum < ROW; rowNum++) {
   aoa[rowNum] = []
   for (let colNum = 0; colNum < COL; colNum++) {
     aoa[rowNum][colNum] = ''
  }
}
   
 // 根据表头以及数据生成,去合并列和行,会处理mergeArr
 mergeArrFn(mergeArr, header, aoa, dataSource, ROW, COL)
   
 // 最后往aoa中 添加表格数据
 aoa.push(...jsonDataToArray(header, dataSource))

 const ws = XLSX.utils.aoa_to_sheet(aoa)
 // 添加样式
 ExcelStyle(ws, header, ROW)
 // 合并
 ws['!merges'] = mergeArr
 // 创建一个工作簿
 const wb = XLSX.utils.book_new()
 // // 将工作表添加到工作簿
 XLSX.utils.book_append_sheet(wb, ws, 'sheet1')
 // 生成excel
 XLSX.writeFile(wb, fileName + '.xlsx')
}
export default exportExcel

相对前面那个下载excel的demo来说,无非就多了根据传入的header和dataSource去初始化生成aoa以及mergeArr,aoa就是前面demo的二维数组,mergeArr表示我们需要合并的单元格,也就是前面提到的ws['!merges'],我们得到这个mergeArr也是为了赋值给它,还有就是给它添加样式了。


excelRoWCol


这个函数是根据表头去确认这个excel的表头有多少行,有多少列,因为我们传入的column,有children,children里可能还有chidren,是一个的结构,所以我们想要知道有多少行和多少列,无非就是去求这颗树的深度和宽度,所以就是两个算法题了。


// 深度递归函数
function treeDeep (root) {
 if (root) {
   if (root.children && root.children.length !== 0) {
     let maxChildrenLen = 0
     for (const child of root.children) {
       maxChildrenLen = Math.max(maxChildrenLen, treeDeep(child))
    }
     return 1 + maxChildrenLen
  } else {
     return 1
  }
} else {
   return 0
}
}
// 宽度递归函数
function treeWidth (root) {
 if (!root) return 0
 if (!root.children || root.children.length === 0) return 1
 let width = 0
 for (const child of root.children) {
   width += treeWidth(child)
}
 return width
}

function excelRoWCol(header) {
 let row = 0
 let col = 0
 for (const item of header) {
   row = Math.max(treeDeep(item), row)
   col += treeWidth(item)
}
 return {
   row,
   col
}
}

mergeArrFn



mergeArr 这个函数就是在修改这个值


header 表头


aoa 二维数组数


dataSource 数据


headerRowLen 表头行数


headerColLen 表头列数



这个函数有两个作用,第一就是将我们初始化的二维数组,用header进行赋值。第二,就是根据表头以及数据去生成mergeArr(赋值给ws['!merges'])。首先,对于header去遍历每一个表头去生成当前这一列的合并信息。假设一个只有二级表头的表头,如果当前这一列有二级标题,便根据子标题去合并主标题那一行所有的列,如果当前这一列没有子标题,便将这一列的第一行和第二行都和合并了。三级表头,四五级表头也是这样的思路。


function mergeArrFn(mergeArr, header, aoa, dataSource, headerRowLen) {
 // 根据header去生成一部分的 mergeArr
 let temCol = 0
 for (const item of header) {
   generateExcelColumn(aoa, 0, temCol, item, mergeArr)
   temCol += treeWidth(item)
}

 // 根据dataSource去生成一部分的 mergeArr
 let rowStartIndex = headerRowLen
 for (const item of dataSource) {
   generateExcelRow(rowStartIndex, item, mergeArr, header)
   rowStartIndex += treeWidth(item)
}
}

generateExcelColumn


这个函数简单来说就是前面所说的,假设一个只有二级表头的表头,如果当前这一列有二级标题,便根据子标题去合并主标题那一行所有的列,如果当前这一列没有子标题,便将这一列的第一行合第二行都和合并了。三级表头,四五级表头也是这样的思路。具体还是得自己理解代码,都有写注释。



aoa 就是那个aoa


row 就是行数


col 就是列数


curHeader 就是当前那一列


mergeArr 就是那个mergeArr



function generateExcelColumn(aoa, row, col, curHeader, mergeArr) {
 // 当前列的宽度
 const curHeaderWidth = treeWidth(curHeader)
 // 赋值
 aoa[row][col] = curHeader.title
 // 如果有子标题也就是说当前这一行就需要合并了
 if (curHeader.children) {
   // 举个例子,假设有一个表头两行两列,需要把他变成第一行只有一列,第二行依然是两列
   // 就需要变成 {s : { r:0,c:0 }, e : { r:0, c: 0+2-1 }}
   mergeArr.push({s: {r: row, c: col}, e: {r: row, c: col + curHeaderWidth - 1}})

   // 如果子标题还有子标题,就是递归了,要注意更新列数就行
   let tempCol = col
   for (const child of curHeader.children) {
     generateExcelColumn(aoa, row + 1, tempCol, child, mergeArr)
     tempCol += treeWidth(child)
  }
} else {
   // 这里的逻辑就是 如果没有子标题,就正常显示
   // 举个例子,假设整个表头是有三级表头,三级表头也就是有3行,如果第5列是没有任何子级表头的那应该是
   // {s:{r:0,c:5},e:{r:2,c:5}}
   if (row !== aoa.length - 1) {
     mergeArr.push({s: {r: row, c: col}, e: {r: aoa.length - 1, c: col}})
  }
}
}

generateExcelRow


这个函数是根据datasource去生成mergeArr,从mergeArrFn看我们去遍历datasource的每一项,在外层维护rowStartIndex这个变量,我们假设某一项数据的children是一个长度为3的数组,那么通过treeWidth方法(寻找树的宽度)得到的数据就是3,也就是说这一项数据应该占表格3行,但是并不是所有列都是需要3行数据的,所以我们需要去获取到一个不用合并的列prop数组,我们通过这项数据的children的key值去获取,所以这就需要对数据格式有要求了!然后再通过header和getgetLeafProp去获取所有prop,最后遍历判断是否需要去合并行。合并的逻辑是这样的,还是以那个children是一个长度为3的数组为例,如果要合并肯定是3行合并成一行。以第一列为例子,就是 { s : { r : 0, c : 0 }, e : { r : 2 , c : 0 }},下面去遍历props时,下标刚好就是当前的列数。



rowStartIndex 就是从表头的下一行开始


curitem 就是遍历dataSource当前的行


mergeArr 就是mergeArr


header 表头数组



// 合并行
function generateExcelRow(rowStartIndex, curitem, mergeArr, header) {
 // 当前行的高度
 const curHeaderWidth = treeWidth(curitem)
 // 不需要合并的列prop
 const noMerge = (curitem.children && curitem.children.length > 0) ? Object.keys(curitem.children[0]) : []
 // 找到所有prop
 const props = []
 for (const item of header) {
   props.push(...getLeafProp(item))
}
 // 遍历props
 props.forEach((item, index) => {
   // 不是子元素就要合并
   if (!noMerge.includes(item)) {
     mergeArr.push({s: {r: rowStartIndex, c: index}, e: {r: rowStartIndex + curHeaderWidth - 1, c: index}})
  }
})
}

jsonDataToArray


这个函数就是为了生成一个二维数组,因为有子标题,所以可能需要递归。逻辑上也比较简单,假设表头是header,数据源是data,header经过处理后变成了props数组,而data根据props处理后就得到了我们想要的数据。


const header = [
  {
       title: 'a'
       prop: 'aprop'
  },
  {
       title: 'b',
       children:[
          {
               title:'c',
               prop:'cprop'
          },
          {
               title:'d',
               prop:'dprop'
          }
      ]
  },
  {
       title:'e',
       prop:'eprop'
  }
]
const data = [
  {
       aprop:'a1',
       b:{
      cprop:'c1',
      dprop:'d1'
  },
       e:'e1'
  },
  {
       aprop:'a2',
       b:{
      cprop:'c2',
      dprop:'d2'
  },
       eprop:'e2'
  },
]
// 得到的porps
['aprop','cprop','dprop','eprop']

// 最后得到的是这个
[
  ['a1','c1','d1','e1']
  ['a2','c2','d2','e2']
]

getLeafProp其实就是去找所有叶子节点的算法题,recursiveChildrenData就是根据我们得到的props去从data中拿到对应的值,然后如果遇到children就递归去拿,要注意的是就是children要第一条是不要的,children第一条是和这一项数据是一样的。


function jsonDataToArray (header, data) {
 const props = []
 for (const item of header) {
   props.push(...getLeafProp(item))
}
 return recursiveChildrenData(props, data)
}
// 获取叶子节点所有的prop,也就是excel表格每一列的prop
function getLeafProp(root) {
 const result = []
 if (root.children) {
   for (const child of root.children) {
     result.push(...getLeafProp(child))
  }
} else {
   result.push(root.prop)
}
 return result
}
// 从数据中获取对应porps的值
function recursiveChildrenData(props, data) {
 const result = []
 for (const rowData of data) {
   const row = []
   for (const index of props) {
     row.push(rowData[index])
  }
   result.push(row)
   if (rowData.children) {
     result.push(...recursiveChildrenData(props, rowData.children).slice(1))
  }
}
 return result
}

ExcelStyle


这个方法倒是简单,这里其实还可以将表头以及单元格样式抽离出去成为主函数exportExcel的配置项。这个函数干了啥呢,首先就是从columns中拿到每一列的宽度,处理成 ws['!cols']想要的格式,ws['!cols']这个就是sheetJS的配置表格列宽的一个属性。然后就是一些单元格样式,具体去看xslx-js-style的官网。decode_range和encode_cell这两个方法有简单介绍,具体大家去看sheetJS官网吧。



ws 就是 那个表格数据实例


columns 是表头数组


ROW 是表头有多少行


XLSX.utils.decode_range: 用于解析 Excel 工作表中的范围字符串并将其转换为结构化的对象


XLSX.utils.encode_cell:是将一个包含行号和列号的对象编码为 Excel 中常见的单元格地址表示形式



function ExcelStyle (ws, header, ROW) {
 // 列宽
 const widthes = []
 for (const item of header) {
   widthes.push(...getLeafwidth(item))
}
 // 处理成 ws['!cols'] 想要的格式
 const wsCOLS = widthes.map(item => {
   return {
     wpx: item || 100
  }
})
 ws['!cols'] = wsCOLS
 // 定义所需的单元格格式
 const cellStyle = {
   font: { name: '宋体', sz: 11, color: { auto: 1 } },
   // 单元格对齐方式
   alignment: {
     // / 自动换行
     wrapText: 1,
     // 水平居中
     horizontal: 'center',
     // 垂直居中
     vertical: 'center'
  }
}
 // 定义表头
 const headerStyle = {
   border: {
     top: { style: 'thin', color: { rgb: '000000' } },
     left: { style: 'thin', color: { rgb: '000000' } },
     bottom: { style: 'thin', color: { rgb: '000000' } },
     right: { style: 'thin', color: { rgb: '000000' } }
  },
   fill: {
     patternType: 'solid',
     fgColor: { theme: 3, 'tint': 0.3999755851924192, rgb: 'DDD9C4' },
     bgColor: { theme: 7, 'tint': 0.3999755851924192, rgb: '8064A2' }
  }
}
 // 添加样式
 const range = XLSX.utils.decode_range(ws['!ref'])
 for (let row = range.s.r; row <= range.e.r; row++) {
   for (let col = range.s.c; col <= range.e.c; col++) {
     // 找到属性名
     const cellAddress = XLSX.utils.encode_cell({ c: col, r: row })
     if (ws[cellAddress]) {
       // 前几行是表头,添加表头样式
       if (row < ROW) {
         ws[cellAddress].s = headerStyle
      }
       ws[cellAddress].s = {
         ...ws[cellAddress].s,
         ...cellStyle
      }
    }
  }
}
}

// 和getLeafProp类似,只是找的字段不一样
function getLeafwidth(root) {
 const result = []
 if (root.children) {
   for (const child of root.children) {
     result.push(...getLeafwidth(child))
  }
} else {
   result.push(root.width)
}
 return result
}

总结


其实这次也是我第一次自己前端导出excel的需求,之前基本都是后端干的,给个地址直接模拟a标签下载就行了。本来呢,我看项目中也是有封装导出excel的方法的,但是有点晦涩难懂啊,看了下导出的效果,也并不能实现需求。我一直觉得在原有基础的去添加一些相似的功能逻辑,真不如直接重新封装一个方法。然后我测试过了将所有代码赋值到同一个js文件,正常引入传对应的数据结构是能跑通的。其实是有点问题的,就是在根据数据行合并的时候,如果是children里面还children,也就是也要递归,我有点不好拿捏判断递归的时机,加上本来对递归就是一知半解,搞得有点混乱,大家感兴趣的可以试试。


作者:落课
来源:juejin.cn/post/7447368539936587776
收起阅读 »

pnpm v10正式发布,重磅更新,历时3个月,12个版本

web
犹抱琵琶半遮面,千呼万唤始出来,pnpm v10 终于正式发布了。 众所周知,笔者有关注行业技术最新进展的爱好,这次的 pnpm v10 版本,也已经跟踪了好几个月了。 而这次,v10 正式版终于发布了。 版本时间pnpm 10.02025年01月08日pnp...
继续阅读 »


犹抱琵琶半遮面,千呼万唤始出来,pnpm v10 终于正式发布了。


众所周知,笔者有关注行业技术最新进展的爱好,这次的 pnpm v10 版本,也已经跟踪了好几个月了。


而这次,v10 正式版终于发布了。


版本时间
pnpm 10.02025年01月08日
pnpm 10.0 RC 32025年01月05日
pnpm 10.0 RC 22024年12月29日
pnpm 10.0 RC 12024年12月27日
pnpm 10.0 RC 02024年12月16日
pnpm 10.0 Beta 32024年12月12日
pnpm 10.0 Beta 22024年12月09日
pnpm 10.0 Beta 12024年11月29日
pnpm 10.0 Alpha 42024年11月25日
pnpm 10.0 Alpha 32024年11月25日
pnpm 10.0 Alpha 22024年11月15日
pnpm 10.0 Alpha 12024年11月15日
pnpm 10.0 Alpha 02024年10月08日

以上是笔者整理的 pnpm v10 发布过程,从 草案,到 测试版,到 候选版,再到最后的 正式版,可谓是花了不少功夫啊。


也从侧面说明了,pnpm 团队对这次 v10 版本的重视程度,必然是有大事发生,那么话不多说,我们看看本次的更新内容吧。


依赖项的生命周期脚本不会在安装期间执行


这是一个重要变化,依赖包的 生命周期脚本 不会自动执行了。


那么问题来了,可能有些读者还不知道什么是 生命周期脚本生命周期脚本 英文名叫做 Lifecycle scripts


包括以下几种:



  1. 安装相关脚本

    • preinstall:在安装软件包之前执行。

    • install:在安装软件包时执行。

    • postinstall:在安装软件包之后执行。



  2. 发布相关脚本

    • prepare:在发布软件包之前执行。

    • prepublishOnly:只在 npm publish 时执行。

    • prepack:在打包软件包之前执行。

    • postpack:在打包软件包之后执行。



  3. 运行相关脚本

    • prestart/start/poststart:在运行 npm start 时执行。

    • prerestart/restart/postrestart:在运行 npm restart 时执。

    • prestop/stop/poststop:在运行 npm stop 时执行。

    • pretest/test/posttest:在运行 npm test 时执行。




pnpnm v10 开始,这些依赖包中的生命周期脚本都不会自动执行了,这样可以进一步提高安全性。


投票


官方也发起了一个投票:pnpm 可以在安装期间阻止依赖项的生命周期脚本。但这是一个可选功能。我们应该默认阻止它们吗?


最终赞成禁用生命周期脚本的占大多数。


那么我们要让某些依赖包的脚本可以自动执行的话,应怎么做呢?


{
"pnpm": {
"onlyBuiltDependencies": ["fsevents"]
}
}

如上示例,pnpm 提供了一个 onlyBuiltDependencies 参数,所有可以自动执行生命周期脚本的包,都要手动写到里面。


这么一来呢,确实提高了安全性,但是对于开发者来说,也提高了不少复杂性。


因为,可能有些依赖包,或者说依赖包的依赖包,需要自动执行脚本才能生效。


如果采用手动模式,那就很可能很难找到,到底要执行哪个包的生命周期脚本,提高了安全性的同时,也降低了开发的便捷性。


pnpm link 行为更新


这个可能有很多人还没用过,主要用途有 2 个:



  1. 替换已安装的软件包

    • 当你正在开发一个依赖包,想在另一个项目中测试它时,可以使用 pnpm link 将本地版本链接到目标项目。

    • 这样可以避免频繁地发布和安装依赖包,提高开发效率。



  2. 添加全局可用的二进制文件

    • 如果你开发了一个包含命令行工具的软件包,可以使用 pnpm link 将其二进制文件注册到全局,以便在任何地方都可以执行。

    • 这对于开发 CLI 工具非常有用。




那么这次的主要变化有 2 个。



  1. 通过 pnpm link 默认创建的是全局包,在之前,则需要 pnpm link -g 才可以创建全局包。

  2. workspace 的多包项目中,override 被添加到工作区的根目录,将依赖项链接到工作区中的所有项目。


总而言之,就是能全局的就全局,把影响范围扩大化,免得抠抠搜搜的。


可能有读者不知道 override 是啥,这里也科普一下:


假设项目中有两个依赖 A 和 B,它们都依赖于同一个包 lodash,但是需要使用不同的版本。


那么可以使用 overrides 来指定使用 lodash 的特定版本:


{
"dependencies": {
"A": "^1.0.0",
"B": "^2.0.0"
},
"pnpm": {
"overrides": {
"lodash": "^4.17.21"
}
}
}

这样就可以确保项目中使用的 lodash 版本是 4.17.21,而不管 A 和 B 各自需要的版本是什么。


如果某个依赖包存在问题,也可以使用 overrides 来替换它:


{
"dependencies": {
"problem-package": "^1.0.0"
},
"pnpm": {
"overrides": {
"problem-package": "my-forked-package@^1.0.1"
}
}
}

在这个例子中,我们将 problem-package 替换为 my-forked-package 的 1.0.1 版本。


可能我写的文章稍微啰嗦了点,主要是考虑到读者可能存在不同的经验水平,所以一些概念也扩展科普一下。


使用 SHA256 进行安全哈希处理


各种哈希算法已更新为 SHA256,以增强安全性和一致性:



  • node_modules/.pnpm 内的长路径现在使用 SHA256 进行哈希处理。

  • 锁定文件中的长对等依赖关系哈希现在使用 SHA256 而不是 MD5。

  • pnpm-lock.yamlpackageExtensionsChecksum 字段中存储的哈希现在为 SHA256。

  • 副作用缓存密钥现在使用 SHA256。

  • 锁定文件中的 pnpmfile 校验和现在使用 SHA256。


配置更新



  1. manage-package-manager-versions:默认启用。pnpm 现在默认根据 package.json 中的 packageManager 字段管理自己的版本。

  2. public-hoist-pattern:默认情况下不会提升任何内容。名称中包含 eslintprettier 的包不再提升到 node_modules 的根目录。

  3. 已将 @yarnpkg/extensions 升级至 v2.0.3,这可能会改变您的 pnpm-lock 文件。

  4. virtual-store-dir-max-length:Windows 上的默认值已减少到 60 个字符。

  5. 减少脚本的环境变量:在脚本执行期间,会设置较少的 npm_package_* 环境变量。仅保留 nameversionbinenginesconfig

  6. 即使 NODE_ENV=production,所有依赖项现在都会安装。


从现在开始,NODE_ENV=production 也会安装所有依赖,包括开发依赖,这对于像我这样的强迫症来说,有点难以接受,没有用到的依赖我为啥要安装?


查看官方文档,可以通过 pnpm add --prod 来只安装 dependencies 依赖。


全局存储更新



  1. 全局 store 升级到 v10

  2. 一些注册表允许使用不同的软件包名称或版本发布相同的内容。为了适应这种情况,商店中的索引文件现在使用内容哈希和软件包标识符来存储。

    1. 验证锁文件中的完整性是否与正确的包相对应,在 Git 冲突解决不佳后可能并非如此。

    2. 允许相同的内容被不同的包或者同一个包的不同版本引用。



  3. 更高效的副作用索引。存储中的索引文件结构已更改。现在通过仅列出文件差异而不是所有文件,可以更有效地跟踪副作用。

  4. 新的索引目录存储了包内容映射。以前,这些文件位于文件中。


其他重大变化



  • # 字符现在在 node_modules/.pnpm 内的目录名称中被转义。

  • 运行 pnpm add --global pnpmpnpm add --global @pnpm/exe 现在会失败并出现错误消息,指导您改用 pnpm 自我更新。

  • 通过 URL 添加的依赖项现在在锁文件中记录最终解析的 URL,确保完全捕获任何重定向。

  • pnpm deploy 命令现在仅适用于具有 inject-workspace-packages=true 的工作区。引入此限制是为了让我们能够使用工作区锁定文件为已部署的项目创建适当的锁定文件。

  • 删除了从 lockfile v6v9 的转换。如果您需要 v6v9 的转换,请使用 pnpm CLI v9

  • pnpm test 现在将 test 关键字后的所有参数直接传递给底层脚本。这与 pnpm run test 的行为一致。以前您需要使用 -- 前缀。

  • pnpm deploy 现在尝试从共享锁文件创建专用锁文件以进行部署。如果没有共享锁文件或 force-legacy-deploy 设置为 true,它将回退到没有锁文件的部署。


次要变化


添加了对一种名为 configurational dependencie 的新依赖项类型的支持


这些依赖项在所有其他类型的依赖项之前安装 (在 dependenciesdevDependenciesoptionalDependencies 之前)。


配置依赖项不能具有其自身或生命周期脚本的依赖项,应使用精确版本和完整性校验和添加它们。


示例:


{
"pnpm": {
"configDependencies": {
"my-configs": "1.0.0+sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="
}
}
}

新的 verify-deps-before-run 设置


此设置控制 pnpm 在运行脚本之前如何检查 node_modules,有以下值:



  • install:如果 node_modules 已过时,则自动运行 pnpm install。

  • warn:如果 node_modules 已过时,则打印警告。

  • prompt:如果 node_modules 已过时,则提示用户确认运行 pnpm install。

  • error:如果 node_modules 已过时,则抛出错误。

  • false:禁用依赖性检查。


新的 inject-workspace-packages 设置允许对所有本地工作区依赖项进行硬链接,而不是对其进行符号链接。


以前,这可以使用 dependencyMeta[].injected 来实现,现在仍然受支持。


更快的重复安装


在重复安装时,pnpm 会执行快速检查以确保 node_modules 是最新的。


pnpm add 与默认工作区目录集成


添加依赖项时,pnpm add 会检查默认工作区目录。


如果依赖项和版本要求与目录匹配,pnpm add 将使用 catalog: 协议。


如果没有指定版本,它将匹配目录的版本。


如果不匹配,它将恢复为标准行为。


pnpm dlx 解析调整


pnpm dlx 现在将软件包解析为其确切版本,并将这些确切版本用作缓存键。


这可确保 pnpm dlx 始终安装最新请求的软件包。


node_modules 验证


某些命令没有 node_modules 验证,不应修改 node_modules 的命令 (例如 pnpm install --lockfile-only) 不再验证或清除 node_modules。


以上就是本次 pnpm v10 的更新内容,感谢阅读,欢迎点赞,评论和转发。


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

我不允许还有人不知道前端实现时刻洪水模拟的方法!🔥

web
二维水动力 HydroDynamic2D 二维水动力介绍 二维水动力模型对象 HydroDynamic2D,基于真实数据驱动生成水动力模型(根据不同时刻下每个网格的流向、流速、高程、水位) 二维水动力模型考虑了水流在平面上的变化,适用于河道弯曲、水流方向多变的...
继续阅读 »

二维水动力 HydroDynamic2D


二维水动力介绍


二维水动力模型对象 HydroDynamic2D,基于真实数据驱动生成水动力模型(根据不同时刻下每个网格的流向、流速、高程、水位)


二维水动力模型考虑了水流在平面上的变化,适用于河道弯曲、水流方向多变的情况。这种模型能够更准确地反映水流在平面上的分布情况,适用于需要精确模拟水流动态的场景,如城市排水系统设计、洪水模拟。二维模型的优势在于能够提供更详细的水流信息,但计算复杂度较高,需要更多的计算资源和时间。


二维水动力效果2.gif

本篇文章主要介绍在DTS 数字孪生引擎中实现二维水动力效果。在DTS SDK中开放了 HydroDynamic2D对象 添加二维水动力,并可以通过多种数据源进行添加,如 Bin、Sdb、Shp、Tif 的方式。


本文章主要介绍shp加载的方式,这种方式相对其他方式会更简单通用。


shp数据源添加方式


所需数据源


二维水动力是用数据驱动生成渲染效果的接口,所以数据源及其重要。


要利用shp为数据源进行添加,使用的是addByShp()方法,其与数据源相关的参数有两个:shpFilePath shpDataFilePath


shpFilePath其实就是水动力模型中水面网格的范围与高程,shpDataFilePath则代表每个网格的水深以及流速、流向



  • shpFilePath: 添加二维水动力模型整体范围的shp文件路径,取值示例:"C:/shpFile/xxx.shp"。



    • 此shp文件包含水动力模型所有网格的范围

    • shp类型为Polygon

    • 坐标系必须与工程坐标系保持一致

    • 必须包含 ID和Elev 两个字段:ID是网格ID;Elev是网格的高程值,单位是米




image-20241216172254779.png

  • shpDataFilePath: 可选参数,仅在update()方法执行生效。更新二维水动力模型时包含水面网格的dat类型文件路径,取值示例:"C:/datFile/xxx.dat"。

  • dat文件是一种二进制文件,它提取了某一时刻包含的所有水面网格的信息,并把这些信息依次写入了二进制文件dat。

  • 一个水面网格信息包含如下一组四个值:id (int),h (double),u(double),v(double),必须完全符合顺序以及数据类型。

  • id对应shp属性表ID字段,h是网格对应的水深(单位是米),uv是流速和流向(单位米/秒,u朝东,v朝北)。

  • 更新效果需要准备多个时刻的.dat文件,如下图所示

    image-20241216172314255.png


添加方法


1、准备测试数据

这里给大家准备好了一些数据资源,包括了实现的数据源、代码以及dat数据转换的程序,大家可以自行下载测试



百度网盘数据资源连接:pan.baidu.com/s/1XS3UDkrB…




  • 【文件资源】@path : 放到cloud文件资源路径

  • 【示例代码】code : demo源代码,直接用demo工程场景运行即可

  • 【dat数据转换】jsonToDat : json转dat代码,分别含有node.js、java、python示例代码


准备好两份数据分别是shpFilePath填写的shp文件,以及shpDataFilePath填写的dat文件集。文件可以直接用本地路径读取,建议放置到Cloud文件资源路径下,用@path的方式引用


这里可以用孪创启航营给大家准备的数据进行测试,在提供的文件夹的【文件资源】@path\【孪创启航营】HydroDynamic2D


2、通过shp网格数据初始化水动力模型

通过add()初始化水动力模型,并使用focus()定位到网格位置,但没有具体内容,还需要调用update添加.dat数据驱动效果。


add()参数文章末尾有详解


//添加shp数据源
fdapi.hydrodynamic2d.clear()

let hydrodynamic2d_add = {
id: 'hdm_shp', // HydroDynamic2D对象ID
collision: false, //开启碰撞sd
displayMode: 0, // 显示样式,取值范围:[0,1],0水体样式(默认值),1热力样式
waterMode: 0, // 水面显示模式,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式
arrowColor: [1, 1, 1, 0], // 箭头颜色和透明度
speedFactor: 0.1, // 速度因子
rippleDensity: 1, // 水波纹辐射强度
rippleTiling: 3, // 水波纹辐射平铺系数
shpFilePath: '@path:【孪创启航营】HydroDynamic2D/shp/grid.shp' // 添加二维水动力模型整体范围的shp文件路径
}
await fdapi.hydrodynamic2d.add(hydrodynamic2d_add)

await fdapi.hydrodynamic2d.focus('hdm_shp', 200)

3、根据.dat更新水动力模型

写一个定时器,根据不同时刻,调用hydrodynamic2d.update()更新shpDataFilePath路径,达到水动力更新的效果。



  • 参数updateTime 是更新动画的插值时间,单位为秒,一般与更新定时器的时间一致即可。


let index = 0
let hydrodynamicModel_for_update = {
id: 'hdm_shp', // HydroDynamic2D对象ID
updateTime: 1, // 更新动画的插值时间
shpDataFilePath: ''// 更新二维水动力模型时包含水面网格的dat类型文件路径
}

// 使用dat数据填充shp网格
let updateTimer = setInterval(async () => {
hydrodynamicModel_for_update.shpDataFilePath = '@path:【孪创启航营】HydroDynamic2D/dat/hydrodynamic_' + index + '.dat'

if (index > 9) {
clearInterval(updateTimer)
} else {
await __g.hydrodynamic2d.update(hydrodynamicModel_for_update) // 水动力更新
index = index + 1
}
}, 1000)

通过以上就可以达成二维水动力的创建以及更新了。


4、实现二维水动力热力效果

二维水动力支持热力效果,可以根据.dat文件中的水深字段进行配色


二维水动力热力效果.gif

仅需要把add()中的displayMode参数设置为1热力样式,再通过valueRangecolors进行热力样式的调整



  • valueRange (array) ,二维水动力模型颜色插值对应的数值区间

  • colors (object) 二维水动力模型自定义调色板对象,包含颜色渐变控制、无效像素颜色和调色板区间数组



    • gradient (boolean) 是否渐变

    • invalidColor (Color) 无效像素点的默认颜色,默认白色

    • colorStops (array) 调色板对象数组,每一个对象包含热力值和对应颜色值,结构示例:[{"value":0, "color":[0,0,1,1]}],每一个调色板对象支持以下属性:

      • color (Color) 值对应的调色板颜色

      • value (number) 值




    const addHeat = async () => {
    fdapi.hydrodynamic2d.clear()

    let hydrodynamic2d_add = {
    id: 'hdm_shp_heat', // HydroDynamic2D对象ID
    offset: [0, 0, 0], // 二维水动力模型的整体偏移,默认值:[0, 0, 0]
    displayMode: 1, // 显示样式,取值范围:[0,1],0水体样式(默认值),1热力样式
    waterMode: 2, // 水面显示模式,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式
    arrowColor: [1, 1, 1, 0.5], // 箭头颜色和透明度
    collision: false, //开启碰撞sd
    arrowTiling: 3, // 箭头平铺系数
    speedFactor: 0.1, // 速度因子
    rippleDensity: 1, // 水波纹辐射强度
    rippleTiling: 2, // 水波纹辐射平铺系数
    shpFilePath: '@path:【孪创启航营】HydroDynamic2D/shp/grid_heat.shp',

    valueRange: [1, 1.3], // 二维水动力模型颜色插值对应的数值区间
    alphaMode: 1, //使用colors色带透明度
    colors: {
    gradient: true,
    invalidColor: [0, 0, 0, 1],
    colorStops: [
    {
    value: 0,
    color: [0, 0, 1, 0.2]
    },
    {
    value: 0.25,
    color: [0, 1, 1, 0.2]
    },
    {
    value: 0.5,
    color: [0, 1, 0, 0.2]
    },
    {
    value: 0.75,
    color: [1, 1, 0, 0.2]
    },
    {
    value: 1,
    color: [1, 0, 0, 0.2]
    }
    ]
    }
    }
    await fdapi.hydrodynamic2d.add(hydrodynamic2d_add)

    await fdapi.hydrodynamic2d.focus('hdm_shp_heat', 200)

    let index = 0
    let hydrodynamicModel_for_update = {
    id: 'hdm_shp_heat',
    updateTime: 1,
    shpDataFilePath: ''
    }

    //使用dat数据填充shp网格
    let updateTimer = setInterval(async () => {
    hydrodynamicModel_for_update.shpDataFilePath = '@path:【孪创启航营】HydroDynamic2D/dat/hydrodynamic_' + index + '.dat'

    if (index > 9) {
    clearInterval(updateTimer)
    } else {
    await __g.hydrodynamic2d.update(hydrodynamicModel_for_update)
    index = index + 1
    }
    }, 1000)
    }



demo运行


缺乏数据源的小伙伴可以尝试运行我们准备好的demo示例,感受一下水动力的效果与参数调用。



  1. **下载资源:**下载百度网盘数据资源

  2. 替换资源:把【文件资源】@path的文件放到cloud文件资源路径下

  3. **启动cloud:**cloud启动demo工程

  4. 替换sdk:【示例代码】code\lib\aircity中的ac.min.jsac.min.js,替换为cloud右上角"sdk"路径的对应文件

  5. **运行:**双击运行示例代码】code\二维水动力.html 代码里的 shpFilePathshpDataFilePath路径得和第2步中一致


二维水动力效果1.gif

.dat 数据转换?


在数据源中,网格对应的水深、流速、流向数据,大家获取到可能不是标准的dat数据,有可能是json、csv甚至是excel数据。所以这里教大家如何把常见的数据转为dat二进制文件!


大象进冰箱需要三步,咱们转数据也需要三步



  1. 解析数据:读取文件,把不同数据源中的id,h,u,v(网格id、水深、流速流向u、流速流向v)提取出来。

  2. 转为二进制数据:把id,h,u,v转化为二进制的格式。

  3. 文件创建并写入:把二进制的格式数据保存为.dat文件


其中解析数据每份数据可能各不相同,都需要单独编写。这里我以一个json数据格式为例子,教大家如何转换为.dat,例如我们有一个data.json文件数据示例如下:


[
{
"index": 0,
"time": "08:30:00",
"data": [
{
"id": 0,
"h": 2,
"u": 0,
"v": 0
},
{
"id": 1,
"h": 2,
"u": 0,
"v": 0
}
]
},
{
"index": 1,
"time": "09:00:00",
"data": [
{
"id": 0,
"h": 2,
"u": 0,
"v": 0
},
{
"id": 1,
"h": 2.001,
"u": 0.1,
"v": 0.1
}
]
}
]


我们可以使用不同的编程手段来处理,如node.js、python、java,这里直接把转换的代码贴给大家~


注意:这三种编程手段都需要单独的安装对应的环境,如果没有环境可以选择一种自行百度安装



node官网:Node.js — 在任何地方运行 JavaScript


python官网:python.org


java官网:Java | Oracle



node.js


  1. 解析数据:使用require('./data.json')同步地引入并解析JSON数据文件,将其内容存储在jsonData变量中。

  2. 转为二进制数据:pamarToBuffer函数将idhuv转换为小端字节序的二进制Buffer。

  3. 文件创建并写入:遍历JSON数据,对每个时间点,使用path.join构建.dat文件路径,fs.createWriteStream创建写入流,datStream.write写入二进制Buffer,最后datStream.end关闭写入流。


// 引入必要的模块
const fs = require('fs') // 用于文件的读写操作
const path = require('path') // 用于处理文件路径
const jsonData = require('./data.json') // 引入 JSON 数据文件

// 确保 ./dat 目录存在
const datDir = path.join(__dirname, 'dat')
if (!fs.existsSync(datDir)) {
fs.mkdirSync(datDir)
}

// 遍历 JSON 数据 time_i 是当前时间点的索引
for (let time_i = 0; time_i < jsonData.length; time_i++) {
// 创建 .dat 文件路径
const datFilePath = path.join(datDir, `hydrodynamic_${time_i}.dat`)
// 创建写入流
const datStream = fs.createWriteStream(datFilePath)

// 获取并遍历时间点的数据
const timeData = jsonData[time_i].data
for (let grid_i = 0; grid_i < timeData.length; grid_i++) {
// 数据转换和写入
const { id, h, u, v } = timeData[grid_i]
const buffer = pamarToBuffer(id, h, u, v)
datStream.write(buffer)
}

datStream.end()
}

function pamarToBuffer(id, h, u, v) {
// 创建一个 Buffer 来存储二进制数据
const buffer = Buffer.alloc(4 + 8 + 8 + 8) // 分配足够的空间:4 字节用于 id,3 个 8 字节用于 double 值
// 向 Buffer 中写入数据
buffer.writeInt32LE(id, 0) // 从索引 0 开始写入 id(32 位整数)
buffer.writeDoubleLE(h, 4) // 从索引 4 开始写入 h(64 位浮点数)
buffer.writeDoubleLE(u, 12) // 从索引 12 开始写入 u(64 位浮点数)
buffer.writeDoubleLE(v, 20) // 从索引 20 开始写入 v(64 位浮点数)

return buffer
}


python


  1. 解析数据:使用json.load(f)方法从打开的JSON文件对象f中读取并解析数据,将JSON格式的数据转换为Python的字典或列表结构,存储在变量json_data中。

  2. 转为二进制数据:使用struct.pack('=iddd', id, h, u, v)方法将这些数据按照指定的格式(=表示本地字节顺序,i表示整数,d表示双精度浮点数)打包成二进制数据。

  3. 文件创建并写入:使用open函数以二进制写入模式打开(或创建)文件,最后通过write方法将转换好的二进制数据写入到该文件中。


import json
import os
import struct

# 读取JSON文件
json_file_path = './data.json'
with open(json_file_path, 'r') as f:
json_data = json.load(f)

# 定义输出目录
output_dir = os.path.join(os.getcwd(), 'dat')
if not os.path.exists(output_dir):
os.makedirs(output_dir)

# 遍历JSON数据
for time_i, time_node in enumerate(json_data):
# 创建.dat文件路径
dat_file_path = os.path.join(output_dir, f"hydrodynamic_{time_i}.dat")

# 打开文件以二进制写入模式
with open(dat_file_path, 'wb') as dat_file:
# 获取并遍历时间点的数据
time_data = time_node['data']
for grid_i, data_element in enumerate(time_data):

# 数据转换和写入
id = int(data_element['id'])
h = float(data_element['h'])
u = float(data_element['u'])
v = float(data_element['v'])

# 使用struct模块将数据转换为二进制格式
binary_data = struct.pack('=iddd', id, h, u, v)
# 写入二进制数据到文件
dat_file.write(binary_data)

print("Data processing complete.")

Java

java需要安装对应的 jackson json解析依赖才能使用,这里给大家提供了一个最简洁的版本,只需要有了对应的java环境运行目录下的start.bat文件即可生成dat文件。



  1. 解析数据:使用ObjectMapperdata.json文件中读取JSON数据,并解析为TimePoint对象的列表。

  2. 转为二进制数据:convertToBytes方法将TimePointData对象的idhuv字段转换为小端字节序的字节数组。

  3. 文件创建并写入:遍历TimePoint列表,为每个时间点创建.dat文件,并使用FileOutputStream将转换后的字节数组写入文件。


import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

class TimePointData {
int id;
double h;
double u;
double v;

public TimePointData(int id, double h, double u, double v) {
this.id = id;
this.h = h;
this.u = u;
this.v = v;
}

}

class TimePoint {
List<TimePointData> data;

public TimePoint(List<TimePointData> data) {
this.data = data;
}

}

public class JsonToDatConverter {

public static void main(String[] args) {
String jsonFilePath = "data.json";
String datDir = "dat";

// 读取JSON文件
ObjectMapper objectMapper = new ObjectMapper();
try {
JsonNode rootNode = objectMapper.readTree(Files.newInputStream(Paths.get(jsonFilePath), StandardOpenOption.READ));
List<TimePoint> timePoints = parseJsonToTimePoints(rootNode);

Path dirPath = Paths.get(datDir);
if (!Files.exists(dirPath)) {
Files.createDirectory(dirPath);
}

for (int time_i = 0; time_i < timePoints.size(); time_i++) {
TimePoint timePoint = timePoints.get(time_i);
String datFilePath = Paths.get(datDir, "hydrodynamic_" + time_i + ".dat").toString();

try (FileOutputStream fos = new FileOutputStream(datFilePath)) {
for (TimePointData data : timePoint.data) {
byte[] bytes = convertToBytes(data.id, data.h, data.u, data.v);
fos.write(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

private static List<TimePoint> parseJsonToTimePoints(JsonNode rootNode) {
if (rootNode == null || !rootNode.isArray()) {
throw new IllegalArgumentException("Invalid JSON structure: 'timePoints' field is missing or not an array");
}

List<TimePoint> timePoints = new ArrayList<>();
for (JsonNode timePointNode : rootNode) {
JsonNode dataNode = timePointNode.get("data");
if (dataNode == null || !dataNode.isArray()) {
throw new IllegalArgumentException(
"Invalid JSON structure: 'data' field is missing or not an array within a 'timePoints' object");
}

List<TimePointData> dataList = new ArrayList<>();
for (JsonNode dataItemNode : dataNode) {
int id = dataItemNode.get("id").asInt();
double h = dataItemNode.get("h").asDouble();
double u = dataItemNode.get("u").asDouble();
double v = dataItemNode.get("v").asDouble();
dataList.add(new TimePointData(id, h, u, v));
}

timePoints.add(new TimePoint(dataList));
}

return timePoints;
}

private static byte[] convertToBytes(int id, double h, double u, double v) {
ByteBuffer buffer = ByteBuffer.allocate(4 + 8 + 8 + 8).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(id);
buffer.putDouble(h);
buffer.putDouble(u);
buffer.putDouble(v);
return buffer.array();
}
}

二维水动力添加参数详解


通用参数

通用参数比较简单理解,这里就简单列举出来



  • id (string) HydroDynamic2D对象ID

  • groupId (string) 可选,Gr0up分组

  • userData (string) 可选,用户自定义数据

  • offset (array) 二维水动力模型的整体偏移,默认值:[0, 0, 0]

  • collision (boolean) 是否开启碰撞,注意:开启后会影响加载效率


数据参数

数据参数前面介绍所需数据源已有详细介绍



  • shpFilePath(string)添加二维水动力模型整体范围的shp文件路径,取值示例:"C:/shpFile/xxx.shp"。

    • 此shp文件包含水动力模型所有网格的范围

    • shp类型为Polygon

    • 坐标系必须与工程坐标系保持一致

    • 必须包含 ID和Elev 两个字段:ID是网格ID;Elev是网格的高程值,单位是米



  • shpDataFilePath (string)可选参数,仅在update()方法执行生效。更新二维水动力模型时包含水面网格的dat类型文件路径,取值示例:"C:/datFile/xxx.dat"。

    • 注意:dat文件是一种二进制文件,它提取了某一时刻包含的所有水面网格的信息,并把这些信息依次写入了二进制文件dat,一个水面网格信息包含如下一组四个值:id,h,u,v。id对应shp属性表ID字段(int类型),h是网格对应的水深(double类型,单位是米),uv是流速和流向(double类型,单位米/秒,u朝东,v朝北)。




显示样式参数


  • displayMode (number) 显示样式,取值范围:[0,1],0水体样式(默认值),1热力样式



    • displayMode为0时,样式就只需要控制waterModewaterColor设置水体样式



      • waterMode (number) 水面显示模型,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式

      • waterColor (Color) 水体颜色和透明度,注意:仅在displayMode=0时生效



    • displayMode为1时,样式就需要通过valueRangecolors控制热力样式



      • valueRange (array) ,二维水动力模型颜色插值对应的数值区间

      • colors (object) 二维水动力模型自定义调色板对象,包含颜色渐变控制、无效像素颜色和调色板区间数组



        • gradient (boolean) 是否渐变

        • invalidColor (Color) 无效像素点的默认颜色,默认白色

        • colorStops (array) 调色板对象数组,每一个对象包含热力值和对应颜色值,结构示例:[{"value":0, "color":[0,0,1,1]}],每一个调色板对象支持以下属性:

          • color (Color) 值对应的调色板颜色

          • value (number) 值





      • colors代码示例


        // colors示例
        {
        gradient: true,// 是否渐变
        invalidColor: [0, 0, 0, 1],// 无效像素点的默认颜色
        colorStops: [
        {
        value: 0,
        color: [0, 0, 1, 1]
        },
        {
        value: 0.25,
        color: [0, 1, 1, 1]
        },
        {
        value: 0.5,
        color: [0, 1, 0, 1]
        },
        {
        value: 0.75,
        color: [1, 1, 0, 1]
        },
        {
        value: 1,
        color: [1, 0, 0, 0]
        }
        ]
        }






  • alphaComposite (boolean) 是否使用混合透明度 取值:true / false 默认:true

  • alphaMode (number) 透明模式,取值:[0,1],0 : 使用colors调色板的不透明度值 1 : 使用系统默认值


箭头相关参数

箭头方向根据每个格网的uv流向决定



  • arrowDisplayMode (number) 箭头显示模式 取值范围:[0,1],0默认样式(受arrowColor参数影响),1热力样式(受arrowColors调色板参数影响)



    • arrowDisplayMode 为0,则设置arrowAlphaMode = 0,并通过arrowColor调整箭头的颜色和透明度

      • arrowColor (Color) 箭头颜色和透明度



    • arrowDisplayMode 为1,则设置arrowAlphaMode = 1,并通过arrowColors调整箭头的颜色和透明度

      • arrowColors (object)箭头颜色调色板 仅在arrowDisplayMode=1时生效,河道箭头热力样式下的调色板配色对象,包含颜色渐变控制、无效像素颜色和调色板区间数组

        • 格式同上方的显示样式参数colors







  • arrowAlphaMode (number) 箭头透明度模式,仅在arrowDisplayMode=0时生效,取值:[0,1],0使用arrowColor的透明度,1使用调色板的透明度

  • arrowTiling (number) 箭头平铺系数 值越小则箭头越小越密集,反之则更大更疏松


箭头.png
水面效果参数


  • foamWidth (number) 泡沫宽度取值范围:[0~10000],默认值:1米

  • foamIntensity (number) 泡沫强度 取值范围:[0~1],默认值:0.5

  • speedFactor (number) 速度因子


速度因子.gif

  • flowThreshold (array) 水浪效果漫延的范围 即把水动力模型[minSpeed,maxSpeed],最小最大流速的范围映射到[0~~1],取值示例:[0.1,0.4],取值范围[0-1]


水浪效果漫延的范围.png

  • rippleDensity (number)水波纹辐射强度


水波纹辐射强度.gif

  • rippleTiling (number) 水波纹辐射平铺系数


水波纹辐射平铺系数.gif


以上就是本篇文章的所有内容,相信大家看完这篇文章后可以轻松的通过DTS实现二维水动力效果。


在DTS中还有各式各样的水分析相关接口,如FloodFill 水淹分析、Fluid 流体仿真对象、HydroDynamic1D 一维水动力、WaterFlowField 水流场,大家可以根据自身需求选择,这里给大家推荐一篇《开闸放水》的教程,后续也会陆续推出更多教程~


不再需要UE美术,前端轻松解决水利开闸放水难题!!!


作者:女前端浅入数字孪生
来源:juejin.cn/post/7452181029994971147
收起阅读 »

神了,Chrome 这个记录器简直是开发测试提效神器🚀🚀🚀

web
在开发工作中,你是否遇到过这样的场景: 当你需要开发某个功能时,这个功能依赖一系列的点击或者选择操作,才能获取到最终的数据。而在开发和调试的过程中,你往往需要多次验证流程的正确性。早期的时候,这种验证通常非常繁琐——你可能需要反复提交表单、重新执行操作流程,才...
继续阅读 »

在开发工作中,你是否遇到过这样的场景:


当你需要开发某个功能时,这个功能依赖一系列的点击或者选择操作,才能获取到最终的数据。而在开发和调试的过程中,你往往需要多次验证流程的正确性。早期的时候,这种验证通常非常繁琐——你可能需要反复提交表单、重新执行操作流程,才能完成一次完整的自测。


如今,这一切变得更加高效了。


现在,我们可以使用记录器(Recorder)来优化这一开发流程。这个工具允许你将整个操作过程录制下来,保存为一个可复现的操作记录。每次需要重新验证或提交流程时,只需一键执行这条记录,便能完成所有的重复性操作。


更棒的是,这个功能还支持二次编辑。如果你需要在某个步骤后面新增额外的操作,或者减少不必要的步骤,都可以轻松修改操作记录,而无需重新录制整个流程。



文章同步在公众号:萌萌哒草头将军,欢迎关注哦~



🚀 功能亮点与用途


1. 高效的开发与调试


对于开发者来说,这个功能不仅可以节省大量时间,还能确保操作流程的准确性,避免因手动操作而导致的遗漏或错误。


2. 性能监控的得力助手


谷歌推出这个功能的主要目的是为了帮助开发者更方便地监听用户在某些操作流程中的性能体验。例如,在查看录制的操作流程时,你可以直接点击某个步骤,跳转到性能面板(Performance Panel),并且工具会自动锁定当前帧的数据。这种体验优化相比以往手动查找性能问题,提升了不少效率。


3. 测试自动化的天然工具


如果你是一名测试人员,这个功能同样非常实用。操作流程录制完成后,你可以直接将其导出为Puppeteer脚本,方便地将其集成到你的自动化测试中,进一步提升测试的覆盖率和效率。


🚀 使用方法


我们以表单提交为例子展示


image.png


以下是如何使用记录器功能的步骤:


1. 💎 打开记录器


image.png


并点击创建新录制按钮


image.png


2. 💎 开始录制流程


可以重命名下,方便后续复用,然后点击最下方的开始录制按钮


image.png


我们在填写完表单,并且点击 sumbit按钮,然后点击控制台的结束录制按钮,可以看到我们的每个步骤都被记录下来


image.png


3. 💎 执行录制



  1. 记录器 面板中,点击 播放 按钮,浏览器会自动按照录制的流程重新执行操作。

  2. 你可以在执行过程中观察页面行为,确认流程是否正确。

  3. 如果遇到下面的情况,说明是超时了,需要设置下超时时间


image.png


点击这个地方展开就可以重新设置超时限制参数了


image.png


然后你点击播放按钮就一切正常了


nalmal.gif


4. 💎 查看和编辑录制


你可以在 记录器 面板中,看到录制的每个步骤,包含操作类型(如点击、输入、导航等)和目标元素。


你也可以点击每个步骤进行详细查看,也可以通过右键菜单进行编辑,例如增加新步骤、删除步骤或修改操作。


🚀 应用场景


1. 💎 表单提交及验证


录制复杂的表单提交流程,方便反复验证数据的提交逻辑是否正确。这个场景如上,相信你也感受它的便利性了。


2. 💎 性能优化


在模拟用户真实操作的同时,快速捕捉性能瓶颈,定位问题并优化。


点击性能面板按钮,等待自动回填数据,然后跳到性能面板,为了压缩我把很多帧去掉了


preform.gif


最终你可以在如下的性能面板开始分析了


353.png


3. 💎 自动化测试开发


如果需要将录制的流程用于自动化测试,可以点击 导出 按钮,将其导出为 Puppeteer 脚本或者 json数据,这样可以减少编写测试脚本的时间,通过导出的 Puppeteer 脚本直接复用操作流程。我不是测试人员就不多赘述了。


🚀 小结


“记录器”功能的出现,不仅让开发和调试更加高效,还为性能监控和测试自动化提供了重要支持。它减少了重复操作的浪费,让开发者和测试人员都能将更多精力集中在核心工作上。


是不是觉得这个功能非常有趣又实用?赶紧试试看吧!


如果有用记得关注我的公众号:萌萌哒草头将军


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

程序员就得会偷懒,重写了一个electron小工具,解放美女运营老师!

web
前言 接前一篇美女运营天天找我改配置,给她写了个脚本,终于安静了 之前只是写了一个脚本,本地运行,通过读取文件流获取文件数据,格式化对应数据,运营老师也不会安装node,还是需要我去操作。现在我用electron生成一个桌面应用直接生成后复制json,去配置,...
继续阅读 »

前言


接前一篇美女运营天天找我改配置,给她写了个脚本,终于安静了


之前只是写了一个脚本,本地运行,通过读取文件流获取文件数据,格式化对应数据,运营老师也不会安装node,还是需要我去操作。现在我用electron生成一个桌面应用直接生成后复制json,去配置,全程不需要我参与了。


之前的脚本


const fs = require('fs')
const csv = require('csv-parser');

const csvfilePath = './xxx.csv';
const uidsfilePath = './uids.json';

const results = [];
let newarr = [];
let lineCount = 0;

fs.createReadStream(csvfilePath)
.pipe(csv({ headers: true }))
.on('data', (data) => {
results.push(data);
lineCount++;
})
.on('end',async () => {
console.log(results[0])
await format(results);
fs.writeFile(uidsfilePath, JSON.stringify(newarr), () => {
console.log('done')
})
});
const format = (results) => {
newarr = results.map(item => {
if(item._0 === 'key' || item._1 === 'value') {
return {}
}
return {
label: `${item._1}-${item._0}`,
value: item._1
}
})
}

electron


简介


Electron 是一个用于使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用程序的框架。它由 GitHub 开发,基于 Chromium 和 Node.js。这意味着开发者可以利用他们熟悉的 Web 开发技术来创建桌面应用。


优势



  • 跨平台开发

  • 快速开发迭代

  • 丰富的生态系统


架构与核心概念



  • 主进程和渲染进程:


主进程:主进程是整个 Electron 应用的核心,它负责创建和管理应用程序的窗口。主进程通过BrowserWindow模块来创建浏览器窗口,这个窗口就是用户看到的应用界面的载体。


渲染进程:渲染进程主要负责渲染应用的用户界面。每个BrowserWindow都有自己独立的渲染进程,它使用 Chromium 浏览器内核来解析 HTML 和 CSS 文件,执行 JavaScript 代码。



  • 进程间通信(IPC):
    由于 Electron 应用有主进程和渲染进程之分,进程间通信就显得尤为重要。Electron 提供了ipcMain(用于主进程)和ipcRenderer(用于渲染进程)模块来实现进程间的消息传递。


使用vue3和vite创建vue的项目然后引入electron


安装vite


npm create vite@latest electron-desktop-tool

安装 引入electron&插件


npm install -D electron // electron
npm install -D electron-builder //用于打包可安装exe程序和绿色版免安装exe程序
npm install -D electron-devtools-installer // 调试
npm install -D vite-plugin-electron // vite构建插件

创建主进程


在vue 同级src目录下,创建src-electron 文件夹 新建main.js


// src-electron/main.js
const { app, BrowserWindow } = require('electron')
const { join } = require('path')

// 屏蔽安全警告
// ectron Security Warning (Insecure Content-Security-Policy)
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'

// 创建浏览器窗口时,调用这个函数。
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
title: 'electron-vite',
// icon: join(__dirname, '../public/logo.ico'),
})

// win.loadURL('http://localhost:3000')
// development模式
if(process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL)
// 开启调试台
win.webContents.openDevTools()
}else {
win.loadFile(join(__dirname, '../dist/index.html'))
}
}

// Electron 会在初始化后并准备
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

配置插件入口


在vite.config.ts中配置vite-plugin-electron 插件入口


import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
electron({
// 主进程入口文件
entry: './src-electron/main.js'
})
],
/*开发服务器选项*/
server: {
// 端口
port: 3000,
}
})

配置package.json


在package.json 新增入口文件 "main": "./src-electron/main.js",


原神启动 emmm electron启动


运行 npm run dev 启动项目


请在此添加图片描述


打包配置


首先配置一下打包的命令,在package.json "scripts"里面配置这个打包命令


  "scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"postinstall": "patch-package",
"electron:build": "vite build && electron-builder",
"pack32": "vite build && electron-builder --win --ia32",
"pack64": "vite build && electron-builder --win --x64"
},

同样package.json 需要添加打包配置


  "scripts": {
...
},
"build": {
"productName": "ElectronDeskTopTool",
"appId": "dyy.dongyuanwai",
"copyright": "dyy.dongyuanwai © 2024",
"compression": "maximum",
"asar": true,
"directories": {
"output": "release/"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"perMachine": true,
"deleteAppDataOnUninstall": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "ElectronDeskTopTool"
},
"win": {
"icon": "./public/logo.ico",
"artifactName": "${productName}-v${version}-${platform}-setup.${ext}",
"target": [
{
"target": "nsis"
}
]
},
"mac": {
"icon": "./public/logo.ico",
"artifactName": "${productName}-v${version}-${platform}-setup.${ext}"
},
"linux": {
"icon": "./public/logo.ico",
"artifactName": "${productName}-v${version}-${platform}-setup.${ext}"
}
},

然后npm run electron:build


请在此添加图片描述


页面效果


请在此添加图片描述


github地址


后续还会继续更新~


作者:一起重学前端
来源:juejin.cn/post/7445289957893259327
收起阅读 »

antd 对 ai 下手了!Vue 开发者表示羡慕!

web
前端开发者应该对 Ant Design 不陌生,特别是 React 开发者,antd 应该是组件库的标配了。 近年来随着 AI 的爆火,凡是想要接入 AI 的都想搞一套自己的 AI 交互界面。专注于 AI 场景组件库的开源项目倒不是很多见,近日 antd 宣布...
继续阅读 »


前端开发者应该对 Ant Design 不陌生,特别是 React 开发者,antd 应该是组件库的标配了。


近年来随着 AI 的爆火,凡是想要接入 AI 的都想搞一套自己的 AI 交互界面。专注于 AI 场景组件库的开源项目倒不是很多见,近日 antd 宣布推出 Ant Design X 1.0 🚀 ,这是一个基于 Ant Design 的全新 AGI 组件库,使用 React 构建 AI 驱动的用户交互变得更简单了,它可以无缝集成 AI 聊天组件和 API 服务,简化 AI 界面的开发流程。


该项目已在 Github 开源,拥有 1.6K Star!



看了网友的评论,看来大家还是需要的!当前的 Ant Design X 只支持 React 项目,看来 Vue 开发者要羡慕了...



ant-design-x 特性



  • 🌈 源自企业级 AI 产品的最佳实践:基于 RICH 交互范式,提供卓越的 AI 交互体验

  • 🧩 灵活多样的原子组件:覆盖绝大部分 AI 对话场景,助力快速构建个性化 AI 交互页面

  • ⚡ 开箱即用的模型对接能力:轻松对接符合 OpenAI 标准的模型推理服务

  • 🔄 高效管理对话数据流:提供好用的数据流管理功能,让开发更高效

  • 📦 丰富的样板间支持:提供多种模板,快速启动 LUI 应用开发

  • 🛡 TypeScript 全覆盖:采用 TypeScript 开发,提供完整类型支持,提升开发体验与可靠性

  • 🎨 深度主题定制能力:支持细粒度的样式调整,满足各种场景的个性化需求


支持组件


以下圈中的部分为 ant-design-x 支持的组件。可以看到主要都是基于 AI Chat 场景的组件设计。现在你可以基于这些组件自由组装搭建一个自己的 AI 界面。



ant-design-x 也提供了一个完整 AI Chat 的 Demo 演示,可以查看 Demo 的代码并直接使用。



更多组件详细内容可参考 组件文档


使用


以下命令安装 @ant-design/x 依赖。


注意,ant-design-x 是基于 Ant Design,因此还需要安装依赖 antd


yarn add antd @ant-design/x

import React from 'react';
import {
// 消息气泡
Bubble,
// 发送框
Sender,
} from '@ant-design/x';

const messages = [
{
content: 'Hello, Ant Design X!',
role: 'user',
},
];
const App = () => (
<div>
<Bubble.List items={messages} />
<Sender />
</div>

);

export default App;

Ant Design X 前生 ProChat


不知道有没有小伙伴们使用过 ProChat,这个库后面的维护可能会有些不确定性,其维护者表示 “24 年下半年后就没有更多精力来维护这个项目了,Github 上的 Issue 存留了很多,这边只能尽量把一些恶性 Bug 修复



如上所示,也回答了其和 Ant Design X 的关系:ProChat 是 x 的前生,新用户请直接使用 x,老用户也请尽快迁移到 x


感兴趣的朋友们可以去试试哦!


作者:五月君
来源:juejin.cn/post/7444878635717443595
收起阅读 »

前端ssr项目被打崩后,连夜做限流!

web
token-bucket-limiter-redis 是一个令牌桶算法 + redis 的高效限流器,用于Node服务接口限流。 当然作为一个前端你可能很少接触Node接口开发,用的接口应该都是后端同学提供的,他们有自己的限流策略,但是你一定使用过SSR框架来...
继续阅读 »

token-bucket-limiter-redis 是一个令牌桶算法 + redis 的高效限流器,用于Node服务接口限流。


当然作为一个前端你可能很少接触Node接口开发,用的接口应该都是后端同学提供的,他们有自己的限流策略,但是你一定使用过SSR框架来开发服务端渲染项目,那么此时你的项目就只能靠我们自己来做限流了,否则遇到突发流量时,你的项目可能很容易崩溃。



  • 使用令牌桶算法实现

  • 支持基于内存和基于 redis 存储的两种选择,满足分布式限流需要

  • 高性能,令牌生产的方式为每次请求进来时一次性生产上一次请求到本次请求这一段时间内的令牌,而不是定时器生成令牌

  • 快速,使用 lua 脚本与redis通讯,lua 支持将多个请求通过脚本的形式一次发送到服务器,减少通讯,并且脚本支持缓存,多客户端可以复用

  • 安全,lua 脚本保证redis命令执行的原子性

  • 内存效率高,键过期后自动删除,不占用过多内存

  • 提供多种极端场景下的降级和容错措施



其他限流方法的对比,大家可以自行搜索,这里就不赘述了,令牌桶算法是更适合大部分场景的限流方案。


令牌桶算法:按照一定的速率生产令牌并放入令牌桶中,最大容量为桶的容量,如果桶中令牌已满,则丢弃令牌,请求过来时先到桶中拿令牌,拿到令牌则放行通过,否则拒绝请求。这种算法能够把请求均匀的分配在时间区间内,又能接受服务可承受范围内的突发请求。所以令牌桶算法在业内较为常用。




该项目github地址:token-bucket-limiter-redis



安装


npm i --save token-bucket-limiter-redis

引入


import { RateLimiterTokenBucket, RateLimiterTokenBucketRedis } from 'token-bucket-limiter-redis';

使用


限流方案我们分为无状态限流器和有状态限流器两种:


有状态的限流器(区分key的限流器):这种限流器会根据某种标识(如IP地址、用户ID、url等)来进行区分,并对每个标识进行单独的限流。可以更精细地控制每个用户或者每个IP的访问频率。


无状态的限流器(不区分key的限流器):这种限流器不会区分请求的来源,只是简单地对所有请求进行统一的限制。


基于内存的无状态限流器


const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 100,
capacity: 1000,
});

const globalTokens = globalRateLimiter.getToken();

if(globalTokens > 0){
// pass
}


基于内存的有状态限流器,自定义key


const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
});

const key = ip + uid; // 标识用户信息的key

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


这里附上 node 端获取ip的方法



function getClientIp(req) {
// 获取 X-Real-IP 头部字段
const xRealIP = req.headers['x-real-ip'];

// 优先使用 X-Real-IP 头部字段
if (xRealIP) {
return xRealIP;
}

// 获取 X-Forwarded-For 头部字段,通常包含一个或多个IP地址,最左侧的是最初的客户端IP
const xForwardedFor = req.headers['x-forwarded-for'];

// 如果 X-Real-IP 不存在,但 X-Forwarded-For 存在,则使用最左侧的IP地址
if (xForwardedFor) {
const ipList = xForwardedFor.split(',');
return ipList[0].trim();
}

// 获取连接的远程IP地址
const remoteAddress = req.connection?.remoteAddress;
// 如果都不存在,使用连接的远程IP地址
if (remoteAddress) {
return remoteAddress;
}

return '';
}

基于内存的有状态限流器,使用ip作为默认key


const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
});

// 使用 ip 作为key,无需传入,自动获取ip
const globalTokens = globalRateLimiter.getTokenUseIp(req);

// 使用 ip 加上自定义的其他key,如传入则组合在ip后 ip+uid
const globalTokens = globalRateLimiter.getTokenUseIp(req, uid);

if(globalTokens > 0){
// pass
}



注意,单纯使用ip作为限流key可能会有问题,有以下几种可能过个机器的外网ip相同的情况:



  • 使用共享的公共 IP 地址: 在一些特殊的网络环境下,多个设备可能共享同一个公共 IP 地址,如咖啡馆、图书馆等提供 Wi-Fi 服务的地方。在这种情况下,所有连接到同一网络的设备都会共享相同的公共 IP。

  • 使用代理服务器: 如果多个机器通过相同的代理服务器访问互联网,它们可能会在外网上表现为相同的 IP 地址,因为代理服务器向互联网发起请求,而不是直接来自每个终端设备。

  • 使用 NAT(网络地址转换): 在家庭或企业网络中,使用了 NAT 技术的路由器可能会导致多个内部设备共享同一个外网 IP 地址,同一公司下的内网设备公网ip可能是同一个。



综上,如果你需要考虑以上集中情况的话,你需要结合其他可以标识用户身份的key,如uid,浏览器指纹等:


// 使用 ip 加上自定义的其他key,如传入则组合在ip后 ip+uid
const globalTokens = globalRateLimiter.getTokenUseIp(req, uid);

附上浏览器指纹获取方法:


function generateFingerprint() {
try {
// 收集一些浏览器属性
const userAgent = navigator.userAgent || '';
const screenResolution = `${window.screen.width}x${window.screen.height}`;
const language = navigator.language || '';
const platform = navigator.platform || '';

// 将这些属性组合成一个简单的指纹
const fingerprint = userAgent + screenResolution + language + platform;

// 返回指纹
return fingerprint;
} catch (error) {
return '';
}
}

在 express 中使用


const express = require('express');
const app = express();

const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
});

// 全局中间件
app.use((req, res, next) => {
console.log('Express global middleware');
// 使用 ip 作为key,无需传入,自动获取ip
const tokens = globalRateLimiter.getTokenUseIp(req);

if(tokens > 0){
next();
}else {
res.status(429).send({ message: 'Too Many Requests' })
}
});

app.listen(3000, () => {
console.log('Express app listening on port 3000');
});

在 koa 中使用


const Koa = require('koa');
const app = new Koa();

const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test',
});

// 全局中间件
app.use(async (ctx, next) => {
console.log('Koa global middleware');
// 使用 ip 作为 key,无需传入,自动获取 ip
const tokens = globalRateLimiter.getTokenUseIp(ctx.req);

if (tokens > 0) {
await next();
} else {
ctx.status = 429;
ctx.body = { message: 'Too Many Requests' };
}
});

app.listen(3000, () => {
console.log('Koa app listening on port 3000');
});


在 fastify 中使用


const fastify = require('fastify')();

const globalRateLimiter = new RateLimiterTokenBucket({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test',
});

// 全局中间件
fastify.addHook('onRequest', (request, reply, done) => {
console.log('Fastify global middleware');
// 使用 ip 作为 key,无需传入,自动获取 ip
const tokens = globalRateLimiter.getTokenUseIp(request);

if (tokens > 0) {
done();
} else {
reply.status(429).send({ message: 'Too Many Requests' });
}
});

fastify.listen(3000, (err) => {
if (err) throw err;
console.log('Fastify app listening on port 3000');
});


基于redis的无状态限流器,传入redis客户端


支持分布式限流,外部传入redis客户端 (由ioredis包创建)


import Redis from 'ioredis';

const redis = new Redis({});

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 100,
capacity: 1000,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisClient: redis,
});

const key = 'myproject'; // 使用全局唯一key (当key省略时,默认为RateLimiterTokenBucketGlobalKey)

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


基于redis的有状态限流器,传入redis客户端


支持分布式限流,外部传入redis客户端 (ioredis)


import Redis from 'ioredis';

const redis = new Redis({});

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisClient: redis,
});

const key = ip + uid; // 标识用户信息的key
const globalTokens = globalRateLimiter.getToken(key);

// 使用 ip 作为key
const globalTokens = globalRateLimiter.getTokenUseIp(req);

// 使用 ip + 自定义key
const globalTokens = globalRateLimiter.getTokenUseIp(req, key);

if(globalTokens > 0){
// pass
}


基于redis的有状态限流器,使用内置redis


外部仅需传入redis配置(ioredis)


const redisOptions = {
port: 6379, // Redis 端口
host: 'localhost', // Redis 主机名
password: 'password' // 如果有的话,你的 Redis 密码
db: 0,
};

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisOptions: redis,
});

const key = ip + uid; // 标识用户信息的key

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


添加内存阻塞策略


内存阻塞策略可以保护redis服务器,抵御DDoS攻击


const redisOptions = {
port: 6379, // Redis 端口
host: 'localhost', // Redis 主机名
password: 'password' // 如果有的话,你的 Redis 密码
db: 0,
};

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisOptions: redis,

// 内存阻塞策略(只计算当前服务器或实例的请求数,非分布式)
inMemoryBlockOnConsumed: 50, // 如果某个key在一分钟内消耗的令牌数量超过 50,将在内存中阻塞该key的请求,不会发起redis,防止DDoS攻击
inMemoryBlockDuration: 10, // 阻塞持续时间s
});

const key = ip + uid; // 标识用户信息的key

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


getToken 方法支持第二个参数,传入判断阻塞的标识键,通常是ip或用户id,因为我们要阻塞的是某个具体的用户或机器,不传的话默认使用第一个参数,即令牌标识键。


当你使用无状态限流器,或是有状态限流器的键无法标识某个具体用户时可能需要填写该参数:


const key = 'myproject'; // 无状态限流器
const key = 'url'; // 有状态限流器,但是只限制某个路由

const blockKey = 'ip'; // 阻塞标识键须使用ip或用户id

const globalTokens = globalRateLimiter.getToken(key, blockKey);

// 使用 ip + 自定义key
const globalTokens = globalRateLimiter.getTokenUseIp(req, key, blockKey);

if(globalTokens > 0){
// pass
}


内存阻塞策略优先于redis限流器以及redis保险策略,即使redis不可用时内存阻塞策略依旧生效。


添加保险策略,配置当redis服务错误时是否自动使用内存限制器


const redisOptions = {
port: 6379, // Redis 端口
host: 'localhost', // Redis 主机名
password: 'password' // 如果有的话,你的 Redis 密码
db: 0,
};

const globalRateLimiter = new RateLimiterTokenBucketRedis({
tokenPerSecond: 5,
capacity: 5,
keyPrefix: 'test', // 指定限流器所属项目或模块
redisOptions: redis,

// 内存阻塞策略
inMemoryBlockOnConsumed: 50, // 如果某个key在一分钟内消耗的令牌数量超过 50,将在内存中阻塞该key的请求,不会发起redis,防止DDoS攻击
inMemoryBlockDuration: 10, // 阻塞持续时间s

// 保险策略,使用内存限流器
insuranceLimiter: true,
insuranceLimiterTokenPerSecond: 3, // 如果未填写将取tokenPerSecond的值
insuranceLimiterCapacity: 3, // 如果未填写将取capacity的值
});

const key = ip + uid; // 标识用户信息的key

const globalTokens = globalRateLimiter.getToken(key);

if(globalTokens > 0){
// pass
}


开启保险策略后,支持传入保险限制器的每秒令牌数和令牌桶容量,如果不传,将取redis限流器的值。


当你的服务是集群部署时,例如使用 pm2 的集群模式时,会用到这些选项,因为使用redis时令牌是共享的,而集群模式下每个服务是一个实例,每个实例有自己的内存空间,所以你要适当地考虑使用内存限流器时每个实例的限流速率。


注意事项



  1. 基于内存的限流器更适用于单机限流的场景,集群或分布式部署时,如果你不能计算出每一个实例的合适限流配置的话推荐使用基于redis的限流器。


FAQ


不使用定时器生成令牌有什么好处?


时间精度:定时器的精度可能会受到系统调度和网络延迟的影响,这可能导致令牌的生成速率无法精确控制。


资源消耗:如果令牌桶的数量非常多,那么需要维护的定时器也会非常多,这可能会消耗大量的系统资源。


时间同步:由于精度问题,如果系统中存在多个令牌桶,且每个令牌桶都使用自己的定时器,那么这些定时器之间可能并不同步。


冷启动问题:如果使用定时器生成令牌,那么在服务刚启动时,令牌桶可能会是空的,这可能导致在服务启动初期无法处理请求。


除了ip还有哪些可以标识具体用户的key



  • 浏览器指纹

  • 用户id

  • 用户名

  • 邮箱

  • 手机号

  • 其他可以标识用户身份的key


// 生成浏览器指纹
export function generateFingerprint() {
try {
// 收集一些浏览器属性
const userAgent = navigator.userAgent || '';
const screenResolution = `${window.screen.width}x${window.screen.height}`;
const language = navigator.language || '';
const platform = navigator.platform || '';

// 将这些属性组合成一个简单的指纹
const fingerprint = userAgent + screenResolution + language + platform;

// 返回指纹
return fingerprint;
} catch (error) {
return '';
}
}

作者:Pursue_LLL
来源:juejin.cn/post/7454095190379888666
收起阅读 »

百亿补贴为什么用 H5?H5 未来会如何发展?

web
23 年 11 月末,拼多多市值超过了阿里。我想写一篇《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。 眼看着灵感烂在手里,我决定把两篇文章合为一篇,与你分享。提前说明,我并非百亿补贴的开发人员,本文的...
继续阅读 »

23 年 11 月末,拼多多市值超过了阿里。我想写一篇《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。


眼看着灵感烂在手里,我决定把两篇文章合为一篇,与你分享。提前说明,我并非百亿补贴的开发人员,本文的内容是我的推理。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


我的证据


在 Android 开发者模式下,开启显示布局边界,你可以看到「百亿补贴」是一个完整大框,这说明「百亿补贴」在 App 内是 H5。拷贝分享链接,在浏览器打开,可以看到资源中有 React,说明「百亿补贴」技术栈是 React。


pdd-stack.png


不只是拼多多,利用同样的方法,你可以发现京东、淘宝的「百亿补贴」技术栈也是 H5。


pdd-jd-taobao.png


那么,为什么电商巨头会选择做「百亿补贴」时会选择 H5 呢?


我的推理逻辑


解答问题前,我先说明下推理逻辑。巨头可能选择 H5 的原因千千万万,但最有说服力的原因,肯定具有排他性


什么是排他性?


举个例子,成功人物为什么成功,如果我回答「成功人士会喝水」,你肯定不满意。如果我回答「成功人士坚持不懈」,你会更满意一些。喝水分明是成功人士成功的原因,不喝水人会渴死,没办法成功。你为什么对这个答案不满意呢?


因为「喝水」不具备排他性,普通人也会喝水;而「坚持不懈」比「喝水」更具排他性,大部分普通人没有这个特质。


按照排他性,我需要说明百亿补贴只有 H5 能干,其他技术栈不能干,这样才有说服力。


百亿补贴为什么用 H5?


现在进入正题。粗略来看,大前端的技术栈分为 Native 和跨平台两大类。前者包括 3 小类,分别是 Android、iOS、纯血鸿蒙;后者也包括 3 小类,分别是基于 Web 的方案、基于系统 UI 框架的方案(比如 React Native)、自己绘制 UI 的方案(比如 Flutter)。


其中,基于 Web 的方案,又可以细分为纯 H5 和 DSL 转 H5(比如 Taro)。


graph TB;
大前端 --> Native;
Native --> Android;
Native --> iOS;
Native --> 纯血鸿蒙;
大前端 --> 跨平台;
跨平台 --> 基于Web的方案;
跨平台 --> 基于系统UI框架的方案;
跨平台 --> 自己绘制UI的方案;
基于Web的方案 --> H5;
基于Web的方案 --> DSL转H5;

我们需要排除 H5 外的其他方案。


原因一:百亿补贴迭代频繁


百亿补贴的业务形式,是一个常住 H5,搭配上多个流动 H5。(「常住」和「流动」是我借鉴「常住人口」和「流动人口」造的词)



  • 常住 H5 链接保持不变,方便用户二次访问。

  • 流动 H5 链接位于常住 H5 的不同位置,方便分发用户流量。


具体到拼多多,它至少有 3 个流量的分发点,可点击的头图、列表上方的活动模块和侧边栏,3 者可以投放不同链接。下图分别投放了 3.8 女神节链接、新人链接和品牌链接:


pdd-activity.png


可以想到,几乎每到一个节日、每换一个品牌,「百亿补贴」就需要更新一次。


这样频繁的迭代,框架必须满足快速开发、快速部署、一次开发多端复用条件。因此可以排除掉 Native 技术栈,留下动态化技术栈。


原因二:百亿补贴需要投放小程序和其他 App


如图所示,你可以在微信上搜索拼多多,可以看到百亿补贴不仅在 App 上投放,还在微信小程序里投放。


pdd-wx.png


此时我们几乎可以排除掉 React Native 和 Flutter 技术栈。因为社区虽然有方案让 React Native、Flutter 适配小程序,但并不成熟,不适合用到生产项目中。


此外,如果你在抖音、B 站和小红书搜索百亿补贴,你可以看到百亿补贴在这些 App 上都有投放广告。


pdd-advertisement.png


这点可以完全排除 React Native 和 Flutter 技术栈。据我所知,目前没有主流 App,会愿意让第三方在自己的 App 里运行 React Native 和 Flutter。


原因三:百亿补贴核心流量在 APP


现在只剩下了基于 Web 的 2 种技术方案,也就是 H5 和 DSL 转出来的 H5(比如 Taro)。


百亿补贴的 HTML 结果,更符合原生 H5 的组织结构,而不是 Taro 这种 DSL 转出来的结构。


我对此的解释是,百亿补贴的核心流量在 App。核心流量在 APP 时。投放小程序是锦上添花,把 H5 嵌入到小程序 Webview 就能满足要求,不需要卷性能。


如果百亿补贴的核心流量在小程序,那么大概率就会使用 DSL 框架,转出来小程序代码和 H5 代码。


综上所述,迭代频繁、需要投放小程序和其他 App,核心流量在 App,是百亿补贴选择 H5 的 3 个主要原因。


H5 未来会如何发展


知道百亿补贴选择 H5 的 3 个原因后,我们可以得到结论,如果 3 个前提不变,未来很长一段时间内,H5 依然是电商活动的主流方案。


不过,主流方案并不意味着一成不变,我认为未来 H5 会有 2 个发展趋势:


趋势一:离线包、SSR 比例增加


H5 有诸多优势的同时,也有着先天缺陷,那就是下载成功率低、容易白屏。


解决这个问题,社区主流的两个方案是离线包和 SSR。


离线包可以将网页的静态资源(如 HTML、CSS、JS、图片等)缓存到本地,用户访问 H5 页面时,可以直接读取本地的离线资源,从而提升页面的加载速度。阿里云腾讯云等云服务商都有自己的离线包方案。


SSR 即服务器端渲染,它可以减少白屏时间,让用户更快看到页面。传统的 CSR(客户端渲染)初始时只渲染空白的 HTML 框架,然后再去获取数据并渲染内容。而在 SSR 中,服务器在接收到客户端请求时,会在服务器端利用数据和模板生成完整的 HTML 页面,再把页面发送给客户端浏览器。


不难想到,业务陷入瓶颈后,企业开始看中性能,大部分前端开发者都会来卷一卷离线包、 SSR,它们的比例会进一步增加。


趋势二:定制化要求苛刻


近年 C 端市场增长缓慢,企业重点从扩张新客,变成留存老客。


这个背景下,定制化要求变得越来越苛刻,目的是让用户区分各种活动。用互联网黑话来说,就是「建立用户心智」。


下面是拼多多、京东、淘宝、12306、中国移动和招商银行的活动 H5,尽管它们结构都差不多,但长得是千奇百怪。


fluid.png


12306-yidong-zhaoshang.png


我估计未来,电商活动 H5 的外观将变得极具个性,各位前端同学可以在卷卷 CSS、动效方向。


总结


本文介绍了我认为「百亿补贴」会选用 H5 的 3 大原因:



  • 百亿补贴迭代频繁

  • 百亿补贴需要投放小程序、其他 App

  • 百亿补贴核心流量是自己的 App


以及我 H5 未来发展趋势的 2 个预测:



  • 离线包、SSR 比例增加

  • 定制化要求苛刻


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


作者:小霖家的混江龙
来源:juejin.cn/post/7344325496983732250
收起阅读 »

pnpm 的崛起:如何降维打击 npm 和 yarn🫡

web
今天研究了一下 pnpm 的机制,发现它确实很强大,甚至可以说对 yarn 和 npm 形成了降维打击 我们从包管理工具的发展历史,一起看下到底好在哪里? npm2 在 npm 3.0 版本之前,项目的 node_modules 会呈现出嵌套结构,也就是说,我...
继续阅读 »

今天研究了一下 pnpm 的机制,发现它确实很强大,甚至可以说对 yarnnpm 形成了降维打击


我们从包管理工具的发展历史,一起看下到底好在哪里?


npm2


在 npm 3.0 版本之前,项目的 node_modules 会呈现出嵌套结构,也就是说,我安装的依赖、依赖的依赖、依赖的依赖的依赖...,都是递归嵌套的


node_modules
├─ express
│ ├─ index.js
│ ├─ package.json
│ └─ node_modules
│ ├─ accepts
│ │ ├─ index.js
│ │ ├─ package.json
│ │ └─ node_modules
│ │ ├─ mime-types
| | | └─ node_modules
| | | └─ mime-db
| │ └─ negotiator
│ ├─ array-flatten
│ ├─ ...
│ └─ ...
└─ A
├─ index.js
├─ package.json
└─ node_modules
└─ accepts
├─ index.js
├─ package.json
└─ node_modules
├─ mime-types
| └─ node_modules
| └─ mime-db
└─ negotiator

设计缺陷


这种嵌套依赖树的设计确实存在几个严重的问题



  1. 路径过长问题: 由于包的嵌套结构 , node_modules 的目录结构可能会变得非常深,甚至可能会超出系统路径长度上限 ,毕竟 windows 系统的文件路径默认最多支持 256 个字符

  2. 磁盘空间浪费: 多个包之间难免会有公共的依赖,公共依赖会被多次安装在不同的包目录下,导致磁盘空间被大量浪费 。比如上面 express 和 A 都依赖了 accepts,它就被安装了两次

  3. 安装速度慢:由于依赖包之间的嵌套结构,npm 在安装包时需要多次处理和下载相同的包,导致安装速度变慢,尤其是在依赖关系复杂的项目中


当时 npm 还没解决这些问题, 社区便推出了新的解决方案 ,就是 yarn。 它引入了一种新的依赖管理方式——扁平化依赖。


看到 yarn 的成功,npm 在 3.0 版本中也引入了类似的扁平化依赖结构


yarn


yarn 的主要改进之一就是通过扁平化依赖结构来解决嵌套依赖树的问题


具体来说铺平,yarn 尽量将所有依赖包安装在项目的顶层 node_modules 目录下,而不是嵌套在各自的 node_modules 目录中。


这样一来,减少了目录的深度,避免了路径过长的问题 ,也尽可能避免了依赖被多次重复安装的问题


|350


我们可以在 yarn-example 看到整个目录,全部铺平在了顶层 node_modules 目录下,展开下面的包大部分是没有二层 node_modules


然而,有些依赖包还是会在自己的目录下有一个 node_modules 文件夹,出现嵌套的情况,例如 yarn-example 下的http-errors 依赖包就有自己的 node_modules,原因是:


当一个项目的多个依赖包需要同一个库的不同版本时,yarn 只能将一个版本的库提升到顶层 node_modules 目录中。 对于需要这个库其他版本的依赖,yarn 仍然需要在这些依赖包的目录下创建一个嵌套的 node_modules 来存放不同版本的包


比如,包 A 依赖于 lodash@4.0.0,而包 B 依赖于 lodash@3.0.0。由于这两个版本的 lodash 不能合并,yarn 会将 lodash@4.0.0 提升到顶层 node_modules,而 lodash@3.0.0 则被嵌套在包 B 的 node_modules 目录下。


幽灵依赖


虽然 yarn 和 npm 都采用了扁平化的方案来解决依赖嵌套的问题,但这种方案本身也有一些缺陷,其中幽灵依赖是一个主要问题。


幽灵依赖,也就是你明明没有在 package.json 文件中声明的依赖项,但在项目代码里却可以 require 进来
这个也很容易理解,因为依赖的依赖被扁平化安装在顶层 node_modules 中,所以我们能访问到依赖的依赖


但是这样是有隐患的,因为没有显式依赖,未来某个时候这些包可能会因为某些原因消失(例如新版本库不再引用这个包了,然后我们更新了库),就会引发代码运行错误


浪费磁盘空间


而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题


那社区有没有解决这俩问题的思路呢? pnpm 就是其中最成功的一个


pnpm


pnpm 通过全局存储和符号链接机制从根源上解决了依赖重复安装和路径长度问题,同时也避免了扁平化依赖结构带来的幽灵依赖问题
pnpm 的优势概括来说就是“快、准、狠”:



  • 快:安装速度快

  • 准:安装过的依赖会准确复用缓存,甚至包版本升级带来的变化都只 diff,绝不浪费一点空间

  • 狠:直接废掉了幽灵依赖


执行 npm add express,我们可以在 pnpm-example 看到整个目录,由于只安装了 express,那 node_modules 下就只有 express


|400


那么所有的(次级)依赖去哪了呢? binggo,在node_modules/.pnpm/目录下,.pnpm/ 以平铺的形式储存着所有的包


|400


三层寻址



  1. 所有 npm 包都安装在全局目录 ~/.pnpm-store/v3/files 下,同一版本的包仅存储一份内容,甚至不同版本的包也仅存储 diff 内容。

  2. 顶层 node_modules 下有 .pnpm 目录以打平结构管理每个版本包的源码内容,以硬链接方式指向 pnpm-store 中的文件地址。

  3. 每个项目 node_modules 下安装的包以软链接方式将内容指向 node_modules/.pnpm 中的包。
    所以每个包的寻找都要经过三层结构:node_modules/package-a > 软链接 node_modules/.pnpm/package-a@1.0.0/node_modules/package-a > 硬链接 ~/.pnpm-store/v3/files/00/xxxxxx


    这就是 pnpm 的实现原理。官方给了一张原理图,可以搭配食用


    |600


    前面说过,npm 包都被安装在全局 pnpm store ,默认情况下,会创建多个存储(每个驱动器(盘符)一个),并在项目所在盘符的根目录


    所以,同一个盘符下的不同项目,都可以共用同一个全局 pnpm store,绝绝子啊👏,大大节省了磁盘空间,提高了安装速度


    |600



软硬链接


也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。


那么,这里的软连接、硬链接到底是什么东西?


硬链接是指向磁盘上原始文件所在的同一位置 (直接指向相同的数据块)


软连接可以理解为新建一个文件,它包含一个指向另一个文件或目录的路径 (指向目标路径)



总结


npm2 的嵌套结构: 每个依赖项都会有自己的 node_modules 目录,导致了依赖被重复安装,严重浪费了磁盘空间💣;在依赖层级比较深的项目中,甚至会超出 windows 系统的文件路径长度💣


npm3+ 和 Yarn 的扁平化策略: 尽量将所有依赖包安装在项目的顶层 node_modules 目录下,解决了 npm2 嵌套依赖的问题。但是该方案有一个重大缺陷就是“幽灵依赖”💣;而且依赖包有多个版本时,只会提升一个,那其余版本依然会被重复安装,还是有浪费磁盘空间的问题💣


pnpm全局存储和符号链接机制: 结合软硬链和三层寻址,解决了依赖被重复安装的问题,更加变态的是,同一盘符下的不同项目都可以共用一个全局 pnpm store。节省了磁盘空间,并且根本不存在“幽灵依赖”,安装速度还贼快💪💪💪


作者:柏成
来源:juejin.cn/post/7410923898647461938
收起阅读 »

微信公众平台:天下之事合久必分分久必合

web
微信公众平台的故事,还得从 “再小的个体,也有自己的品牌” 的 Slogan 开始说起。 时间线开始 2012年8月17日 这一天,微信公众平台正式向普通用户开放,也就从这一天开始,普通人可以在微信上注册属于自己个人或组织的公众号。 2012年8月23日 这...
继续阅读 »

QQ_1734967828741.png


微信公众平台的故事,还得从 “再小的个体,也有自己的品牌” 的 Slogan 开始说起。


QQ_1734972139162.png


时间线开始


2012年8月17日


这一天,微信公众平台正式向普通用户开放,也就从这一天开始,普通人可以在微信上注册属于自己个人或组织的公众号。


2012年8月23日


这一天,微信公众平台正式上线。各大博主、媒体纷纷注册加入了这个平台,开始在微信公众平台上创作,建立自己的读者圈子,打造自己的IP。


2012年11月29日


从这天起,微信图文群发系统升级发布,图文并茂的文章可以通过微信公众平台发送给关注的粉丝了。


这时候,很多企业嗅到了春天来临的味道,招聘互联网编辑的岗位越来越多。


2013年2月6日


这一天,微信的公众号支持开发者模式了,开发者们的春天(噩梦)开始了。


很多公众号开始提供更多的功能了,比如微信公众平台文档里十年没变的那些什么话费查询、机票航班查询等:



于是公众平台开始对外提供了 “火警请按119,急救请按120” 的鸡肋开发能力——关键词回复、关注消息等。



虽然十多年过去了,我依然碰到了很多人不太理解微信公众号的这玩意的交互流程……


2013年3月19日


2013年3月20日,公众平台灰度了“自定义菜单”,当然,还只是内测。


此时的微信公众号,除了可以推送消息之外,也支持在后台编辑公众号菜单,指定菜单可以回复不同的内容或者打开一个 URL。


2013年8月5日


这天,微信发布了 v5.0 大版本,同时也带来了很多好玩的东西。


为了区分平台内公众号的各种主体,微信公众号在这一天分了家:订阅号 + 服务号。


区别在哪呢?



嗯,内测的自定义菜单给服务号开放了。但是阉割了服务号群发的频率:每月4条。



同时,对可申请的主体也做了限制:



个人只能申请订阅号了。组织类不限制。



然后当年很糟心,但现在很开心的事情发生了:订阅号从消息列表折叠到了 “订阅号” 栏目里。


好,很直接。



不过直至今日,服务号依然还可以在消息列表中直接显示。



2013年10月29日


这天,微信公众平台推出了认证的功能,认证之后有一些特权:



  • 语音识别、客服接口、获取用户地理位置、获取用户基本信息、获取关注列表和用户分组接口的权限

  • 仅认证服务号支持的 OAuth2.0网页授权、生成带参数二维码 等

  • 认证了可以送你一个



此时,微信公众号支持通过 腾讯微博新浪微博 等第三方平台的认证来同步认证服务号。此时不管是个人还是组织的号,都可以认证。但是还没有充值即认证的功能。



我猜是运营开始往赚钱上靠了,毕竟 不充钱的腾讯产品不是好产品。


2013年12月24日


说时迟那是快,这不就来了。


从今天起,你可以花 300 块钱来认证你的号了,前提是,你得是 组织 号,个人的不支持。(当然,部分类型的主体认证是不收费的,比如 政务 媒体 等)


2014年3月


今天,微信公众平台支持接入微信支付了。不过,无论你是订阅号还是服务号,都需要通过企业认证之后,再申请开通微信支付。


这一年,开发者们忙起来了。


创建订单、创建支付请求参数、签名、回调处理、支付结果查询 等等事情接踵而至。


微信开发者的圈子和生态慢慢的繁荣了起来。


2014年9月18日



哎,到哪都逃不掉 ToBCURD 业务。



随着微信开发者生态的繁荣,微信意识到了很多开发者在微信的服务号上做 ToB 的业务,要不要独立一个出来呢?


那就叫 企业号 吧,于是微信公众号的第三个兄弟也来了。


在2014年-2017年这段时间,有一个网站很火,叫 很*微信开发者社区(weixin.com)请记住这个名字,一会要考。


2016年1月11日


2016微信公开课PRO版在广州举行,那个男人(张小龙,微信之父) 首次公开演讲。


这天,张小龙说,微信要做 应用号,要让用户 用完即走


2016年5月


这段时间,上面的社区使用的 weixin.com 最终被南山必胜客拿下。手动狗头:)


2016年9月22日


微信开始内测 小程序。又一次噩梦开始了。


2016年11月3日


微信开始公测 小程序


2017年1月9日


微信小程序 正式上线。



小应用?应用号?



2017年6月29日


随着企业号的发展,微信意识到这与微信的个人社交出现了很多的冲突,于是,微信在2017年6月29日,抽离出了企业微信,牛马们开始使用这个工具来为老板创收了。


2017年12月28日


微信小游戏上线,大家一起来 打飞机


2020年1月22日


微信视频号开始内测,本文讲的微信公众平台系列故事本以为到此会结束了。然而:


2024年11月


这个月,微信把 订阅号 改名为 公众号 了。



服务号:那我呢???



我怎么总觉得有大事要发生?


最近


个人可以注册服务号了,而且注册的服务号依然是在消息列表里,还没有被折叠。



企业:??? 当年费劲巴力注册了服务号,一个月还只有四条,我的特权呢???



当然,目前注册的服务号都是没有认证的,我试了试,目前个人主体的服务号不支持认证。也就是所有的高级开发接口权限一个都没有。


我还是觉得要有大事发生了。



完全没看懂微信公众平台这个骚操作。



总结


今天简单聊了聊微信公众平台的一些小故事,如有错误,欢迎评论区指正和讨论。


只是作为曾经风风火火的微信公众平台开发者,心里感慨颇多。


Bye.


作者:Hamm
来源:juejin.cn/post/7451561994799890483
收起阅读 »

手把手教你做个煎饼小程序,摊子开起来

web
前言 周饼伦在街头摊煎饼,摊后人群熙熙攘攘。他忙得不可开交,既要记住面前小哥要加的培根,又要记住身后奶奶要加的生菜,这时又来了个小妹妹,点的煎饼既要培根又要腊肠。他把鸡蛋打进煎饼后,竟突然忘了前面光头大叔要加的料,是火腿还是鸡柳?一时记不清,好像是火腿,对。然...
继续阅读 »

前言


周饼伦在街头摊煎饼,摊后人群熙熙攘攘。他忙得不可开交,既要记住面前小哥要加的培根,又要记住身后奶奶要加的生菜,这时又来了个小妹妹,点的煎饼既要培根又要腊肠。他把鸡蛋打进煎饼后,竟突然忘了前面光头大叔要加的料,是火腿还是鸡柳?一时记不清,好像是火腿,对。然而当把煎饼交给大叔,大叔却怒了,说要的是鸡柳。😡


这可咋办?周饼伦赶忙道歉,大叔却语重心长地说:“试试用小程序云开发吧!最近的数据模型新功能好用得很!” 周饼伦亮出祖传手艺,边摊煎饼边开发小程序,把新开发的小程序点餐页面二维码贴在摊前。从此再没出过错,终于能安心摊煎饼啦!


设计思路


图片


客户扫摊子上面贴的二维码后,会进入点餐页面,在选好要加的配料之后,点击确定就可以点餐,随后,即可在云后台上看到食客提交的数据


图片


实现过程


周饼伦就把当前摊位的主食、配菜,以及各自相应的价格贴在了摊位上,也要把食客的点餐内容记在脑里或者用笔写在纸上。


点餐页要实现两个功能:1.展示当前摊位有的主食、配菜、口味 2.提交订单到周饼伦的订单页面。


煎饼摊子主食(staple food)目前只有摊饼、青菜饼,主食下面有的配菜(side dish),有鸡柳、生菜、鸡蛋、火腿、腊肠。


同理,数据库里面也需要呈现相应的结构。


数据表的实现


数据模型现在提供了一种便捷的能力来,可以快速创建一套可用的数据表来记录摊煎饼的相关数据。


图片


在云后台中新增了一个基于 MySQL 的数据模型,数据模型相当于一张纸,可以在上面记录任何想要记录的数据,比如周饼伦摊位的提供的菜品


图片


创建了基于云开发MySQL数据库的主食表,主食表中包含主食名称,主食价格


图片


图片


字段的详细设置如下


图片


图片


加了主食、配菜两个表之后,将当前的主食和配菜一起加进数据表中

图片


图片


现在就实现了记录当前摊子的主食和配菜。还需要一个订单表,来记录用户的点餐数据


图片


配菜的类型是一个数组文本,用来记录配菜的类型,结构如下


图片


接着需要分别设置每个数据模型的权限。在使用小程序查看订单时,也是以用户的身份来读取的,所以,需要配置用户权限,通过页面访问来控制用户能够访问到哪些页面


图片


图片


图片


至此,数据表就已经大功告成!现在完全可以使用三个表来记录当前摊子的菜品、营业情况。


但是,别忘了周饼伦的目的不止于此,为了周饼伦实现早日暴富,当上CEO,所以,还要利用小程序实现一个界面,来给”上帝“们点餐,并且提供各位CEO查看订单


小程序实现过程


一. 初始化 SDK


在云后台的数据管理中的右侧中,可以方便的查询到使用的文档


图片


新建一个基于云开发的小程序,删除不必要的页面,并且按照文档的步骤进行初始化👇


1.按照指引在 miniprogram 目录下初始化 npm 环境并安装 npm 包


请注意,这里需要在 miniprogram 目录下初始化 npm ,不然需要编辑 project.config.json 手动指定 npm 包的位置


在 miniprogram 目录下打开终端


图片


2.初始化当前 npm 并且安装 @cloudbase/wx-cloud-client-sdk npm 包


npm init -y & npm install @cloudbase/wx-cloud-client-sdk --save

图片


3.在小程序中构建 npm


图片


4.在小程序 app.js 中初始化环境


// app.js
App({
globalData: {
// 在这里提供全局变量 models 数据模型方法,方便给页面使用
models: null
},
onLaunch: async function () {
const {
init
} = require('@cloudbase/wx-cloud-client-sdk')
// 指定云开发环境 ID
wx.cloud.init({
env: "ju-9g1guvph88886b02",
});
const client = init(wx.cloud);
const models = client.models;
// 可以取消注释查看效果
// const { data } = await models.stapleFood.list({
// filter: {
// where: {}
// },
// pageSize: 10,
// pageNumber: 1,
// getCount: true,
// });
// console.log('当前的主食数据:');
// console.log(data.records);
}
});

二. 下单页面的实现


首先创建一个页面 goods-list 页面作为首页


顾客如果浏览下单页面,那么就需要看到当前可以选择的主食、配菜,还有他们分别的价格。所以首先我们需要把主食、配菜加载进来


// 加载主食
const stapleFood = (await models.stapleFood.list({
filter: {
where: {}
},
pageSize: 100, // 一次性加载完,
pageNumber: 1,
getCount: true,
})).data.records;

// 加载配菜
const sideDish = (await models.sideDish.list({
filter: {
where: {}
},
pageSize: 100, // 一次性加载完,
pageNumber: 1,
getCount: true,
})).data.records;

// pages/goods-list/index.js
Page({
data: {
// 总价格
totalPrize: 0,
// 选中的主食
selectedStapleFoodName: '',
// 选中的配菜
selectedSideDishName: [],
// 所有的主食
stapleFood: [],
// 所有的配菜
sideDish: [],

以下是全部的js代码


// pages/goods-list/index.js
Page({
data: {
// 总价格
totalPrize: 0,
// 选中的主食
selectedStapleFoodName: '',
// 选中的配菜
selectedSideDishName: [],
// 所有的主食
stapleFood: [],
// 所有的配菜
sideDish: [],
},

async onLoad(options) {
const models = getApp().globalData.models;
console.log('models', models)

// 加载主食
const stapleFood = (await models.stapleFood.list({
filter: {
where: {}
},
pageSize: 100, // 一次性加载完,
pageNumber: 1,
getCount: true,
})).data.records;

// 加载配菜
const sideDish = (await models.sideDish.list({
filter: {
where: {}
},
pageSize: 100, // 一次性加载完,
pageNumber: 1,
getCount: true,
})).data.records;

console.log({
stapleFood,
sideDish
});

this.setData({
stapleFood: stapleFood,
sideDish: sideDish
})
},

// 选中主食
onSelectStapleFood(event) {
this.setData({
selectedStapleFoodName: event.currentTarget.dataset.data.name
});

this.computeTotalPrize();
},

// 选中配菜
onSelectedSideDish(event) {
console.log(event);
// 选中配菜名字
const sideDishName = event.currentTarget.dataset.data.name;

// 如果已经选中,则取消选中
if (this.data.selectedSideDishName.includes(sideDishName)) {
this.setData({
selectedSideDishName: this.data.selectedSideDishName.filter((name) => (name !== sideDishName))
});
} else {
// 未选中,则选中
this.setData({
selectedSideDishName: this.data.selectedSideDishName.concat(sideDishName)
});
}

this.computeTotalPrize();
},

// 重新计算价格
computeTotalPrize() {
// 主食价格
let staplePrize = 0;
if (this.data.selectedStapleFoodName) {
staplePrize = this.data.stapleFood.find((staple) => staple.name === this.data.selectedStapleFoodName).prize;
}

// 配菜价格
let sideDish = 0;
this.data.selectedSideDishName.forEach((sideDishName) => {
sideDish += this.data.sideDish.find((sideDishItem) => (
sideDishItem.name === sideDishName
)).prize;
});

// 总价格
this.setData({
totalPrize: staplePrize + sideDish
})
},

// 提交
async onSubmit() {
// 提示正在加载中
wx.showLoading({
title: '正在提交订单',
});

const models = getApp().globalData.models;
const { data } = await models.order.create({
data: {
served: false, // 是否已出餐
sideDish: this.data.selectedSideDishName, // 配菜
stapleFoodName: this.data.selectedStapleFoodName, // 主食名称
prize: this.data.totalPrize, // 订单总价格
}
});

console.log(data);
wx.hideLoading();
}
});

接着来实现页面


<!--pages/goods-list/index.wxml-->
<view>
<view class="title">
<image src='/asset/pancake.png'></image>
<text class="title">请选择主食</text>
</view>

<!-- 主食展示 -->
<view class="staple-food">
<view wx:for="{{stapleFood}}" wx:key="_id">
<view bindtap="onSelectStapleFood" data-data="{{item}}" class="staple-food-item {{selectedStapleFoodName === item.name ? 'selected' : ''}}">
<image src="{{item.imageUrl}}"></image>
<view class="prize">{{item.prize}}¥</view>
</view>
</view>
</view>

<!-- 选择配菜 -->
<view class="title">
<image src='/asset/sideDish.png'></image>
请选择配菜
</view>

<!-- 配菜展示 -->
<view class="side-dish">
<view wx:for="{{sideDish}}" wx:key="_id">
<!-- 使得class动态绑定支持 includes 语法 -->
<wxs module="tool">
var includes = function (array, text) {
return array.indexOf(text) !== -1
}
module.exports.includes = includes;
</wxs>
<view class="side-dish-item {{tool.includes(selectedSideDishName, item.name) ? 'selected' : ''}}" bindtap="onSelectedSideDish" data-data="{{item}}">
<image src="{{item.imageUrl}}"></image>
<view class="prize">{{item.prize}}¥</view>
</view>
</view>
</view>

<!-- 底部菜单 -->
<view class="bottom-content">
<view class='bottom-info'>
<view wx:if="{{!!selectedStapleFoodName}}">主食:{{selectedStapleFoodName}}</view>
<view wx:if="{{selectedSideDishName.length !== 0}}">配菜:{{selectedSideDishName}}</view>
</view>

<view class="bottom-operate">
<view class="total-prize">当前价格<text class="prize">{{totalPrize}}¥</text></view>
<view class="submit-button {{!selectedStapleFoodName ? 'disabled' : ''}}" bind:tap="onSubmit">下单</view>
</view>
</view>
</view>

再添加一点点的样式


/* pages/goods-list/index.wxss */
.title {
display: flex;
align-items: center;
gap: 16rpx;
padding: 0 20rpx;
}

.title image {
height: 46rpx;
width: 46rpx;
}

.staple-food {
display: flex;
margin-bottom: 60rpx;
overflow: auto;
}

.staple-food-item {
margin: 20rpx 10rpx;
display: flex;
flex-direction: column;
border: 1px solid #f3f0ee;
box-shadow: 6rpx 6rpx 6rpx #dfdfdf, -6rpx -6rpx 6rpx #dfdfdf;
border-radius: 6rpx;
padding: 8rpx;
}

.staple-food-item.selected, .side-dish-item.selected {
box-shadow: 6rpx 6rpx 6rpx #58b566, -6rpx -6rpx 6rpx #58b566, 6rpx -6rpx 6rpx #58b566, -6rpx 6rpx 6rpx #58b566;
}

.staple-food-item image {
border-radius: 6rpx;
width: 300rpx;
height: 300rpx;
}

.prize {
padding: 6rpx 6rpx 0;
text-align: right;
color: orangered
}

.side-dish {
padding: 20rpx 12rpx;
display: flex;
gap: 12rpx;
overflow: auto;
}

.side-dish image {
height: 200rpx;
width: 200rpx;
}

.side-dish-item {
border-radius: 8px;
padding: 16rpx;
box-shadow: 6rpx 6rpx 6rpx #dfdfdf, -6rpx -6rpx 6rpx #dfdfdf;
}

.bottom-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
}

.bottom-info {
padding: 30rpx;
display: flex;
flex-direction: column;
color: grey;
font-size: 0.5em;
}

.bottom-content .total-prize {
padding: 0 30rpx;
}

.bottom-operate {
border-top: 1px solid #dfdfdf;
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
height: 100rpx;
}

.submit-button {
width: 350rpx;
color: white;
background: #22b85c;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

.submit-button.disabled {
background: grey;
/* 注意,这里设置了当按钮置灰的时候,不可点击 */
pointer-events: none;
}

于是,煎饼摊的小程序就大功告成了!


接着就可以在云后台管理订单了,在将订单完成之后,即可在云后台将订单的状态修改成已完成。


图片


我们还可以做的更多…


是否可以在订单中新增一个点餐号,这样就知道是哪个顾客点的餐?是否可以使用数据模型的关联关系将配菜、主食和订单关联起来?


是否可以在小程序中创建一个管理订单的页面?是否可以添加优惠券数据表,来给客户一些限时优惠?


期待大家的体验反馈!


代码地址:github.com/vancece/qiL…


点击体验:tcb.cloud.tencent.com/cloud-admin…


作者:腾讯云云开发
来源:juejin.cn/post/7413376270518042651
收起阅读 »

代码与蓝湖ui颜色值一致!但页面效果出现色差问题?

web
前言 最近在开发新需求,按照蓝湖的ui图进行开发,但是在开发完部署后发现做出来的页面部分元素的颜色和设计图有出入,有色差!经过一步步的排查最终破案,解决。仅以此篇记录自己踩坑、学习的过程,也希望可以帮助到其他同学。 发现问题 事情是这样的,那是一个愉快的周五的...
继续阅读 »

前言


最近在开发新需求,按照蓝湖的ui图进行开发,但是在开发完部署后发现做出来的页面部分元素的颜色和设计图有出入,有色差!经过一步步的排查最终破案,解决。仅以此篇记录自己踩坑、学习的过程,也希望可以帮助到其他同学。


发现问题


事情是这样的,那是一个愉快的周五的下午,和往常一样我开心的提交了代码后进行打包发版,然后通知负责人查看我的工作成果。


但是,过了不久后,负责人找到了我,说我做出来的效果和ui有点出入,有的颜色有点不一样。我一脸懵逼,心想怎么可能呢,我是根据ui图来的,ui的颜色可是手把手从蓝湖复制到代码中的啊。


随后他就把页面和ui的对比效果图发了出来:


image.png


上图中左侧是蓝湖ui图,右侧是页面效果图。我定睛一看,哇趣!!!好像是有点不一样啊。 感觉右侧的比左侧的更亮一些。于是我赶紧本地查看我的页面和ui,果然也是同样问题! 开发时真的没注意,没发现这个问题!!!


排查问题


于是,我迅速开始进行问题排查,看看到底是什么问题,是值写错了?还是那里的问题。


ui、页面、代码对比


下图中:最上面部分是蓝湖ui图、下面左侧是我的页面、右侧是我的页面代码样式


image.png


仔细检查后发现颜色的值没错啊,我的代码中背景颜色、边框颜色的值都和ui的颜色值是一致的! 但这是什么问题呢??? 值都一样为什么渲染到页面会出现色差?


起初,我想到的是屏幕的问题,因为不同分辨率下展示出来的页面效果是会有差距的。但是经过查看发现同事的win10笔记本、我的mac笔记本、外接显示器上都存在颜色有色差这个问题!!!


ui、页面、源文件对比


通过对比ui、页面、颜色值,不同设备展示效果可以初步确认:和显示器关系不大。当我在百思不解的时候,我突然想到了ui设计师!ui提供的ui图是蓝湖上切出来的,那么她的源文件颜色是什么呢?


于是我火急火燎的联系到了公司ui小姐姐,让她发我源文件该元素的颜色值,结果值确实是一样的,但是!!! 源文件展示出来的效果好像和蓝湖上的不太一样!


然后我进行了对比(左侧蓝湖、右上页面、右下源文件):


image.png


可以看到源文件和我页面的效果基本一致!到这一步基本可以确定我的代码是没问题的!


尝试解决


首先去网上找了半天没有找到想要的答案,于是我灵光一现,想到了蓝湖客服!然后就询问了客服,为什么上传后的ui图内容和源文件有色差?


image.png


image.png


沟通了很久,期间我又和ui小姐姐在询问她的软件版本、电脑版本、源文件效果、设置等内容就不贴了,最终得到如下解答:


image.png


解决方式


下载最新版蓝湖插件,由于我们的ui小姐姐用的 sketch 切图工具,然后操作如下:


1.下载安装最新版蓝湖插件: lanhuapp.com/mac?formHea…


2.安装新版插件后--插件重置


3.后台程序退出 sketch,重新启动再次尝试打开蓝湖插件.


4.插件设置打开高清导出上传(重要!)


5.重新切图上传蓝湖


最终效果


左侧ui源文件、右侧蓝湖ui:
image.png


页面效果:


image.png


可以看到我的页面元素的border好像比ui粗一些,感觉设置0.5px就可以了,字体效果的话是因为我还没来得及下载ui对应的字体文件。


但是走到这一步发现整体效果已经和ui图到达了95%以上相似了,不至于和开始有那么明显的色差。


总结


至此,问题已经基本是解决。遇到问题不能怕,多想一想,然后有思路后就一步一步排查、尝试解决问题。当解决完问题后会发现心情舒畅!整个人都好起来了,也会增加自信心!


作者:尖椒土豆sss
来源:juejin.cn/post/7410712345226035200
收起阅读 »

基于Vue.js和高德地图API来实现一个简易的天气预报

web
今天就让我们来使用 Vue.js 和高德地图开放平台提供的 API,实现一个关于天气预报的小demo,实时查询当前城市的天气以及未来三天的天气预测,且实现切换城市查询。实现效果如下; 准备工作 既然要使用真实的数据,那么就需要用到高德地图开放平台提供的天气查...
继续阅读 »

今天就让我们来使用 Vue.js 和高德地图开放平台提供的 API,实现一个关于天气预报的小demo,实时查询当前城市的天气以及未来三天的天气预测,且实现切换城市查询。实现效果如下;


PixPin_2024-12-15_00-13-38.gif


准备工作


既然要使用真实的数据,那么就需要用到高德地图开放平台提供的天气查询 API,先高德地图api注册为开发者。然后点击文档与支持,选择JS API。


image.png


然后登录到控制台创建一个应用并且添加一个key,服务平台为Web端(JS API)。
16b5ba85e6c5f128b699fe8d521bb67.jpg


终端npm create vite@latest使用vite创建项目,npm install下载该项目需要用的包,npm run dev运行项目。


image.png


将天气预报的功能全部开发在weather.vue里面,再将这个组件import weather from "./components/weather.vue"引入到app.vue中。


image.png


js代码概览


image.png


具体代码步骤实现


开始weather.vue里面的代码了。


html 部分


<div>
// 头部
<div class="head">
<div class="city-name">
<i class="iconfont icon-dingwei"></i>
{{ state.city }}
</div>
<div @click="toggle" class="city-change">
<i class="iconfont icon-24gf-city3"></i>
切换城市
</div>
</div>


// 中间部分实时温度
<div class="main">
<div class="weather-info">
<p class="temp">{{ state.weather.temperature }}℃</p>
<div class="info">{{ state.weather.weather }}</div>
<div class="detail">
<div class="item">
<i class="iconfont icon-shuidi"></i>
<span>湿度</span>
<span>{{ state.weather.humidity }}</span>
</div>
<div class="item">
<i class="iconfont icon-feng"></i>
<span>风向</span>
<span>{{ state.weather.windDirection }}</span>
</div>
<div class="item">
<i class="iconfont icon-fengli"></i>
<span>风力</span>
<span>{{ state.weather.windPower }}</span>
</div>
</div>
</div>

// 未来三日的天气预报
<div class="future">
<div class="future-title">三日天气预报</div>
<div class="future-content">
<div v-for="(item,i) in state.future" class="forecast">
<p class="week">周{{ chinese[Number(item.week)-1] }}</p>
<i :class="getWeatherIcon(item.dayWeather)"></i>
<p><span class="left">{{ item.dayTemp }}℃</span> <span class="right"> / {{ item.nightTemp }}℃</span></p>
</div>
</div>
</div>
</div>


// 切换城市input框
<div v-show="state.isVisible" >
<input id="newCity" @keydown.enter="handle" type="text" v-model="state.newCity" placeholder="请输入你要查询的城市">
</div>
</div>


然后使用css样式美化成如下界面


image.png


js部分


接下来就是渲染其中的数据了,首先使用高德 api 来获取定位数据,查看官方文档,JS API结合 Vue 使用,首先安装Loader,如下所示,复制到当前文件终端安装。
image.png


然后复制代码粘贴;
image.png


AMapLoader 是高德地图 js API 的加载器,它可以在前端项目中加载和初始化高德地图的 js API。


import AMapLoader from '@amap/amap-jsapi-loader';
import { onMounted, reactive } from 'vue'

onMounted(() => {   // 在浏览器上出现内容时候触发
// 加载官方提供的方法
window._AMapSecurityConfig = {
securityJsCode: "", // 密钥
};
AMapLoader.load({
key: "", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: ["AMap.Scale"], //需要使用的的插件列表,如比例尺'AMap.Scale',支持添加多个如:['...','...']
})

// 加载完上面代码高德提供的服务后,执行then后面的操作
.then((AMap) => {
// 获取定位
getLocalCity(AMap) // 使用一个函数,将获取地址信息放到这个函数中
})
})

获取城市信息


官方文档:
image.png


const getLocalCity = (AMap) => {
AMap.plugin('AMap.CitySearch', function () {
var citySearch = new AMap.CitySearch()
citySearch.getLocalCity(function (status, result) {
if (status === 'complete' && result.info === 'OK') {
// 查询成功,result即为当前所在城市信息
console.log(result.city); // 会打印当前城市
state.city = result.city //将城市改为定位获取到的城市
getWeather(AMap) // 获取天气
}
})
})
}

image.png


利用该地址获取实时天气数据
image.png


const getWeather = (AMap) => {
//加载天气查询插件
AMap.plugin("AMap.Weather", function () {
//创建天气查询实例
var weather = new AMap.Weather();
//执行实时天气信息查询
weather.getLive(state.city, function (err, data) { // 将城市替换成state.city
console.log(err, data); // 获取天气数据,详情见下表
state.weather = data // 将数据赋值给 state.weather
getForecast(AMap) // 后面用来获取未来三天的天气
});
});
}

image.png
将这一整个对象赋值给state.weather然后再state.weather.渲染到页面上。


获取未来三天天气


const getForecast = (AMap) => {
AMap.plugin("AMap.Weather", function () {
//创建天气查询实例
var weather = new AMap.Weather();
//执行实时天气信息查询
weather.getForecast(state.city, function (err, data) {
console.log(err, data);
state.future = data.forecasts // 获取天气预报数据

//err 正确时返回 null
//data 返回天气预报数据,返回数据见下表
});
});
}

image.png


最后就是切换城市中的input框的实现;


<div v-show="state.isVisible" >
<input id="newCity" @keydown.enter="handle" type="text" v-model="state.newCity" placeholder="请输入你要查询的城市">
</div>

添加以一个v-show方法,然后绑定一个键盘敲击事件触发handle,并用v-model获取输入的数据并将其存储到state.newCity


const handle = () => {
state.isVisible =!state.isVisible // 回车键将框不显示
state.city = state.newCity // 城市变为输入的城市
getWeather(AMap) // 重新获取该城市天气以及该城市未来天气
}

const toggle = () => {
state.isVisible =!state.isVisible // 使得点击切换城市框会显示和消失
}

以上就是实现获取定位城市,该城市的实时天气,以及未来三天的天气预测,切换查询其它城市的功能具体代码了。


总结


以上使用了Vue.js 组件化的方式来构建界面,利用高德地图 API 获取定位和天气数据,利用 Vue 的响应式机制来实时更新页面数据,通过使用官方文档中 AMapLoader 加载高德地图的JS API,使得我们能高效处理地图相关功能,希望这个小 demo 能够对你的前端开发有所帮助,同时记得给文章点点赞哦🤗。


image.png


作者:六个点
来源:juejin.cn/post/7448246468471521307
收起阅读 »

一个网页打造自己的天气预报

web
概念解释 通过数据接口,简化功能开发。天气数据从哪里来?如果是自己采集,不光要写后端代码,最难的是维护啊,所以必须《天气预报》此类APP特别适合 前后端分离的,以下用一个简单的例子快速打通前后端的调用 前端使用HTML页面,通过JS的Fetch发起请求,向天气...
继续阅读 »

b64dacfad036df7512a0dbcd6a7ceb12.png


概念解释


通过数据接口,简化功能开发。天气数据从哪里来?如果是自己采集,不光要写后端代码,最难的是维护啊,所以必须《天气预报》此类APP特别适合 前后端分离的,以下用一个简单的例子快速打通前后端的调用

前端使用HTML页面,通过JS的Fetch发起请求,向天气API拿到JSON数据回显到页面上。

比较麻烦的是找到免费易用天气API接口。


前后端分离


前端负责用户界面展示和交互体验,

后端负责业务逻辑和数据处理。

这里后端直接使用免费的天气API,所以后端可以视为云服务。图上的左半部分。



  1. 前端:HTML+JS+Fetch请求

  2. 后端:云服务API(天气数据接口网站)

  3. 数据:JSON格式传输


数据接口


简化理解为一个返回JSON数据的网页。


项目《天气预报》


一、后端 云服务API(天气数据接口网站)


1. 注册激活帐号(目标得到APPID和APPSecret即可)


找到免费方便的天气API数据接口网页,这里使用 http://www.yiketianqi.com/
(非广告 只是顺手找到,如果有更方便的欢迎评论区留言),每天有1000次免费调用

注册记下自己 APPIDAPPSecret ,前端请求时要用
图片.png


2. 数据接口文档


一定要注册帐号,才能看到自己的 APPIDAPPSecret
文档中 http://www.yiketianqi.com/index/doc
直接复制下图(1) 就是前端用到的 目标数据接口


图片.png


3.测试天气数据API


以下URL供 前端请求时替换成自己的 APPIDAPPSecret



gfeljm.tianqiapi.com/api?unescap…



使用浏览器打开即可,可以通过浏览器观察,其实前端有这个URL就开业啦
图片.png


二、前端 HTML+JS+Fetch请求


1. 基础fetch请求页面


使用fetch方法发起请求,特别注意每一步返回的数据是否为Promise,需要使用async和await消除回调


const appid = 68621484
const appsecret = `XXXXX`
let URL = `http://gfeljm.tianqiapi.com/api?unescape=1&version=v63&appid=${appid}&appsecret=${appsecret}`

获取(URL)

async function 获取(URL){
let 响应 = await fetch(URL)
let 数据 = await 响应.json()

return 数据
}

注意这个页面通过浏览器 查看网络请求XHR


图片.png


2. 完整静态HTML页面


制作一个简易的HTML页面,显示出关键数据。更多数据需要参考接口文档。


图片.png


使用Fetch发起请求,获得数据后,使用innerHTML属性替换掉元素内容。

同时使用模版字符串,没有使用任何CSS样式。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>fetch请求</title>
</head>
<body>

<div id="A">
</div>

<script>

const appid = 68621484
const appsecret = `fZnW1ikK`
let URL = `http://gfeljm.tianqiapi.com/api?unescape=1&version=v63&appid=${appid}&appsecret=${appsecret}`


main()

async function main(){
let data = await 获取(URL)


const listItems = data.hours.map(hour => `
<li>
时间:${hour.hours}<br>
天气状况:${hour.wea}<br>
天气图标:<img src="images/weather_icons/${hour.wea_img}.png" alt="${hour.wea}"><br>
温度:${hour.tem}°C<br>
风向:${hour.win}<br>
风速:${hour.win_speed}<br>
能见度:${hour.vis} km<br>
空气质量:${hour.aqi}<br>
</li>
`
).join('');

A.innerHTML = `

<h2>城市:${data.city} (${data.cityEn})</h2>
国家:${data.country} (${data.countryEn})<br>
日期:${data.date} ${data.week}<br>
更新时间:${data.update_time}<br>
天气状况:${data.wea}<br>
天气图标:<img src="images/weather_icons/${data.wea_img}.png" alt="${data.wea}"><br>
当前温度:${data.tem}°C<br>
最高温度:${data.tem1}°C<br>
最低温度:${data.tem2}°C<br>
风向:${data.win}<br>
风速:${data.win_speed} (${data.win_meter})<br>
湿度:${data.humidity}<br>
能见度:${data.visibility}<br>
气压:${data.pressure} hPa<br>
降雨量:${data.rain_pcpn} mm<br>
空气质量指数:${data.air}<br>
PM2.5:${data.air_pm25}<br>
空气质量等级:${data.air_level}<br>
空气质量提示:${data.air_tips}
<ul>
${listItems}
</ul>
`
;


console.log(data)
}

async function 获取(URL){
let 响应 = await fetch(URL)
let 数据 = await 响应.json()

return 数据
}


</script>
</body>
</html>

3. 补充CSS样式


图片.png
ul{
display: flex;
flex-wrap: wrap;
list-style: none;
}

li{
width: 300px;
background-color: palegreen;
margin: 10px;
padding: 15px;
border-radius: 50%;
text-align: center;
}

span{
padding: 15px;
background-color: orange;
cursor: pointer;
}

4. 补充JS多城市查询


天气.gif


4.1 增加对应的HTML代码

图片.png


4.2 增加对应的JS代码

图片.png


三、项目图示总结


使用Fetch和async/await极大的简化了前端代码。后端数据接口就是一个URL地址。
整个后端具备云服务的特征,可以视作云服务数据接口,如图所示


图片.png


四、天气接口


1.易客云天气API 推荐:⭐⭐⭐⭐⭐


对新手比较友好。
tianqiapi.com/


2.高德地图 需要注册开发者(推荐:⭐⭐⭐)


lbs.amap.com/api/webserv…


3.心知天气(推荐:⭐)


免费的API数据只有一行,且文档藏得太深难用
http://www.seniverse.com/


欢迎大家提供更多更好的天气API。


作者:百万蹄蹄向前冲
来源:juejin.cn/post/7441560184010014735
收起阅读 »

“有办法让流程图动起来吗?”“当然有!”:一起用LogicFlow实现动画边

web
引言 在流程图中,边(Edge) 的主要作用是连接两个节点,表示从一个节点到另一个节点的关系或流程。在业务系统中,边通常代表某种逻辑连接,比如状态转移、事件触发、任务流动等。对于复杂的流程图,边不仅仅是两点之间的连接,它还可以传递信息、约束流程的顺序,并通过不...
继续阅读 »

引言


在流程图中,边(Edge) 的主要作用是连接两个节点,表示从一个节点到另一个节点的关系或流程。在业务系统中,边通常代表某种逻辑连接,比如状态转移、事件触发、任务流动等。对于复杂的流程图,边不仅仅是两点之间的连接,它还可以传递信息、约束流程的顺序,并通过不同的样式或标记来表达不同的含义。


不同的场景下,边可能需要具备丰富的样式或交互,比如箭头表示方向、虚线表示条件判断、动画表示动态效果等。因此,灵活定义和实现自定义边对于流程图的可视化设计尤为重要。


LogicFlow的边


为了灵活适配不同场景下的需求,LogicFlow的边模型是由 线条、箭头、文本、调整点五个模块组成。用户可以继承基础边类,对边的线条、箭头、文本和调整点进行自定义。


edge-struct.png
在技术实现上,LogicFlow设计了一个基础边模型BaseEdge,它定义了LogicFlow边的基本属性,如起点、终点、路径、样式等,并提供了操作这些属性的基本方法,提供逻辑处理和渲染的基础,通过继承基础边的数据类BaseEdgeModel和视图类BaseEdge,可以实现自定义边的逻辑和交互。


基础边:BaseEdge


属性方法简介

BaseEdgeModel中定义了一些核心属性,用于描述边的几何结构和样式。


属性释义
sourceNodeId起始节点Id
targetNodeId目标节点Id
startPoint起点信息,默认存储的是起始节点上连接该边锚点的坐标信息
endPoint终点信息,默认存储的是目标节点上连接该边锚点的坐标信息
text边文本信息,存储边上文本的内容和位置
properties自定义属性,用于存储不同业务场景下的定制属性
pointsList路径顶点坐标列表

围绕着这些核心属性,LogicFlow设计了支撑边运转的核心方法


方法用途
initEdgeData初始化边的数据和状态
setAnchors设置边的端点,startPoint和endPoint会在这个被赋值
initPoints设置边路径,pointsList会在这个阶段被赋值
formatText将外部传入的文本格式化成统一的文本对象

还有一些渲染使用的样式方法


方法用途
getEdgeStyle设置边样式
getEdgeAnimationStyle设置边动画
getAdjustPointStyle设置调整点样式
getTextStyle设置文本样式
getArrowStyle设置箭头样式
getOutlineStyle设置边外框样式
getTextPosition设置文本位置

运转过程

边实例化时,数据层Model类内部会先调用initeEdgeData方法,将无需处理的属性直接存储下来,设置为监听属性然后触发setAnchors、initPoints和formatText方法,生成边起终点、路径和文本信息存储并监听。


model-run.png


视图层渲染时,Model中存储的数据会以外部参数的形式传给组件,由不同渲染方法消费。每个渲染方法都是从Model存储的核心数据中获取图形信息、从样式方法中获取图形渲染样式,组装到svg图形上。最终由render函数将不同模块方法返回的内容呈现出来。


view-run.png


内置衍生边


LogicFlow内部基于基础边衍生提供三种边:直线边、折线边和曲线边。


直线边

在基础边的之上做简单的定制:



  1. 支持样式快速设置

  2. 限制文本位置在线段中间

  3. 使用svg的line元素实现线条的绘制


ViewModel
LogicFlow-packages-core-src-view-edge-LineEdge-tsx-at-master-·-didi-LogicFlow-10-29-2024_09_07_PM.pngimage.png

直线边数据层和视图层源码逻辑


折线边

折线边在Model类的实现上针对边路径计算做了比较多的处理,会根据两个节点的位置、重叠情况,使用 A*查找 结合 曼哈顿距离 计算路径,实时自动生成pointsList数据。在View类中则重写了getEdge方法,使用svg polyline元素渲染路径。


录屏2024-10-30 10.52.14.gif


曲线边

曲线边和折线边类似,Model类针对边路径计算做了较多处理,不一样的是,为了调整曲线边的弧度,曲线边额外还提供了两个调整点,边路径也是根据边起终点和两个调整点的位置和距离计算得出,View类里使用svg的path元素渲染路径。


录屏2024-10-30 10.54.48.gif


一起实现一条自定义动画边


自定义边的实现思路和内置边的实现类似:继承基础边 → 重写Model类/View类的方法 → 按需增加自定义方法 → 命名并导出成模块


今天就带大家一起实现一条复杂动画边,话不多说,先看效果:


animate-line-high-quality.gif


要实现这样效果的边,我们核心只需要做一件事:重新定义边的渲染内容。


在实际写代码时,主要需要继承视图类,重写getEdge方法。


实现基础边

那我们先声明自定义边,并向getEdge方法中增加逻辑,让它返回基础的折线边。


为了方便预览效果,我们在画布上增加节点和边数据。


自定义边实现

import { h, PolylineEdge, PolylineEdgeModel } from '@logicflow/core'

class CustomAnimateEdge extends PolylineEdge {
// 重写 getEdge 方法,定义边的渲染
getEdge() {
const { model } = this.props
const { points, arrowConfig } = model
const style = model.getEdgeStyle()
return h('g', {}, [
h('polyline', {
points,
...style,
...arrowConfig,
fill: 'none',
strokeLinecap: 'round',
}),
])
}
}

class CustomAnimateEdgeModel extends PolylineEdgeModel {}

export default {
type: 'customAnimatePolyline',
model: CustomAnimateEdgeModel,
view: CustomAnimateEdge,
}


定义画布渲染内容

lf.render({
nodes: [
{
id: '1',
type: 'rect',
x: 150,
y: 320,
properties: {},
},
{
id: '2',
type: 'rect',
x: 630,
y: 320,
properties: {},
},
],
edges: [
{
id: '1-2-1',
type: 'customPolyline',
sourceNodeId: '1',
targetNodeId: '2',
startPoint: { x: 200, y: 320 },
endPoint: { x: 580, y: 320 },
properties: {
textPosition: 'center',
style: {
strokeWidth: 10,
},
},
text: { x: 390, y: 320, value: '边文本3' },
pointsList: [
{ x: 200, y: 320 },
{ x: 580, y: 320 },
],
},
{
id: '1-2-2',
type: 'customPolyline',
sourceNodeId: '1',
targetNodeId: '2',
startPoint: { x: 150, y: 280 },
endPoint: { x: 630, y: 280 },
properties: {
textPosition: 'center',
style: {
strokeWidth: 10,
},
},
text: { x: 390, y: 197, value: '边文本2' },
pointsList: [
{ x: 150, y: 280 },
{ x: 150, y: 197 },
{ x: 630, y: 197 },
{ x: 630, y: 280 },
],
},
{
id: '1-2-3',
type: 'customPolyline',
sourceNodeId: '2',
targetNodeId: '1',
startPoint: { x: 630, y: 360 },
endPoint: { x: 150, y: 360 },
properties: {
textPosition: 'center',
style: {
strokeWidth: 10,
},
},
text: { x: 390, y: 458, value: '边文本4' },
pointsList: [
{ x: 630, y: 360 },
{ x: 630, y: 458 },
{ x: 150, y: 458 },
{ x: 150, y: 360 },
],
},
{
id: '1-2-4',
type: 'customPolyline',
sourceNodeId: '1',
targetNodeId: '2',
startPoint: { x: 100, y: 320 },
endPoint: { x: 680, y: 320 },
properties: {
textPosition: 'center',
style: {
strokeWidth: 10,
},
},
text: { x: 390, y: 114, value: '边文本1' },
pointsList: [
{ x: 100, y: 320 },
{ x: 70, y: 320 },
{ x: 70, y: 114 },
{ x: 760, y: 114 },
{ x: 760, y: 320 },
{ x: 680, y: 320 },
],
},
],
})

然后我们就能获得一个这样内容的画布:


绚丽动画折线-LogicFlow-Examples-10-30-2024_11_08_AM.png


添加动画

LogicFlow提供的边动画能力其实是svg 属性和css属性的集合,目前主要支持了下述这些属性。


type EdgeAnimation = {
stroke?: Color; // 边颜色, 本质是svg stroke属性
strokeDasharray?: string; // 虚线长度与间隔设置, 本质是svg strokeDasharray属性
strokeDashoffset?: NumberOrPercent; // 虚线偏移量, 本质是svg strokeDashoffset属性
animationName?: string; // 动画名称,能力等同于css animation-name
animationDuration?: `${number}s` | `${number}ms`; // 动画周期时间,能力等同于css animation-duration
animationIterationCount?: 'infinite' | number; // 动画播放次数,能力等同于css animation-iteration-count
animationTimingFunction?: string; // 动画在周期内的执行方式,能力等同于css animation-timing-function
animationDirection?: string; // 动画播放顺序,能力等同于css animation-direction
};

接下来我们就使用这些属性实现虚线滚动效果。


边的动画样式是取的 model.getEdgeAnimationStyle() 方法的返回值,在内部这个方法是取全局主题的edgeAnimation属性的值作为返回的,默认情况下默认的动画是这样的效果:


default-edge-animation.gif


开发者可以通过修改全局样式来设置边动画样式;但如果是只是指定类型边需要设置动画部分,则需要重写getEdgeAnimationStyle方法做自定义,就像下面这样:


class ConveyorBeltEdgeModel extends PolylineEdgeModel {
// 自定义动画
getEdgeAnimationStyle() {
const style = super.getEdgeAnimationStyle()
style.strokeDasharray = '40 160' // 虚线长度和间隔
style.animationDuration = '10s' // 动画时长
style.stroke = 'rgb(130, 179, 102)' // 边颜色
return style
}
}

然后在getEdge方法中加上各个动画属性


// 改写getEdge方法内容
const animationStyle = model.getEdgeAnimationStyle()
const {
stroke,
strokeDasharray,
strokeDashoffset,
animationName,
animationDuration,
animationIterationCount,
animationTimingFunction,
animationDirection,
} = animationStyle

return h('g', {}, [
h('polyline', {
// ...
strokeDasharray,
stroke,
style: {
strokeDashoffset: strokeDashoffset,
animationName,
animationDuration,
animationIterationCount,
animationTimingFunction,
animationDirection,
},
}),
])

我们就得到了定制样式的动画边:


base-edge-animation.gif


添加渐变颜色和阴影

最后来增加样式效果,我们需要给这些边增加渐变颜色和阴影。
SVG提供了元素linearGradient定义线性渐变,我们只需要在getEdge返回的内容里增加linearGradient元素,就能实现边颜色线性变化的效果。
实现阴影则是使用了SVG的滤镜能力实现。


// 继续改写getEdge方法内容
return h('g', {}, [
h('linearGradient', { // svg 线性渐变元素
id: 'linearGradient-1',
x1: '0%',
y1: '0%',
x2: '100%',
y2: '100%',
spreadMethod: 'repeat',
}, [
h('stop', { // 坡度1,0%颜色为#36bbce
offset: '0%',
stopColor: '#36bbce'
}),
h('stop', { // 坡度2,100%颜色为#e6399b
offset: '100%',
stopColor: '#e6399b'
})
]),
h('defs', {}, [
h('filter', { // 定义滤镜
id: 'filter-1',
x: '-0.2',
y: '-0.2',
width: '200%',
height: '200%',
}, [
h('feOffset', { // 定义输入图像和偏移量
result: 'offOut',
in: 'SourceGraphic',
dx: 0,
dy: 10,
}),
h('feGaussianBlur', { // 设置高斯模糊
result: 'blurOut',
in: 'offOut',
stdDeviation: 10,
}),
h('feBlend', { // 设置图像和阴影的混合模式
mode: 'normal',
in: 'SourceGraphic',
in2: 'blurOut',
}),
]),
]),
h('polyline', {
points,
...style,
...arrowConfig,
strokeDasharray,
stroke: 'url(#linearGradient-1)', // 边颜色指向渐变元素
filter: 'url(#filter-1)', // 滤镜指向前面定义的滤镜内容
fill: 'none',
strokeLinecap: 'round',
style: {
strokeDashoffset: strokeDashoffset,
animationName,
animationDuration,
animationIterationCount,
animationTimingFunction,
animationDirection,
},
}),
])

就得到了我们的自定义动画边


录屏2024-10-29 19.57.02.gif


结尾


在流程图中,边不仅仅是节点之间的连接,更是传递信息、表达逻辑关系的重要工具。通过 LogicFlow,开发者可以轻松地创建和自定义边,以满足不同的业务场景需求。从基础的直线边到复杂的曲线边,甚至动画边,LogicFlow 都为开发者提供了高度的灵活性和定制能力。


希望能通过这篇文章抛砖引玉,帮助你了解在 LogicFlow 中创建和定制边的核心技巧,打造出符合你业务需求的流程图效果。


如果这篇文章对你有帮助,请为我们的项目点上star,非常感谢ღ( ´・ᴗ・` )


项目传送门:github.com/didi/LogicF…


作者:LogicFlow
来源:juejin.cn/post/7431379490969010212
收起阅读 »

svg实现地铁线路图

web
简介最近学习了svg,想着使用svg实现地铁线路图 其中黄色是1号线,蓝色是2号线,橙色是3号线实现:react+svg+数据结构-图。考虑范围包括了每站时间,但未包括了换站时间。考虑到换站时间可以加到每2个交换站的路程里功能功能:选择2个地铁站,标...
继续阅读 »

简介

最近学习了svg,想着使用svg实现地铁线路图

insta.gif 其中黄色是1号线,蓝色是2号线,橙色是3号线

实现:react+svg+数据结构-图。

考虑范围包括了每站时间,但未包括了换站时间。考虑到换站时间可以加到每2个交换站的路程里

功能

功能:选择2个地铁站,标出最短路程。

求最少换站路线,暂未做

实现思路

  1. 简化问题,先将所有地铁站分2类,交换站和非交换站。那么交换站可以充当图中的。那么从a=>b, 变成a=>交换站=>交换站=>b的问题,需要写死的是非交换站(a,b)能到达的交换站(下面的adjcent数组), 其中a=>交换站 和b=>交换站 相对静止,但是我这里也考虑到了非交换站到交换站需要的时间(time)

地铁线路图

image.png

image.png

  1. 首先根据每条地铁图数据绘制出地铁线路图,并添加上点击事件,这里要处理好地铁线路图的数据,数据需要相对准确,因为后面需要计算出最短路径。

image.png

image.png

  1. 求最短距离,使用的是Floyd最短路算法(全局/多源最短路)。 其中原理:计算a->b的最短路径,遍历所有,查找是否有最捷径路径 a->x x->b
for(k=1;k<=n;k++) 
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][k]+e[k][j]) // i->j i->k k->j
e[i][j]=e[i][k]+e[k][j];

然而拿到最短路程后,但是并未拿到路程,拿到的是比如,a点到所有点的最短路程。你们可以思考一下如果获取最短路径。

大概长这样

image.png

  1. 求最短路径 使用一个对象,存储每次找到较短路径。 changeRodePath[${is}to${js}] = [ [is, ks], [ks, js], ]
  function getAllPointShortest(n, e) {
let changeRodePath = {};
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (e[i][j] > e[i][k] + e[k][j]) {
e[i][j] = e[i][k] + e[k][j];
console.log("-------------------------");
const is = changeStation[i];
const ks = changeStation[k];
const js = changeStation[j];
changeRodePath[`${is}to${js}`] = [
[is, ks],
[ks, js],
];
console.log(changeStation[i], changeStation[j]);
console.log(changeStation[i], changeStation[k]);
console.log(changeStation[k], changeStation[j]);
// 2_2 2_5
//2_2 1_2
//1_2 2_5
}
}
}
}
setChangeRodePath(changeRodePath);
return e;
}

当选中2个站时,先取出adjacent,然后求出最短路程,

         let path = {};
adjacent0.forEach((p0,i1) => {
adjacent1.forEach((p1,i2) => {
const index0 = changeStation.indexOf(p0);
const index1 = changeStation.indexOf(p1);
let t=time0[i1]+time1[i2]
if ((rodePath[index0][index1]+t) < minPath) {
minPath = rodePath[index0][index1];
path = { p0, p1};
}
});
});

具体多少不重要,重要的是通过 let pathm = changeRodePath[${path.p0}to${path.p1}],递归查找是否有更短的捷径,因为,2_1 =>3_9 的路径是:2_1 =>1_3=>1_5=>1_8,所以不一定有捷径a->c c—b, 可能是 a->c c->b, 然后发现有更短路径,c->d d->b,那么a-b 路程就变成了a->c->d->b。回到正题,递归之后就能取到最短路径了,然后通过2个交换点取得路径。

没有就更简单了

5.取对应的line,去渲染,这里分2类,交换站之间的路径(最短路径),头和尾。然后分别渲染polyline(使用对应line 的颜色)

function getPl(item, attr, listen) {
return (
<g {...attr} {...listen}>
<polyline //绘制line
{...item}
fill="none"
color={item.colorH}
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
/>

{item.usePointn.map((point) => { // line 上的站
return (
<use
x={point.x}
onClick={() =>
choosePoint(point)}
y={point.y}
fill={point.color}
href="#point"
>use>
);
})}
g>
);
}

代码准备

// 上图所示,数据随便造,需要合理时间,不然得到的路程奇奇怪怪的

代码部分

html

  
width: "80vw", height: "100vh" }}>
<svg
id="passWay"
viewBox="0 0 800 600"
xmlns="http://www.w3.org/2000/svg"
>

<defs>
<g id="point">
<circle r="4">circle>
<circle r="3" fill="#fff">circle>
g>
defs>
// 所有地铁线路图
{polyline.map((item) => {
return getPl(
item,
{},
{
onMouseEnter: (e) => onMouseEnterShow(e, item),
onMouseOut: () => {
clearTimeout(t1.current);
t1.current = null;
},
}
);
})}
// mask
{ choosePoints.length==2 && (
<rect
x="0"
y="0"
width={"100%"}
height={"100%"}
fillOpacity={0.9}
fill="white"
>
rect>
)}
// 最短路程
{choosePoints && choosePoints.length==2 && showReduLine.map(line=>{
return getPl(line, {}, {})
})
}
svg>

通过line 获取 polyline

  function getLineP(line) {
const usePointn = [];
let path = "";
line.points.forEach((item, index) => {
const { x, y, isStart, isChange, isEnd } = item;

usePointn.push({ ...item, color: line.color });
if (index == 0) {
path = `${x},${y} `;
} else {
path += `${x},${y} `;
}
});
const polylinen = {
usePointn,
stroke: line.color,
...line,
pointStation: line.points,
points: path,
};
return polylinen;
}

选出2站绘制路程

  function comfirPath(point0, point1, p0, p1, pathm) {

let pShow0= getLines(point0,p0)
let pShow1= getLines(point1,p1)
let pathsCenter=[]
if (pathm) {
function recursion(pathm){
pathm.map(([p0,p1])=>{
let pathn = changeRodePath[`${p0}to${p1}`];
if(pathn){
recursion(pathn)
}else{
// 中间的line 不用按顺序
pathsCenter.push(getChangeStationLine(p0,p1))
}
})
}
recursion(pathm)
}else{
pathsCenter=[getChangeStationLine(p0,p1)]
}
const pyAll= [pShow0,pShow1,...pathsCenter].map(line=>{
const py= getLineP({
points:line,
})
py.stroke=line.color
return py
})
setShowReduLine(pyAll); // 绘制
}

参考: 1.# [数据结构拾遗]图的最短路径算法


作者:无名小兵
来源:juejin.cn/post/7445208959151767604

收起阅读 »

插件系统为什么在前端开发中如此重要?

web
插件系统是一种软件架构模式,允许开发者通过添加外部模块或组件来扩展和定制软件应用的功能,而无需修改其核心代码。这种方式为软件提供了高度的可扩展性、灵活性和可定制性。 用过构建工具的同学都知道,grunt, webpack, gulp 都支持插件开发。后端框架比...
继续阅读 »

插件系统是一种软件架构模式,允许开发者通过添加外部模块或组件来扩展和定制软件应用的功能,而无需修改其核心代码。这种方式为软件提供了高度的可扩展性、灵活性和可定制性。


用过构建工具的同学都知道,grunt, webpack, gulp 都支持插件开发。后端框架比如 egg koa 都支持插件机制拓展,前端页面也有许多可拓展性的要求。插件化无处不在,所有的框架都希望自身拥有最强大的可拓展能力,可维护性,而且都选择了插件化的方式达到目标。


什么是插件系统


插件系统主要由三个关键部分组成:



  1. 核心系统(Host Application):这是主软件应用,提供了插件可以扩展或修改的基础功能。

  2. 插件接口(Plugin Interface):定义了插件和核心系统之间的交互协议。插件接口规定了插件必须遵循的规则和标准,以便它们能够被核心系统识别和使用。

  3. 插件(Plugins):根据插件接口规范开发的外部模块或组件,用于扩展核心系统的功能。插件可以被添加或移除,而不影响核心系统的运行。


20240316121736


插件的执行流程和实现方式


插件的执行流程是指从插件被加载到执行其功能直至卸载的一系列步骤。



  1. 设计核心系统:首先,我们需要一个核心系统。这个系统负责维护基础功能,并提供插件可以扩展或修改的接口。



    • 核心系统的生命周期:定义核心系统的关键阶段,例如启动、运行中、关闭等。每个阶段可能会触发特定的事件。

    • 暴露的 API:确定哪些内部功能是可以被插件访问的。这包括数据访问、系统服务调用等接口。



  2. 插件的结构设计:插件需要有一个清晰的结构,使其能够容易地集成到核心系统中。一个典型的插件结构可能包含:



    • 初始化代码:插件加载时执行的代码,用于设置插件的运行环境。

    • 处理函数:实现插件功能的核心代码,根据插件的目的可以有多个。

    • 资源清理:插件卸载时需要执行的清理代码,以确保资源被适当释放。



  3. 插件的注册和加载:开发者通过配置文件、命令或图形界面在核心系统中注册插件,系统随后根据注册信息安装并加载插件,这个过程涉及读取插件元数据、执行初始化代码,以及将插件绑定到特定的生命周期事件或 API 上。

  4. 插件的实现:插件的实现依赖于核心系统提供的生命周期钩子和 API。



    • 利用生命周期钩子:插件可以注册函数来响应核心系统的生命周期事件,例如在系统启动完成后执行初始化操作,或在系统关闭前进行资源清理。

    • 调用暴露的 API:插件通过调用核心系统暴露的 API 来实现其功能。这些 API 可以提供系统信息、修改数据、触发事件等功能。



  5. 代码执行流程:插件通过注册自身到核心系统,绑定处理函数至特定事件或 API,以响应系统生命周期变化或 API 调用执行特定任务。在适当时机,如系统关闭或更新时,插件被卸载,其资源得以清理并从系统中移除。


通过这个流程,插件系统提供了一个灵活、可扩展的方式来增强和定制核心系统的功能。插件的开发者可以专注于插件逻辑的实现,而无需修改核心系统的代码。同时,核心系统能够保持稳定性和安全性,因为插件的执行是在明确定义的接口和约束条件下进行的。


插件的几种形式


插件的主要形式主要分为以下几种形式:



  1. 约定式插件

  2. 注入式插件

  3. 事件式插件

  4. 插槽式插件


约定式插件


约定式插件通常在那些采用“约定优于配置”理念的框架或工具中很常见。以 Webpack 为例,它过各种加载器(Loaders)和插件(Plugins)提供强大的扩展性,而这些扩展往往遵循一定的约定,以简化配置的复杂性。


在 Webpack 配置中使用插件时,通常不需要指定插件工作的具体点,只需要将插件加入到配置的 plugins 数组中。Webpack 根据内部的运行机制和生命周期事件,自动调用这些插件,执行相关的任务。


例如,使用 HtmlWebpackPlugin 可以自动生成一个 HTML 文件,并自动将打包后的 JS 文件注入到这个 HTML 文件中。开发者只需要按照约定将 HtmlWebpackPlugin 加入到 plugins 数组中,无需指定具体的注入点或方式,Webpack 就会自动完成这些任务。


const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
// 其他配置...
plugins: [
new HtmlWebpackPlugin({
template: "./src/template.html",
}),
],
};

通过这种约定式的插件机制,Webpack 极大地简化了开发者的配置工作,同时保持了强大的灵活性和扩展性。用户只需遵循简单的约定,如将插件实例添加到 plugins 数组,Webpack 便能自动完成复杂的集成工作,如资源打包、文件处理等,从而提高了开发效率和项目的可维护性。这正体现了约定式插件的主要优势:通过遵循一套预定义的规则,减少配置的需求,同时提供强大的功能扩展能力。


注入式插件


注入式插件通过在应用程序的运行时或编译时将插件的功能注入到应用程序中,从而扩展应用程序的功能。这种方式往往依赖于一种中间件或框架来实现插件的动态加载和执行。一个典型的例子就是 NestJs 世界中广泛使用的依赖注入(DI)功能。


除此之外,尽管 Webpack 更常被人们提及其约定式插件机制,但我们可以从一个角度将 Loaders 视为一种注入式插件,在 Webpack 配置中,Loaders 允许你在模块被添加到依赖图中时,预处理文件。可以看作是在编译过程中“注入”了额外的处理步骤。这些处理步骤可以包括将 TypeScript 转换为 JavaScript、将 SASS 转换为 CSS,或者将图片和字体文件转换为 Webpack 可以处理的格式。


module.exports = {
// ...其他配置
module: {
rules: [
{
test: /\.js$/, // 使用正则表达式匹配文件路径,处理.js文件
exclude: /node_modules/, // 排除node_modules目录
use: {
loader: "babel-loader", // 指定使用babel-loader
options: {
presets: ["@babel/preset-env"], // 使用预设配置转换ES6+代码
},
},
},
],
},
// ...其他配置
};

通过 loader 的配置,Webpack 实现了一种灵活的“注入式”扩展机制,允许开发者根据需要为构建过程注入各种预处理步骤。


事件插件化


事件插件化是一种基于事件驱动编程模式的插件化机制,其中插件通过监听和响应系统中发生的特定事件来工作。这种机制允许插件在不直接修改主程序代码的情况下增加或改变程序的行为。


Node.js 的 EventEmitter 类是实现事件插件化的一个很好的例子。假设我们正在开发一个应用程序,该程序需要在完成某个任务后执行一系列的操作,这些操作由不同的插件来实现。


首先,创建一个基于 EventEmitter 的任务执行器,它在完成任务时会发出一个事件:


const EventEmitter = require("events");

class TaskExecutor extends EventEmitter {
execute(taskFunc) {
console.log("Executing task...");
taskFunc();
this.emit("taskCompleted", "Task execution finished");
}
}

接着,我们可以开发插件来监听 taskCompleted 事件。每个插件都可以注册自己的监听器来响应事件:


// Plugin A
executor.on("taskCompleted", (message) => {
console.log(`Plugin A responding to event: ${message}`);
});

// Plugin B
executor.on("taskCompleted", (message) => {
console.log(`Plugin B responding to event: ${message}`);
});

最后,创建 TaskExecutor 的实例,并执行一个任务,看看插件如何响应:


const executor = new TaskExecutor();

// 注册插件
// ...此处省略插件注册代码...

executor.execute(() => {
console.log("Task is done.");
});

运行上述代码时,TaskExecutor 执行一个任务,并在任务完成后发出 taskCompleted 事件。注册监听该事件的所有插件(在这个例子中是插件 A 和插件 B)都会接到通知,并执行相应的响应操作。这种模式使得开发者可以很容易地通过添加更多的事件监听器来扩展应用程序的功能,而无需修改 TaskExecutor 或其他插件的代码,实现了高度的解耦和可扩展性。


插槽插件化


在 React 中,插槽插件化的概念可以通过组件的 children 属性或使用特定的插槽来实现。这种模式允许开发者定义一个组件框架,其中一些部分可以通过传入的子组件来填充,从而实现自定义内容的注入。这类似于 Vue 中的插槽(slots)功能,但在 React 中,它通过 props.children 或通过特定的 props 来传递组件来实现。


function Card({ children }) {
return <div className="card">{children}</div>;
}

function App() {
return (
<Card>
<h2>标题</h2>
<p>这是一段文本</p>
</Card>

);
}

通过这种方式,React 支持了组件的插槽化,使组件的复用和自定义变得更加容易。这种模式在构建可扩展和可复用的 UI 组件库时尤其有用。


代码实现


接下来我们通过插件来实现一个计算器,可以实现加减乘除


插件核心实现


class Calculator {
constructor(options = {}) {
const { initialValue = 0 } = options;
this.currentValue = initialValue;
}
getCurrentValue() {
return this.currentValue;
}
setValue(value) {
this.currentValue = value;
}
plus(addend) {
this.setValue(this.currentValue + addend);
}
minus(subtrahend) {
this.setValue(this.currentValue - subtrahend);
}
multiply(multiplicand) {
this.setValue(this.currentValue * multiplicand);
}
division(divisor) {
if (divisor === 0) {
console.error("不允许除零。");
return;
}
this.setValue(this.currentValue / divisor);
}
}

// test
const calculator = new Calculator();
calculator.plus(10);
console.log(calculator.getCurrentValue()); // 10
calculator.minus(5);
console.log(calculator.getCurrentValue()); // 5
calculator.multiply(2);
console.log(calculator.getCurrentValue()); // 10
calculator.division(2);
console.log(calculator.getCurrentValue()); // 5

实现 hooks


核心系统想要对外提供生命周期钩子,就需要一个事件机制。


class Hooks {
constructor() {
this.listeners = {};
}
on(eventName, handler) {
let listeners = this.listeners[eventName];
if (!listeners) {
this.listeners[eventName] = listeners = [];
}
listeners.push(handler);
}
off(eventName, handler) {
const listeners = this.listeners[eventName];
if (listeners) {
this.listeners[eventName] = listeners.filter((l) => l !== handler);
}
}
trigger(eventName, ...args) {
const listeners = this.listeners[eventName];
const results = [];
if (listeners) {
for (const listener of listeners) {
const result = listener.call(null, ...args);
results.push(result);
}
}
return results;
}
destroy() {
this.listeners = {};
}
}

这个 Hooks 类是一个事件监听器或事件钩子的简单实现,它允许你在应用程序的不同部分之间传递消息或事件,而不必直接引用那些部分。


暴露生命周期(通过 Hooks)


然后将 hooks 运用在核心系统中 -- JavaScript 计算器。


每个钩子对应的事件:



  • pressedPlus 做加法操作

  • pressedMinus 做减法操作

  • pressedMultiply 做乘法操作

  • pressedDivision 做乘法操作

  • valueWillChanged 即将赋值 currentValue,如果执行此钩子后返回值为 false,则中断赋值。

  • valueChanged 已经赋值 currentValue


class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0, plugins = [] } = options;
this.currentValue = initialValue;
plugins.forEach((plugin) => plugin.apply(this.hooks));
}

getCurrentValue() {
return this.currentValue;
}

setValue(value) {
const result = this.hooks.trigger("valueWillChanged", value);
if (result.length !== 0 && result.some((_) => !_)) {
} else {
this.currentValue = value;
this.hooks.trigger("valueChanged", this.currentValue);
}
}

plus(addend) {
this.hooks.trigger("pressedPlus", this.currentValue, addend);
this.setValue(this.currentValue + addend);
}

minus(subtrahend) {
this.hooks.trigger("pressedMinus", this.currentValue, subtrahend);
this.setValue(this.currentValue - subtrahend);
}

multiply(factor) {
this.hooks.trigger("pressedMultiply", this.currentValue, factor);
this.setValue(this.currentValue * factor);
}

division(divisor) {
if (divisor === 0) {
console.error("Division by zero is not allowed.");
return;
}
this.hooks.trigger("pressedDivision", this.currentValue, divisor);
this.setValue(this.currentValue / divisor);
}
}

插件实现


插件要实现 apply 方法,在 Calculator 的 constructor 调用时,才能确保插件 apply 执行后会绑定(插件内的)处理函数到生命周期。


apply 的入参是 this.hooks,通过 this.hooks 来监听生命周期并添加处理器。


class LogPlugins {
apply(hooks) {
hooks.on("pressedPlus", (currentVal, addend) =>
console.log(`${currentVal} + ${addend}`)
);
hooks.on("pressedMinus", (currentVal, subtrahend) =>
console.log(`${currentVal} - ${subtrahend}`)
);
hooks.on("pressedMultiply", (currentVal, factor) =>
console.log(`${currentVal} * ${factor}`)
);
hooks.on("pressedDivision", (currentVal, divisor) =>
console.log(`${currentVal} / ${divisor}`)
);
hooks.on("valueChanged", (currentVal) =>
console.log(`结果: ${currentVal}`)
);
}
}

class LimitPlugins {
apply(hooks) {
hooks.on("valueWillChanged", (newVal) => {
if (100 < newVal) {
console.log("result is too large");
return false;
}
return true;
});
}
}

LogPlugins 的目的是记录计算器操作的详细日志。通过监听 Calculator 类中定义的事件(如加、减、乘、除操作和值变化时的事件),这个插件在这些操作执行时打印出相应的操作和结果。


LimitPlugins 的目的是在值变更前进行检查,以确保计算器的结果不会超出预设的限制(在这个例子中是 100)。如果预计的新值超出了限制,这个插件会阻止值的更改并打印一条警告消息。


通过这两个插件,Calculator 类获得了额外的功能,而无需直接在其代码中加入日志记录和值限制检查的逻辑。


完整代码


最后我们应该贴上全部代码:


class Hooks {
constructor() {
this.listener = {};
}

on(eventName, handler) {
if (!this.listener[eventName]) {
this.listener[eventName] = [];
}
this.listener[eventName].push(handler);
}

trigger(eventName, ...args) {
const handlers = this.listener[eventName];
const results = [];
if (handlers) {
for (const handler of handlers) {
const result = handler(...args);
results.push(result);
}
}
return results;
}

off(eventName, handler) {
const handlers = this.listener[eventName];
if (handlers) {
this.listener[eventName] = handlers.filter((cb) => cb !== handler);
}
}

destroy() {
this.listener = {};
}
}

class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0, plugins = [] } = options;
this.currentValue = initialValue;
plugins.forEach((plugin) => plugin.apply(this.hooks));
}

getCurrentValue() {
return this.currentValue;
}

setValue(value) {
const result = this.hooks.trigger("valueWillChanged", value);
if (result.length !== 0 && result.some((_) => !_)) {
} else {
this.currentValue = value;
this.hooks.trigger("valueChanged", this.currentValue);
}
}

plus(addend) {
this.hooks.trigger("pressedPlus", this.currentValue, addend);
this.setValue(this.currentValue + addend);
}

minus(subtrahend) {
this.hooks.trigger("pressedMinus", this.currentValue, subtrahend);
this.setValue(this.currentValue - subtrahend);
}

multiply(factor) {
this.hooks.trigger("pressedMultiply", this.currentValue, factor);
this.setValue(this.currentValue * factor);
}

division(divisor) {
if (divisor === 0) {
console.error("Division by zero is not allowed.");
return;
}
this.hooks.trigger("pressedDivision", this.currentValue, divisor);
this.setValue(this.currentValue / divisor);
}
}

class LogPlugins {
apply(hooks) {
hooks.on("pressedPlus", (currentVal, addend) =>
console.log(`${currentVal} + ${addend}`)
);
hooks.on("pressedMinus", (currentVal, subtrahend) =>
console.log(`${currentVal} - ${subtrahend}`)
);
hooks.on("pressedMultiply", (currentVal, factor) =>
console.log(`${currentVal} * ${factor}`)
);
hooks.on("pressedDivision", (currentVal, divisor) =>
console.log(`${currentVal} / ${divisor}`)
);
hooks.on("valueChanged", (currentVal) =>
console.log(`结果: ${currentVal}`)
);
}
}

class LimitPlugins {
apply(hooks) {
hooks.on("valueWillChanged", (newVal) => {
if (100 < newVal) {
console.log("result is too large");
return false;
}
return true;
});
}
}

// 运行测试
const calculator = new Calculator({
initialValue: 0,
plugins: [new LogPlugins(), new LimitPlugins()],
});
calculator.plus(10);
calculator.minus(5);
calculator.multiply(2);
calculator.division(5);
calculator.plus(1000); // 尝试加到超过限制的值

最终输出结果如下图所示:


20240316211128


参考资料


-简单实现一个插件系统(不引入任何库),学会插件化思维


-当我们说插件系统的时候,我们在说什么


总结


通过这两个插件的例子,我们可以看到插件化设计模式在软件开发中的强大之处。它允许开发者在不修改原有代码基础上扩展功能、增加新的处理逻辑,使得应用更加模块化和易于维护。这种模式特别适用于那些需要高度可扩展性和可定制性的应用程序。


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🚗🚗🚗


如果你对开源项目感兴趣的,可以加我微信 yunmz777


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

没想到,axios下载文件竟然比fetch好用

web
前言 还是和上篇一样,是关于导出excel的问题。好像是生产上导出的excel有问题,具体是啥没和我说,当然和我上篇写的没有什么关系,这是另一个模块历史遗留的问题。反正到我手里的任务就是更改导出的接口让后端去做表格。 原来的写法 原来的写法很粗暴,直接用win...
继续阅读 »

前言


还是和上篇一样,是关于导出excel的问题。好像是生产上导出的excel有问题,具体是啥没和我说,当然和我上篇写的没有什么关系,这是另一个模块历史遗留的问题。反正到我手里的任务就是更改导出的接口让后端去做表格。


原来的写法


原来的写法很粗暴,直接用window.location去跳转下载链接就把excel下载了,后端具体怎么做我的不清楚,前端的逻辑就是有一个固定的地址,然后通过query去传参让后端知道该导出什么样的excel表格。


function exportExcel(params){
const url = 'xxxxx/exportExcel?id=params.id&type=params.type'
   window.location = url
}

content-disposition


基础没学好应该也是会这样的一个疑问,为什么我在浏览器中输入一个地址就会下载文件,是的我也是,所以我去查了一下,主要是由于Content-Disposition 这个响应头字段。它告诉浏览器该文件是作为附件下载,还是在浏览器中直接打开。如果该字段的值为 attachment,则浏览器会将文件下载到本地;如果该字段的值为inline,则浏览器会尝试在浏览器中直接打开文件。


image-20241220101529424.png


语法格式




  • 其基本语法格式为:Content-Disposition: attachment; filename="filename.ext"Content-Disposition: inline; filename="filename.ext"

  • 其中,attachment表示将内容作为附件下载,这是最常见的用于文件下载的设置;而inline则表示在浏览器中内联显示内容,即直接在浏览器窗口中展示,而不是下载。

  • filename参数用于指定下载文件的名称,若不指定,浏览器可能会根据服务器返回的其他信息或自身的默认规则来确定文件名。



标题党?


才不是啊,因为我要对接的接口变成post请求,用原来这种方式肯定是不行的,这个时候我就想到了我之前写过的类似需求,就是用fetch。但是一直请求不成功,后端一直报请求参数异常。


fetch


function exportExcel(data){
fetch(`xxxxxxx/ExportExcel`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Authorization': 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
}
}).then(res => {
const readableStream = res.body
if (readableStream) {
return new Response(readableStream).blob()
} else {
console.error('No readable stream available.')
}
}).then(blob => {
// 创建一个下载链接
const downloadLink = document.createElement('a')
downloadLink.href = URL.createObjectURL(blob)
// 设置下载属性,指定文件名
downloadLink.download = '测试.xlsx'
// 模拟点击下载链接
downloadLink.click()
// 释放 URL 对象
URL.revokeObjectURL(downloadLink.href)
})
}

我感觉我写的没有什么毛病啊,fetch第一个then回调转成blob数据类型,第二个then模拟a标签点击下载。但是后端老给报参数类型异常。


image-20241220105642273.png


我本来想让后端给我看看什么原因的,是什么参数没传对,还是什么请求头不对,但是他就老给甩一张swagger的请求成功的截图,根本不会帮你去看日志是因为什么原因。当然,swagger能调成功,说明接口肯定是没问题的,肯定是我没有传对东西,但是就挺烦的,都没有沟通欲望了,想着自己去换种方式去解决,然后我就想着用axios去试一下,没想到成功了


axios


function exportExcel(data) {
 axios({
   method: 'post',
   url: `xxxxx/ExportExcel`,
   data,
   responseType: 'blob'// 这里就是转化为blob文件流
}).then(res => {
   console.log(res, 'res')
     // 创建一个下载链接
   const downloadLink = document.createElement('a')
   downloadLink.href = URL.createObjectURL(res.data)
   // 设置下载属性,指定文件名
   downloadLink.download = '测试.xlsx'
   // 模拟点击下载链接
   downloadLink.click()
   // 释放 URL 对象
   URL.revokeObjectURL(downloadLink.href)
})
}

这里通过responseType设置blob值,就会自动将响应的东西转成blob二进制的格式内容,然后还是通过模拟a标签下载。相比于fetch,我们要在第二个then中对数据进行转换,而axios配置一个参数就行了。


总结


现在大部分的项目中,基本都是使用axios封装的交互方法,所以我们其实用axios是最好的,只需要配置一个参数就可以下载excel,相较于fetch来说,代码是比较简洁一点。虽然我这里fetch是没有成功的,但是放心,肯定是没有问题,是可以这样下载excel的,我估摸着应该是请求头的原因吧,可能是后端做了什么对请求头的处理,我也不知道,但是我之前做这个需求都是用fetch肯定没问题。


作者:落课
来源:juejin.cn/post/7450310230536208418
收起阅读 »

🌿一个vue3指令让el-table自动轮播

web
前言 本文开发的工具,是vue3 element-plus ui库专用的,需要对vue3指令概念有一定的了解 ​ 最近开发的项目中,需要对项目中大量的列表实现轮播效果,经过一番折腾.最终决定不使用第三方插件,手搓一个滚动指令. 效果展示 实现思路 第一步...
继续阅读 »

img


前言



本文开发的工具,是vue3 element-plus ui库专用的,需要对vue3指令概念有一定的了解



​ 最近开发的项目中,需要对项目中大量的列表实现轮播效果,经过一番折腾.最终决定不使用第三方插件,手搓一个滚动指令.


效果展示


列表滚动.webp


实现思路


第一步先确定功能



  • 列表自动滚动

  • 鼠标移入停止滚动

  • 鼠标移出继续滚动

  • 滚轮滚动完成,还可以继续在当前位置滚动

  • 元素少于一定条数时,不滚动


滚动思路


image-20241226223121217.png


image-20241226223310536.png


通过观察el-table的结构可以发现el-scrollbar__view里面放着所有的元素,而el-scrollbar__wrap是一个固定高度的容器,那么只需要获取到el-scrollbar__wrap这个DOM,并且再给一个定时器,不断的改变它的scrollTop值,就可以实现自动滚动的效果,这个值必须要用一个变量来存储,不然会失效


停止和继续滚动思路


设置一个boolean类型变量,每次执行定时器的时候判断一下,true就滚动,否则就不滚动


滚轮事件思路


为了每次鼠标在列表中滚动之后,我们的轮播还可以在当前滚动的位置,继续轮播,只需要在鼠标移出的时候,将当前el-scrollbar__wrapscrollTop赋给前面存储的变量,这样执行定时器的时候,就可以继续在当前位置滚动


不滚动的思路


​ 只需要判断el-scrollbar__view这个容器的高度,是否大于el-scrollbar__wrap的高度,是就可以滚动,不是就不滚动。


大致的思路是这样的,下面上源码


实现代码


文件名:tableAutoScroll.ts


interface ElType extends HTMLElement {
timer: number | null
isScroll: boolean
curTableTopValue: number
}
export default {
created(el: ElType) {
el.timer = null
el.isScroll = true
el.curTableTopValue = 0
},
mounted(el: ElType, binding: { value?: { delay?: number } }) {
const { delay = 15 } = binding.value || {}
const tableDom = el.getElementsByClassName(
'el-scrollbar__wrap'
)[0] as HTMLElement
const viewDom = el.getElementsByClassName(
'el-scrollbar__view'
)[0] as HTMLElement

const onMouseOver = () => (el.isScroll = false)
const onMouseOut = () => {
el.curTableTopValue = tableDom.scrollTop
el.isScroll = true
}

tableDom.addEventListener('mouseover', onMouseOver)
tableDom.addEventListener('mouseout', onMouseOut)

el.timer = window.setInterval(() => {
const viewDomClientHeight = viewDom.scrollHeight
const tableDomClientHeight = el.clientHeight

if (el.isScroll && viewDomClientHeight > tableDomClientHeight) {
const curScrollPosition = tableDom.clientHeight + el.curTableTopValue
el.curTableTopValue =
curScrollPosition === tableDom.scrollHeight
? 0
: el.curTableTopValue + 1
tableDom.scrollTop = el.curTableTopValue
}
}, delay)
},
unmounted(el: ElType) {
if (el.timer !== null) {
clearInterval(el.timer)
}
el.timer = null

const tableDom = el.getElementsByClassName(
'el-scrollbar__wrap'
)[0] as HTMLElement
tableDom.removeEventListener('mouseover', () => (el.isScroll = false))
tableDom.removeEventListener('mouseout', () => {
el.curTableTopValue = tableDom.scrollTop
el.isScroll = true
})
},
}

上面代码中,我在 created中初始化了三个变量,分别用于存储,定时器对象 、是否滚动判断、滚动当前位置。


mounted中我还获取了一个options,主要是为了可以定制滚动速度


用法



  1. 将这段代码放在你的文件夹中

  2. main.ts中注册这个指令


    import tableAutoScroll from './modules/tableAutoScroll.ts'
    const directives: any = {
    tableAutoScroll,
    }
    /**
    * @function 批量注册指令
    * @param app vue 实例对象
    */

    export const install = (app: any) => {
    Object.keys(directives).forEach((key) => {
    app.directive(key, directives[key]) // 将每个directive注册到app中
    })
    }



image-20241226224940418.png
image-20241226225027524.png


我这边是将自己的弄了一个批量注册,正常使用就像官网里面注册指令就可以了


在需要滚动的el-table上使用这个指令就可以


image-20241226225257264.png


<!-- element 列表滚动指令插件 -->
<template>
<div class="container">
<el-table v-tableAutoScroll :data="tableData" height="300">
<el-table-column prop="date" label="时间" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="address" label="Address" />
</el-table>
<!-- delay:多少毫秒滚动一次 -->
<el-table
v-tableAutoScroll="{
delay: 50,
}"
:data="tableData"
height="300"
>
<el-table-column prop="date" label="时间" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="address" label="Address" />
</el-table>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
const tableData = ref<any>([])
onMounted(() => {
tableData.value = Array.from(Array(100), (item, index) => ({
date: '时间' + index,
name: '名称' + index,
address: '地点' + index,
}))
console.log('👉 ~ tableData.value=Array.from ~ tableData:', tableData)
})
</script>

<style lang="scss" scoped>
.container {
height: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
gap: 100px;
.el-table {
width: 500px;
}
}
</style>

上面这个例子,分别演示两种调用方法,带参数和不带参数


最后


做了这个工具之后,突然有很多思路,打算后面再做几个,做成一个开源项目,一个开源的vue3指令集


作者:BAO_OA
来源:juejin.cn/post/7452667228006678540
收起阅读 »

马上2025年了,你还在用组件式弹窗? 来看看这个吧~

web
闲言少叙,直切正题。因为我喜欢命令式弹窗,所以就封装了它做为了业务代码的插件!如今在实际项目中跑了大半年,挺方便也挺灵活的! 如何使用 // vue2 npm install @e-dialog/v2 // main.js 入口文件 import Vue f...
继续阅读 »

闲言少叙,直切正题。因为我喜欢命令式弹窗,所以就封装了它做为了业务代码的插件!如今在实际项目中跑了大半年,挺方便也挺灵活的!


如何使用


// vue2
npm install @e-dialog/v2

// main.js 入口文件
import Vue from 'vue'
import App from './App'

//导包
import eDialog from '@e-dialog/v2'
//注册插件
Vue.use(eDialog, {
width:'50%',//全局配置
top:'15vh',
//...省略
})

new Vue({
el: '#app',
render: h => h(App)
})


// vue3
npm install @e-dialog/v3

// main.js 入口文件
import { createApp } from 'vue'
import App from './App.vue'
//导包
import eDialog from '@e-dialog/v3'

// 创建实例
const setupAll = async () => {
const app = createApp(App)
app.use(eDialog,{
width:'50%',//全局配置
top:'15vh',
//...省略
})
app.mount('#app')
}

setupAll()

插件简介


vue2是基于element ui elDialog组件做的二次封装,vue3则是基于element-plus elDialog组件做的二次封装,属性配置这一块可以全部参考element UI文档!



微信截图_20241215192735.png


扩展的属性配置

参数说明类型默认值
isBtn是否显示底部操作按钮booleantrue
draggable是否开启拖拽,vue3版本element-plus内置了该属性booleantrue
floorBtnSize底部操作按钮的尺寸medium、small、minismall
sureBtnText确定按钮的文案string确定
closeBtnText关闭按钮的文案string关闭
footer底部按钮的插槽,是一个函数返回值必须是JSXfunction-

底部插槽用法

// index.vue
<template>
<!-- vue2示例 -->
<div>
<el-button @click="handleDialog">弹窗</el-button>
</div>

</template>

<script>
//弹窗内容
import Edit form './edit.vue'
export default {
methods: {
handleDialog() {
this.$Dialog(Edit, props,function(vm,next){
//vm可以通过vm.formData拿到数据
},{
isBtn:false //如果定义了插槽,建议关闭底部操作按钮,不然会出现布局问题
footer:function(h,next){
return (
<el-button onClick={()=>{this.handleCheck(next)}}>按钮</el-button>
)
}
})
},

//按钮点击触发
handleCheck(next){
//next是一个手动关闭函数
console.log('业务逻辑')
}
}
}
</script>



页面使用:vue2

// index.vue
<template>
<!-- vue2示例 -->
<div>
<el-button @click="handleDialog">弹窗</el-button>
</div>

</template>

<script>
//弹窗内容
import Edit form './edit.vue'
export default {
methods: {
handleDialog() {
/**
* @function $Dialog是一个全局方法,自动挂载到了vue的原型
* @description 它总共接收4个参数
* @param 组件实例 | html字符串
* @param props要传递到组件的参数对象。
* @param 点击确定按钮的回调,回调里面的第一个参数是弹窗内容的组件实例,第二个参数是关闭弹窗的执行函数
* @param 配置对象,可以覆盖全局的配置
* @return void
*/


this.$Dialog(Edit, props,function(vm,next){
//vm可以通过vm.formData拿到数据
},{
//配置对象
})
}

}
}
</script>


//edit.vue
<template>
<div>弹窗内容!</div>
</template>


<script>
export default {
props:[/*这里可以接收$Dialog第二个参数props的数据*/]
data() {
return {
formData:{
a:'',
b:'',
c:''
}
}
},

}
</script>



页面使用:vue3

// index.vue
<template>
<!-- vue2示例 -->
<div>
<el-button @click="handleDialog">弹窗</el-button>
</div>

</template>

<script setup>
//弹窗内容
import Edit form './edit.vue'
const { proxy } = getCurrentInstance();
const $Dialog = proxy.useDialog()

function handleDialog() {
/**
* @function $Dialog是一个全局方法,自动挂载到了vue的原型
* @description 它总共接收4个参数
* @param 组件实例 | html字符串
* @param props要传递到组件的参数对象。
* @param 点击确定按钮的回调,回调里面的第一个参数是弹窗内容的组件实例,第二个参数是关闭弹窗的执行函数
* @param 配置对象,可以覆盖全局的配置
* @return void
*/


$Dialog(Edit, props,function(vm,next){
//vm可以通过vm.formData拿到数据
},{
//配置对象
})
}
</script>


//edit.vue
<template>
<div>弹窗内容!</div>
</template>

<script setup>
const formData = reactive({
a:'',
b:'',
c:''
})
defineExpose({ formData }) //这里注意一点要把外部要用的抛出去,如果不抛,则$Dialog回调将拿不到任何数据
</script>



函数参数设计理念


  1. 如果你弹窗内容比较复杂,例如涉及一些表单操作。最好建议抽离成一个组件,导入到Dialog第一个入参里面,如果只是简单的静态文本,则直接可以传HTML。

  2. 如果你Dialog导入的是组件,那么你有可能需要给组件传参。所以Dialog第二个入参就是给你开放的入口。

  3. 如果你点击确认按钮可能需要执行一些逻辑,例如调用API接口。所以你可能在Dialog第三个回调函数里面写入逻辑。回调函数会把第一个入参组件的实例给你传递回来,你拿到实例就可以干任何事情咯!

  4. Dialog第四个参数考虑到不同页面的配置不同。可以灵活设置。


vue2源码地址(github.com/zy1992829/e…)


vue3源码地址(github.com/zy1992829/e…)


喜欢的朋友可以去看一看,顺便帮忙点个星星。这个就不贴源码了。。


作者:阳火锅
来源:juejin.cn/post/7448661024440401957
收起阅读 »

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

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

前言


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


image.png


一、带响应式Props解构赋值


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



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



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

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

const { testCount } = props;
</script>


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



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

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

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


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



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

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

</script>

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


二、Props默认值新写法


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



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



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


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

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

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


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

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

小结


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


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

2024 年了! CSS 终于加入了 light-dark 函数!

web
一. 前言 随着 Web 技术的不断发展,用户体验成为了设计和开发过程中越来越重要的因素之一。为了更好地适应用户的视觉偏好,CSS 在 2024 年正式引入了一项新的功能 —— light-dark() 函数。 这项功能的加入主要在于简化网页对于浅色模式(Li...
继续阅读 »



一. 前言


随着 Web 技术的不断发展,用户体验成为了设计和开发过程中越来越重要的因素之一。为了更好地适应用户的视觉偏好,CSS 在 2024 年正式引入了一项新的功能 —— light-dark() 函数。


这项功能的加入主要在于简化网页对于浅色模式(Light Mode)与深色模式(Dark Mode)的支持,使得我们能够更快更轻松轻松地实现不同的主题切换。


接下来,我们就来详细了解一下我们在开发网页是如何实现主题切换的!


以下 Demo 示例,支持跟随系统模式和自定义切换主题,先一睹为快吧!


juejin6.gif


二. 传统方式


light-dark() 函数出现之前,开发者通常需要通过 JavaScript 或者 CSS 变量配合媒体查询来实现主题切换。例如:


使用 CSS 变量 + 媒体查询


开发者会定义一套 CSS 变量,然后基于用户的偏好设置(如:prefers-color-scheme: darkprefers-color-schema: light)来改变这些变量的值。


/* 默认模式 */
:root {
--background-color: white;
--text-color: black;
}

/* dark模式 */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #333;
--text-color: #fff;
}
}

也可以使用 JavaScript 监听主题切换


JavaScript 可以监听用户更改其操作系统级别的主题设置,并相应地更新网页中的类名或样式表链接。


// 检测是否启用了dark模式
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark-mode')
} else {
document.body.classList.remove('dark-mode')
}

以上这种方法虽然有效,但增加了代码复杂度,特别是当需要处理多个元素的颜色变化时,我们可能需要更多的代码来支持主题。


接下来我们看一下 light-dark 是如何实现的?


三. 什么是 light-dark?


image.png


light-dark() 是在 2024 年新加入的一种新的 CSS 函数,它允许我们根据用户的系统颜色方案(浅色或深色模式)来自动选择合适的颜色值。这个函数的引入简化了创建响应用户偏好主题的应用程序和网站的过程,而无需使用媒体查询或其他复杂的逻辑。


1. 基本用法


具体的说,light-dark() 函数接受两个参数,分别对应于浅色模式下的颜色值和深色模式下的颜色值。



  • 第一个参数是在浅色模式下使用的颜色。

  • 第二个参数是在深色模式下使用的颜色。


当用户的设备设置为浅色模式时,light-dark() 会返回第一个参数的颜色;当用户的设备设置为深色模式时,则返回第二个参数的颜色。


基本语法如下:


color: light-dark(浅色模式颜色, 深色模式颜色);

因此,light-dark() 提供了一种更简洁的方式来直接在 CSS 中指定两种模式下的颜色,而不需要额外的脚本或复杂的 CSS 结构。例如:


body {
background-color: light-dark(white, #333);
color: light-dark(black, #fff);
}

这里的 light-dark(白色, 深灰色) 表示如果用户处于浅色模式下,则背景色为白色;如果是深色模式,则背景色为深灰色。同样适用于文本颜色等其他属性。


2. 结合其他 CSS 特性


light-dark() 可以很好地与其他 CSS 特性结合使用,如变量、渐变等,以创造更加丰富多样的效果。当结合其他 CSS 特性使用 light-dark() 将更加灵活的创造页面的效果。


(1) 结合 CSS 变量


你可以利用 CSS 变量来存储颜色值,然后在 light-dark() 内引用这些变量,这样就能够在一处更改颜色方案并影响整个站点。


CSS 变量(也称为自定义属性)允许你存储可重复使用的值,这使得在不同的主题之间切换变得非常方便。你可以设置基础颜色变量,然后利用 light-dark() 来决定这些变量的具体值。


:root {
--primary-color: light-dark(#007bff, #6c757d);
--background-color: light-dark(white, #212529);
--text-color: light-dark(black, white);
}

body {
background-color: var(--background-color);
color: var(--text-color);
}

(2) 结合媒体查询


虽然 light-dark() 本身就可以根据系统偏好自动调整颜色,但有时候你可能还需要针对特定的屏幕尺寸或分辨率进行额外的样式调整。这时可以将 light-dark() 与媒体查询结合使用。


@media (max-width: 600px) {
body {
--button-bg: light-dark(#f8f9fa, #343a40); /* 更小的屏幕上按钮背景色 */
--button-text: light-dark(black, white);
}
button {
background-color: var(--button-bg);
color: var(--button-text);
}
}

(3) 结合伪类


light-dark() 也可以与伪类一起工作,比如 :hover, :focus 等,以实现不同状态下的颜色变化。


button {
background-color: light-dark(#007bff, #6c757d);
color: light-dark(white, black);
}

button:hover,
button:focus {
background-color: light-dark(#0056b3, #5a6268);
}

(4) 结合渐变


如果你希望在浅色模式和深色模式下使用不同的渐变效果,同样可以通过 light-dark() 来实现。


.header {
background: linear-gradient(light-dark(#e9ecef, #343a40), light-dark(#dee2e6, #495057));
}

(5) 结合阴影


对于元素的阴影效果,你也可以根据不同主题设置不同的阴影颜色和强度。


.box-shadow {
box-shadow: 0 4px 8px rgba(light-dark(0, 255), light-dark(0, 255), light-dark(0, 255), 0.1);
}

通过上述方法,你可以充分利用 light-dark() 函数的优势,并与其他 CSS 特性结合,创造出既美观又具有高度适应性的网页设计。这样不仅提高了用户体验,还简化了开发过程中的复杂度。


四. 兼容性


在 2024 年初时,light-dark() 函数作为 CSS 的一个新特性被加入到规范中,并且开始得到一些现代浏览器的支持。


image.png


其实,通过上图我们可以看到,light-dark() 在主流浏览器在大部分版本下都是支持了,所以我们可以放心的使用它。


但是同时我们也要注意,在一些较低的浏览器版本上仍然不被支持,比如 IE。因此,为了确保兼容性,在生产环境中使用该功能前需要检查目标浏览器是否支持这一特性。


如果浏览器不支持 light-dark(),可能需要提供回退方案,比如使用传统的媒体查询 @media (prefers-color-scheme: dark) 或者通过 JavaScript 来动态设置颜色。


五. 总结


通过本文,我们了解到,light-dark() 函数是 CSS 中的一个新特性,它允许开发者根据用户的系统偏好(浅色或深色模式)来自动切换颜色。


通过与传统模式开发深浅主题的比较,我们可以总结出 light-dark() 的优势应该包括:



  • 使用简洁:不需要编写额外的媒体查询,简洁高效。

  • 自动响应:能够随着系统的颜色方案改变而自动切换颜色。

  • 易于维护:所有与颜色相关的样式可以在同一处定义。

  • 减少代码量:相比使用多个媒体查询,可以显著减少 CSS 代码量。


light-dark() 函数是 CSS 领域的一项进步,它不仅简化了响应式设计的过程,也体现了对终端用户个性化体验的重视。随着越来越多的现代浏览器开始支持这一特性,我们未来可以在更多的应用场景中使用这一特性!


文档链接


light-dark


码上掘金演示


可以点击按钮切换主题,也可以切换系统的暗黑模式跟随:






🔥 我正在参加2024年度人气创作者评选,每投2票可以抽奖! 点击链接投票



作者:前端梦工厂
来源:juejin.cn/post/7443828372775764006
收起阅读 »

⚡聊天框 - 微信加载历史数据的效果原来这样实现的

web
前言 我记得2021年的时候做过聊天功能,那时业务也只限微信小程序 那时候的心路历程是: 卧槽,让我写一个聊天功能这么高大上?? 嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧 然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,...
继续阅读 »

前言


我记得2021年的时候做过聊天功能,那时业务也只限微信小程序


那时候的心路历程是:



卧槽,让我写一个聊天功能这么高大上??


嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧


然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,真的能做到微信的那种效果吗



然后一堆调研加测试,总算在小程序中查看历史记录没那么鬼畜了,但是总是感觉不是最佳解决方案。



那时打出的子弹,一直等到现在击中了我



最近又回想到了这个痛点,于是网上想看看有没有大佬发解决方案,结果还真被我找到了。


image.png


正文开始


1,效果展示


上才艺~~~


222.gif


2,聊天页面


2.1,查看历史聊天记录的坑


常规写法加载历史记录拼接到聊天主体的顶部后,滚动条会回到顶部、不在原聊天页面


直接上图


111.gif


而我们以往的解决方案也只是各种利用缓存scroll的滚动定位把回到顶部的滚动条重新拉回加载历史记录前的位置,好让我们可以继续在原聊天页面。


但即使我们做了很多优化,也会有安卓和苹果部分机型适配问题,还是不自然,可能会出现页面闪动


其实吧,解决方案只有两行css代码~~~


2.2,解决方案:flex神功


想优雅顺滑的在聊天框里查看历史记录,这两行css代码就是flex的这个翻转属性


dispaly:flex;
flex-direction: column-reverse

灵感来源~~~


333.gif


小伙伴可以看到,在加载更多数据时



滚动条位置没变、加载数据后还是原聊天页面的位置



这不就是我们之前的痛点吗~~~


所以,我们只需要翻转位置,用这个就可以优雅流畅的实现微信的加载历史记录啦


flex-direction: column-reverse


官方的意思:指定Flex容器中子元素的排列方向为列(从上到下),并且将其顺序反转(从底部到顶部)


如果感觉还是抽象,不好理解的话,那就直接上图,不加column-reverse的样子


image.png


加了column-reverse的样子


image.png


至此,我们用column-reverse再搭配data数据的位置处理就完美解决加载历史记录的历史性问题啦


代码放最后啦~~~


2.3,其他问题


2.3.1,数据过少时第一屏展示


因为用了翻转,数据少的时候会出现上图的问题


只需要.mainArea加上height:100%


然后额外写个适配盒子就行


flex-grow: 1; 
flex-shrink: 1;

image.png


2.3.2,用了scroll-view导致的问题


这一part是因为我用了uniappscroll-view组件导致的坑以及解决方案,小伙伴们没用这个组件的可忽略~~~


如下图,.mainArea使用了height:100%后,继承了父级高度后scroll-view滚动条消失了。


image.png


.mainArea去掉height:100%后scroll-view滚动条出现,但是第一屏数据过多时不会滚动到底部展示最新信息


image.png


解决方案:第一屏手动进行滚动条置顶


scrollBottom() {
if (this.firstLoad) return;
// 第一屏后不触发
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.select("#mainArea")
.boundingClientRect((data) => {
console.log(data);
if (data.height > +this.chatHeight) {
this.scrollTop = data.height; // 填写个较大的数
this.firstLoad = true;
}
})
.exec();
});
},

3,服务端


使用koa自己搭一个websocket服务端


3.1 服务端项目目录


image.png


package.json


{
"name": "websocketapi",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.14.2",
"koa-router": "^12.0.1",
"koa-websocket": "^7.0.0"
}
}


koa-tcp.js


const koa = require('koa')
const Router = require('koa-router')
const ws = require('koa-websocket')

const app = ws(new koa())
const router = new Router()

/**
* 服务端给客户端的聊天信息格式
* {
id: lastid,
showTime: 是否展示时间,
time: nowDate,
type: type,
userinfo: {
uid: this.myuid,
username: this.username,
face: this.avatar,
},
content: {
url:'',
text:'',
w:'',
h:''
},
}
消息数据队列的队头为最新消息,以次往下为老消息
客户端展示需要reverse(): 客户端聊天窗口最下面需要为最新消息,所以队列尾部为最新消息,以此往上为老消息
*/



router.all('/websocket/:id', async (ctx) => {
// const query = ctx.query
console.log(JSON.stringify(ctx.params))
ctx.websocket.send('我是小服,告诉你连接成功啦')
ctx.websocket.on('message', (res) => {
console.log(`服务端收到消息, ${res}`)
let data = JSON.parse(res)
if (data.type === 'chat') {
ctx.websocket.send(`我也会说${data.text}`)
}
})
ctx.websocket.on('close', () => {
console.log('服务端关闭')
})
})

// 将路由中间件添加到Koa应用中
app.ws.use(router.routes()).use(router.allowedMethods())

app.listen(9001, () => {
console.log('socket is connect')
})



切到server目录yarn


然后执行nodemon koa-tcp.js


没有nodemon的小伙伴要装一下


image.png


代码区


完整项目Github传送门


聊天页面的核心代码如下(包含data数据的位置处理和与服务端联动)



完结


这篇文章我尽力把我的笔记和想法放到这了,希望对小伙伴有帮助。


到这里,想给小伙伴分享两句话



现在搞不清楚的事,不妨可以先慢下来,不要让自己钻到牛角尖了


一些你现在觉得解决不了的事,可能需要换个角度



欢迎转载,但请注明来源。


最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。


image.png


作者:尘落笔记
来源:juejin.cn/post/7337114587123335180
收起阅读 »

在高德地图上实现建筑模型动态单体化

web
前言 前段时间系统性地学习了国内知名GIS平台的三维应用解决方案,受益匪浅,准备分几期对所学内容进行整理归纳和分享,今天来讲讲城市3D模型的处理方案。 城市3D模型图层在Web GIS平台中能够对地面物体进行单独选中、查询、空间分析等操作,是属于比较普遍的功能...
继续阅读 »

前言


前段时间系统性地学习了国内知名GIS平台的三维应用解决方案,受益匪浅,准备分几期对所学内容进行整理归纳和分享,今天来讲讲城市3D模型的处理方案。


城市3D模型图层在Web GIS平台中能够对地面物体进行单独选中、查询、空间分析等操作,是属于比较普遍的功能需求。这样的图层一般不会只有单一的数据来源,而是个集成了倾斜投影、BIM建模、动态参数建模的混合型图层,并且需要对模型进行适当的处理优化以提升其性能,因此在GIS平台不会选择支持单独地选中地图,我们需要对模型进行拆分——即单体化处理。


对建筑模型单体化处理通常有三种方案,各有其优点和适用场景,对比如下:


方案实现原理优势缺陷
切割单体化将三维模型与二维面进行切割操作成为单独可操作对象能够实现非常精细的单体化效果,适用于精细度要求高的场景数据处理量大,对计算机性能要求较高
ID 单体化预先为每个三维模型对象和二维矢量数据分配一个可关联的 ID数据处理相对简单,不需要进行复杂的几何运算,适用于数据量相对较小、模型结构相对简单的场景可能会出现 ID 管理困难的情况,且对于模型的几何形状变化适应性较差
动态单体化实时判断鼠标点击或选择操作的位置与三维模型的关系,动态地将选中的部分从整体模型中分离出来进行单独操作无需预先处理数据,可根据用户的交互实时进行单体化操作,适用于需要快速响应用户交互的场景对计算机的图形处理能力和性能要求较高

在这些方案中,动态单体化无需对模型进行预处理,依数据而变化使用较为灵活,接下来我们通过一个简单的例子来演示该方案的整体实现过程,源代码在这里供下载交流。


Honeycam_2024-08-18_16-31-38.gif


需求分析


假设我们拿到一个城市行政区域内的三维建筑数据,对里面该区域里面的几栋关键型建筑进行操作管理,我们需要把建筑模型放置到对应的地图位置上,可宏观地浏览模型;支持通过鼠标选中建筑的楼层,查看当前楼层的相关数据;楼层数据支持动态变更,且可以进行结构性存储。因此我们得到以下功能需求:



  1. 在Web地图上建立3D区域模型图层

  2. 根据当前光标位置动态高亮楼层,并展示楼层基本信息

  3. 建筑单体化数据为通用geoJSON格式,方便直接转换为csv或导入数据库


技术栈说明


工具名称版本用途
高德地图 JSAPI2.0为GIS平台提供基础底图和服务
three.js0.157主流webGL引擎之一,负责实现展示层面的功能
QGIS3.32.3GIS数据处理工具,用于处理本文的矢量化数据
cesiumlab3.1.11三维数据处理工具集,用于将模型转换为互联网可用的3DTiles
blender3.6模型处理工具,用于对BIM模型进行最简单的预处理

实现步骤


制作3DTiles


城市级的三维模型通常以无人机倾斜投影获取到的数据最为快捷,数据格式为OSGB且体积巨大不利于分享,由于手头没有合适的倾斜投影数据,我们以一个小型的BIM模型为例进行三维瓦片化处理也是一样的。



  1. 模型预处理。从sketchfab寻找到一个合适的建筑模型,下载其FBX格式并导入到模型处理工具(C4D、blender等)进行简单的预处理,调整模型的大小、重置坐标轴原点的位置到模型的几何中心,然后导出带材质的FBX模型备用,这里blender如何带材质地导出模型有一些技巧。


    image.png


  2. 启动cesiumlab,点击“通用模型切片”选项,选择预处理好的模型,指定它的地理位置(ENU: 维度,经度),点击确认


    image 1.png


  3. 在最后的“数据存储”设置原始坐标为打开、存储类型为散列(最终输出多个文件)、输出路径,提交处理等待3DTiles生成


    image 2.png


  4. 生成过程结束后我们来到分发服务选项,点击chrome的图标就能够进入3DTiles的预览了,注意看路径这一列,这里面包含了入口文件tileset.json的两个路径(文件存储目录和静态资源服务地址),后面开发中我们会用到它。


    image 3.png


  5. 至此模型准备完毕,我们可以把输出出的3Tiles目录放到开发i工程中,也可以单独部署为静态资源服务,保证tileset.json可访问即可。


    image 4.png


  6. 开发3DTiles图层,详细的教程之前已经分享过了,这里直接上代码。



    // 默认地图状态
    const mapConf = {
    name: '虚拟小区',
    tilesURL: '../static/tiles/small-town/tileset.json',
    //...
    }

    // 添加3DTiles图层
    async function initTilesLayer() {

    const layer = new TilesLayer({
    container,
    id: 'tilesLayer',
    map: getMap(),
    center: [113.536206, 22.799285],
    zooms: [4, 22],
    zoom: mapConf.zoom,
    tilesURL: mapConf.tilesURL,
    alone: false,
    interact: false
    })

    layer.on('complete', ({ scene }) => {
    // 调整模型的亮度
    const aLight = new THREE.AmbientLight(0xffffff, 3.0)
    scene.add(aLight)
    })

    layerManger.add(layer)
    }


  7. 这一阶段实现的效果如下


    Honeycam_2024-08-18_17-45-29.gif



创建单体化数据



  1. 使用QGIS处理矢量数据,绘制建筑模型轮廓的矢量面。由于本示例是虚拟的,我们需要自己创建矢量数据,把上一个步骤完成的内容高空垂直截图导入到QGIS上配准位置,作为描绘的参考图。


    image 5.png


  2. 创建形状文件图层,进入编辑模式绘制建筑轮廓


    image 6.png


  3. 选择图层右键打开属性表,开始编辑每个建筑的基础数据,导出为monobuildingexample1.geojson


    image 7.png


  4. 对关键建筑“商业办公楼A”和“商业办公楼B”的楼层数据进行进一步编辑(bottomAltitude为每层楼的离地高度,extendAltitude为楼层高度),这块数据与GIS无关,我直接用wps图表去做了,完成后将csv文件转换成为json格式,然后与monobuildingexample1.geojson做一个组合,得到最终的geoJSON数据。


    image 8.png



开发动态单体化图层


底座和数据准备好,终于可以进行动态单体化图层开发了,实现原理其实非常简单,根据上一步获得的建筑矢量和楼层高度数据,我们就可以在于模型匹配的地理位置上创建若干个“罩住”楼栋模型的盒状网格体,并监听网格体的鼠标拾取状态,即可实现楼层单体化交互。


image 9.png



  1. 我们的数据来自monobuildingexample1.geojson,生成每个楼层侧面包围盒的核心代码如下,通过path数据和bottomAltitued、extendAltitude就能得到网格体的所有顶点。



    /**
    * 根据路线创建侧面几何面
    * @param {Array} path [[x,y],[x,y],[x,y]...] 路线数据
    * @param {Number} height 几何面高度,默认为0
    * @returns {THREE.BufferGeometry}
    */

    createSideGeometry (path, region) {
    if (path instanceof Array === false) {
    throw 'createSideGeometry: path must be array'
    }
    const { id, bottomAltitude, extendAltitude } = region

    // 保持path的路线是闭合的
    if (path[0].toString() !== path[path.length - 1].toString()) {
    path.push(path[0])
    }

    const vec3List = [] // 顶点数组
    let faceList = [] // 三角面数组
    let faceVertexUvs = [] // 面的UV层队列,用于纹理和几何信息映射

    const t0 = [0, 0]
    const t1 = [1, 0]
    const t2 = [1, 1]
    const t3 = [0, 1]

    for (let i = 0; i < path.length; i++) {
    const [x1, y1] = path[i]
    vec3List.push([x1, y1, bottomAltitude])
    vec3List.push([x1, y1, bottomAltitude + extendAltitude])
    }

    for (let i = 0; i < vec3List.length - 2; i++) {
    if (i % 2 === 0) {
    // 下三角
    faceList = [
    ...faceList,
    ...vec3List[i],
    ...vec3List[i + 2],
    ...vec3List[i + 1]
    ]
    // UV
    faceVertexUvs = [...faceVertexUvs, ...t0, ...t1, ...t3]
    } else {
    // 上三角
    faceList = [
    ...faceList,
    ...vec3List[i],
    ...vec3List[i + 1],
    ...vec3List[i + 2]
    ]
    // UV
    faceVertexUvs = [...faceVertexUvs, ...t3, ...t1, ...t2]
    }
    }

    const geometry = new THREE.BufferGeometry()
    // 顶点三角面
    geometry.setAttribute(
    'position',
    new THREE.BufferAttribute(new Float32Array(faceList), 3)
    )
    // UV面
    geometry.setAttribute(
    'uv',
    new THREE.BufferAttribute(new Float32Array(faceVertexUvs), 2)
    )

    return geometry
    }


  2. 经过前面步骤,得到网格体如下


    Honeycam_2024-08-18_18-24-35.gif


  3. 添加默认状态和选中状态下材质


    initMaterial () {
    const { initial, hover } = this._conf.style
    // 顶部材质
    this._mt = {}
    this._mt.initial = new THREE.MeshBasicMaterial({
    color: initial.color,
    transparent: true,
    opacity: initial.opacity,
    side: THREE.DoubleSide,
    wireframe: true
    })
    this._mt.hover = new THREE.MeshBasicMaterial({
    color: hover.color,
    transparent: true,
    opacity: hover.opacity,
    side: THREE.DoubleSide
    })
    }


  4. 添加拾取事件,对选中的网格体Mesh设置选中材质,并对外派发事件


    // 处理拾取事件
    onPicked ({ targets, event }) {
    let attrs = null
    if (targets.length > 0) {
    const cMesh = targets[0]?.object
    if (cMesh?.type == 'Mesh') {
    // 设置选中状态
    this.setLastPick(cMesh)
    attrs = cMesh._attrs
    } else {
    // 移除选中状态
    this.removeLastPick()
    }
    } else {
    this.removeLastPick()
    }
    /**
    * 外派模型拾取事件
    * @event ModelLayer#pick
    * @type {object}
    * @property {Number} screenX 图层场景
    * @property {Number} screenY 图层相机
    * @property {Object} attrs 模型属性
    */

    this.handleEvent('pick', {
    screenX: event?.pixel?.x,
    screenY: event?.pixel?.y,
    attrs
    })
    }


  5. 外部监听到拾取事件,调动浮层展示详情


    /**
    * 建筑单体化图层
    * @return {Promise<void>}
    */

    async function initMonoBuilding() {
    const data = await fetchData('../static/mock/monobuildingexample1.geojson')
    const layer = new MonoBuildingLayer({
    //...
    data
    })
    layerManger.add(layer)

    layer.on('pick', (event) => {
    updateMarker(event)
    })
    }
    // 更新浮标
    function updateMarker(event) {
    const { screenX, screenY, attrs } = event

    if (attrs) {
    // 更新信息浮层
    const { id, name, belong, bottomAltitude, extendAltitude } = attrs
    tip.style.left = screenX + 20 + 'px'
    tip.style.top = screenY + 10 + 'px'
    tip.innerHTML = `
    <ul>
    <li>id: ${id}</li>
    <li>楼层: ${name}</li>
    <li>离地高度: ${bottomAltitude}米</li>
    <li>楼层高度: ${extendAltitude}米</li>
    <li>所属: ${belong}</li>
    </ul>
    `

    tip.style.display = 'block'
    // 更新鼠标手势
    container.classList.add('mouse_hover')
    } else {
    tip.style.display = 'none'
    container.classList.remove('mouse_hover')
    }
    }


  6. 最终得到的交互效果如下


    Honeycam_2024-08-18_18-56-30.gif


  7. 把3DTiles图层和点标记图层加上叠加显示,得到本示例最终效果


    Honeycam_2024-08-18_19-03-00.gif



待拓展功能



  1. 对建筑模型单体的进一步细化


    楼层功能还可以细化到每个楼层中各个户型,也许每个楼层都有独特的户型分布图,这个应该结合内部的墙体轮廓一起展示,选个弹窗在子内容页进行下一步操作,还是直在当前场景下钻到楼层内部?具体交互流程我还没想好。


  2. 如何处理异体模型


    目前的方案仅针对规规矩矩的立方体建筑楼栋,而对于鸟巢、大裤衩、小蛮腰之类的异形地标性建筑,每个楼层的轮廓可能都是不一样的,因此在数据和代码方面仍需再做改进。


    image 10.png



本示例使用到的高德JSAPI


3D自定义图层AMap.GLCustomLayer


AMap.Map地图对象类


点标记: 用于在地图上添加点状地图要素


空间数据计算的函数库 GeometryUtil


相关工具链接


Sketchfab上免费下载的小区模型


使用blender导出带材质的FBX文件


在线将cvs文件转换为JSON


作者:Gyrate
来源:juejin.cn/post/7404007685643501595
收起阅读 »

震惊,开源项目vant 2.13.5 被投毒,挖矿!

web
2024年12月19日,vant仓库新增一条issue,vant 2.13.5 被投毒,挖矿。 具体原因 可能是团队一名成员的 token 被盗用 与本次事件关联的攻击 攻击者在利用 @landluck 的 token 进行攻击后,进一步拿到了同个 GitHu...
继续阅读 »

2024年12月19日,vant仓库新增一条issue,vant 2.13.5 被投毒,挖矿


具体原因


可能是团队一名成员的 token 被盗用


与本次事件关联的攻击


攻击者在利用 @landluck 的 token 进行攻击后,进一步拿到了同个 GitHub 组织下的维护者 @chenjiahan 的 token,并发布了带有相同恶意代码的 Rspack 1.1.7 版本。


Rspack 团队已经在一小时内完成该版本的废弃处理,并发布了 1.1.8 修复版本,参考 web-infra-dev/rspack#8767 (comment)


目前相关 token 已经全部清理。


相关版本


以下异常版本被盗号者注入了脚本,已经全部标记为废弃,请勿使用!


image.png


有使用的大家可以升级版本,降低影响。


作者:一诺滚雪球
来源:juejin.cn/post/7450001084067627058
收起阅读 »

禁止调试,阻止浏览器F12开发者工具

web
写在前面 这两天突然想看看文心一言的http通信请求接口,于是想着用F12看看。 谁知道刚打开开发者工具,居然被动debugger了。 直接被JS写死的debugger关键字下了断点。行吧,不让调试就不让调试吧,关闭开发者工具之后,直接跳到了空白页。 其...
继续阅读 »

写在前面


这两天突然想看看文心一言的http通信请求接口,于是想着用F12看看。



谁知道刚打开开发者工具,居然被动debugger了。



直接被JS写死的debugger关键字下了断点。行吧,不让调试就不让调试吧,关闭开发者工具之后,直接跳到了空白页。



其实几年之前就碰到过类似的情况,不过当时才学疏浅,也没当回事,就没研究过。这次又碰到了,毕竟已经不是当年的我了,于是便来研究研究。


分析


大家都知道浏览器的开发者工具能干啥,正经的用法:开发时调试代码逻辑,修改布局样式;不正经的用法:改改元素骗骗人,找找网站接口写爬虫,逆向js破解加密等等,所以说前端不安全,永远不要相信用户的输入。


而这次碰到的这个情况确实可以在用户端做一些防御操作,但是也可以绕过。 (PS:感谢评论区大佬指教:开发者工具Ctrl+F8可以禁用断点调试,学到了)


先做一波分析。


首先,防止你用F12调试,先用debugger关键字阻止你进行任何操作。随后,在你关闭之后,又直接跳转到空白页,不让你接着操作。


这就需要一个开发者工具检测的机制了,发现你打开了开发者工具,就给你跳走到空白页。


所以,关键就是要实现开发者工具的检测。


实现


经过查阅一番,发现原来这个debugger可能并不仅仅是阻止你进行调试的功能,同时还兼具判断开发者工具是否打开的作用。怎么实现?


debugger本身只是调试,阻止你继续对前端进行调试,但是代码中并不知道用户是否打开了开发者工具,所以就无法进行更进一步的操作,例如文心一言的跳转到空白页。


但是,有一点,你打开开发者工具之后,debugger下了断点,程序就停到那里了,如果你不打开开发者工具,程序是不会停止到断点的。没错,这就是我们可以判断的方式,时间间隔。正常情况下debugger前后的时间间隔可以忽略不计。但是,当你打开开发者工具之后,这个时间间隔就产生了,判断这个时间间隔,就可以知道是否打开了开发者工具。


直接上示例代码


<!DOCTYPE html>
<html>
<header>
<title>test</title>
</header>
<body>
<h1>test</h1>
</body>
<script>
setInterval(function() {

var startTime = performance.now();
// 设置断点
debugger;
var endTime = performance.now();
// 设置一个阈值,例如100毫秒
if (endTime - startTime > 100) {
window.location.href = 'about:blank';
}

}, 100);

</script>

</html>

通过设置一个定时循环任务来进行检测。


在不打开发者工具的情况下,debugger是不会执行将页面卡住,而恰恰是利用debugger的这一点,如果你打开开发者工具一定会被debugger卡住,那么上下文时间间隔就会增加,在对时间间隔进行判断,就能巧妙的知道绝对开了开发者工具,随后直接跳转到空白页,一气呵成。


测试



现在来进行测试,打开F12


关闭开发者工具。



完美!


写在后面


这样确实可以阻挡住通过在开发者工具上获取信息,但是仅仅是浏览器场景。我想要拿到对话的api接口也不是只有这一种方法。


感谢评论区大佬指教:开发者工具Ctrl+F8可以禁用断点调试


或者说,开个代理抓包不好吗?hhh



作者:银空飞羽
来源:juejin.cn/post/7337188759055663119
收起阅读 »

Cesium从入门到入坟

web
大扎好,我系渣渣辉,斯一扔,介四里没有挽过的船新版本,挤需体验三番钟,里造会干我一样,爱象节款js裤 Cesium 概述 Cesium是国外一个基于JavaScript编写的使用WebGL的地图引擎,支持3D,2D,2.5D形式的地图展示,也是目前最流行的三...
继续阅读 »

大扎好,我系渣渣辉,斯一扔,介四里没有挽过的船新版本,挤需体验三番钟,里造会干我一样,爱象节款js裤



Cesium 概述


Cesium是国外一个基于JavaScript编写的使用WebGL的地图引擎,支持3D,2D,2.5D形式的地图展示,也是目前最流行的三维数字地球渲染引擎。


Cesium 基础介绍


首先我们需要登录上Cesium的官网,网址是 cesium.com/ ,获取源代码可以在Platform菜单项的Downloads中下载 。
接下来,第一个比较重要的事情就是我们需要注册一个免费账户以获取Cesium世界地形资产所需的访问令牌,而这个账户的token决定了哪些资产咱们可以使用;而第二个比较重要的事情就是Cesium的文档中心( cesium.com/learn/cesiu… ),我们在实际使用的过程中会经常来查阅这些API。


Cesium 的使用


由于我是使用的vue-cli生成的项目,所以直接安装vite-plugin-cesium依赖项,当然你也可以使用直接下载源码,在HTML中引入的方式。如果使用的是vite-plugin-cesium,你还需要在vite.config.ts中添加一下Cesium的引用。


import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import VueDevTools from 'vite-plugin-vue-devtools'
// add this line
import cesium from 'vite-plugin-cesium';

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
VueDevTools(),
// add this line
cesium()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

初始化地球


<script setup lang="ts">
import { onMounted } from 'vue'
import * as Cesium from 'cesium'
onMounted(() => {
const defaultToken = 'your access token'
Cesium.Ion.defaultAccessToken = defaultToken
const viewer = new Cesium.Viewer('cesiumContainer', {
//这里是配置项
})
})
</script>

<template>
<div id="cesiumContainer" class="cesium-container"></div>
</template>


<style scoped>
#cesiumContainer {
width: 100vw;
height: 100vh;
}
</style>


效果如下:


1.gif


现在我们就可以看到Cesium生成的地球了,可以对其进行二维和三维状态的切换,也可以用其自带的播放器,对时间轴进行一个播放,支持正放和倒放,Cesium还自带了搜索地理位置组件,并且兼容了中文。


Cesium 常用的类


1. Viewer


它是Cesium展示三维要素内容的主要窗口,不仅仅包含了三维地球的视窗,还包含了一些基础控件,在定义Viewer对象的时候需要设定基础部件、图层等的初始化状态,下面演示一下部分属性的使用。


  const viewer = new Cesium.Viewer('cesiumContainer', {
// 这里是配置项
// 动画播放控件
animation: false,
// 时间轴控件
timeline: false,
// 全屏按钮
fullscreenButton: true,
// 搜索位置按钮
geocoder: true,
// 帮助按钮
navigationHelpButton: false,
// VR按钮
vrButton: true
})

除了上述的控件属性之外,还有entities这种实体合集属性,主要用于加载实体模型,几何图形并对其进行样式设置,动效修改等,我们可以通过下述代码生成一个绿色的圆点。


const entity = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 400),
point: {
pixelSize: 100,
color: new Cesium.Color(0, 1, 0, 1)
}
})
viewer.trackedEntity = entity

效果如下:
image.png


当然,我们也可以用entities来加载模型文件,下面我们用飞机模型试试


  /** 通过entities加载一个飞机模型 */
const orientation = Cesium.Transforms.headingPitchRollQuaternion(
position,
new Cesium.HeadingPitchRoll(-90, 0, 0)
)
const entity = viewer.entities.add({
position: position,
orientation: orientation,
model: {
uri: '/Cesium_Air.glb',
minimumPixelSize: 100,
maximumScale: 10000,
show: true
}
})
viewer.trackedEntity = entity

效果如下:


3.jpg


2. Camera


Cesium中可以通过相机来描述和操作场景的视角,而通过相机Camera操作场景的视角还有下面的几种方法



  • 飞行fly,比如flyTo,flyHome,flyToBoundingSphere

  • 缩放zoom,比如zoomIn,zoomOut

  • 移动move,比如moveBackward,moveDown,moveForward,moveLeft,moveRight,moveUp

  • 视角look,比如lookDown,lookLeft,lookRight,lookUp

  • 扭转twist,比如twistLeft,twistRight

  • 旋转rotate,比如rotateDown,rotateLeft,rotateRight,rotateUp

  • 其他方法,比如setView,lookAt


viewer.scene.camera.setView({
// 设定相机的目的地
destination: position,
// 设定相机视口的方向
orientation: {
// 控制视口方向的水平旋转,即沿着Y轴旋转
heading: Cesium.Math.toRadians(0),
// 控制视口方向的上下旋转,即沿着X轴旋转
pitch: Cesium.Math.toRadians(-20),
// 控制视口的翻转角度,即沿着Z轴旋转
roll: 0
}
})

我们尝试使用setView后可以发现,相机视角直接被定位到了下图的位置


1.jpg


3. DataSourceCollection


DataSourceCollection是Cesium中加载矢量数据的主要方式之一,它最大的特点是支持加载矢量数据集和外部文件的调用,主要有三种调用方法,分别为 CzmlDataSourceKmlDataSourceGeoJsonDataSource,分别对应加载Czml,Kml,GeoJSON格式的数据,在使用过程中我们只需要将矢量数据转换为以上任意一种格式就可以在Cesium中实现矢量数据的加载和存取。


  viewer.dataSources.add(Cesium.GeoJsonDataSource.load('/ne_10m_us_states.topojson'))

效果如下:
2.jpg
这时候我们看到图层已经被加载上去了~


Cesium的坐标体系


通过上面的示例我们可以得知Cesium具有真实地理坐标的三维球体,但是用户是通过二维屏幕与Cesium进行操作的,假设我们需要将一个三维模型绘制到三维球体上,我们就需要再地理坐标和屏幕坐标之间做转换,而这就需要涉及到Cesium的坐标体系。


Cesium主要有5种坐标系:



  • WGS84经纬度坐标系

  • WGS84弧度坐标系

  • 笛卡尔空间直角坐标系

  • 平面坐标系

  • 4D笛卡尔坐标系


他们的基础概念大家感兴趣的可以百度查阅一下,我也说不太清楚,问我他们的区别我也只能用 恰特鸡屁踢 敷衍你,下面我们演示一下怎么将WGS84左边西转换为笛卡尔空间直角坐标系:


const cartesian3 = Cesium.Cartesian3.fromDegrees(longitude, latitude, height)

我们可以通过经纬度进行转换,当然我们还有其他的方式,比如Cesium.Cartesian3.fromDegreesArray(coordinates),这里的coordinates格式为不带高度的数组。


Cesium加载地图和地形


加载地图


我们使用ArcGis地图服务来加载新地图,Cesium也给其提供了相关的加载方法:


  const esri = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
)
/** 这里是配置项 */
const viewer = new Cesium.Viewer('cesiumContainer', {
baseLayerPicker: false,
})
// 加载ArcGis地图
viewer.imageryLayers.addImageryProvider(esri)

效果如下:


4.jpg


我们再来看一下之前的地球效果来对比对比:


5.jpg


可以明显看出来ArcGisMapServer提供的地图更加的清晰和立体。



注:加载ArcGis地图服务请使用我上述提供的代码,从Cesium中文网看到的示例代码可能很久没更新了,使用会报错~



当然我们还可以加载一些特定场景的地图,比如夜晚的地球,官网上直接给出了示例代码:


// addImageryProvider方法用于添加一个新的图层
viewer.imageryLayers.addImageryProvider(await Cesium.IonImageryProvider.fromAssetId(3812))

效果如下:


6.jpg


加载地形


我们回到刚刚的ArcGis地图,我们进入到地球内部查看一些山脉,会发现从俯视角度来看山脉是有轮廓的,但是当我们旋转相机后会发现,实际上地球表面是平的,并没有显示出地形,效果如下:


7.jpg


这时候我们就需要加载出地形数据了


const esri = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
)
/** 这里是配置项 */
const viewer = new Cesium.Viewer('cesiumContainer', {
baseLayerPicker: false,
terrainProvider: await Cesium.CesiumTerrainProvider.fromIonAssetId(1, {
// 可以增加法线,用于提高光照效果
requestVertexNormals: true,
// 可以增加水面特效
requestWaterMask: true
})
})
// 加载ArcGis地图
viewer.imageryLayers.addImageryProvider(esri)

效果如下:


2.gif


8.jpg


可以看到原先的平面通过加载了地形数据,已经有了山势起伏,河流湖泊~


Cesium加载建筑体


我们在实际开发中,比如搭建一个智慧城市,光有地图和地形是远远不够的,还需要加载城市中的建筑模型信息,这时候我们就需要用到Cesium中建筑体的添加和使用的相关功能了,我们以官网的纽约市的模型数据为例:


  /** 添加建筑物 */
const tileset = viewer.scene.primitives.add(await Cesium.Cesium3DTileset.fromIonAssetId(75343))
/** 添加相机信息 */
const position = Cesium.Cartesian3.fromDegrees(-74.006, 40.7128, 100)
viewer.camera.setView({
destination: position,
orientation: {
heading: 0,
pitch: 0,
roll: 0.0
}
})

效果如下:


9.jpg


我们看到纽约市建筑物的数据已经加载出来了,但是看起来都是白白的过于单调,我们还可以通过style来修改建筑物的样式


tileset.style = new Cesium.Cesium3DTileStyle({
color: {
conditions: [
['${Height} >= 300', 'rgba(45,0,75,0.5)'],
['${Height} >= 100', 'rgb(170,162,204)'],
['${Height} >= 50', 'rgb(102,71,151)'],
['true', 'rgb(127,59,8)']
]
},
show: '${Height} > 0',
meta: {
description: '"Building id ${id} has height ${Height}."'
}
})

现在我们再来看一下效果:


10.jpg


可以看出我们根据建筑物的不同高度,设定了不同的颜色,比如超过300米的建筑就带有透明效果了,比较上图的效果更有层次感。


最后


关于Cesium我也是初窥门径,具体的学习和使用大家还是要以 英文官网 为准,中文网上很多都过时了,使用的时候可能会报错,我已经帮大家踩好坑了😭,也欢迎大家在评论区里多沟通交流,互相学习~


WechatIMG24.jpg


作者:魔术师Grace
来源:juejin.cn/post/7392558409742925874
收起阅读 »

震惊!🐿浏览器居然下毒!

web
发生什么事了 某天,我正在愉快的摸鱼,然后我看到测试给我发了条消息,说我们这个系统在UC浏览器中有问题,没办法操作,点了经常没反应(测试用得iPhone14,是一个h5的项目)。我直接懵了,这不是都测了好久了吗,虽然不是在uc上测得,chrome、safari...
继续阅读 »

发生什么事了


某天,我正在愉快的摸鱼,然后我看到测试给我发了条消息,说我们这个系统在UC浏览器中有问题,没办法操作,点了经常没反应(测试用得iPhone14,是一个h5的项目)。我直接懵了,这不是都测了好久了吗,虽然不是在uc上测得,chrome、safari、自带浏览器等,都没这个问题,代码应该是没问题的,uc上为啥会没有反应呢?难道是有什么隐藏的bug,需要一定的操作顺序才能触发?我就去找了测试,让他重新操作一下,看看是啥样的没反应。结果就是,正常进入列表页(首页),正常点某一项,正常进入详情页,然后点左上角返回,没反应。我上手试了下,确实,打开vconsole看了下,也没有报错。在uc上看起来还是必现的,我都麻了,这能是啥引起的啊。


找问题


在其他浏览器上都是好好的,uc上也不报错,完全看不出来代码有啥bug,完全没有头绪啊!那怎么办,刷新看看:遇事不决,先刷新,还不行就清空缓存刷新。刷新了之后,哎,好了!虽然不知道是什么问题,但现在已经好了,就当作遇到了灵异事件,我就去做其他事了。


过了一会,测试来找我了,说又出现了,不止是详情页,进其他页面也返回不了。这就难受住了呀,说明肯定是有问题的,只是还没找到原因。我就只好打开vconsole,一遍一遍的进入详情页点返回;刷新再进;清掉缓存,再进。


然后,我就发现,network中,出现了一个没有见过的请求


20240906-222447.png


20240906-222456.png
根据track、collect这些单词来判断,这应该是uc在跟踪、记录某些操作,requestType还是个ping;我就在想,难道是这个请求的问题?但是请求为啥会导致,我页面跳转产生问题?然后我又看到了intercept(拦截)、pushState(history添加记录)拦截了pushState?

这个项目确实使用的是history路由,问题也确实出在路由跳转的时候;而且出现问题的时候,路由跳转,浏览器地址栏中的地址是没有变化的,返回就g了(看起来是后退没有反应,实际是前进时G了)。这样看,uc确实拦截了pushState的操作。那它是咋做到的?


原来如此


然后,我想起来,前段时间在掘金上看到了一篇,讲某些第三方cdn投毒的事情,那么uc是不是在我们不知情的情况下,改了我们的代码。然后我切到了vconsole的element,展开head,发现了一个不属于我们项目的script,外链引入了一段js,就挂在head的最顶上。通过阅读,发现它在window的history上加了点料覆写了forward和pushState(forward和pushState是继承来的方法)

正常的history应该是这样:


image.png
复写的类似这样:


image.png
当然,有些系统或框架,为了实现某些功能,比如实现触发popstate的效果,也会复写

但uc是纯纯的为了记录你的操作,它这玩意主要还有bug,会导致路由跳转出问题,真是闹麻了


如何做


删掉就好了,只要删掉uc添加的,当我们调用相关方法时,history就会去继承里找


// 判断是否是uc浏览器
if (navigator.userAgent.indexOf('UCBrowser') > -1) {
if (history.hasOwnProperty('pushState')) {
delete window.history.forward
delete window.history.pushState
}
// 找到注入的script
const ucScript = document.querySelector('script[src*="ucbrowser_script"]')
if (ucScript) {
document.head.removeChild(ucScript)}
}
}

吐槽


你说你一个搞浏览器的,就不能在底层去记录用户行为吗,还不容易被发现。主要是你这玩意它有bug呀,这不是更容易被发现吗。(这是23年11月份遇到的问题,当时产品要求在qq/百度/uc这些浏览器上也都测一下才发现的,现在记录一下,希望能帮助到其他同学)


作者:忍者扔飞镖
来源:juejin.cn/post/7411358506048766006
收起阅读 »

耗时6个月做的可视化大屏编辑器, 开源!

web
hi, 大家好, 我是徐小夕.  5年前就开始着手设计和研发可视化大屏编辑器, 当时低代码在国内还没有现在那么火, 有人欢喜有人哀, 那个时候我就比较坚定的认为无码化搭建未来一定是个趋势, 能极大的帮助企业提高研发效率和降低研发成本, 所以 all in 做了...
继续阅读 »

hi, 大家好, 我是徐小夕. 


5年前就开始着手设计和研发可视化大屏编辑器, 当时低代码在国内还没有现在那么火, 有人欢喜有人哀, 那个时候我就比较坚定的认为无码化搭建未来一定是个趋势, 能极大的帮助企业提高研发效率和降低研发成本, 所以 all in 做了2年, 上线了一个相对闭环的MVP可视化大屏搭建平台——V6.Dooring.


图片


通过在技术社区不断的分享可视化搭建的技术实践和设计思路, 也参与了很多线上线下的技术分享, 慢慢市场终于“热了”起来.(机缘巧合)


V6.Dooring的技术架构的设计, 到团队组建, 再到帮助企业做解决方案, 当时几乎所有的周末都花在这上面了, 想想收获还是挺大的, 接触到了形形色色的企业需求, 也不断完整着可视化大屏编辑器的功能, 最后推出了一个还算通用的解决方案:


图片


当然上面介绍的还都不是这篇文章的重点.


重点是, 时隔4年, 我们打算把通用的可视化大屏解决方案, 开源! 


一方面是供大家学习参考, 更好的解决企业自身的业务需求, 另一方面可以提供一个技术交流的平台, 大家可以对可视化搭建领域的技术实践, 提出自己的想法和观点, 共同打造智能化, 体验更好的搭建产品.


先上github地址: github.com/MrXujiang/v…


V6.Dooring开源大屏编辑器演示


图片


其实最近几年我在掘金专栏分享了很多零代码可视化搭建的技术实现和产品设计:


可视化低代码技术精粹


 这里为了让大家更近一步了解V6-Dooring可视化大屏编辑器, 我还是会从技术设计产品解决方案设计的角度, 和大家详细分享一下, 让大家在学习我们可视化大屏开源方案的过程中, 对可视化搭建技术产品, 有更深入的理解.


如果大家觉得有帮助, 不要忘记点赞 + 收藏哦, 后面我会持续分享最干的互联网干货.


你将收获



  • 可视化大屏产品设计思路

  • 主流可视化图表库技术选型

  • 大屏编辑器设计思路

  • 大屏可视化编辑器Schema设计

  • 用户数据自治探索


方案实现


1.可视化大屏产品设计思路


目前很多企业或多或少的面临“信息孤岛”问题,各个系统平台之间的数据无法实现互通共享,难以实现一体化的数据分析和实时呈现。


相比于传统手工定制的图表与数据仪表盘,可视化大屏制作平台的出现,可以打破抵消的定制开发, 数据分散的问题,通过数据采集、清洗、分析到直观实时的数据可视化展现,能够多方位、多角度、全景展现各项指标,实时监控,动态一目了然。


针对以上需求, 我们设计了一套可视化大屏解决方案, 具体包含如下几点:


图片


上图是笔者4个月前设计的基本草图, 后期会持续更新. 通过以上的设计分解, 我们基本可以搭建一个可自己定制的数据大屏.


2.主流可视化图表库技术选型


图片


目前我调研的已知主流可视化库有:



  • echart 一个基于 JavaScript 的老牌开源可视化图表库

  • D3.js 一个数据驱动的可视化库, 可以不需要其他任何框架独立运行在现代浏览器中,它结合强大的可视化组件来驱动 DOM 操作

  • antv 包含一套完整的可视化组件体系

  • Chart.js 基于 HTML5 的 简单易用的 JavaScript 图表库

  • metrics-graphics 建立在D3之上的可视化库, 针对可视化和布置时间序列数据进行了优化

  • C3.js 通过包装构造整个图表所需的代码,使生成基于D3的图表变得容易


我们使用以上任何一个库都可以实现我们的可视化大屏搭建的需求, 各位可以根据喜好来选择.


3.大屏编辑器设计思路


在上面的分析中我们知道一个大屏编辑器需要有个编辑器核心, 主要包含以下部分:



  • 组件库

  • 拖拽(自由拖拽, 参考线, 自动提示)

  • 画布渲染器

  • 属性编辑器


如下图所示:


图片


组件库我们可以用任何组件封装方式(react/vue等), 这里沿用H5-Dooring的可视化组件设计方式, 对组件模型进行优化和设计.


类似的代码如下:


import { Chart } from '@antv/f2';
import React, { memo, useEffect, useRef } from 'react';

import styles from './index.less';
import { IChartConfig } from './schema';

const XChart = (props:IChartConfig) => {
  const { data, color, size, paddingTop, title } = props;
  const chartRef = useRef(null);
  useEffect(() => {
      const chart = new Chart({
        el: chartRef.current || undefined,
        pixelRatiowindow.devicePixelRatio// 指定分辨率
      });

      // step 2: 处理数据
      const dataX = data.map(item => ({ ...item, valueNumber(item.value) }));

      // Step 2: 载入数据源
      chart.source(dataX);

      // Step 3:创建图形语法,绘制柱状图,由 genre 和 sold 两个属性决定图形位置,genre 映射至 x 轴,sold 映射至 y 轴
      chart
        .interval()
        .position('name*value')
        .color('name');

      // Step 4: 渲染图表
      chart.render();
  }, [data]);
  return (
    <div className={styles.chartWrap}>
      <div className={styles.chartTitle} style={{ colorfontSize: sizepaddingTop }}>
        {title}
      </div>
      <canvas ref={chartRef}></canvas>
    </div>

  );
};

export default memo(XChart);

以上只是一个简单的例子, 更具业务需求的复杂度我们往往会做更多的控制, 比如动画(animation), 事件(event), 数据获取(data inject)等.


图片


当然实际应用中大屏展现的内容和形式远比这复杂, 我们从上图可以提炼出大屏页面的2个直观特征:



  • 可视化组件集

  • 空间坐标关系


因为我们可视化大屏载体是页面, 是html, 所以还有另外一个特征: 事件/交互。综上我们总结出了可视化大屏的必备要素:


图片


我们只要充分的理解了可视化大屏的组成和特征, 我们才能更好的设计可视化大屏搭建引擎, 基于以上分析, 我设计了一张基础引擎的架构图:


图片


接下来我就带大家一起来拆解并实现上面的搭建引擎。


大屏搭建引擎核心功能实现


俗话说: “好的拆解是成功的一半”, 任何一个复杂任务或者系统, 我们只要能将其拆解成很多细小的子模块, 就能很好的解决并实现它. (学习也是一样)


接下来我们就逐一解决上述基础引擎的几个核心子模块:



  • 拖拽器实现

  • 物料中心设计

  • 动态渲染器实现

  • 配置面板设计

  • 控制中心概述

  • 功能辅助设计


1.拖拽器实现


拖拽器是可视化搭建引擎的核心模块, 也是用来解决上述提到的大屏页面特征中的“空间坐标关系”这一问题。我们先来看一下实现效果:


图片


组件拖拽可以采用市面已有的 Dragable 等插件, 也可以采用 H5-Dooring 的智能网格拖拽. 这里笔者选择自由拖拽来实现. 已有的有:



  • rc-drag

  • sortablejs

  • react-dnd

  • react-dragable

  • vue-dragable


等等. 具体拖拽呈现流程如下:


图片


具体拖拽流程就是:



  1. 使用H5 dragable API拖拽左侧组件(component data)进入目标容器(targetBox)

  2. 监听拖拽结束事件拿到拖拽事件传递的data来渲染真实的可视化组件

  3. 可视化组件挂载, schema注入编辑面板, 编辑面板渲染组件属性编辑器

  4. 拖拽, 属性修改, 更新

  5. 预览, 发布


组件的schema参考H5-Dooring DSL设计.


2.物料中心设计


物料中心主要为大屏页面提供 “原材料”。为了设计健壮且通用的物料, 我们需要设计一套标准组件结构和属性协议。并且为了方便物料管理和查询, 我们还需要对物料进行分类, 我的分类如下:



  • 可视化组件 (柱状图, 饼图, 条形图, 地图可视化等)

  • 修饰型组件 (图片, 轮播图, 修饰素材等)

  • 文字类组件 (文本, 文本跑马灯, 文字看板)


具体的物料库演示如下:


图片


这里我拿一个可视化组件的实现来举例说明:


import React, { memo, useEffect } from 'react'
import { Chart } from '@antv/g2'

import { colors } from '@/components/BasicShop/common'

import { ChartConfigType } from './schema'

interface ChartComponentProps extends ChartConfigType {
  idstring
}

const ChartComponentReact.FC<ChartComponentProps> = ({
  id, data, width, height,
  toggle, legendPosition, legendLayout, legendShape,
  labelColor, axisColor, multiColor, tipEvent, titleEvent,
  dataType, apiAddress, apiMethod, apiData, refreshTime,
}
) =>
 {
  useEffect(() => {
    let timer:any = null;
    const chart = new Chart({
      container`chart-${id}`,
      autoFittrue,
      width,
      height
    })

    // 数据过滤, 接入
    const dataX = data.map(item => ({ ...item, valueNumber(item.value) }))
    chart.data(dataX)
    
    // 图表属性组装
    chart.legend(
      toggle
        ? {
          position: legendPosition,
          layout: legendLayout,
          marker: {
            symbol: legendShape
          },
        }
        : false,
    )

    chart.tooltip({
      showTitlefalse,
      showMarkersfalse,
    })

    // 其他图表信息源配置, 方法雷同, 此处省略
    // ...

    chart.render()

  }, [])

  return <div id={`chart-${id}`} />
}

export default memo(ChartComponent)

以上就是我们的基础物料的实现模式, 可视化组件采用了g2, 当然大家也可以使用熟悉的echart, D3.js等. 不同物料既有通用的 props , 也有专有的 props, 取决于我们如何定义物料的Schema


在设计 Schema 前我们需要明确组件的属性划分, 为了满足组件配置的灵活性和通用性, 我做了如下划分:



  • 外观属性 (组件宽高, 颜色, 标签, 展现模式等)

  • 数据配置 (静态数据, 动态数据)

  • 事件/交互 (如单击, 跳转等)


有了以上划分, 我们就可以轻松设计想要的通用Schema了。我们先来看看实现后的配置面板:


图片


这些属性项都是基于我们定义的schema配置项, 通过 解析引擎 动态渲染出来的, 有关 解析引擎 和配置面板, 我会在下面的章节和大家介绍。我们先看看组件的 schema 结构:


const Chart: ChartSchema = {
  editAttrs: [
    {
      key: 'layerName',
      type: 'Text',
      cate: 'base'
    },
    {
      key: 'y',
      type: 'Number',
      cate: 'base',
    },
    ...DataConfig, // 数据配置项
    ...eventConfig, // 事件配置项
    
  ],
  config: {
    width200,
    height: 200,
    zIndex: 1,
    layerName: '柱状图',
    labelColor: 'rgba(188,200,212,1)',
    // ... 其他配置初始值
    multiColor: ['rgba(91, 143, 249, 1)''rgba(91, 143, 249, 1)''rgba(91, 143, 249,,1)''rgba(91, 143, 249, 1)'],
    data: [
      {
        name: 'A',
        value: 25,
      },
      {
        name: 'B',
        value: 66,
      }
    ],
  },
}

其中 editAttrs 表示可编辑的属性列表, config 为属性的初始值, 当然大家也可以根据自己的喜好, 设计类似的通用schema


我们通过以上设计的标准组件和标准schema, 就可以批量且高效的生产各种物料, 还可以轻松集成任何第三方可视化组件库。


3.动态渲染器实现


我们都知道, 一个页面中元素很多时会影响页面整体的加载速度, 因为浏览器渲染页面需要消耗CPU / GPU。对于可视化页面来说, 每一个可视化组件都需要渲染大量的信息元, 这无疑会对页面性能造成不小的影响, 所以我们需要设计一种机制, 让组件异步加载到画布上, 而不是一次性加载几十个几百个组件(这样的话页面会有大量的白屏时间, 用户体验极度下降)。


动态加载器就是提供了这样一种机制, 保证组件的加载都是异步的, 一方面可以减少页面体积, 另一方面用户可以更早的看到页面元素。目前我们熟的动态加载机制也有很多, Vue 和 React 生态都提供了开箱即用的解决方案(虽然我们可以用 webpack 自行设计这样的动态模型, 此处为了提高行文效率, 我们直接基于现成方案封装)。我们先看一下动态渲染组件的过程:


图片


上面的演示可以细微的看出从左侧组件菜单拖动某个组件图标到画布上后, 真正的组件才开始加载渲染。


这里我们以 umi3.0 提供的 dynamic 函数来最小化实现一个动态渲染器. 如果不熟悉 umi 生态的朋友, 也不用着急, 看完我的实现过程和原理之后, 就可以利用任何熟悉的动态加载机制实现它了。实现如下:


import React, { useMemo, memo, FC } from 'react'
import { dynamic } from 'umi'

import LoadingComponent from '@/components/LoadingComponent'


const DynamicFunc = (cpName: string, category: string) => {
  return dynamic({
    async loader() {
      //  动态加载组件
      const { defaultGraph } = await import(`@/components/materies/${cpName}`)

      return (props: DynamicType) => {
        const { config, id } = props
        return <Graph {...configid={id} />
      }
    },
    loading() => <LoadingComponent />
  })
}

const DynamicRenderEngineFC<DynamicType> = memo((props) => {
  const { 
  type
  config, 
  // 其他配置... 
  } = props
  const Dynamic = useMemo(() => {
    return DynamicFunc(config)
  }, [config])

  return <Dynamic {...props} />
})

export default DynamicRenderEngine

是不是很简单? 当然我们也可以根据自身业务需要, 设计更复杂强大的动态渲染器。


4.配置面板设计


实现配置面板的前提是对组件 Schema 结构有一个系统的设计, 在介绍组件库实现中我们介绍了通用组件 schema 的一个设计案例, 我们基于这样的案例结构, 来实现 动态配置面板


图片


由上图可以知道, 动态配置面板的一个核心要素就是 表单渲染器。表单渲染器的目的就是基于属性配置列表 attrs 来动态渲染出对应的表单项。我之前写了一篇文章详细的介绍了表单设计器的技术实现的文章, 大家感兴趣也可以参考一下: Dooring可视化之从零实现动态表单设计器。


我这里来简单实现一个基础的表单渲染器模型:


const FormEditor = (props: FormEditorProps) => {
  const { attrs, defaultValue, onSave } = props;

  const onFinish = (values: Store) => {
    // 保存配置项数据
    onSave && onSave(values);
  };
  
  const handlechange = (value) => {
    // 更新逻辑
  }

  const [form] = Form.useForm();

  return (
    <Form
      form={form}
      {...formItemLayout}
      onFinish={onFinish}
      initialValues={defaultValue}
      onValuesChange={handlechange}
    >
      {
        attrs.map((item, i) => {
        return (
          <React.Fragment key={i}>
            {item.type === 'Number' && (
              <Form.Item label={item.name} name={item.key}>
                <InputNumber />
              </Form.Item>
            )}
            {item.type === 'Text' && (
              <Form.Item label={item.name} name={item.key}>
                <Input placeholder={item.placeholder} />
              </Form.Item>
            )}
            {item.type === 'TextArea' && (
              <Form.Item label={item.name} name={item.key}>
                <TextArea rows={4} />
              </Form.Item>
            )}
            // 其他配置类型
          </React.Fragment>
        );
      })}
    </Form>
  );
};

如果大家想看更完整的配置面板实现, 可以参考开源项目 H5-Dooring | H5可视化编辑器


我们可以看看最终的配置面板实现效果:


图片


5.控制中心概述 & 功能辅助设计


控制中心的实现主要是业务层的, 没有涉及太多复杂的技术, 所以这里我简单介绍一下。因为可视化大屏页面展示的信息有些可能是私密数据, 只希望一部分人看到, 所以我们需要对页面的访问进行控制。其次由于企业内部业务战略需求, 可能会对页面进行各种验证, 状态校验, 数据更新频率等, 所以我们需要设计一套控制中心来管理。最基本的就是访问控制, 如下:


图片


功能辅助设计 主要是一些用户操作上的优化, 比如快捷键, 画布缩放, 大屏快捷导航, 撤销重做等操作, 这块可以根据具体的产品需求来完善。大家后期设计搭建产品时也可以参考实现。


可视化大屏数据自治探索


目前我们实现的搭建平台可以静态的设计数据源, 也可以注入第三方接口, 如下:


图片


我们可以调用内部接口来实时获取数据, 这块在可视化监控平台用的场景比较多, 方式如下:


图片


参数(params)编辑区可以自定义接口参数. 代码编辑器笔者这里推荐两款, 大家可以选用:



  • react-monaco-editor

  • react-codemirror2


使用以上之一可以实现minivscode, 大家也可以尝试一下.


辅助功能


可视化大屏一键截图 一键截图功能还是沿用H5-Dooring 的快捷截图方案, 主要用于对大屏的分享, 海报制作等需求, 我们可以使用以下任何一个组件实现:



  • dom-to-image

  • html2canvas


撤销重做撤销重做功能我们可以使用已有的库比如react-undo, 也可以自己实现, 实现原理:图片有点链表的意思, 我们将每一个状态存储到数组中, 通过指针来实现撤销重做的功能, 如果要想更健壮一点, 我们可以设计一套“状态淘汰机制”, 设置可保留的最大状态数, 之前的自动淘汰(删除, 更高大上一点的叫出栈). 这样可以避免复杂操作中的大量状态存储, 节约浏览器内存.

标尺参考线 标尺和参考线这里我们自己实现, 通过动态dom渲染来实现参考线在缩放后的动态收缩, 实现方案核心如下:


arr.forEach(el => {
  let dom = [...Array.from(el.querySelectorAll('.calibrationNumber'))][0] as HTMLElement;
  if (dom) {
    dom.style.transform = `translate3d(-4px, -8px, 0px) scale(${(multiple + 0.1).toFixed(
      1,
    )})`;
  }
});

详细源码可参考: H5-Dooring | 参考线设计源码


如果大家有好的建议也欢迎随时交流反馈, 开源不易, 别忘了star哦~


github地址: github.com/MrXujiang/v…


作者:徐小夕
来源:juejin.cn/post/7451246345568387091
收起阅读 »

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

web
今年大部分时间都是在编码上和写文章上,但是也不知道自己都学到了啥,那就写篇文章来盘点一下目前的技术栈吧,也作为下一年的参考目标,方便知道每一年都学了些啥。 我的技术栈 首先我先来对整体的技术做一个简单的介绍吧,然后后面再对当前的一些技术进行细分吧。 Reac...
继续阅读 »

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


20241223154451


我的技术栈


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


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


React


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


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


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


NextJs


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


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


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


Typescript


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


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


React Native



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



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


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


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


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


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


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


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


Nestjs


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



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



Prisma & mysql


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


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


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


Redis


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


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

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

import { isObject } from "@/utils";

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

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

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

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

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

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

return null;
}
}

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

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

return null;
}
}

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

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

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

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

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

return [];
}
}

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

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

前端工程化


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


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


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


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


20241223165138


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



  1. NextJs

  2. React Native

  3. prisma

  4. NestJs

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


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


总结


学无止境,任重道远。


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



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


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

📊 弃用 Echarts!这一次我选择 - Vue Data UI!

web
大家好,我是 xy👨🏻‍💻。今天,我要向大家隆重推荐一款令人惊艳的可视化图表库——Vue Data UI,一个赋予用户权力的数据可视化 Vue3 组件库!🎉 🌈 前言 Vue Data UI 诞生于一个问题:如果你的仪表板这么好,为什么你的用户要求 CSV ...
继续阅读 »

大家好,我是 xy👨🏻‍💻。今天,我要向大家隆重推荐一款令人惊艳的可视化图表库——Vue Data UI,一个赋予用户权力的数据可视化 Vue3 组件库!🎉



🌈 前言


Vue Data UI 诞生于一个问题:如果你的仪表板这么好,为什么你的用户要求 CSV 导出功能?



这个开源库的目的是为最终用户提供一组围绕图表和表格的内置工具,以减少重新计算导出数据的麻烦。当然,Vue Data UI 保留了导出为 CSV 和 PDF 的选项,以防万一。


数据,是现代商业决策的基石。但如何将复杂的数据转化为直观、易理解的视觉信息?这正是 Vue Data UI 致力于解决的问题。


🚀 丰富的图表类型,颜值爆表


探索数据的无限可能,Vue Data UI 带你领略数据之美!目前官方共提供 54可视化组件,满足您的各种需求:



  • 🌟 迷你图表:小巧精致,适合快速展示数据。

  • 📈 折线图:流畅的线条,清晰展现数据趋势。

  • 🍕 饼图:直观展示数据占比,一目了然。

  • 📋 仪表盘:动态展示关键指标,提升决策效率。

  • 🔍 雷达图:全面展示多变量数据,洞察数据全貌。

  • 🎨 3D 图表:立体展示数据,增强视觉冲击力。

  • 🚀 其它:更多组件查看-vue-data-ui.graphieros.com/examples。







📊 强大的图表生成器


告别繁琐,迎接效率!Vue Data UI 提供了一款超强大的图表可视化生成器,可视化编辑,所见即所得



  • 通过直观的可视化界面,编写数据集,调整配置设置。

  • 一切配置皆可可视化,无需再翻阅大量 API 文档。

  • 直接复制组件代码,快速集成到您的项目中。




一键复制组件代码,重点:组件代码


📈 提供高定制化 APi


Vue Data UI 不仅仅是一个图表库,它是您项目中的定制化利器。提供了丰富的 API插槽 属性,确保您的每一个独特需求都能得到满足。



  • 利用提供的 API,您可以对图表的每一个细节进行精细调整。

  • 插槽属性让您能够插入自定义的 HTMLVue 组件,实现真正的个性化设计。



比如我们需要在一个图表中注入另外一个图表


注入一个箭头


🛠️ 易于集成,快速上手


官方文档有很显眼的一句:1 import , 3 props , 54 components



安装


npm i vue-data-ui
# or
yarn add vue-data-ui

组件使用


<script setup>
import { ref } from "vue";
import { VueDataUi } from "vue-data-ui";
import "vue-data-ui/style.css";

const dataset = ref([...]);
const config = ref({...});
</script>

<template>
<div style="width:600px;">
<VueDataUi
component="VueUiXy"
:dataset="dataset"
:config="config"
/>

</div>
</template>

如果您也是一名前端开发,请一定要尝试下这个可视化组件库,因为这个可视化库真的太酷啦!


最后给大家送上官网地址:vue-data-ui.graphieros.com/


写在最后



如果觉得本文对你有帮助,希望能够给我点赞支持一下哦 💪 也可以关注wx公众号:前端开发爱好者 回复加群,一起学习前端技能 公众号内包含很多实战精选资源教程,欢迎关注



作者:前端开发爱好者
来源:juejin.cn/post/7419272082595708955
收起阅读 »

🌿一个vue3指令让el-table自动轮播

web
前言 本文开发的工具,是vue3 element-plus ui库专用的,需要对vue3指令概念有一定的了解 ​ 最近开发的项目中,需要对项目中大量的列表实现轮播效果,经过一番折腾.最终决定不使用第三方插件,手搓一个滚动指令. 效果展示 实现思路 第一步...
继续阅读 »

img


前言



本文开发的工具,是vue3 element-plus ui库专用的,需要对vue3指令概念有一定的了解



​ 最近开发的项目中,需要对项目中大量的列表实现轮播效果,经过一番折腾.最终决定不使用第三方插件,手搓一个滚动指令.


效果展示


列表滚动.webp


实现思路


第一步先确定功能



  • 列表自动滚动

  • 鼠标移入停止滚动

  • 鼠标移出继续滚动

  • 滚轮滚动完成,还可以继续在当前位置滚动

  • 元素少于一定条数时,不滚动


滚动思路


image-20241226223121217.png


image-20241226223310536.png


通过观察el-table的结构可以发现el-scrollbar__view里面放着所有的元素,而el-scrollbar__wrap是一个固定高度的容器,那么只需要获取到el-scrollbar__wrap这个DOM,并且再给一个定时器,不断的改变它的scrollTop值,就可以实现自动滚动的效果,这个值必须要用一个变量来存储,不然会失效


停止和继续滚动思路


设置一个boolean类型变量,每次执行定时器的时候判断一下,true就滚动,否则就不滚动


滚轮事件思路


为了每次鼠标在列表中滚动之后,我们的轮播还可以在当前滚动的位置,继续轮播,只需要在鼠标移出的时候,将当前el-scrollbar__wrapscrollTop赋给前面存储的变量,这样执行定时器的时候,就可以继续在当前位置滚动


不滚动的思路


​ 只需要判断el-scrollbar__view这个容器的高度,是否大于el-scrollbar__wrap的高度,是就可以滚动,不是就不滚动。


大致的思路是这样的,下面上源码


实现代码


文件名:tableAutoScroll.ts


interface ElType extends HTMLElement {
timer: number | null
isScroll: boolean
curTableTopValue: number
}
export default {
created(el: ElType) {
el.timer = null
el.isScroll = true
el.curTableTopValue = 0
},
mounted(el: ElType, binding: { value?: { delay?: number } }) {
const { delay = 15 } = binding.value || {}
const tableDom = el.getElementsByClassName(
'el-scrollbar__wrap'
)[0] as HTMLElement
const viewDom = el.getElementsByClassName(
'el-scrollbar__view'
)[0] as HTMLElement

const onMouseOver = () => (el.isScroll = false)
const onMouseOut = () => {
el.curTableTopValue = tableDom.scrollTop
el.isScroll = true
}

tableDom.addEventListener('mouseover', onMouseOver)
tableDom.addEventListener('mouseout', onMouseOut)

el.timer = window.setInterval(() => {
const viewDomClientHeight = viewDom.scrollHeight
const tableDomClientHeight = el.clientHeight

if (el.isScroll && viewDomClientHeight > tableDomClientHeight) {
const curScrollPosition = tableDom.clientHeight + el.curTableTopValue
el.curTableTopValue =
curScrollPosition === tableDom.scrollHeight
? 0
: el.curTableTopValue + 1
tableDom.scrollTop = el.curTableTopValue
}
}, delay)
},
unmounted(el: ElType) {
if (el.timer !== null) {
clearInterval(el.timer)
}
el.timer = null

const tableDom = el.getElementsByClassName(
'el-scrollbar__wrap'
)[0] as HTMLElement
tableDom.removeEventListener('mouseover', () => (el.isScroll = false))
tableDom.removeEventListener('mouseout', () => {
el.curTableTopValue = tableDom.scrollTop
el.isScroll = true
})
},
}

上面代码中,我在 created中初始化了三个变量,分别用于存储,定时器对象 、是否滚动判断、滚动当前位置。


mounted中我还获取了一个options,主要是为了可以定制滚动速度


用法



  1. 将这段代码放在你的文件夹中

  2. main.ts中注册这个指令


    import tableAutoScroll from './modules/tableAutoScroll.ts'
    const directives: any = {
    tableAutoScroll,
    }
    /**
    * @function 批量注册指令
    * @param app vue 实例对象
    */

    export const install = (app: any) => {
    Object.keys(directives).forEach((key) => {
    app.directive(key, directives[key]) // 将每个directive注册到app中
    })
    }



image-20241226224940418.png
image-20241226225027524.png


我这边是将自己的弄了一个批量注册,正常使用就像官网里面注册指令就可以了


在需要滚动的el-table上使用这个指令就可以


image-20241226225257264.png


<!-- element 列表滚动指令插件 -->
<template>
<div class="container">
<el-table v-tableAutoScroll :data="tableData" height="300">
<el-table-column prop="date" label="时间" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="address" label="Address" />
</el-table>
<!-- delay:多少毫秒滚动一次 -->
<el-table
v-tableAutoScroll="{
delay: 50,
}"
:data="tableData"
height="300"
>
<el-table-column prop="date" label="时间" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="address" label="Address" />
</el-table>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
const tableData = ref<any>([])
onMounted(() => {
tableData.value = Array.from(Array(100), (item, index) => ({
date: '时间' + index,
name: '名称' + index,
address: '地点' + index,
}))
console.log('👉 ~ tableData.value=Array.from ~ tableData:', tableData)
})
</script>

<style lang="scss" scoped>
.container {
height: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
gap: 100px;
.el-table {
width: 500px;
}
}
</style>

上面这个例子,分别演示两种调用方法,带参数和不带参数


最后


做了这个工具之后,突然有很多思路,打算后面再做几个,做成一个开源项目,一个开源的vue3指令集


作者:BAO_OA
来源:juejin.cn/post/7452667228006678540
收起阅读 »