注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

龙年到~ 我做了一个龙年红包封面,一大堆人问我教程

web
前言 就在昨天微信公众号给了我一个年终总结赠送了我六百的红包额度,那么我心想白送我? 要知道买额度现在都要一块钱一个红包封面了呢,所以我打算自己做一个红包封面但是听说审核很难过诶~ 没关系我已经踩坑完毕做出来了一个红包封面现在我就把流程分享给大家~ 亲测百分之...
继续阅读 »

前言


就在昨天微信公众号给了我一个年终总结赠送了我六百的红包额度,那么我心想白送我? 要知道买额度现在都要一块钱一个红包封面了呢,所以我打算自己做一个红包封面但是听说审核很难过诶~ 没关系我已经踩坑完毕做出来了一个红包封面现在我就把流程分享给大家~ 亲测百分之百通过!


红包封面展示


img


后台数据


img


这是我做的快去领取吧~


制作的第一个龙年红包上线


制作红包封面


制作红包封面需要 PS 等技术,啊? 我不会啊 我就想到了在线制作海报封面的网站(会 PS 也可以自己画图随便画画都可以只要是原创即可)


我使用的是图怪兽自己在线制作完毕之后喊朋友帮我下载的他有VIP 哈哈哈,也可以进行截图懂我意思吧?


这里我就实现制作了一张海报封面图片,大概话费 30 分钟素材网站上面都有发挥你的想象好吗~


img


紧接着无水印下载,没有 VIP 的按上面说的方法或者评论说一下我帮你~


压缩图片


红包封面它的大小只能是 500kb 的大小


img


丢给熊猫压缩压缩,直接给我压缩到 4 百多 KB


img


打开红包封面平台


微信红包封面开放平台: cover.weixin.qq.com/cgi-bin/mmc…


如果没有注册就注册一个


点击定制封面,进去上传图片


img


上传红包封面进行裁剪到你自己喜欢的感觉即可


img


一些选填的我这里就没准备就没去上传了,接着我们继续上传封面故事


大小不能超过 300 kb 我们继续丢给熊猫压缩压缩可能就没作用了,这下要用到 PS 了


img


打开在线 PS


随便找一个都可以我用的是这个 http://www.tuyitu.com/ps/sources/


点击文件 files 打开你的红包封面图片


img


img


点击图像, 图像大小 我们 宽改为 750 高改为 1250 官方要求的哦



如果把握不住那就用这个裁剪图片网站 tinypng.com/



img


img


修改完毕之后我们进行导出


img


将大小调整到 300kb 如果画质不好那么就去图像修改画布的大小与图片温和即可


img


img


前往红包开放平台上传我们的封面故事,在自己写一段故事描述,我这里就使用混元大模型给我生成一个龙年的祝福语~


img


img


我也祝大家: 龙腾盛世展才华,年贺新春喜洋洋 祝福亲友事事顺,心圆梦圆福满堂!!!


最后一步


证据材料 如果不上传这个 百分之 99 会给退回


img


PSD 源文件


使用在线 PS 打开我们的红包封面图片,在进行另存为 PSD 即可


img


直接进行上传,提交之后等待审核即可,百分之百成功!!


img


img


工作日 10 分钟就审核完毕了,耐心等待~ 如果制作成功 可以贴在评论区一起领取使用呀~




作者:杨不易呀
来源:juejin.cn/post/7325647896047583273
收起阅读 »

Linux操作系统简介:为何成为全球开发者热门选择?

Linux是一种自由和开放源代码的操作系统。这意味着任何人都可以查看、修改和分发Linux的源代码,而不需要支付任何费用。这种开放性使得Linux能够快速地发展和进步,吸引了全球数以万计的开发者共同参与其中,形成了一个庞大的开源社区。那么,Linux究竟是什么...
继续阅读 »

Linux是一种自由和开放源代码的操作系统。这意味着任何人都可以查看、修改和分发Linux的源代码,而不需要支付任何费用。这种开放性使得Linux能够快速地发展和进步,吸引了全球数以万计的开发者共同参与其中,形成了一个庞大的开源社区。

那么,Linux究竟是什么?它又是如何影响我们的生活的呢?让我们一起探索一下。

一、Linux操作系统介绍

在介绍Linux之前,先带大家了解一下什么是自由软件。自由软件的自由(free)有两个含义:第一,是可免费提供给任何用户使用;第二,是指它的源代码公开和自由修改。

所谓自由修改是指用户可以对公开的源代码进行修改,以使自由软件更加完善,还可在对自由软件进行修改的基础上开发上层软件。

Description

下面我们再来看看Linux操作系统的概念:

Linux,一般指GNU/Linux(单独的Linux内核并不可直接使用,一般搭配GNU套件,故得此称呼),是一种免费使用和自由传播的类UNIX操作系统,其内核由林纳斯·本纳第克特·托瓦兹(Linus Benedict Torvalds)于1991年10月5日首次发布。

它主要受到Minix和Unix思想的启发,是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统。它支持32位和64位硬件,能运行主要的Unix工具软件、应用程序和网络协议。

二、Linux系统的特点

那么,Linux为什么如此重要呢?这主要得益于它的以下几个特点:

开源免费:

Linux系统是完全免费的,任何人都可以免费使用、修改和分发。这使得Linux得以迅速传播,吸引了大量的开发者参与其中,共同推动其发展。

稳定性高:

Linux系统的稳定性非常高,长时间运行不会出现死机、蓝屏等问题。这也是为什么许多大型企业和政府部门都选择Linux作为服务器操作系统的原因。

兼容性好:

Linux支持几乎所有的硬件平台,包括x86、ARM、PowerPC等。这使得Linux可以在各种不同的设备上运行如个人电脑、手机、路由器等。同时,Linux系统还支持多种编程语言,为开发者提供了广阔的发挥空间。

强大的定制性:

Linux操作系统具有很强的定制性,用户可以根据自己的需求对系统进行深度定制。这使得Linux成为了服务器、嵌入式设备、超级计算机等领域的首选操作系统。

丰富的软件资源:

由于Linux的开源特性,许多优秀的开源软件都选择在Linux平台上发布。这些软件涵盖了从办公应用、图像处理、编程语言到数据库等各种领域,为用户提供了丰富的选择。

社区支持:

Linux拥有一个庞大的开源社区,用户可以在这里寻求帮助、分享经验、讨论问题。这种社区的支持使得Linux用户能够更好地解决问题,提高自己的技能。

三、Linux的应用

Linux的影响力已经远远超出了计算机领域,在服务器、嵌入式、开发、教育等领域都有着广泛应用。

服务器领域:

在服务器领域,Linux已经成为了主流的操作系统。据统计,世界上超过70%的服务器都在运行Linux。

Description
在云计算领域,Linux也占据了主导地位。许多知名的云服务提供商,如Amazon、Google、Microsoft等,都提供了基于Linux的云服务。

嵌入式领域:

由于Linux系统具有高度的可定制性和稳定性,因此在嵌入式领域也有着广泛的应用。

Description

如智能家居设备、无人机、机器人等都使用了Linux作为其操作系统,都离不开Linux系统的支持。这是因为Linux具有高度的可定制性和稳定性,可以满足这些设备的特殊需求。

开发领域:

Linux系统是程序员们的最爱,许多知名的开源项目都是基于Linux系统开发的,如Apache、MySQL、PHP等。

Description

此外,Linux系统还是云计算、大数据等领域的重要基础。

教育领域:

Linux系统在教育领域的应用也日益普及,许多高校和培训机构都开设了Linux相关课程,培养了大量的Linux人才。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里立即免费学习!

四、Linux系统的组成

Linux系统一般有4个主要部分:内核,Shell,文件系统和应用程序。

Description

Linux内核: 内核是系统的“内脏“,是运行程序和管理像磁盘及打印机等硬件设备的核心程序。

Linux shell: shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并送入内核中执行。实际上shell是一个命令解释器,解释由用户输入命令并且把他们送到内核。

Linux 文件系统: 文件系统是文件存放在磁盘等存储设备上的组织方法。Linux能支持多种目前流行的文件系统,如XFS、EXT2/3/4、FAT、VFAT、ISO9660、NFS、CIFS等。

Linux应用程序: 标准的Linux系统都有一套称为应用程序的程序集,包括文本编辑器、编程语言、X Window、办公软件、Internet工具、数据库等。

五、总结

总的来说,Linux是一个强大、灵活、稳定和安全的操作系统,它正在改变我们的生活和工作方式。无论你是一名开发者,还是一名普通用户,都应该了解和学习Linux,因为它将会给你带来无尽的可能性和机会。

在未来的日子里,我们将会看到Linux在更多的领域发挥其强大的影响力。无论是在数据中心、云计算、物联网,还是在人工智能、机器学习等领域,Linux都将扮演着重要的角色。

收起阅读 »

Object.assign 这算是深拷贝吗

web
在JavaScript中,Object.assign() 是一个用于合并对象属性的常见方法。然而,对于许多开发者来说,关于它是否执行深拷贝的认识可能存在一些混淆。先说答案Object.assign() 不属于深拷贝,我们接着往下看。 Object.assign...
继续阅读 »

在JavaScript中,Object.assign() 是一个用于合并对象属性的常见方法。然而,对于许多开发者来说,关于它是否执行深拷贝的认识可能存在一些混淆。先说答案Object.assign() 不属于深拷贝,我们接着往下看。


Object.assign() 概览


首先,让我们回顾一下 Object.assign() 的基本用法。该方法用于将一个或多个源对象的属性复制到目标对象,并返回目标对象。这一过程是浅拷贝的,即对于嵌套对象或数组,只是拷贝了引用而非创建新的对象。


const obj = { a: 1, b: { c: 2 } };
const obj2 = { d: 3 };

const mergedObj = Object.assign({}, obj, obj2);

console.log(mergedObj);
// 输出: { a: 1, b: { c: 2 }, d: 3 }

浅拷贝的陷阱


浅拷贝的特性意味着如果源对象中包含对象或数组,那么它们的引用将被复制到新的对象中。这可能导致问题,尤其是在修改新对象时,原始对象也会受到影响。


const obj = { a: 1, b: { c: 2 } };
const clonedObj = Object.assign({}, obj);
clonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(clonedObj); // { a: 1, b: { c: 3 } }

在这个例子中,修改 clonedObj 的属性也会影响到原始对象 obj


因此,如果我们需要创建一个全新且独立于原始对象的拷贝,我们就需要进行深拷贝。而 Object.assign() 并不提供深拷贝的功能。


深拷贝的需求


如果你需要进行深拷贝而不仅仅是浅拷贝,就需要使用其他的方法,如使用递归或第三方库来实现深度复制。以下是几种常见的深拷贝方法:


1. 使用 JSON 序列化和反序列化


const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = JSON.parse(JSON.stringify(obj));
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

这种方法利用了 JSON 的序列化反序列化过程,通过将对象转换为字符串,然后再将字符串转换回对象,实现了一个全新的深拷贝对象。


需要注意的是,这种方法有一些限制,例如无法处理包含循环引用的对象,以及一些特殊对象(如 RegExp 对象)可能在序列化和反序列化过程中失去信息。


2. 使用递归实现深拷贝


function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

const clonedObj = Array.isArray(obj) ? [] : {};

for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}

return clonedObj;
}

const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = deepClone(obj);
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

这是一个递归实现深拷贝的方法。它会递归地遍历对象的属性,并创建它们的副本。这种方法相对灵活,可以处理各种情况。


但需要注意在处理大型对象或深度嵌套的对象时可能会导致栈溢出。


3. 使用第三方库


许多第三方库提供了强大而灵活的深拷贝功能,其中最常用的是 lodash 库中的 _.cloneDeep 方法。


const _ = require('lodash');

const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = _.cloneDeep(obj);
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

使用第三方库的优势在于它们通常经过精心设计和测试,可以处理更多的边界情况,并提供更好的性能。


作者:星光漫步者
来源:juejin.cn/post/7325040809697591296
收起阅读 »

什么,你还不会调试线上 vue 组件?

web
前言 彦祖们,在日常开发中,不知道你们是否遇到过这样的场景 在本地测试开发 vue 组件的时候非常顺畅 一上生产环境,客户说数据展示错误,样式不对... 但是你在本地测试了几次,都难以复现 定位方向 这时候作为老 vuer,自然就想到了 vue devtool...
继续阅读 »

前言


彦祖们,在日常开发中,不知道你们是否遇到过这样的场景


在本地测试开发 vue 组件的时候非常顺畅


一上生产环境,客户说数据展示错误,样式不对...


但是你在本地测试了几次,都难以复现


定位方向


这时候作为老 vuer,自然就想到了 vue devtools


但是新问题又来了,线上环境我们如何开启 vue devtools 呢?


案例演示


让我们以 element-ui 官网为例


先看下此时的 chrome devtools 是没有 Vue 的选项卡的
image.png


一段神奇的代码


其实很简单,我们只需要打开控制台,运行一下以下代码


var Vue, walker, node;
walker = document.createTreeWalker(document.body,1);
while ((node = walker.nextNode())) {
if (node.__vue__) {
Vue = node.__vue__.$options._base;
if (!Vue.config.devtools) {
Vue.config.devtools = true;
if (window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit("init", Vue);
console.log("==> vue devtools now is enabled");
}
}
break;
}
}

image.png


显示 vue devtools now is enabled


证明我们已经成功开启了 vue devtools


功能验证


然后再重启一下 chrome devtool 看下效果


image.png


我们会发现此时多了一个 Vue 选项卡,功能也和我们本地调试一样使用


对于遇到 vue 线上问题调试,真的非常好用!


写在最后


本次分享虽然没有什么技术代码,重在白嫖


感谢彦祖们的阅读


个人能力有限


如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟


作者:前端手术刀
来源:juejin.cn/post/7324643000700502031
收起阅读 »

background简写,真细啊!

web
背景原因 今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒: background: url('./bg.png') no-repeat center contain ; 搞定! 上面设置的依次是 背景图片 背...
继续阅读 »

背景原因


今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒:


background:  url('./bg.png') no-repeat center contain ;

搞定!


上面设置的依次是 背景图片 背景平铺模式 背景位置 背景图片是保有其原有的尺寸还是拉伸到新的尺寸。


so easy~


看我ctrl + s 保存代码,编译。


嗯? 怎么不生效? 俺的背景呢?
打开控制台一看,好家伙,压根没生效:


image.png


问题排查


第一反应是这些属性有固定顺序,但是凭我练习两年半的经验,不应该啊,之前也是这样用的啊,遂打开MDN,仔细翻阅....


发现了下面这段话:


image.png


这让我更加确信 写的没毛病啊!!


background-attachment、background-color、background-image、background-position、background-repeat、background-size
这些属性可以以任意顺序书写。


见了鬼了,待我排查两小时(摸鱼...)


原因浮现


在仔细阅读文档后发现,其实在文档的上面,还有另外一段话:


image.png


我恍然大悟,索嘎,以后看文档不能马虎了,得仔细查阅,过于经验主义了,这都是细节啊!


background使用注意事项和总结


其实,使用background时,大部分时候 属性的顺序是可以任意位置书写的,
但是有两个属性有点特殊,那就是background-size和background-position,


当background简写同时有这两个属性时,那么必须background-position在前,background-size在后,且两者只能紧挨着书写并且以 "/"分隔。
例如:


错误: background: url('./bg.png') no-repeat center  contain ; // 没有以 "/"分隔
错误: background: url('./bg.png') center no-repeat contain ; // 没有紧挨着书写
错误: background: url('./bg.png') no-repeat contain / center; //background-size写在了 background-position的前面

正确: background: url('./bg.png') no-repeat center / contain ;


写在最后


其实MDN在关于background的文档最开头的例子中就有写:


image.png


只不过没有用语言描述出来,一般没有认真看很难发现,所以有时候能够静下心来认真查阅文档,真的会发现很多细节(甩锅:这tm是谁写的文档,出来挨打).


作者:可狗可乐
来源:juejin.cn/post/7234825495333158949
收起阅读 »

面试官: forEach怎么停止

web
介绍 在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的...
继续阅读 »

介绍


在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的解释和实际的代码示例来消除这一概念的神秘感。


在深入探讨之前,请在我的个人网站上探索更多关于 Web 开发的深度文章:


了解 JavaScript 中的 forEach 🤔


JavaScript 的 forEach 方法是迭代数组的流行工具。它为每个数组元素执行一次提供的函数。然而,与传统的 forwhile 循环不同,forEach 旨在为每个元素执行函数,没有内置机制来提前停止或中断循环。


const fruits = ["apple", "banana", "cherry"];
fruits.forEach(function(fruit) {
console.log(fruit);
});

这段代码将输出:


apple
banana
cherry

forEach 的局限性 🚫


1. forEach 中的 break


forEach 的一个关键限制是无法使用传统的控制语句比如 breakreturn 来停止或中断循环。如果您试图在 forEach 内使用 break,将遇到语法错误,因为 break 不适用于回调函数中。


尝试中断 forEach


通常,break 语句用于在满足某个条件时提前退出循环。


const numbers = [1, 2, 3, 4, 5];
numbers.forEach(number => {
if (number > 3) {
break; // 语法错误:非法 break 语句
}
console.log(number);
});

当您试图在 forEach 循环中使用 break 时,JavaScript 抛出一个语法错误。这是因为 break 被设计为在传统循环(如 forwhiledo...while)中使用,在 forEach 的回调函数中不被识别。


2. forEach 中的 return


在其他循环或函数中,return 语句退出循环或函数,如果指定的话返回一个值。


forEach 的上下文中,return 不会跳出循环。相反,它仅仅退出回调函数的当前迭代,并继续下一个数组元素。


尝试返回 forEach


const numbers = [1, 2, 3, 4, 5]; 
numbers.forEach(number => {
if (number === 3) {
return; // 仅退出当前迭代
}
console.log(number);
});

输出


1
2
4
5

在这个例子中,return 跳过了打印 3,但是循环继续剩余的元素。


使用异常中断 forEach 循环 🆕


尽管不建议常规使用,但从技术上来说,通过抛出异常可以停止 forEach 循环。尽管这种方法非正统,一般不建议使用,因为它影响代码的可读性和错误处理,但它可以有效地停止循环。


const numbers = [1, 2, 3, 4, 5];
try {
numbers.forEach(number => {
if (number > 3) {
throw new Error('Loop stopped');
}
console.log(number);
});
} catch (e) {
console.log('Loop was stopped due to an exception.');
}
// 输出: 1, 2, 3, 循环由于异常而停止。

在这个例子中,当满足条件时,抛出一个异常,提前退出 forEach 循环。但是,重要的是要正确处理这些异常,以避免意外的副作用。


用于中断循环的 forEach 替代方法 💡


使用 for...of 循环


for...of 循环是在 ES6(ECMAScript 2015)中引入的,它提供了一种现代的、简洁的和可读的方式来迭代类似数组、字符串、映射、集合等可迭代对象。与 forEach 相比,它的关键优势在于它与 breakcontinue 等控制语句兼容,在循环控制方面提供了更大的灵活性。


for...of 的优点:



  • 灵活性:允许使用 breakcontinuereturn 语句。

  • 可读性:提供清晰简洁的语法,使代码更易读和理解。

  • 通用性:能够迭代各种可迭代对象,不仅仅是数组。


for...of 的实际示例


考虑以下场景,我们需要处理数组的元素,直到满足某个条件:


const numbers = [1, 2, 3, 4, 5];  

for (const number of numbers) {
if (number > 3) {
break; // 成功中断循环
}
console.log(number);
}

输出:


1
2
3

在这个例子中,循环迭代 numbers 数组中的每个元素。一旦遇到大于 3 的数字,它利用 break 语句退出循环。这在 forEach 中是不可能的。


其他方法



  • Array.prototype.some():可以使用它来通过返回 true 来模拟中断循环。

  • Array.prototype.every():当返回 false 值时,此方法停止迭代。


结论 🎓


尽管 JavaScript 中的 forEach 方法提供了直接的数组迭代方式,但它缺乏在循环中段中断或停止的灵活性。理解这个限制对开发人员来说至关重要。幸运的是,像 for...of 循环以及 some()every() 等方法提供了必要的控制来处理更复杂的场景。掌握这些概念不仅可以增强你的 JavaScript 技能,还可以让你为艰巨的面试问题和实际编程任务做好准备。


作者:今天正在MK代码
来源:juejin.cn/post/7324384460136611850
收起阅读 »

html中的lang起到什么作用?

web
今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果…… 在chrome上是这样的 再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……...
继续阅读 »

今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果……


在chrome上是这样的


image.png


再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……


image.png


记录一下,避坑~


C3C69257283D24A892D56FA7AD82A2B1.png


代码贴在下面,感兴趣的可以去试一下


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>Document</title>
</head>

<body>
<p class="MsoNormal" style="width: 300px;background: yellow;">这一党章内容增写入宪法第一条第二款。<span lang="EN-US"></span>中国特色</p>
</body>

</html>

作者:lwlcode
来源:juejin.cn/post/7324750286329282597
收起阅读 »

MyBatis实战指南(三):相关注解及使用

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。一、什么是注解(Annotation)首先,我们需要明...
继续阅读 »

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。

一、什么是注解(Annotation)

首先,我们需要明白什么是注解。注解 Annotation 是从JDK1.5开始引入的新技术。

Description

在Java中,注解是一种用于描述代码的元数据,它可以被编译器、库和其他工具读取和使用。MyBatis的注解就是用来简化XML配置的,它们可以让你的代码更加简洁、易读。

注解的作用:

  • 不是程序本身,对程序作出解释
  • 可以被其他程序读取到

Annotation格式:

注解是以@注解名的方式在代码中实现的,可以添加一些参数值

如:@SuppressWarnings(value=“unchecked”)

注解使用的位置:

package、class、method、field 等上面,相当于给他们添加了额外的辅助信息。

注解的分类:

1.元注解:

  • @Target:用于描述注解的使用范围

  • @Retention:用于描述注解的生命周期

  • @Documented:说明该注解将被包含在javadoc 中

  • @Inherited:说明子类可以继承父类中的该注解

  • @Repeatable:可重复注解

2.内置注解:

  • @Override: 重写检查

  • @Deprecated:过时

  • @SuppressWarnings: 压制警告

  • @FunctionalInterface: 函数式接口

3.自定义注解:

  • public @interface MyAnno{}

二、Mybatis常用注解

首先介绍一下Mybatis注解的使用方法:

第一步,在全局配置文件里的配置映射



    


第二步,在mapper接口的方法的上面添加注解

@Select("select * from user where uid = #{uid}")

    public User findUserById(int uid);

第三步,创建会话调用此方法。

接下来,我们来看看MyBatis中最常用的几个注解:

(1)@Select

作用:标记查询语句。

@Select用于标记查询语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Select注解时,需要在注解中指定SQL语句。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

User getUserById(@Param("id") Long id);

(2)@Insert

作用:标记插入语句。

@Insert用于标记插入语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Insert注解时,需要在注解中指定SQL语句。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

int addUser(User user);

(3)@Update

作用:标记更新语句。

@Update用于标记更新语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Update注解时,需要在注解中指定SQL语句。

示例:

@Update("UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}")

int updateUser(User user);

(4)@Delete

作用:标记删除语句。

@Delete用于标记删除语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Delete注解时,需要在注解中指定SQL语句。

示例:

@Delete("DELETE FROM users WHERE id = #{id}")

int deleteUserById(@Param("id") Long id);

(5)@Results

作用:用于指定多个@Result注解。

@Results用于标记结果集映射,该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@Results注解时,需要指定映射规则。

示例:


@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(6)@Result

作用:用于指定查询结果集的映射关系。

@Result用于标记单个属性与结果集中的列之间的映射关系。该注解可以用于接口方法或XML文件中,通常与@Results注解一起使用。使用@Result注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(7)@ResultMap

作用:用于指定查询结果集的映射关系。

@ResultMap用于标记结果集映射规则。该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@ResultMap注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@ResultMap("userResultMap")

User getUserById(@Param("id") Long id);

(8)@Options

作用:用于指定插入语句的选项。

@Options用于指定一些可选的配置项。该注解可以用于接口方法或XML文件中,通常与@Insert、@Update、@Delete等注解一起使用。使用@Options注解时,可以指定一些可选的配置项。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@Options(useGeneratedKeys = true, keyProperty = "id")

int insertUser(User user);

(9)@SelectKey

作用:用于指定查询语句的主键生成方式。

@SelectKey用于在执行INSERT语句后获取自动生成的主键值。该注解可以用于接口方法或XML文件中,通常与@Insert注解一起使用。使用@SelectKey注解时,需要指定生成主键的SQL语句和将主键值赋给Java对象的哪个属性。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", before = false, resultType = Long.class)

int insertUser(User user);

(10)@Param

作用:用于指定方法参数名称。

@Param用于为SQL语句中的参数指定参数名称。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Param注解时,需要指定参数名称。

示例:

@Select("SELECT * FROM users WHERE name = #{name} AND age = #{age}")

List getUsersByNameAndAge(@Param("name") String name, @Param("age") Integer age);

(11)@One

作用:用于指定一对一关联关系。

@One用于在一对一关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@One注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。



  

  

  

  







  

  

  

  


上述代码中,@One注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对一关联查询。在departmentResultMap中,使用@One注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为manager,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@One注解之外,还可以使用@Many注解来指定一对多关联查询的映射方式。

总之,@One注解是MyBatis中用于在一对一关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对一关联查询的结果映射。

(12)@Many

作用:用于指定一对多关联关系。

@Many用于在一对多关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@Many注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。

示例:



  

  

  

  







  

  

  


上述代码中,@Many注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对多关联查询。在departmentResultMap中,使用@Many注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为members,ofType参数指定了集合中元素的类型为User,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@Many注解之外,还可以使用@One注解来指定一对一关联查询的映射方式。

总之,@Many注解是MyBatis中用于在一对多关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对多关联查询的结果映射。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

(13)@ResultType

作用:用于指定查询结果集的类型。

@ResultType用于指定查询结果的类型。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@ResultType注解时,需要指定查询结果的类型。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

@ResultType(User.class)

User getUserById(Long id);

(14)@TypeDiscriminator

作用:用于指定类型鉴别器,用于根据查询结果集的不同类型映射到不同的Java对象。

@TypeDiscriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@TypeDiscriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:



  

  

  

  

    

    

    

  








  

  







  








  


上述代码中,@TypeDiscriminator注解用于指定不同子类型的映射方式。在vehicleResultMap中,使用@TypeDiscriminator注解指定了类型列的名称为type,javaType参数指定了类型列的Java类型为String,标签中的value属性分别对应不同的子类型(car、truck、bus),resultMap属性用于指定不同子类型的结果集映射规则。

除了使用@TypeDiscriminator注解之外,还可以使用标签来指定不同子类型的映射方式。

总之,@TypeDiscriminator注解是MyBatis中用于在自动映射时指定不同子类型的映射方式的注解之一,可以方便地实现自动映射不同子类型的结果集映射规则。

(15)@ConstructorArgs

作用:用于指定Java对象的构造方法参数。

@ConstructorArgs用于指定查询结果映射到Java对象时使用的构造函数和构造函数参数。该注解可以用于XML文件中,通常与标签一起使用。使用@ConstructorArgs注解时,需要指定构造函数参数的映射关系。

示例:



  

  

    

    

  



(16)@Arg

作用:用于指定Java对象的构造方法参数。

@Arg用于指定查询结果映射到Java对象时构造函数或工厂方法的参数映射关系。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Arg注解时,需要指定参数的映射关系。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

User getUserById(@Arg("name") String name, @Arg("age") int age);

(17)@Discriminator

作用:用于指定类型鉴别器的查询结果。

@Discriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Discriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:


@Select("SELECT * FROM vehicle WHERE type = #{type}")

@Discriminator(column = "type", javaType = String.class, cases = {

  @Case(value = "car", type = Car.class),

  @Case(value = "truck", type = Truck.class),

  @Case(value = "bus", type = Bus.class)

})


List getVehiclesByType(String type);

(18)@CacheNamespace

作用:用于指定缓存的命名空间。

@CacheNamespace用于指定Mapper接口中的查询结果是否进行缓存。该注解可以用于Mapper接口上,用于指定Mapper接口中所有方法默认的缓存配置。使用@CacheNamespace注解时,需要指定缓存配置的属性。

示例:

@CacheNamespace(

  implementation = MyBatisRedisCache.class,

  eviction = MyBatisRedisCache.Eviction.LRU,

  flushInterval = 60000,

  size = 10000,

  readWrite = true,

  blocking = true

)


public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(19)@Flush

作用:用于在插入、更新或删除操作之后自动清空缓存。

@Flush是用于在Mapper接口中指定在执行方法前或方法后刷新缓存。该注解可以用于Mapper接口方法上,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Flush注解时,需要指定刷新缓存的时机。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Flush(flushCache = FetchType.AFTER)

User getUserById(Long id);

(20)@MappedJdbcTypes

作用:用于指定Java对象属性与数据库列的映射关系。

@MappedJdbcTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedJdbcTypes注解时,需要指定Java类型和对应的JDBC类型。

示例:

public class User {

  private Long id;

  @MappedJdbcTypes(JdbcType.VARCHAR)

  private String name;

  private Integer age;

  // ...

}

(21)@MappedTypes

作用:用于指定Java对象与数据库类型的映射关系。

@MappedTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedTypes注解时,需要指定Java类型。

示例:

@MappedTypes(User.class)

public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(22)@SelectProvider

作用:用于指定动态生成SQL语句的提供者。

@SelectProvider是用于在Mapper接口中动态生成查询SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@SelectProvider注解时,需要指定Provider类和Provider方法。

示例:

@SelectProvider(type = UserSqlProvider.class, method = "getUserByIdSql")

User getUserById(Long id);

(23)@InsertProvider

作用:用于指定动态生成SQL语句的提供者。

@InsertProvider用于在Mapper接口中动态生成插入SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@InsertProvider注解时,需要指定Provider类和Provider方法。

示例:

@InsertProvider(type = UserSqlProvider.class, method = "insertUserSql")

int insertUser(User user);

(24)@UpdateProvider

作用:用于指定动态生成SQL语句的提供者。

@UpdateProvider用于在Mapper接口中动态生成更新SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@UpdateProvider注解时,需要指定Provider类和Provider方法。

示例:

@UpdateProvider(type = UserSqlProvider.class, method = "updateUserSql")

int updateUser(User user);

(25)@DeleteProvider

作用:用于指定动态生成SQL语句的提供者。

@DeleteProvider用于在Mapper接口中动态生成删除SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@DeleteProvider注解时,需要指定Provider类和Provider方法。

示例:

@DeleteProvider(type = UserSqlProvider.class, method = "deleteUserSql")

int deleteUser(Long id);

以上就是MyBatis的相关注解及使用示例了,实际开发中不一定每个都能用到,但是可以收藏起来,有备无患嘛!

总的来说,MyBatis的注解是一个非常强大的工具,它可以帮助你减少XML配置的工作量,让你的代码更加简洁、易读。但是,它也有一定的学习成本,你需要花一些时间去理解和掌握它。希望这篇文章能帮助你更好地理解和使用MyBatis的注解。

收起阅读 »

总是听说 Vue3 选择 Proxy 的原因是性能更好,不如直接上代码对比对比

web
逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.definePropert...
继续阅读 »

逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.defineProperty 性能更好,因此自己创建了一个小 demo 来对比二者在不同场景下的性能。



以下测试仅在 谷歌浏览器 中进行,不同浏览器内核不同,结果可能有差异。可以访问此 在线地址 测试其他环境下的性能。



封装响应式


本文不会详细解析基于 Object.definePropertyProxy 的封装代码,这些内容在多数文章中已有介绍。Vue3 对嵌套对象的响应式处理进行了优化,采用了一种惰性添加的方式,仅在对象被访问时才添加响应式。相比之下,Vue2 采用了一次性递归处理整个对象的方式添加响应式。为了确保比较的公平性,本文下面的 Object.defineProperty 代码也采用了相同的惰性添加策略。


Object.defineProperty


/** Object.defineProperty 深度监听 */
export function deepDefObserve(obj, week) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
let value = obj[key]

Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
if (
typeof value === "object" &&
value !== null &&
week &&
!week.has(value)
) {
week.set(value, true)
deepDefObserve(value)
}
return value
},
set(newValue) {
value = newValue
},
})
}
return obj
}

Proxy


/** Proxy 深度监听 */
export function deepProxy(obj, proxyWeek) {
const myProxy = new Proxy(obj, {
get(target, property) {
let res = Reflect.get(target, property)
if (
typeof res === "object" &&
res !== null &&
proxyWeek &&
!proxyWeek.has(res)
) {
proxyWeek.set(res, true)
return deepProxy(res)
}
return res
},
set(target, property, value) {
return Reflect.set(target, property, value)
},
})
return myProxy
}

测试性能


测试场景有五个:



  1. 使用两个 API 创建响应式对象的耗时,即 const obj = reactive({}) 的耗时

  2. 测量对已创建的响应式对象的属性进行访问的速度,即 obj.a 的读取时间。

  3. 测量修改响应式对象属性值的耗时,即执行 obj.a = 1 所需的时间。

  4. 创建多个响应式对象,并模拟访问和修改它们属性的操作,以评估在多对象场景下的性能表现。

  5. 针对嵌套对象进行响应式性能测试,以评估在复杂数据结构下的性能表现。


初始化性能


const _0_calling = {
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
Object.defineProperty(data, keys[i], {
get() {},
set() {},
})
}
},
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = new Proxy(data, {
get() {},
set() {},
})
},
}

image.png


很明显,Proxy 的性能优于 Object.defineProperty


读取性能


const readDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const readProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _1_read = {
useObjectDefineProperty() {
readDefData.a
readDefData.b
readDefData.e
},
useProxy() {
readProxyData.a
readProxyData.b
readProxyData.e
},
}

image.png


Object.defineProperty 明显优于 Proxy


写入性能


const writeDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const writeProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _2_write = {
count: 2,
useObjectDefineProperty() {
writeDefData.a = _2_write.count++
writeDefData.b = _2_write.count++
},
useProxy() {
writeProxyData.a = _2_write.count++
writeProxyData.b = _2_write.count++
},
}

image.png


Object.defineProperty 优于 Proxy,不过差距不大。


多次创建及读写


export const _4_create_read_write = {
count: 2,
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
deepDefObserve(data)
data.a = _4_create_read_write.count++
data.b = _4_create_read_write.count++
data.a
data.c
},
proxyWeek: new WeakMap(),
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = deepProxy(data, _4_create_read_write.proxyWeek)
proxy.a = _4_create_read_write.count++
proxy.b = _4_create_read_write.count++
proxy.a
proxy.c
},
}

image.png


Proxy 优势更大,但这个场景并不多见,很少会出现一次性创建大量响应式对象的情况,对属性的读写场景更多。


对嵌套对象的性能


对内部的每个属性都进行读或写操作


const deepProxyWeek = new WeakMap()
const defWeek = new WeakMap()
export const _5_deep_read_write = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
defWeek
),
useObjectDefineProperty() {
_5_deep_read_write.defData.res.code = _5_deep_read_write.count++
_5_deep_read_write.defData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.defData.res.message.error
_5_deep_read_write.defData.res.data[0].id
_5_deep_read_write.defData.res.data[0].name
_5_deep_read_write.defData.res.data[1].id
_5_deep_read_write.defData.res.data[1].name
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
deepProxyWeek
),
useProxy() {
_5_deep_read_write.proxyData.res.code = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.message.error
_5_deep_read_write.proxyData.res.data[0].id
_5_deep_read_write.proxyData.res.data[0].name
_5_deep_read_write.proxyData.res.data[1].id
_5_deep_read_write.proxyData.res.data[1].name
},
}

image.png


Object.defineProperty 会稍好一些,但两者的差距不大。


只读取修改嵌套对象的浅层属性


const _6_deepProxyWeek = new WeakMap()
const _6_defWeek = new WeakMap()
export const _6_update_top_level = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_deepProxyWeek
),
useObjectDefineProperty() {
_6_update_top_level.defData.res.code = _6_update_top_level.count++
_6_update_top_level.defData.res.message.error
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_defWeek
),
useProxy() {
_6_update_top_level.proxyData.res.code = _6_update_top_level.count++
_6_update_top_level.proxyData.res.message.error
},
}

image.png


这个场景 Proxy 略优于 Object.defineProperty


总结


Proxy 在对象创建时的性能明显优于Object.defineProperty。而在浅层对象的读写性能方面,Object.defineProperty 表现更好。但是当对象的嵌套深度增加时,Object.defineProperty 的优势会逐渐减弱。尽管在性能测试中,Object.defineProperty 的读写优势可能更适合实际开发场景,但在 谷歌浏览器 中,Proxy 的性能与 Object.defineProperty 并没有拉开太大差距。因此,Vue3 选择 Proxy 不仅仅基于性能考量,还因为 Proxy 提供了更为友好、现代且强大的 API ,使得操作更加灵活。


作者:clench
来源:juejin.cn/post/7324141201802821672
收起阅读 »

封装v-loading指令 从此释放双手

web
封装v-loading指令 从此释放双手 前言 ​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以...
继续阅读 »

封装v-loading指令 从此释放双手


前言


​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以拍个照片发朋友圈装逼.


​ 但是想象很美好, 坐着地铁到了自习室, 发现大家伙都是在非常安静, 唯一能出声音的就是翻书或者写字的声音. 为了不影响其他人故意挑一个靠窗户的位置,接着对着电脑开始疯狂进攻. 我的键盘声很快传遍了整见屋子. 虽然别人没有说什么, 但是自己觉得好像是故意来捣乱的, 今天的键盘声音显得格外的大声. 没多久我知趣的溜溜球. 美团体验卷直接gg, 哈哈哈, 问题不大~


​ 言归正传, 今天给大家分享的是利用 VUE3 实现v-loading的加载效果 , 先看一下实现效果吧~


2023-09-24-00-48-48.gif


这一类效果在使用组件库, 例如饿了么中出现的频率很高, 使用方法也很简单, 给对应的结构添加上


v-loading="布尔值"即可, 是不是很好奇是怎么实现的? 那么就和旋风冲锋小瑜开始冲!


实现思路



  • loading肯定也是一个组件, 其中包含加载效果还有提示文字, 并且使用的时候可以去修改文字以及开启或者关闭加载动画

  • 实现的周期是在 异步开始前, 开启loading, 在异步处理[数据加载]完成后 关闭loading

  • 既然是在模版中通过 v-xxx来实现的, 那么肯定就是一个自定义指令, Vue提供指令, 也就是去操作DOM[组件实例]


那么按照以上的实现思路, 一步一步去完成, 首先搭设一个Demo的模版结构和样式


搭设基本模版


利用Vue3搭设demo架子, 头部tab栏, 切换路由 , main区域的显示内容


App.vue


<script setup lang="ts"></script>

<template>
<div class="container">
// Tab栏
<Tabs></Tabs>
// 一级路由出口
<router-view></router-view>
</div>
</template>


<style lang="scss">
.container {
width: 100vw;
height: 100vh;
background-color: #1e1e1e;
}
</style>


router路由


 routes: [
{
path: '/',
redirect: '/huawei'
},
{
path: '/huawei',
component: () => import('@/views/Huawei/index.vue'),
meta: {
title: '华为'
}
},
{
path: '/rongyao',
component: () => import('@/views/Rongyao/index.vue'),
meta: {
title: '荣耀'
}
},
{
path: '/xiaomi',
component: () => import('@/views/Xiaomi/index.vue'),
meta: {
title: '小米'
}
},
{
path: '/oppo',
component: () => import('@/views/Oppo/index.vue'),
meta: {
title: 'oppo'
}
}
]

Tabs组件


<script setup lang="ts">
import { ref } from 'vue'
const tabList = ref([
{ id: 1, text: '华为', path: '/huawei' },
{ id: 2, text: '荣耀', path: '/rongyao' },
{ id: 3, text: '小米', path: '/xiaomi' },
{ id: 4, text: 'oppo', path: '/oppo' }
])
const activeIndex = ref(0)
</script>

<template>
<div class="tabs-box">
<router-link
class="tab-item"
:to="item.path"
v-for="(item, index) in tabList"
:key="item.id"
>

<span
class="tab-link"
:class="{ active: activeIndex === index }"
@click="activeIndex = index"
>

{{ item.text }}
</span>
</router-link>
</div>
</template>


<style lang="scss" scoped>
.tabs-box {
width: 100%;
display: flex;
justify-content: space-around;
.tab-item {
padding: 10px;
&.router-link-active {
.tab-link {
transition: border 0.3s;
color: gold;
padding-bottom: 5px;
border-bottom: 2px solid gold;
&.active {
border-bottom: 2px solid gold;
color: gold;
}
}
}
}
}
</style>


按照路由去创建4个文件夹,这里按照huawei做举例


<script setup lang="ts">
const src = ref('')
</script>

<template>
<div class="box" >
<div class="img-box">
<img :src="src" alt="" />
</div>
</div>
</template>


创建Loading组件


首先按照最直接的方式, 利用 v-if 以及组件通讯, 实现组件方式的实现


注意 这里是通过 position: absolute; 通过定位的方式进行垂直水平居中, 先埋下伏笔


<script setup lang="ts">

defineProps({
title: {
type: String,
default: '正在加载中...'
}
})

</script>

<template>
<div class="loading-box">
<div class="loading-content">
// loading 动图
<img src="./loading.gif" />
// 底部提示文字
<p class="desc">{{ title }}</p>
</div>
</div>
</template>


<style lang="scss" scoped>
.loading-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
.loading-content {
text-align: center;
img {
width: 35px !important;
height: 35px !important;
}
.desc {
line-height: 20px;
font-size: 12px;
color: #fff;
position: relative;
}
}
}
</style>


在对应组件中使用Loading组件, 利用延时器模拟异步操作


<script>
const src = ref('')
const title = ref('华为加载中...')
const showLoading = ref(true) // 控制loading的显示和隐藏

onMounted(() => {
showLoading.value = true
// 模拟异步请求
window.setTimeout(() => {
src.value =
'https://ms.bdimg.com/pacific/0/pic/-1284887113_-1109246585.jpg?x=0&y=0&h=340&w=510&vh=340.00&vw=510.00&oh=340.00&ow=510.00'
showLoading.value = false
}, 1000)
})
</script>

<template>
<div class="box">
<div class="img-box" v-if="!showLoading">
<img :src="src" alt="" />
</div>
</div>
<Loading v-if="showLoading"></Loading>
</template>


效果一样可以出来, 接下来就利用指令的方式来优化


011.png


V-loading 指令实现


思路:



  • 在dom挂载完成后, 创建Loading实例, 需要挂载到写在具体指令结构上

  • loading需要知道传递的显示文字, 这里通过指令动态的参数传递

  • 当loading组件参数更新后卸载, 关闭loading


1. 使用自定义指令的参数


和内置指令类似,自定义指令的参数也可以是动态的, 下面是Vue官网的截图


动态指令参数.png


在模版中使用 v-loading:[title]="showLoading"


const title = ref('华为加载中...')

<template>
<div class="box" v-loading:[title]="showLoading">
...
</div>

<!-- <Loading v-if="showLoading"></Loading> -->
</template>

2. 利用插件注册指令


在Loading文件下创建js文件


import Loading from './index.vue' // 导入.vue文件
const loadingDirective = {
}
export default loadingDirective

components下创建index.js文件


import loading from '@/components/Loading/index'
export default {
install: (app: App) => {
app.directive('loading', loading)
}

入口文件中注册插件


import MyLoading from '@/components/index'
app.use(MyLoading)

3. 指令 - 节点都挂载完成后调用



  • createApp 作用: 创建一个应用实例 - 创建loading

  • app.mount 作用: 将应用实例挂载在一个容器元素中

  • mounted 参数 el=>获取dom

  • mounted 参数 binding.value => 控制开启和关闭loding 也就是 showLoading

  • mounted 参数 binding.arg => loading显示的文字 [例如华为加载中...]


const loadingDirective = {
/* 节点都挂载完成后调用 */
mounted(el: any, binding: DirectiveBinding) {
/*
value 控制开启和关闭loding
arg loading显示的文字
*/

const { value, arg } = binding
/* 创建loading实例,并挂载 */
const app = createApp(Loading)
// 这一步 instance === loading.vue
// 此时就可以视同loading.vue 也就是组件实例的方法和属性
const instance = app.mount(document.createElement('div'))
/* 为了让elAppend获取到创建的div元素 */
el.instance = instance
/* 如果传入了自定义的文字就添加title */
if (arg) {
instance.setTitle(arg)
}
/* 如果showLoading为true将loading实例挂载到指令元素中 */
if (value) {
// 添加方法方法, 看下文
handleAppend(el)
}
},
}

可以从控制台查看binding中的title以及showLoading的值


02.png


instance.setTitle(arg) 这里既然使用到了组件实例的setTitle方法, 就需要在loading中对应的方法


注意: 在vue3中需要利用defineExpose抛出事件, 让外界可以访问或使用


Loading.vue


const title = ref('')
const setTitle = (val: string) => {
title.value = val
}
// defineProps({ 组件通讯就使用不到了, 注释即可
// title: {
// type: String,
// default: '正在加载中...'
// }
// })

defineExpose({
setTitle,
title
})

<template>
<div class="loading-box">
<div class="loading-content">
<img src="./loading.gif" />
<p class="desc">{{ title }}</p>
</div>
</div>

</template>

4. 指令 - handleAppend(el)方法实现


/* 将loading添加到指令所在DOM */
const handleAppend = (el: any) => {
console.log(el.instance.$el, 'el.instance.$el')
el.appendChild(el.instance.$el)
}

04.png


5. 指令 - updated() 更新后挂载还是消除的逻辑


在第四步中, loading已经可以通过指令显示了, 此时还需要让showLoading为false的时候, 或者这么理解: 当新的值不等于老值的是够关闭loading


此时就可以利用指令中updated钩子去执行这一段关闭的逻辑, 一下是官网的说明


05.png


  /* 更新后调用 */
updated(el: any, binding: DirectiveBinding) {
const { value, oldValue, arg } = binding
if (value !== oldValue) {
/* 更新标题 */
if (arg) {
el.instance.setTitle(arg)
}
// 是显示吗? 如果是就添加 : 如果不是就删除
value ? handleAppend(el) : c(el)
}
}

6. 指令 - handleRemove()方法实现


/* 将loading在DOM中移除 */
const handleRemove = (el: any) => {
removeClass(el, relative as any)
el.removeChild(el.instance.$el)
}

此时基本已经完成了需求, 但是上文我提到了坑点, 原因是loading是通过绝对定位的方式进行水平居中, 那么比如我要在图片中显示loading呢? 我们来实现下这个坑点


7. 坑点的说明


<template>
<div class="box" //关闭 v-loading:[title]="showLoading">
<div class="img-box" v-loading:[title]="showLoading">
<img :src="src" alt="" />
</div>
</div>

<!-- <Loading v-if="true"></Loading> -->
</template>

06.png


很明显发现, 执行现在图片这个盒子上, 并没有水平居中, 审查元素其实也很明显, css样式中是根据子绝父相, 但是此时大盒子并没有提供相对定位, 自然就无法水平居中


那么如何修改呢? 其实只要给绑定指令的盒子添加position: relative;属性即可, 当然absolute或者fixed效果一样可以居中


问题已找到了, 那么在appendChild时判断当前是否存在relative | absolute | fixed 的其中一个, 如果没有就需要classList.add进行添加, 同时在removeChild删除添加的relative | absolute | fixed 即可


8. 完善坑点, 实现水平居中


getComputedStyle() 在MDN上的说明:


方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 CSS 属性的值。私有的 CSS 属性值可以通过对象提供的 API 或通过简单地使用 CSS 属性名称进行索引来访问。


/* 将loading添加到指令所在DOM */
const relative = 'relative'
const handleAppend = (el: any) => {
const style = getComputedStyle(el)
if (!['absolute', 'relative', 'fixed'].includes(style.position)) {
addClass(el, relative as any)
}

el.appendChild(el.instance.$el)
}
// 添加relative
const addClass = (el: any, className: string) => {
if (!el.classList.contains(className)) {
el.classList.add(className)
}
}
// 删除relative
const removeClass = (el: any, className: string) => {
el.classList.remove(className)
}

结尾


夜深人静又是卷到凌晨1点, 只管努力, 其他交给天意~ 旋风冲锋手动撒花 ✿✿ヽ(°▽°)ノ✿


demo地址: gitee.com/tcwty123/v-…


作者:不知名小瑜
来源:juejin.cn/post/7281825352530296843
收起阅读 »

前端对接电子秤、扫码枪设备serialPort 串口使用教程

web
因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。 Serialport 官网地址:serialport.io/ Github:g...
继续阅读 »

因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。



Serialport


官网地址:serialport.io/


Github:github.com/serialport/…


官方描述:使用 JavaScript 访问串行端口。Linux、OSX 和 Windows。



SerialPort是什么?



SerialPort 是一个用于在 Node.js 环境中进行串口通信的库。它允许开发者通过 JavaScript 或 TypeScript 代码与计算机上的串口设备进行交互。SerialPort 库提供了丰富的 API,使得在串口通信中能够方便地进行设置、监听和发送数据。



一般我们的设备(电子秤/扫码枪)会有一根线插入到电脑的USB口或者其他口,电脑上的这些插口就是叫串口。设备上的数据会通过这根线传输到电脑里面,比如电子秤传到电脑里的就是重量数值。那么我们前端怎么接收解析到这些数据的呢?SerialPort的作用就是用来帮我们接收设备传输过来的数据,也可以向设备发送数据。


简单概括一下:SerialPort就是我们前端和设备之间的翻译官,可以接收设备传输过来的数据,也可以向设备发送数据。


SerialPort怎么用?


SerialPort可以在Node项目中使用,也可以在Electron项目中使用,我们一般都是用在Electron项目中,接下来讲一下在Electron项目中SerialPort怎么下载和引入


1、创建Electron项目


mkdir my-electron-app && cd my-electron-app
npm init -y
npm i --save-dev electron

网上有很多Electron教程,这里不再详细说了


在package.json中看一下自己的Electron的版本,下一步会用到


2、下载SerialPort


这里先看一下自己使用的Electron对应的Node版本是什么,打开下面electron官网看表格中的Node那一列


Electron发行时间表:http://www.electronjs.org/zh/docs/lat…


image-20240113215800193.png


如果你Electron对应的Node版本高于v12.0.0,直接下载就行


npm install serialport

如果你Electron对应的Node版本低于或等于v12.0.0,请用对应的Node版本对应下面的serialport版本下载



serialport.io/docs/next/g…




  • 对于 Node.js 版本0.100.12,最后一个正常运行的版本是serialport@4

  • 对于 Node.js 版本4.0,最后一个正常运行的版本是serialport@6.

  • 对于 Node.js 版本8.0,最后一个正常运行的版本是serialport@8.

  • 对于 Node.js 版本10.0,最后一个正常运行的版本是serialport@9.

  • 对于 Node.js 版本12.0,最后一个正常运行的版本是serialport@10.



我项目的Electron版本是11.5.0,对应的Node版本号是12.0,对应的serialport版本号是serialport@10.0.0



3、编译Serialport



  • 安装node-gyp 用于调用其他语言编写的程序(如果已安装过请忽略这一步)


    npm install -g node-gyp




  • 进入@serialport目录


    cd ./node_modules/@serialport/bindings


  • 进行编译,target后面换成当前Electron的版本号


    node-gyp rebuild --target=11.5.0



如果编译的时候报错了就将自己电脑的Node版本切换成当前Electron对应的版本号再编译一次


查看Electron对应Node版本号:http://www.electronjs.org/zh/docs/lat…


编译成功以后就可以在代码里使用Serialport了


4、使用Serialport



serialport官网使用教程:serialport.io/docs/next/g…



4.1、引入Serialport


const { SerialPort } = require('serialport')
// or
import { SerialPort } from 'serialport'

4.2、创建串口(重点!)


创建串口有两种写法,新版本是这样写法new SerialPort(params, callback)


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

旧版本是下面这样的写法new Serialport(path, params, callback),我用的是serialport@10.0.0版本就是这样的写法


const port = new Serialport('COM1', {
 baudRate: 9600,
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

创建串口的时候需要传入两个重要的参数是path和baudRate,path是串口号,baudRate是波特率。最后一个参数是回调函数



不知道怎么查看串口号和波特率看这篇文章


如何查看串口号和波特率?



4.3、手动打开串口


如果autoOpen参数是false,需要使用port.open()方法手动打开


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: false,  // 是否自动打开端口, 默认true
})
// autoOpen参数是false,需要使用port.open()方法手动打开
port.open(function (err) {
 if (err) {
   return console.log('打开失败', err.message)
}
 console.log('打开成功')
})

4.4、接收数据(重点!)


接收到的data是一个Buffer,需要转换为字符串进行查看


port.on('data', function (data) {
 // 接收到的data是一个Buffer,需要转换为字符串进行查看
 console.log('Data:', data.toString('utf-8'))
})

接收过来的data就是设备传输过来的数据,转换后的字符串就是我们需要的数据,字符串里面可能有多个数据,我们把自己需要的数据截取出来就可以了


假设通过电子秤设备获取到的数据就是"205 000 000",中间是四个空格分割的,第一个数字205就是获取的重量,需要把这个重量截取出来。下面是我的示例代码


port.on('data', function (data) {
 try {
     // 获取的data是一个Buffer
     // 1.将 Buffer 转换为字符串 dataString.toString('utf-8')
     let weight = data.toString('utf-8')
     // 2.将字符串分割转换成数组,取数组的第一个值.split('   ')[0]
     weight = weight.split('   ')[0]
     // 3.将取的值 去掉前后空格
     weight = weight.trim()
     // 4.最后转换成数字,获取到的数字就是重量
     weight = Number(weight)
     console.log('获取到重量:'+ weight);
} catch (err) {
   console.error(`
     重量获取报错:${err}
     获取到的Buffer: ${data}
     Buffer转换后的值:${data.toString('utf-8')}
   `
);
}
})

4.5、写入数据


port.write('Hi Mom!')
port.write(Buffer.from('Hi Mom!'))

4.6、实时获取(监听)所有串口


const { SerialPort } = require('serialport')

SerialPort.list().then((ports, err) => {
   // 串口列表
   console.log('获取所有串口列表', ports);
})

更多内容


serialport官网教程:serialport.io/docs/next/g…


作者:Yaoqi
来源:juejin.cn/post/7323464381172301860
收起阅读 »

flex布局之美,以后就靠它来布局了

web
写在前面 在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的align 和valign可以实现水平垂直居中等 再后来,由于CSS 不断完善,便演变出了:标准文档流、浮动布局和定位布局 3种布局 来实现水平垂直居中等各种布局...
继续阅读 »

写在前面


在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的alignvalign可以实现水平垂直居中等


再后来,由于CSS 不断完善,便演变出了:标准文档流浮动布局定位布局 3种布局 来实现水平垂直居中等各种布局需求。


下面我们来看看实现如下效果,各种布局是怎么完成的


image-20240114134424060


实现这样的布局方式很多,为了方便演示效果,我们在html代码种添加一个父元素,一个子元素,css样式种添加一个公共样式来设置盒子大小,背景颜色


<div class="parent">
  <div class="child">我是子元素</div>
</div>

/* css公共样式代码 */
.parent{
   background-color: orange;
   width: 300px;
   height: 300px;
}
.child{
   background-color: lightcoral;
   width: 100px;
   height: 100px;
}

①absolute + 负margin 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;;
   top: 50%;
   left: 50%;
   margin-left: -50px;
   margin-top: -50px;
}

②absolute + transform 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
}

③ flex实现


.parent {
   display: flex;
   justify-content: center;
   align-items: center;
}

通过上面三种实现来看,我们应该可以发现flex 布局是最简单了吧。


对于一个后端开发人员来说,flex布局算是最友好的了,因为它操作简单方便


一、flex 布局简介



flex 全称是flexible Box,意为弹性布局 ,用来为盒状模型提供布局,任何容器都可以指定为flex布局。


通过给父盒子添加flex属性即可开启弹性布局,来控制子盒子的位置和排列方式。


父容器可以统一设置子容器的排列方式,子容器也可以单独设置自身的排列方式,如果两者同时设置,以子容器的设置为准



flex布局


二、flex基本概念



flex的核心概念是 容器,容器包括外层的 父容器 和内层的 子容器,轴包括 主轴辅轴



<div class="parent">
   <div class="child">我是子元素</div>
</div>

2.1 轴



  • 在 flex 布局中,是分为主轴和侧轴两个方向,同样的叫法有 : 行和列、x 轴和y 轴,主轴和交叉轴

  • 默认主轴方向就是 x 轴方向,水平向右

  • 默认侧轴方向就是 y 轴方向,水平向下


    主轴和侧轴



注:主轴和侧轴是会变化的,就看 flex-direction 设置谁为主轴,剩下的就是侧轴。而我们的子元素是跟着主轴来排列的


--flex-direction 值--含义
row默认值,表示主轴从左到右
row-reverse表示主轴从右到左
column表示主轴从上到下
column-reverse表示主轴从下到上

2.2 容器



容器的属性可以作用于父容器(container)或者子容器(item)上



①父容器(container)-->属性添加在父容器上



  • flex-direction 设置主轴的方向

  • justify-content 设置主轴上的子元素排列方式

  • flex-wrap 设置是否换行

  • align-items 设置侧轴上的子元素排列方式(单行 )

  • align-content 设置侧轴上的子元素的排列方式(多行)


②子容器(item)-->属性添加在子容器上



  • flex 属性 定义子项目分配剩余空间,用flex来表示占多少份数

  • align-self控制子项自己在侧轴上的排列方式

  • order 属性定义项目的排列顺序


三、主轴侧轴设置


3.1 flex-direction: row



flex-direction: row 为默认属性,主轴沿着水平方向向右,元素从左向右排列。



row


3.2 flex-direction: row-reverse



主轴沿着水平方向向左,子元素从右向左排列



row-reverse


3.3 flex-direction: column



主轴垂直向下,元素从上向下排列



column


3.4 flex-direction: column-reverse



主轴垂直向下,元素从下向上排列



column-reverse


四、父容器常见属性设置


4.1 主轴上子元素排列方式


4.1.1 justify-content


justify-content 属性用于定义主轴上子元素排列方式


justify-content: flex-start|flex-end|center|space-between|space-around



flex-start:起始端对齐


flex-start


flex-end:末尾段对齐


flex-end


center:居中对齐


center


space-around:子容器沿主轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半。


space-around


space-between:子容器沿主轴均匀分布,位于首尾两端的子容器与父容器相切。


space-between


4.2 侧轴上子元素排列方式


4.2.1 align-items 单行子元素排列


这里我们就以默认的x轴作为主轴



align-items:flex-start:起始端对齐


flex-start


align-items:flex-end:末尾段对齐


flex-end


align-items:center:居中对齐


center


align-items:stretch 侧轴拉伸对齐



如果设置子元素大小后不生效



stretch


4.2.2 align-content 多行子元素排列


设置子项在侧轴上的排列方式 并且只能用于子项出现 换行 的情况(多行),在单行下是没有效果的


我们需要在父容器中添加 flex-wrap: wrap;


flex-wrap: wrap; 是啥意思了,具体会在下一小节中细说,就是当所有子容器的宽度超过父元素时,换行显示



align-content: flex-start 起始端对齐


 /* 父容器添加如下代码 */
display: flex;
align-content: flex-start;
flex-wrap: wrap;

align-content: flex-start


align-content: flex-end :末端对齐


/* 父容器添加如下代码 */
display: flex;
align-content: flex-end;
flex-wrap: wrap;

align-content: flex-end


align-content: center: 中间对齐


/* 父容器添加如下代码 */
display: flex;
align-content: center;
flex-wrap: wrap;

align-content: center


align-content: space-around: 子容器沿侧轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半


/* 父容器添加如下代码 */
display: flex;
align-content: space-around;
flex-wrap: wrap;

align-content: space-around


align-content: space-between:子容器沿侧轴均匀分布,位于首尾两端的子容器与父容器相切。


/* 父容器添加如下代码 */
display: flex;
align-content: space-between;
flex-wrap: wrap;

image-20240114171606954


align-content: stretch: 子容器高度平分父容器高度


/* 父容器添加如下代码 */
display: flex;
align-content: stretch;
flex-wrap: wrap;

align-content: stretch


4.3 设置是否换行



默认情况下,项目都排在一条线(又称”轴线”)上。flex-wrap属性定义,flex布局中默认是不换行的。



flex-wrap: nowrap :不换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: nowrap;

flex-wrap: nowrap


flex-wrap: wrap: 换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: wrap;

flex-wrap: wrap


4.4 align-content 和align-items区别



  • align-items 适用于单行情况下, 只有上对齐、下对齐、居中和 拉伸

  • align-content适应于换行(多行)的情况下(单行情况下无效), 可以设置 上对齐、下对齐、居中、拉伸以及平均分配剩余空间等属性值。

  • 总结就是单行找align-items 多行找 align-content


五、子容器常见属性设置



  • flex子项目占的份数

  • align-self控制子项自己在侧轴的排列方式

  • order属性定义子项的排列顺序(前后顺序)


5.1 flex 属性



flex 属性定义子项目分配剩余空间,用flex来表示占多少份数。



① 语法


.item {
   flex: <number>; /* 默认值 0 */
}

②将1号、3号子元素宽度设置成80px,其余空间分给2号子元素


flex:1


5.2 align-self 属性



align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。


默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。



align-self: flex-start 起始端对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-start;

align-self: flex-start


align-self: flex-end 末尾段对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-end;

align-self: flex-end


align-self: center 居中对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素*/
align-self: center;

align-self: center


align-self: stretch 拉伸对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素 未指定高度才生效*/
align-self: stretch;

align-self: stretch


5.3 order 属性



数值越小,排列越靠前,默认为0。



① 语法:


.item {
   order: <number>;
}

② 既然默认是0,那我们将第二个子容器order:-1,那第二个元素就跑到最前面了


/* 父容器添加如下代码 */
display: flex;
/*第二个子元素*/
order: -1;

order


六、小案例


最后我们用flex布局实现下面常见的商品列表布局


商品列表


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>简单商品布局</title>
   <style>
       .goods{
           display: flex;
           justify-content: center;
      }
       p{
           text-align: center;
      }
       span{
           margin: 0;
           color: red;
           font-weight: bold;
      }
       .goods001{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods002{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods003{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods004{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }

   
</style>
</head>
<body>

   <div class="goods">
       <div class="goods001">
           <img src="./imgs/goods001.jpg" >
           <p>松下(Panasonic)洗衣机滚筒</p>
           <span>¥3899.00</span>
       </div>
       <div class="goods002">
           <img src="./imgs/goods002.jpg" >
           <p>官方原装浴霸灯泡</p>
           <span>¥17.00</span>
       </div>
       <div class="goods003">
           <img src="./imgs/goods003.jpg" >
           <p>全自动变频滚筒超薄洗衣机</p>
           <span>¥1099.00</span>
       </div>
       <div class="goods004">
           <img src="./imgs/goods004.jpg" >
           <p>绿联 车载充电器</p>
           <span>¥28.90</span>
       </div>
   </div>
</body>
</html>

以上就是本期内容的全部,希望对你有所帮助。我们下期再见 (●'◡'●)


作者:xiezhr
来源:juejin.cn/post/7323539673346375719
收起阅读 »

面试官:你之前的工作发布过npm包吗?

web
背景🌟 我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~ 01、步骤一注册 打开npm官网,如果没有账号就注册账号...
继续阅读 »

背景🌟


我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~


01、步骤一注册



打开npm官网,如果没有账号就注册账号,如果有就登陆。



02、步骤二创建文件夹



按需求创建一个文件夹,本文以test为例。



03、步骤三初始化package.json文件



进入test文件夹里面,使用cmd打开命令行窗口,在命令行窗口里面输入npm init初始化package.json文件。也可以在Visual Studio Coode的终端里面使用npm init命令初始化。



04、步骤四初始化package.json文件的过程



创建package.json的步骤


01、package name: 设置包名,也就是下载时所使用的的命令,设置需谨慎。


02、version: 设置版本号,如果不设置那就默认版本号。


03、description: 包描述,就是对这个包的概括。


04、entry point: 设置入口文件,如果不设置会默认为index.js文件。


05、test command: 设置测试指令,默认值就是一句不能执行的话,可不设置。


06、git repository: 设置或创建git管理库。


07、keywords: 设置关键字,也可以不设置。


08、author: 设置作者名称,可不设置。


09、license: 备案号,可以不设置。


10、回车即可生成package.json文件,然后还有一行需要输入yes命令就推出窗口。


11、测试package.json文件是否创建成功的命令npm install -g。



05、步骤五创建index.js文件



test文件夹根目录下创建index.js文件,接着就是编写index.js文件了,此处不作详细叙述。



06、步骤六初始化package-lock.json文件



test根目录下使用npm link命令创建package-lock.json文件。



07、步骤七登录npm账号



使用npm login链接npm官网账号,此过程需要输入Username、Password和Email,需要提前准备好。连接成功会输出Logged in as [Username] on registry.npmjs.org/ 这句话,账号不同,输出会有不同。



08、步骤八发布包到npm服务器



执行npm publish命令发布包即可。



09、步骤九下载安装



下载安装使用包,此例的下载命令是npm install mj-calculation --save



10、步骤十更新包



更新包的命npm version patch,更新成功会输出版本号,版本号会自动加一,此更新只针对本地而言。



11、步骤十一发布包到npm服务器



更新包至npm服务器的命令npm publish,成功会输出版本,npm服务器的版本也会更新。



12、步骤十二删除指定版本



删除指定版本npm unpublish mj-calculation@1.0.2,成功会输出删除的版本号,对应服务器也会删除。



13、步骤十三删除包



撤销已发布的包npm unpublish mj-calculation使用的命令。



14、步骤十四强制删除包



强制撤销已发布的包npm unpublish mj-calculation --force使用的命令。



作者:泽南Zn
来源:juejin.cn/post/7287425222365364259
收起阅读 »

面试被问到一个css属性,我却只会向面试官输出js解决方案。。。

web
事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一...
继续阅读 »

事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一些操作按钮,但是按钮在该区域下方,不能显示出来,怎么能够点击到按钮呢?


听到问题我一开始没反应过来,不是还在疯狂问css吗,怎么突然跳跃到canvas了?于是我就回:不好意思,canvas我不是特别熟。。。,面试官就说不关canvas的事,这样吧,你就把canvas想象成一张普通的图片,怎么能点击到图片下方的按钮呢?


思索片刻,难道面试官在考察我的JS基础了?我就很自信的回答:首先把按钮所在标签放到图片所在标签之下,再把图片的层级(z-index)设置比按钮高一点,这样按钮就被图片挡着不会显示出来,再给按钮和图片所在标签都加上点击事件,此时就可以通过事件冒泡处理按钮的事件了。说完我就非常有把握的看向面试官,以为稳了。但是面试官好像不太满意,于是添加条件说,那如果按钮和图片所在标签不是父子节点关系呢,没有这层关系,你就不能使用事件冒泡了,此时怎么处理?答曰:不知道。。。


回来后赶紧查资料,原来一个css属性就搞定了:pointer-events: none; 这才是面试官想要的答案。


pointer-events是一个CSS属性,它定义了在何种情况下元素可以成为鼠标事件(或触摸事件)的目标。这个属性可以控制元素是否可以被点击、是否可以触发鼠标事件,或者是否应该忽略鼠标事件,让事件传递给下面的元素


使用场景


pointer-events属性主要用于以下几种场景:



  • : 元素不会成为鼠标事件的目标。例如,如果想让一个元素透明对用户的点击,可以将其pointer-events设置为none

  • Auto: 默认值。元素正常响应鼠标事件。

  • VisiblePainted: 元素仅在可见部分响应鼠标事件。

  • 其他值: 还有一些其他值用于SVG元素,如visibleFillvisibleStrokepainted, 等。


示例




以下例子和上述试题很像,把mask当做一张图片,为了方便展示,为其设置了透明度,这样能看到具体按钮位置和展示层级关系。正常情况下点击按钮是不会触发click事件的,因为mask的层级更高,完全遮住了按钮,鼠标只会点击到mask,但若此时为其加上pointer-events: none属性,点击事件会“穿透”该元素并可触发下面元素的事件,即按钮点击事件就可以被触发了!!!


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.outer{
position: relative;
width: 200px;
height: 200px;
}
.mask{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, .7);
pointer-events: none; /* 重要 */
}
</style>
<body>
<div class="outer">
<div class="mask"></div>
<button id="btn">click</button>
</div>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click', function(e){
console.log('click');
})
</script>
</body>
</html>

image.png


作者:自驱
来源:juejin.cn/post/7320169906221826048
收起阅读 »

不要再滥用可选链运算符(?.)啦!

web
前言 之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。 可选链运算符(?.),大家都很熟悉了,直接看个例子: const result = obj?.a?...
继续阅读 »

前言


之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。


可选链运算符(?.),大家都很熟悉了,直接看个例子:


const result = obj?.a?.b?.c?.d

很简单例子,上面代码?前面的属性如果是空值(null或undefined),则result值是undefined,反之如果都不是空值,则会返回最后一个d属性值。


本文不是讲解这种语法的用法,主要是想分析下日常开发中,这种语法 滥用、乱用 的问题。


滥用、乱用


最近在code review一个公司项目代码,发现代码里用到的可选链运算符,很多滥用,用的很无脑,经常遇到这种代码:


const userName = data?.items?.[0]?.user?.name

↑ 不管对象以及属性有没有可能是空值,无脑加上?.就完了。


// react class component
const name = this.state?.name

// react hooks
const [items, setItems] = useState([])
items?.map(...)
setItems?.([]) // 真有这么写的

↑ React框架下,this.state 值不可能是空值,初始化以及set的值都是数组,都无脑加上?.


const item1 = obj?.item1
console.log(item1.name)

↑ 第一行代码说明obj或item1可能是空值,但第二行也明显说明不可能是空值,否则依然会抛错,第一行的?.也就没意义了。


if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2
const name = obj?.item1?.item2?.name
}

↑ if 里已经判断了非空了,内部就没必要判断非空了。


问题、缺点


如果不考虑 ?. 使用的必要性,无脑滥用其实也没问题,不会影响功能,优点也很多:



  1. 不用考虑是不是非空,每个变量或属性后面加 ?. 就完了。

  2. 由于不用思考,开发效率高。

  3. 不会有空引用错误,不会有页面点点就没反应或弹错问题。


但是问题和缺点也很明显,而且也会很严重。分两点分析下:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


1. 可读性、维护性


可读性和维护性其实是一回事,都是指不是源代码作者的开发维护人员,在捋这块代码逻辑、修改bug等情况时,处理问题的效率,代码写的好处理就快,写的烂就处理慢,很简单道理。


const onClick = () => {
const user = props.data?.items?.[0]?.user
if (user) {
// use user to do something
}
}

已这行代码为例,有个bug现象是点击按钮没反应,维护开发看到这块代码,就会想这一串链式属性里,是不是有可能有空值,所以导致了user是空值,没走进if里导致没反应。然后就继续分析上层组件props传输代码,看data值从哪儿传来的,看是不是哪块代码导致data或items空值了。。。


其实呢?从外部传过来的这一串属性里不会有空值的情况,导致bug问题根本不在这儿。


const user = props.data.items[0].user

那把?.都去掉呢?维护开发追踪问题看到这行代码,data items 这些属性肯定不能是空值,不然console就抛错了,但是bug现象里并没有抛错,所以只需要检查user能不能是空值就行了,很容易就排除了很多情况。


总结就是:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。


2. 隐式过滤了异常


api.get(...).then(result => {
const id = result?.id
// use id to do something
})

比如有个需求,从后台api获取数据时,需要把结果里id属性获取到,然后进行数据处理,从业务流程上看,这个api返回的result以及id必须有值,如果没值的话后续的流程就会走不通。


然后后台逻辑由于写的有问题,导致个别情况返回的 result=null,但是由于前端这里加了?.,导致页面没有任何反应,js不抛错,console也没有log,后续流程出错了,这时候如果想找原因就会很困难,对代码熟悉还行,如果不是自己写的就只能看代码捋逻辑,如果是生产环境压缩混淆了就更难排查了。


api.get(...).then(result => {
const id = result.id
// use id to do something
})

?.去掉呢?如果api返回值有问题,这里会立即抛错,后面的流程也就不能进行下去了,无论开发还是生产环境都能在console里快速定位问题,即使是压缩混淆的也能从error看出一二,或者在一些前端监控程序里也能监听到。


其实这种现象跟 try catch 里不加 throw 类似,把隐式异常错误完全给过滤掉了,比如下面例子:


// 这个try本意是处理api请求异常
try {
const data = getSaveData() // 这段js逻辑也在try里,所以如果这个方法内部抛错了,页面上就没任何反应,很难追踪问题
const result = await api.post(url, data)
// result 逻辑处理
} catch (e) {
// 好点的给弹个框,打个log,甚至有的啥都不处理
}

总结就是:把异常给隐式过滤掉了,导致不能快速定位问题。


3. 编译后代码冗余


如果代码是ts,并且编译目标是ES2016,编译后代码会很长。可以看下 http://www.typescriptlang.org/play 效果。


image.png


Babel在个别stage下,编译效果一样。


image.png


但并不是说一点都不用,意思是尽量减少滥用,这样使用的频率会少很多,这种编译代码沉余也会少不少。


应该怎么用?


说了这么多,.? 应该怎么用呢?意思是不用吗?当然不是不能用,这个特性对于开发肯定好处很多的,但是得合理用,不能滥用。



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。


其实说白了就是:什么时候需要判断一个变量或属性非空,什么时候不需要。首先在使用的时候得想下,问号前面的变量或属性值,有没有可能是空值:



  1. 很明显不可能是空值,比如 React类组件里的 this.state this.props,不要用;

  2. 自己定义的变量或属性,而且没有赋值为空值情况,不要用;

  3. 某些方法或者组件里,参数和属性不允许是空值,那方法和组件里就不需要判断非空。(对于比较common的,推荐写断言,或者判断空值情况throw error)

  4. 后台api请求结果里,要求result或其内部属性必须有值,那这些值就不需要判断非空。

  5. 按正常流程走,某个数据不会有空值情况,如果是空值说明前面的流程出问题了,这种情况就不需要在逻辑里判断非空。


const userName = data?.items?.[0]?.user?.name // 不要滥用,如果某个属性有可能是空值,则需要?.
const userName = data.items[0].user?.name // 比如data.items数组肯定不是空数组

const items2 = items1.filter(item => item.checked)
if (items2?.length) { } // 不需要?.

// react class component
const name = this.state?.name // 不需要?.

// react hooks
const [items, setItems] = useState([])
items?.map(...) // 如果setItems没有赋值空值情况,则不需要?.
setItems?.([]) // 不需要?.

const item1 = obj?.item1 // 不需要?.
console.log(item1.name)

const id = obj?.id // 下面代码已经说明不能是空值了,不需要?.
const name = obj.name

if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2 // 不需要?.
const name = obj?.item1?.item2?.name // 不需要?.
}

const id = obj?.item?.id // 不需要?.
api.get(id).then(...) // 这个api如果id是空值,则api会抛错

当然,写代码时还得多想一下属性是否可能是空值,会一定程度的影响开发效率,也一定有开发会觉得很烦,不理解,无脑写?.多容易啊,但是我从另外两个角度分析下:



  1. 我觉得一个合格的开发应该对自己的代码逻辑很熟悉,应该有责任知道哪些值可能是空值,哪些不可能是空值(并不是说所有,也有大部分了),否则就是对自己的代码了解很少,觉得代码能跑就行,代码质量自然就低。

  2. 想想在这个新特性出来之前大家是怎么写的,会对每个变量和属性都加if非空判断或者用逻辑与(&&)吗?不会吧。


总结


本文以一个 code reviewer 角度,分析了 可选链运算符(?.) 特性的滥用情况,以及“正确使用方式”,只是代表我本人的看法,欢迎大佬参与讨论,无条件接受任何反驳。


滥用的缺点:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


“正确用法”:



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。




后记(09月25日更新)


从评论上看,对于可选链的看法,大多声音是能加就加,多加总比少加好,原因就是不想背锅,不想上线后JS动不动就崩了,无论根本原因是不是前端开发没加判断导致的,第一责任人就会找到你,有的甚至会被上级追责,问题就更严重了,而且很难解释清楚;另一方面就是为了赶工期,可选链的其中一个优点就是简单,提高开发效率。


我再从几个方面浅浅的扩展下我的看法,欢迎参与讨论


总之。。。对对对,你们说的都对!


作者:Mark大熊
来源:juejin.cn/post/7280747572707999799
收起阅读 »

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

web
扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
继续阅读 »

扯皮


这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


不过好像目前为止也没见哪个面试官出过...


正文


取消功能


我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


 const p = new Promise((resolve, reject) => {
setTimeout(() => {
// handler data, no resolve and reject
}, 1000);
});
console.log(p); // Promise {<pending>} 💡

但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


const getData = () =>
new Promise((resolve) => {
setTimeout(() => {
console.log("发送网络请求获取数据"); // ❗
resolve("success get Data");
}, 2500);
});

const timer = () =>
new Promise((_, reject) => {
setTimeout(() => {
reject("timeout");
}, 2000);
});

const p = Promise.race([getData(), timer()])
.then((res) => {
console.log("获取数据:", res);
})
.catch((err) => {
console.log("超时: ", err);
});

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


超时中断.png


而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



取消请求.png


其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="send">Send</button>
<button id="cancel">Cancel</button>

<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
console.log("delay cancelled");
resolve();
});
});
}
}
const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

function cancellableDelayedResolve(delay) {
console.log("prepare send request");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
console.log("ajax get data");
resolve();
}, delay);

const cancelToken = new CancelToken((cancelCallback) =>
cancelButton.addEventListener("click", cancelCallback)
);
cancelToken.promise.then(() => clearTimeout(id));
});
}
sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>
</body>
</html>

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


介于自己没理解,我就按照自己的思路封装个不一样的🤣:


const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

class CancelPromise {

// delay: 取消功能期限 request:获取数据请求(必须返回 promise)
constructor(delay, request) {
this.req = request;
this.delay = delay;
this.timer = null;
}

delayResolve() {
return new Promise((resolve, reject) => {
console.log("prepare request");
this.timer = setTimeout(() => {
console.log("send request");
this.timer = null;
this.req().then(
(res) => resolve(res),
(err) => reject(err)
);
}, this.delay);
});
}

cancelResolve() {
console.log("cancel promise");
this.timer && clearTimeout(this.timer);
}
}

// 模拟网络请求
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("this is data");
}, 2000);
});
}

const cp = new CancelPromise(1000, getData);

sendButton.addEventListener("click", () =>
cp.delayResolve().then((res) => {
console.log("拿到数据:", res);
})
);
cancelButton.addEventListener("click", () => cp.cancelResolve());

正常发送请求获取数据:


发送请求.gif


中断 promise:


取消请求.gif


没啥大毛病捏~


进度通知功能


进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "completed"));


emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



// 模拟数据请求
function getData(timer, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, timer);
});
}

let p = new TrackablePromise(async (resolve, reject, notify) => {
try {
const res1 = await getData1();
notify("已获取到一阶段数据");
const res2 = await getData2();
notify("已获取到二阶段数据");
const res3 = await getData3();
notify("已获取到三阶段数据");
resolve([res1, res2, res3]);
} catch (error) {
notify("出错!");
reject(error);
}
});

p.notify((x) => console.log(x));
p.then((res) => console.log("Get All Data:", res));


notify获取数据.gif


对味儿了~😀


End


关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


至于说进度通知功能,仁者见仁智者见智吧...


但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


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

用脚本来写函数式弹窗,更快更爽

web
前言 在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方...
继续阅读 »

前言


在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方式来向弹窗内更新props和处理弹窗的emit事件。


iShot_2023-08-15_10.13.24.gif


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


传统vue编写弹窗


通过变量来直接控制弹窗的开启和关闭。


<template>
<n-button @click="showModal = true">
来吧
</n-button>

<n-modal v-model:show="showModal" preset="dialog" title="Dialog">
<template #header>
<div>标题</div>
</template>
<div>内容</div>
<template #action>
<div>操作</div>
</template>
</n-modal>

</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup () {
return {
showModal: ref(false)
}
}
})
</script>


痛点



  • 深层次的传props让人有很大的心理负担,污染组件props

  • 要关注弹窗show变量的true,false


函数式弹窗


在主页面用Provider包裹一下


// RootPage.vue
<ModalProvider>
<ChildPage></ChildPage>
</ModalProvider>

<script setup lang="ts">
import ModalProvider from "./ModalProvider.vue"
import ChildPage from "./ChildPage.vue"
</script>


在页面内的某个子组件中,直接通过oepn方法打开弹窗


// ChidPage.vue
<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


优势



  • 对于使用者来说简单,没有控制show的心理负担

  • 弹窗内容和其他业务代码分离,不会污染其他组件props和结构

  • 能够充分复用同一个弹窗,通过函数参数的方式来更新弹窗内容,在不同地方打开同一个弹窗


劣势



  • 对于一些简单需求的弹窗,用这种函数式的弹窗会有些臃肿

  • 在使用函数式弹窗前需要做一些操作(Provider和Inject),会在后面提到以及解决方案。


如何使用这种函数式的弹窗


原理


通过在根页面将我们的Modal组件挂载上去,然后通过使用hook去管理这个modal组件的状态值(props,ref),然后通过provide将props和event和我们的modal组件联系起来,再在我们需要使用弹窗的地方使用inject更新props来启动弹窗。


步骤1(❌):编写Modal


这里我使用的是Naive Ui的Modal组件,按喜好选择就行。按照你的需求编写弹窗的内容。定义好props和emits,写好他们的类型申明。


// TestModal.vue
<template>
<n-modal
v-model:show="isShowModal"
preset="dialog"
@after-leave="handleClose"
>

...你的弹窗内容
</n-modal>

</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue"
import { useVModel } from "@vueuse/core"
import { FormRules } from "naive-ui"
interface ModalProps {
show: boolean
testValue: string
}

// 中间区域不要修改
const props = defineProps<ModalProps>()
const formRef = ref()
const loading = ref(false)
const isShowModal = useVModel(props, "show")
// 中间区域不要修改

const rules: FormRules = []

const formData = reactive({
testValue: props.testValue,
})

const callBackData = computed(() => {
return {
formData,
}
})

watch(
() => props.show,
() => {
if (props.show) {
formData.testValue = props.testValue
} else {
formData.testValue = ""
}
}
)

const emits = defineEmits<{
(e: "update:show", value: boolean): void
(e: "close", param: typeof callBackData.value): void
(
e: "confirm",
param: typeof callBackData.value,
close: () => void,
endLoading: () => void
): void
(e: "cancel", param: typeof callBackData.value): void
}>()
function handleCancel() {
// 中间区域不要修改
emits("cancel", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleClose() {
// 中间区域不要修改
emits("close", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleConfirm() {
// 中间区域不要修改
loading.value = true
emits(
"confirm",
callBackData.value,
() => {
loading.value = false
isShowModal.value = false
},
() => {
loading.value = false
}
)
// 中间区域不要修改
}
</script>

步骤2(❌):编写hook来管理弹窗的状态


在这个文件里面,使用hook管理 TestModal 弹窗需要的props和event事件,然后我们通过ts类型体操来获取TestModal的props类型和event类型,我们向外inject一个 open 函数,这个函数可以更新 TestModal 的props和event,同时打开弹窗,他的参数有完整的类型提示,可以让使用者更加明确的使用我们的弹窗。


// use-test-modal.ts
import {
ref,
provide,
InjectionKey,
inject,
VNodeProps,
AllowedComponentProps,
reactive,
} from "vue";
import Modal from "./TestModal.vue";

/**
* 通过引入弹窗组件来获取组件的除show,update:show以外的props和emits来作为open函数的
*/

type ModalInstance = InstanceType<
typeof Modal extends abstract new (...args: any) => any ? typeof Modal : any
>["$props"];
type OpenParam = Omit<
{
readonly [K in keyof Omit<
ModalInstance,
keyof VNodeProps | keyof AllowedComponentProps
>]: ModalInstance[K];
},
"show" | "onUpdate:show"
>;

interface AnyFileChangeModal {
open: (param?: OpenParam) => Promise<void>;
}

/**
* 通过弹窗实例来获取弹窗组件内需要哪些props
*/

type AllProps = Omit<
OpenParam,
"onClose" | "onCancel" | "onConfirm" | "onUpdate:show"
> & { show: boolean };
const anyModalKey: InjectionKey<AnyFileChangeModal> = Symbol("ModalKey");
export function provideTestModal() {
const allProps: AllProps = reactive({
show: false,
} as AllProps);
const closeCallback = ref();
const cancelCallback = ref();
const confirmCallback = ref();
const handleUpdateShow = (value: boolean) => {
allProps.show = value;
};

/**
* @param param 通过函数来更新props
*/

function updateAllProps(param: OpenParam) {
const excludeKey = ["show", "onClose", "onConfirm", "onCancel"];
for (const [key, value] of Object.entries(param)) {
if (!excludeKey.includes(key)) {
allProps[key] = value;
}
}
}
function clearAllProps() {
for (const [key] of Object.entries(allProps)) {
allProps[key] = undefined;
}
}

async function open(param: OpenParam) {
clearAllProps();
updateAllProps(param);
allProps.show = true;
param.onClose && (closeCallback.value = param.onClose);
param.onConfirm && (confirmCallback.value = param.onConfirm);
param.onCancel && (cancelCallback.value = param.onCancel);
}
provide(anyModalKey, { open });
return {
allProps,
closeCallback,
confirmCallback,
cancelCallback,
handleUpdateShow,
};
}

export function injectTestModal() {
return inject(anyModalKey)
}
Ï

步骤3(❌):提供Provider


在这个文件里,我将TestModal放在了根页面,然后将hook返回的props和event绑定给TestModal


// ModalProvider.vue
<template>
<slot />

<TestModal
v-bind="allTestModalProps"
@update:show="handleTestModalUpdateShow"
@close="closeTestModalCallback"
@confirm="confirmTestModalCallback"
@cancel="cancelTestModalCallback"
/>

<!-- 新增Modal -->
</template>

<script setup lang="ts">
import TestModal from "./test-modal/TestModal.vue";
import { provideTestModal } from "./test-modal/use-test-modal";
/** 新增import */

const {
allProps: allTestModalProps,
handleUpdateShow: handleTestModalUpdateShow,
closeCallback: closeTestModalCallback,
confirmCallback: confirmTestModalCallback,
cancelCallback: cancelTestModalCallback,
} = provideTestModal();
/** 新增provide */
</script>


步骤4(❌):通过函数打开弹窗


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


到这里也就结束了。如果这样写一个弹窗,确实可以达到函数式的打开弹窗,但是他实在是太繁琐了,要写这么一堆东西,不如直接修改弹窗的show来的快。如果看过上面的use-test-modal.ts的话,会发现里面有AllProps,这是为了减少方便使用脚本工具来写通用化的代码,可以大大减少人工的重复代码编写,接下来我们使用一个工具来减少这些繁琐的操作。


步骤1(✅):初始化Provider


通过使用工具生成根页面ModalProvder组件。
具体步骤:在你想放置当前业务版本的弹窗文件夹路径下使用终端执行脚本,选择initProvider


iShot_2023-08-15_15.16.14.gif


步骤2(✅):生成弹窗模板


通过继续使用脚本工具,生成弹窗组件以及hook文件。
具体步骤:在你想放该弹窗的文件夹路径下使用终端,使用脚本工具,选择genModal,然后跟着指令操作就行。比如例子中,我们生产名为test的弹窗,然后告诉脚本我们的ModalProvider组件的绝对路径,脚本帮我生产test-modal的文件夹,里面放着testModal.vueuse-test-modal.ts,这里的use-test-modal.ts文件已经是成品了,不需要你去修改,ModalProvider.vue也不需要你去修改,里面的路径关系也帮你处理好了。


iShot_2023-08-15_15.17.33.gif


步骤3(✅):修改弹窗内容


上一步操作中,脚本帮我写好了TestModal.vue组件,我们可以在他的基础上完善我们的业务需求。


步骤4(✅):调用弹窗


我们找到生成的use-test-modal.ts,里面有一个injectXXXModal的方法,我们在哪个地方用到弹窗就引用执行他,返回的对象中有open方法,通过open方法去开启弹窗并且更新弹窗props。


Demo


预览
Demo地址
里面有完整的demo代码


iShot_2023-08-15_18.32.39.gif


脚本工具


仓库地址
这个是我写的脚本工具,用来减少一些重复代码的工作。用法可看readme


总结


本文先通过比较函数式的弹窗和直接传统编写弹窗的优劣,然后引出本文的主旨(如何编写函数式弹窗),再考虑我这一版函数式弹窗的问题,最后通过脚本工具来解决这一版函数式弹窗的劣势,完整的配合脚本工具,可以极大的加快我们的工作效率。
大家不要看这么写要好几个文件夹,实际使用的时候其实就是脚本生成代码后,我们只需要去修改弹窗主题的内容。有什么疑惑或者建议评论区聊。


作者:恐怖屋
来源:juejin.cn/post/7267418473401057321
收起阅读 »

当别人因为React、Vue吵起来时,我们应该做什么

web
大家好,我卡颂。 最近尤大的一个推文引起了不小热议,大概经过是: 有人在推上夸React文档写的好,把可能的坑点都列出来 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户 尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关...
继续阅读 »

大家好,我卡颂。


最近尤大的一个推文引起了不小热议,大概经过是:



  1. 有人在推上夸React文档写的好,把可能的坑点都列出来

  2. 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户



尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关注度自然不低。


再加上国内前端自媒体的一波引导发酵,比如知乎下这个话题相关的问题中的措辞是怒喷,懂得都懂。



在这样氛围与二手信源的影响下,会给人一种大佬都亲手下场撕了的感觉,自然会引来ReactVue各自拥趸的一番激烈讨论。


年年都是一样的套路,毫无新意......


面对这样的争吵,我们应该做什么呢?


首先,回到源头本身,尤大diss的有道理么?有。


React的心智负担重么?确实重。比如useEffec这个API,你能想象文档中一个章节居然有6篇文章是教你如何正确使用useEffec的么?



造成这一现象的原因有很多,比如:



  1. Hooks的实现原理使得必须显式声明依赖

  2. 显式声明依赖无法覆盖useEffect所有场景,为此专门提出一个叫Effect Event的概念,以及一个对应的新hook —— useEffectEvent

  3. useEffect承载了太多功能,比如未来Offscreen的显隐回调(类似Vue中的Keep-Alive)也是通过useEffect实现


当我们继续往前回溯,Hooks必须显式声明依赖React更新机制决定的,而React更新机制又是React实现原理的核心。


本质来说,还是React既往的成功、庞大的社区生态让他积重难返,无法从底层重写。


这是历史必然的进程,如果Vue所有新特性都在Vue2基础上迭代(而不是完全重写的Vue3),我相信也是同样的局面。


所以,当前React的迭代方向是 —— 支持上层框架(比如Next.jsRemix),寄希望于靠这些框架的封装能力弥补React自身心智负担重的缺点。这个策略显然也是成功的。


回到这次争吵本身,尤大不知道React文档为什么要花大篇幅帮开发者避坑(以及背后反映的积重难返)么?他显然是知道的。


他如此回复是因为他所处的位置是框架作者React是他的竞争对手。设想一下,如果你的竞争对手在一些方面确实不如你,但他的用户对此的反应不是“太难用了,我要换个好用的”,而是“一定是我用的姿势不对,你快出个文档好好教教我”


面对这样的用户,换谁都得有一肚子牢骚吧~



让我们再把视角转到React的用户(也就是我们这些普通开发者)上。我们为什么选择React呢?


可能有些人是处于喜好。但大部分开发者之所以用React,完全是因为公司要求用React


React的公司多,招React的岗位多,自然选择React的开发者就多了。


那么为什么用React的公司多呢?这显然是多年前React在先发优势、社区生态两场战役取胜后得到的结果。


总结


所以,我们需要尊重两个事实:



  1. React心智负担重是事实

  2. React的公司多也是事实


两者并不矛盾,他们都是历史进程的产物。


VueReact之间的讨论,即使是从技术层面出发,最后也容易陷入“React心智负担这么重,你们还甘之如饴,你们React党是不是傻”这样的争吵中。


这显然就是忽略了历史的进程。


正确的应对方式是多关心关心自己未来的发展:



  • 如果我的重心在海外,那应该给Next.js更多关注。海外远程团队不是Next就是Nest

  • 如果我的重心在国内,国内流量都被小程序分割了。一个长远的增长点应该是鸿蒙


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

面试官:手写一个“发布-订阅模式”

web
发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。 DOM事件 document.body.addEventListener...
继续阅读 »

发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。


DOM事件


document.body.addEventListener('click',function(){

alert(绑定1);

},false);

document.body.click(); //模拟点击

document.body.addEventListener('click',function(){

alert(绑定2);

},false);

document.body.addEventListener('click',function(){

alert(绑定3);

},false);

document.body.click(); //模拟点击

我们可以增加更多订阅者,不会对发布者的代码造成影响。注意,标准浏览器下用dispatchEvent实现。


自定义事件


① 确定发布者。(例如售票处)


② 添加缓存列表,便于通知订阅者。(预订车票列表)


③ 发布消息。遍历缓存列表。依次触发里面存放的订阅者回调函数(遍历列表,逐个发送短信)。


另外,我们还可以在回调函数填入一些参数,例如车票的价格之类信息。


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (fn) { //增加订阅者
this.clientList.push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
for(var i = 0, fn; fn = this.clientList[i++];){
fn.apply(this, arguments); //arguments 是发布消息时带上的参数
}
}

// 下面进行简单测试:

ticketOffice.on(function(time, path){ //小刚订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});


ticketOffice.on(function(time, path){ //小强订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});

ticketOffice.emit('晚上8:00','深圳-上海');
ticketOffice.emit('晚上8:10','上海-深圳');

至此,我们实现了一个最简单发布-订阅模式。不过这里存在一些问题,我们运行代码可以看到订阅者接收到了所有发布的消息。


// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:10
// 路线:上海-深圳
// 时间:晚上8:10
// 路线:上海-深圳

我们有必要加个key让订阅者只订阅 自己感兴趣的消息。改写后的代码如下:


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// --------- 测试数据 --------------
ticketOffice.on('上海-深圳', function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
});

ticketOffice.on('深圳-上海', function(time){ //小强订阅消息
console.log('小强时间:' + time);
});

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小强时间:晚上8:00
// 小刚时间:晚上8:10

这样子,订阅者就可以只订阅自己感兴趣的事件了。


小强临时行程有变,不想订阅对应的消息了,我们还需要再新增一个移除订阅的方法


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

ticketOffice.remove = function (key, fn) {
let fns = this.clientList[key]
if (!fns) return false

if (!fn) {
fns && (fns.length = 0)
} else {
fns.forEach((cb, i) => {
if (cb === fn) {
fns.splice(i, 1)
}
})
}
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

至此,我们实现了一个相对完善的发布-订阅模式


但是可以看到使用数组进行时间的push和remove可能绑定相同的事件,且事件remove的效率低,我们可以用Set来替换Array


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = new Set();
}
this.clientList[key].add(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// 移除路线的单个订阅
ticketOffice.remove = function (key, fn) {
this.clientList[key]?.delete(fn)
}
// 移除路线的所有订阅
ticketOffice.removeAll = function (key) {
delete this.clientList[key]
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

参考资料


《JavaScript 设计模式与开发实践》


作者:dudulala
来源:juejin.cn/post/7320075000702533671
收起阅读 »

普通的文本输入框无法实现文字高亮?试试这个highlightInput吧!

web
背景 前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到...
继续阅读 »

背景


前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到:没办法,这是老板提的需求,你下去研究研究吧。行吧,老板发话说啥也没用,开干吧!


实现思路


实现标红就需要给文字加上html标签和样式,但是输入框会将html都转为字符串,既然输入框无法实现,那么我们换一种思路,通过div代替输入框来显示输入的文本,那我们是不是就可以实现文本标红了?话不多说,直接上代码(文章结尾会附上demo):


<div class="main">
<div id="shadowInput" class="highlight-shadow-input"></div>
<textarea
id="textarea"
cols="30"
rows="10"
class="highlight-input"
>
</textarea>
</div>

.main {
position: relative;
}
.highlight-shadow-input {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
padding: 8px;
border: 1px;
box-sizing: border-box;
font-size: 12px;
font-family: monospace;
overflow-y: auto;
word-break: break-all;
white-space: pre-wrap;
}
.highlight-input {
position: relative;
width: 100%;
padding: 8px;
box-sizing: border-box;
font-size: 12px;
background: rgba(0, 0, 0, 0);
-webkit-text-fill-color: transparent;
z-index: 999;
word-break: break-all;
}

实现这个功能的精髓就在于将输入框的背景和输入的文字设置为透明,然后将其层级设置在div之上,这样用户既可以在输入框中输入,而输入的文字又不会展示出来,然后将输入的文本处理后渲染到div上。


const textarea = document.getElementById("textarea");
const shadowInput = document.getElementById("shadowInput");
const sensitive = ["敏感词", "禁用词"];
textarea.oninput = (e) => {
let value = e.target.value;
sensitive.forEach((word) => {
value = value.replaceAll(
word,
`<span style="color:#e52e2e">${word}</span>`
).replaceAll("\n", "<br>");;
});
shadowInput.innerHTML = value;
};

监听输入框oninput事件,用replaceAll匹配到敏感词并转为html后渲染到shadowInput上。此外,我们还需要对输入框的滚动进行监听,因为shadowInput是固定高度的,如果用户输入的文本出现滚动条,则需要让shadowInput也滚动到对应的位置


<div><div id="shadowInput" class="highlight-shadow-input"></div></div>

textarea.onscroll = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};
// 此处输入时也需要同步是因为输入触底换行时,div的高度不会自动滚动
textarea.onkeydown = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};

最终实现效果:


至此一个简单的文本输入框实现文字高亮的功能就完成了,上述代码只是简单示例,在实际业务场景中还需要考虑xss注入、特殊字符处理、特殊字符高亮等等复杂问题。


总结


这篇文章主要给遇到有类似业务需求的同学一个参考,以及激发大家的灵感,用这种方法是不是还可以实现一些简单的富文本功能呢?例如文字加删除线、文字斜体加粗等等。有想法或有问题的小伙伴可以在评论区留言一起探讨哦!


demo



作者:宇智波一打七
来源:juejin.cn/post/7295169886177918985
收起阅读 »

多行标签超出展开折叠功能

web
前言  记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。  今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,...
继续阅读 »

前言


 记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。
 今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,类似于多行文本展开折叠功能,如果超过最大行数则显示展开隐藏按钮,如果不超过则不显示按钮。多行文本展开与折叠功能在网上有相当多的文章了,也有许多开源的封装组件,而多行标签展开隐藏的文章却比较少,刚好最近我也遇到了这个功能,所以就单独拿出来与大家分享如何实现。


出处


 【多行标签展开与隐藏】该功能我们平时可能没注意一般在哪里会有,其实最常见的就是各种APP的搜索页面的历史记录这里,下面是我从拼多多(左)和腾讯学堂小程序(右)截下来的功能样式:


多行标签案列图(pdd/txxt)


其它APP一般搜索的历史记录这里都有这个小功能,比如京东、支付宝、淘宝、抖音、快手等,可能稍有点儿不一样,有的是按钮样式,有的是只有展开没有收起功能,可能我们用过了很多年平时都没有注意到这个小功能,有想了解的可以去看一看哈。如果有一天你们需要开发一个搜索页面的话产品就很有可能出这样的一个功能,接下来我们就来看看这种功能我们该如何实现。


功能实现


我们先看实现的效果图,然后再分析如何实现,效果图如下:



【样式一】:标签容器和展开隐藏按钮分开(效果图样式一)


 标签容器和按钮分开的这种样式功能实现起来的话我个人觉得难度稍微简单一些,下面我们看看如何实现这种分开的功能。


第一种方法:通过与第一个标签左偏移值对比实现

原理:遍历每个标签然后通过与第一个标签左偏移值对比,如果有几个相同偏移值则说明有几个换行


具体实现上代码:


<div class="list-con list-con-1">
<div class="label">人工智能div>
<div class="label">人工智能与应用div>
<div class="label">行业分析与市场数据div>
<div class="label">标签标签标签标签标签标签标签标签div>
<div class="label">标签div>
<div class="label">啊啊啊div>
<div class="label">宝宝贝贝div>
<div class="label">微信div>
<div class="label">吧啊啊div>
<div class="label">哦哦哦哦哦哦哦哦div>
div>
<div class="expand expand-1">展开 ∨div>



解析:HTML布局就不用多说了,是个前端都知道该怎么搞,如果不知道趁早送外卖去吧,多说无益,把机会留给其他人。其次CSS应该也是比较简单的,注意的是有个前提需要先规定容器的最大高度,然后使用overflow超出隐藏,这样展开就直接去掉该属性,让标签自己撑开即可。JavaScript部分我这里没有使用啥框架,因为这块实现就是个简单的Demo所以就用纯原生写比较方便,这里我们先获取容器,然后获取容器的孩子节点(这里我们也可以直接通过className查询出所有标签元素),返回的是一个可遍历的变签对象,然后我们记录第一个标签的offsetLeft左偏移值,接下来遍历所有的标签元素,如果有与第一个标签相同的值则累加,最终line表示有几行,如果超过我们最大行数(demo超出2行隐藏)则显示展开隐藏按钮。


第二种方法:通过计算容器高度对比

原理:通过容器底部与标签top比较,如果有top值大于容器底部bottom则表示超出容器隐藏。


具体上代码:




解析:HTMLCSS同方法一同,不同点在于这里是通过getBoundingClientRect()方法来判断,还是遍历所有标签,不同的是如果有标签的top值大于等于了容器的bottom值,则说明了标签已超出容器,则要显示展开隐藏按钮,展开隐藏还是通过容器overflow属性来实现比较简单。


【样式二】:展开隐藏按钮和标签同级(效果图样式二)


 这种样式也是绝大部分APP产品使用的风格,不信你可以打开抖音商城或汽车之家的搜索历史,十个产品九个是这样设计的,不是这样的我倒立洗头。
 这种放在同级的就相对稍微难一点,因为要把展开隐藏按钮塞到标签的最后,如果是隐藏的话就要切割标签展示数量,那下面我就带大家看看我是是如何实现的。


方法一:通过遍历高度判断

原理:同样式一的高度判断一样,通过容器底部bottom与标签top比较,如果有top值大于容器顶部bottom则表示超出容器隐藏,不同的是如何计算标签展示的长度。有个前提是按钮和标签的的宽度要做限制,最好是一行能放一个标签和按钮。


具体实现上代码:


<div id="app3">
<div class="list-con list-con-3" :class="{'list-expand': isExpand}">
<div class="label" v-for="item in labelArr.slice(0, labelLength)">{{ item }}div>
<div class="label expand-btn" v-if="showExpandBtn" @click="changeExpand">{{ !isExpand ? '展开 ▼' : '隐藏 ▲' }}div>
div>
div>


<script>
const { createApp, nextTick } = Vue
createApp({
props: {
maxLine: {
type: Number,
default: 2
}
},
data () {
return {
labelArr: [],
isExpand: false,
showExpandBtn: false,
labelLength: 0,
hideLength: 0
}
},
mounted () {
const labels = ['人工智能', '人工智能与应用', '行业分析与市场数据', '标签标签标签标签标签标签标签', '标签A', '啊啊啊', '宝宝贝贝', '微信', '吧啊啊', '哦哦哦哦哦哦哦哦', '人工智能', '人工智能与应用']

this.labelArr = labels
this.labelLength = labels.length
nextTick(() => {
this.init()
})
},
methods: {
init () {
const listCon = document.querySelector('.list-con-3')
const labels = listCon.querySelectorAll('.label:not(.expand-btn)')
const expandBtn = listCon.querySelector('.expand-btn')

let labelIndex = 0 // 渲染到第几个
const listConBottom = listCon.getBoundingClientRect().bottom // 容器底部距视口顶部距离
for(let i = 0; i < labels.length; i++) {
const _top = labels[i].getBoundingClientRect().top
if (_top >= listConBottom ) { // 如果有标签顶部距离超过容器底部则表示超出容器隐藏
this.showExpandBtn = true
console.log('第几个索引标签停止', i)
labelIndex = i
break
} else {
this.showExpandBtn = false
}
}
if (!this.showExpandBtn) {
return
}
nextTick(() => {
const listConRect = listCon.getBoundingClientRect()
const expandBtn = listCon.querySelector('.expand-btn')
const expandBtnWidth = expandBtn.getBoundingClientRect().width
const labelMaringRight = parseInt(window.getComputedStyle(labels[0]).marginRight)
for (let i = labelIndex -1; i >= 0; i--) {
const labelRight = labels[i].getBoundingClientRect().right - listConRect.left
if (labelRight + labelMaringRight + expandBtnWidth <= listConRect.width) {
this.hideLength = i + 1
this.labelLength = this.hideLength
break
}
}
})
},
changeExpand () {
this.isExpand = !this.isExpand
console.log(this.labelLength)
if (this.isExpand) {
this.labelLength = this.labelArr.length
} else {
this.labelLength = this.hideLength
}
}
}
}).mount('#app3')
script>


解析:同级样式Demo我们使用vue来实现,HTML布局和CSS样式没有啥可说的,还是那就话,不行真就送外卖去比较合适,这里我们主要分析一下Javascript部分,还是先通过getBoundingClientRect()方法来获取容器的bottom和标签的top,通过遍历每个标签来对比是否超出容器,然后我们拿到第一个超出容器的标签序号,就是我们要截断的长度,这里是通过数组的slice()方法来截取标签长度,接下来最关建的如何把按钮拼接上去,因为标签的宽度是不定的,我们要把按钮显示在最后,我们并不确定按钮拼接到最后是不是会导致宽度不够超出,所以我们倒叙遍历标签,如果(最后一个标签的右边到容器的距离right值+标签的margin值+按钮的width)和小于容器宽度,则说明展示隐藏按钮可以直接拼接在后面,否则标签数组长度就要再减一位来判断是否满足。然后展开隐藏功能就通过切换原标签长度和截取的标签长度来完成即可。


方法二:通过与第一个标签左偏移值对比实现

原理:同样式一的方法原理,遍历每个标签然后通过与第一个标签左偏移值对比判断是否超出行数,然后长度截取同方法一一致。


直接上代码:




这里也无需多做解释了,直接看代码即可。


结尾


上面就是【多行标签展开隐藏】功能的基本实现原理,网上相关实现比较少,我也是只用了Javascript来实现,如果可以纯靠CSS实现,有更简单或更好的方法实现可以留言相互交流学。代码没有封装成组件,但是具有一些参考意义,用于生产可以自己去封装成组件使用,完整的代码在我的GitHub仓库。




作者:Liben
来源:juejin.cn/post/7251394142683742269
收起阅读 »

MyBatis实战指南(二):工作原理与基础使用详解

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。一、MyBatis的工作原理1.1 MyBatis的工作原理工作原理图示:1、读取...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。

一、MyBatis的工作原理

1.1 MyBatis的工作原理

工作原理图示:
Description

1、读取MyBatis配置文件

mybatis-config.xml为MyBatis的全局配置文件,配置了MyBatis的运行环境等信息,例如数据库连接信息。

2、加载映射文件(SQL映射文件,一般是XXXMapper.xml)

该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。

XXXMapper.xml可以在mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。

3、构造会话工厂

通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。

4、创建会话对象

由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

5、Executor执行器

MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

6、MappedStatement对象

在 Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

7、输入参数映射

输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

8、输出结果映射

输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

1.2 MyBatis架构

Description

API接口层

提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

MyBatis和数据库的交互有两种方式:使用传统的MyBatis提供的API、使用Mapper接口。

1)使用传统的MyBatis提供的API

这是传统的传递Statement Id和查询参数给SqlSession对象,使用SqlSession对象完成和数据库的交互;
Description
MyBatis提供了非常方便和简单的API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和MyBatis自身配置信息的维护操作。
示例:

SqlSession session = sqlSessionFactory.openSession();
Category c = new Category();
c.setName("新增加的Category");
session.insert("addCategory",c);

上述使用MyBatis的方法,是创建一个和数据库打交道的SqlSession对象,然后根据Statement Id和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。

2)使用Mapper接口

MyBatis将配置文件中的每一个<mapper>节点抽象为一个Mapper接口,而这个接口中声明的方法和跟<mapper>节点中的<select|update|delete|insert>节点项对应,

即<select|update|delete|insert>节点的id值为Mapper接口中的方法名称,parameterType值表示Mapper对应方法的入参类型,而resultMap值则对应了Mapper接口表示的返回值类型或者返回结果集的元素类型。

示例:

SqlSession session = sqlSessionFactory.openSession();
CategoryMapper mapper = session.getMapper(CategoryMapper.class);
List<Category> cs = mapper.list();
for (Category c : cs) {
System.out.println(c.getName());
}

根据MyBatis的配置规范配置后,通过SqlSession.getMapper(XXXMapper.class)方法,MyBatis会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper实例。
Description

使用Mapper接口的某一个方法时,MyBatis会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select(“statementId”,parameterObject)或者SqlSession.update(“statementId”,parameterObject)等等来实现对数据库的操作。

MyBatis引用Mapper接口这种调用方式,纯粹是为了满足面向接口编程的需要。

数据处理层

负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

1)参数映射和动态SQL语句生成

动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis通过传入的参数值,使用OGNL表达式来动态地构造SQL语句,使得MyBatis有很强的灵活性和扩展性。
Description
参数映射指的是对于Java数据类型和JDBC数据类型之间的转换,这里包括两个过程:

  • 查询阶段,我们要将java类型的数据,转换成JDBC类型的数据,通过preparedStatement.setXXX()来设值;

  • 另一个就是对ResultSet查询结果集的JdbcType 数据转换成Java数据类型。

2)SQL语句的执行以及封装查询结果集成List< E>

动态SQL语句生成之后,MyBatis将执行SQL语句并将可能返回的结果集转换成List<E> 。

MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。

基础支撑层

负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

MyBatis层次结构

Description

1.3 Executor执行器

Executor的类别

Mybatis有三种基本的Executor执行器:SimpleExecutor、ReuseExecutor和BatchExecutor。

Description

1、SimpleExecutor

每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

2、ReuseExecutor

执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。

3、BatchExecutor

执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

Executor的配置

指定Executor方式有两种:

1、在配置文件中指定

<settings>
<setting name="defaultExecutorType" value="BATCH" />
</settings>

2、在代码中指定

在获取SqlSession时设置,需要注意的时是,如果选择的是批量执行器时,需要手工提交事务(默认不传参就是SimpleExecutor)。

示例:

// 获取指定执行器的sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 获取批量执行器时, 需要手动提交事务
sqlSession.commit();

1.4 Mybatis是否支持延迟加载

延迟加载是什么

MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询。

例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,mybatis再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。

MyBatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。

假如Clazz 类中有子对象HeadTeacher。两者的关系:

public class Clazz {
private Set<HeadTeacher> headTeacher;
//...
}

是否查出关联对象的示例:

@Test
public void testClazz() {
ClazzDao clazzDao = sqlSession.getMapper(ClazzDao.class);
Clazz clazz = clazzDao.queryClazzById(1);
//只查出主对象
System.out.println(clazz.getClassName());
//需要查出关联对象
System.out.println(clazz.getHeadTeacher().size());
}

延迟加载的设置

在Mybatis中,延迟加载可以分为两种:延迟加载属性和延迟加载集合,association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。

在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false

1)延迟加载的全局设置

延迟加载默认是关闭的。如果需要打开,需要在mybatis-config.xml中修改:

<settings>

<setting name="lazyLoadingEnabled" value="true" />

<setting name="aggressiveLazyLoading" value="false"/>
</settings>

比如class班级与student学生之间是一对多关系。在加载时,可以先加载class数据,当需要使用到student数据时,我们再加载 student 的相关数据。

  • 侵入式延迟加载:指的是只要主表的任一属性加载,就会触发延迟加载,比如:class的name被加载,student信息就会被触发加载。

  • 深度延迟加载: 指的是只有关联的从表信息被加载,延迟加载才会被触发。通常,更倾向使用深度延迟加载。

2)延迟加载的局部设置

如果设置了全局加载,但是希望在某一个sql语句查询的时候不适用延时策略,可以配置局部的加载策略。

示例:

 <association
property="dept" select="com.test.dao.DeptDao.getDeptAndEmpsBySimple"
column="deptno" fetchType="eager"/>

etchType值有2种,

  • eager:立即加载;

  • lazy:延迟加载。

由于局部的加载策略的优先级高于全局的加载策略。指定属性后,将在映射中忽略全局配置参数lazyLoadingEnabled,使用属性的值。

延迟加载的原理

MyBatis使用Java动态代理来为查询对象生成一个代理对象。当访问代理对象的属性时,MyBatis会检查该属性是否需要进行延迟加载。

如果需要延迟加载,则MyBatis将再次执行SQL查询,并将查询结果填充到代理对象中。

二、MyBatis基础使用示例

1、添加MyBatis依赖

首先,我们需要在项目中添加MyBatis的依赖。如果你使用的是Maven项目,可以在pom.xml文件中添加以下依赖:

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

2、创建实体类

假设我们有一个用户表(user),我们可以创建一个对应的实体类User:

public class User {
private int id;
private String name;
private int age;
// getter和setter方法省略
}

3、创建映射文件UserMapper.xml

在MyBatis的映射文件中,我们需要定义一个与实体类对应的接口。例如,我们可以创建一个名为UserMapper的接口:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUserById" parameterType="int" resultType="com.example.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>

4、创建接口UserMapper.java

接下来,我们需要创建一个与映射文件对应的接口。例如,我们可以创建一个名为UserMapper的接口:

package com.example;

import com.example.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") int id);
}

5、使用MyBatis进行数据库操作

最后,我们可以在业务代码中使用MyBatis进行数据库操作。例如,我们可以在一个名为UserService的类中调用UserMapper接口的方法:

public class UserService {
public User getUserById(int id) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(id);
sqlSession.close();
return user;
}
}

总结:

MyBatis是一个非常强大的持久层框架,它可以帮助我们简化数据库操作,提高开发效率。在实际开发中,我们还可以使用MyBatis进行更复杂的数据库操作,如插入、更新、删除等。希望这篇文章能帮助你更好地理解和使用MyBatis。

收起阅读 »

10个让你爱不释手的一行Javascript代码

web
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。 获取数组中的随机元素 使用 Math.rand...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。


获取数组中的随机元素


使用 Math.random() 函数和数组长度可以轻松获取数组中的随机元素:


const arr = [1, 2, 3, 4, 5];
const randomElement = arr[Math.floor(Math.random() * arr.length)];
console.log(randomElement);

数组扁平化


使用 reduce() 函数和 concat() 函数可以轻松实现数组扁平化:


const arr = [[1, 2], [3, 4], [5, 6]];
const flattenedArr = arr.reduce((acc, cur) => acc.concat(cur), []);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6]

对象数组根据某个属性值进行排序


const sortedArray = array.sort((a, b) => (a.property > b.property ? 1 : -1));

从数组中删除特定元素


const removedArray = array.filter((item) => item !== elementToRemove);

检查数组中是否存在重复项


const hasDuplicates = (array) => new Set(array).size !== array.length;

判断数组是否包含某个值


const hasValue = arr.includes(value);

首字母大写


const capitalized = str.charAt(0).toUpperCase() + str.slice(1);

获取随机整数


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

获取随机字符串


const randomStr = Math.random().toString(36).substring(2, length);

使用解构和 rest 运算符交换变量的值:


let a = 1, b = 2
[b, a] = [a, b]
console.log(a, b) // 2, 1

将字符串转换为小驼峰式命名:


const str = 'hello world'
const camelCase = str.replace(/\s(.)/g, ($1) => $1.toUpperCase()).replace(/\s/g, '').replace(/^(.)/, ($1) => $1.toLowerCase())
console.log(camelCase) // "helloWorld"

计算两个日期之间的间隔


const diffInDays = (dateA, dateB) => Math.floor((dateB - dateA) / (1000 * 60 * 60 * 24));

查找日期位于一年中的第几天


const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

复制内容到剪切板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("Hello World");

获取变量的类型


const getType = (variable) => Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();

getType(''); // string
getType(0); // number
getType(); // undefined
getType(null); // null
getType({}); // object
getType([]); // array
getType(0); // number
getType(() => {}); // function

检测对象是否为空


const isEmptyObject = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object;

系列文章



我的更多前端资讯


欢迎大家技术交流 资料分享 摸鱼 求助皆可 —链接


作者:shichuan
来源:juejin.cn/post/7230810119122190397
收起阅读 »

检测自己网站是否被嵌套在iframe下并从中跳出

web
iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。 本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。 末尾放了正在使用的完整代码,想直接用的可以拉到最后。 效果 当存...
继续阅读 »

iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。

本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。


末尾放了正在使用的完整代码,想直接用的可以拉到最后。


效果


当存在嵌套时会出现一个蒙版和窗口,提示用户点击。

点击后会在新窗口打开网站页面。


嵌套展示


嵌套检测


设置响应头


响应头中有一个名为X-Frame-Options的键,可以针对嵌套操作做限制。

它有3个可选值:


DENY:拒绝所有


SAMEORIGIN:只允许同源


ALLOW-FROM origin:指定可用的嵌套域名,新浏览器已弃用


后端检测(以PHP为例)


通过获取$_SERVER中的HTTP_REFERERHTTP_SEC_FETCH_DEST值,可以判断是否正在被iframe嵌套


// 如果不是iframe,就为空的字符串
$REFERER_URL = $_SERVER['HTTP_REFERER'];

// 资源类型,如果是iframe引用的,会是iframe
$SEC_FETCH_DEST = $_SERVER['HTTP_SEC_FETCH_DEST'];

// 默认没有被嵌套
$isInIframe = false;

if (isset($_SERVER['HTTP_REFERER'])) {
$refererUrl = parse_url($_SERVER['HTTP_REFERER']);
$refererHost = isset($refererUrl['host']) ? $refererUrl['host'] : '';

if (!empty($refererHost) && $refererHost !== $_SERVER['HTTP_HOST']) {
$isInIframe = true;
}
}

// 这里通过判断$isInIframe是否为真,来处理嵌套和未嵌套执行的动作。
if($isInIframe){
....
}

前端检测(使用JavaScript)


通过比较window.self(当前窗口对象)和window.top(顶层窗口对象)可以判断是否正在被iframe嵌套


if (window.self !== window.top) {
// 检测到嵌套时该干的事
}

从嵌套中跳出


跳出只能是前端处理,如果使用了PHP等后端检测,可以直接返回前端JavaScript代码,或者HTML的A标签设置转跳。


JavaScript直接转跳(不推荐)


不推荐是因为现在大多浏览器为了防止滥用,会阻止自动弹出新窗口。


window.open(window.location.href, '_blank');

A标签点击转跳(较为推荐)


当发生了用户交互事件,浏览器就不会阻止转跳了,所以这是个不错的方法。


href="https://www.9kr.cc" target="_blank">点击进入博客

JavaScript+A标签(最佳方法)


原理是先使用JavaScript检测是否存在嵌套,

如果存在嵌套,再使用JavaScript加载蒙版和A标签,引导用户点击。


这个方法直接查看最后一节。


正在使用的方法


也就是上一节说的JavaScript+A标签。


先给待会要显示的蒙版和A标签窗口设置样式


/* 蒙版样式 */
.overlay1 {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景颜色 */
z-index: 9999; /* 确保蒙版位于其他元素之上 */
display: flex;
align-items: center;
justify-content: center;
}

/* 窗口样式 */
.modal1 {
background-color: #fff;
padding: 20px;
border-radius: 5px;
}

然后是检测和加载蒙版+A标签的JavaScript代码


if (window.self !== window.top) {
// 创建蒙版元素
var overlay = document.createElement('div');
overlay.className = 'overlay1';

// 创建窗口元素
var modal = document.createElement('div');
modal.className = 'modal1';

// 创建A标签元素
var link = document.createElement('a');
link.href = 'https://www.9kr.cc';
link.target = '_blank'; // 在新窗口中打开链接
link.innerText = '点击进入博客';
//link.addEventListener('click', function(event) {
// event.preventDefault(); // 阻止默认链接行为
// alert('Test');
//});

// 将A标签添加到窗口元素中
modal.appendChild(link);

// 将窗口元素添加到蒙版元素中
overlay.appendChild(modal);

// 将蒙版元素添加到body中
document.body.appendChild(overlay);
}

博客的话,只需要在主题上设置自定义CSS自定义JavaScript即可


博客后台设置




作者:Edit
来源:juejin.cn/post/7272742720841252901
收起阅读 »

原来小程序分包那么简单!

web
前言 没有理论,只有实操,用最直接的方式来了解和使用小程序分包。 文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。 为什么要有小程序分包? 因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖...
继续阅读 »

前言


没有理论,只有实操,用最直接的方式来了解和使用小程序分包。


文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。


为什么要有小程序分包?


因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖或者一下静态图片,代码包难免超过2M。所以需要小程序分包功能将小程序中所有的代码分别打到不同的代码包里去,避免小程序只能上传2M的限制


目前小程序分包大小有以下限制:



  • 整个小程序所有分包大小不超过 20M(开通虚拟支付后的小游戏不超过30M)

  • 单个分包/主包大小不能超过 2M


如何对小程序进行分包?


本质上就是,配置一下app.json(小程序)或app.config.ts(Taro)中的subpackages字段。注意,分包的这个root路径和原本的pages是同级的。


如下图


image.png


这样配置好了,最基本的分包就完成了。


如何配置多个子包?


subpackages是个数组,在下面加上一样的结构就好了。


image.png
image.png


如何判断分包是否已经生效?


打开微信开发者工具,点击右上角详情 => 基本信息 => 本地代码,展开它。出现 主包,/xxxx/就是分包生效了。


如下图


image.png


所有页面都可以打到分包里面吗?


也不是,小程序规定,Tabbar页面不可以,一定需要在主包里。否则他直接报错。


分包中的依赖资源如何分配?


我们先来了解一下小程序分包资源


 引用原则
`packageA` 无法 require `packageB` JS 文件,但可以 require 主包、`packageA` 内的 JS 文件;使用 [分包异步化](https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/async.html) 时不受此条限制
`packageA` 无法 import `packageB` 的 template,但可以 require 主包、`packageA` 内的 template
`packageA` 无法使用 `packageB` 的资源,但可以使用主包、`packageA` 内的资源

原因: 分包是依赖主包运行的,所以主包是必然会被加载的,所以当分包引用主包的时候,主包的相关数据已经存在了,所以可以被引用。而分包不能引用其他分包的数据,也是因为加载顺序的问题。如果分包A引用分包B的数据,但分包B尚未被加载,则会出现引用不到数据的问题。


如果主包和分包同时使用了一个依赖,那么这个依赖会被打到哪里去?


会被打到主包


因为主包不能引用分包的资源,但是子包可以引用主包的资源,所以为了两个包都能引用到资源,只能打到主包中


比如以下情况
image.png


分包和主包同时使用了dayjs,那么这个依赖会被打入到主包中。


如果某一个依赖只在分包中使用呢?


如果某一个资源只在某一个分包中使用,那就会被打入到当前分包。


如果两个子包同时使用同一个资源呢?那资源会被打进哪里。


主包,因为两个子包的资源不能互相引用,所以与其给每一个子包都打入一个独立资源。小程序则会直接把资源打到主包中,这样,两个子包就都可以使用了。


分包需要担心低版本的兼容问题吗


不用


由微信后台编译来处理旧版本客户端的兼容,后台会编译两份代码包,一份是分包后代码,另外一份是整包的兼容代码。 新客户端用分包,老客户端还是用的整包,完整包会把各个 subpackage 里面的路径放到 pages 中。


独立分包


什么是独立分包


顾名思义,独立分包就是可以独立运行的分包。


举个例子,如果你的小程序启动页面是分包(普通分包)中的一个页面,那么小程序需要优先下载主包,然后再加载普通分包,因为普通分包依赖主包运行。但是如果小程序从独立分包进入进入小程序,则不需要下载主包,独立分包自己就可以运行。


普通分包所有的限制对独立分包都有效。


为什么要有独立分包,普通分包不够吗


因为独立分包不需要依赖主包,如果有作为打开小程序的的入口的必要,加载速度会比普通分包快,给客户的体验感更好。毕竟谁也不想打开一个页面等半天。


举个例子,如果小程序启动的时候是打开一个普通分包页面。则加载顺序是:加载主包 => 再加载当前分包


但如果小程序启动的时候是打开一个独立分包页面,则加载顺序是:直接加载独立分包,无需加载主包


独立分包相对于普通分包,就是省去了加载主包的时间和消耗。


独立分包如何配置


配置和普通分包一样,加一个independent属性设为true即可。


image.png


独立分包的缺点


既然独立分包可以不依赖主包,那我把每个分包都打成独立分包可以吗。


最好别那么干


理由有四点


1.独立分包因为不依赖主包,所以他不一定能获取到小程序层面的全局状态,比如getApp().也不是完全获取不到,主包被加载的时候还是可以获取到的。概率性出问题,最好别用。


2.独立分包不支持使用插件


3.小程序的公共文件不适用独立分包。比如Taro的app.less或小程序的app.wxss


上述三个,我觉的都挺麻烦的。所以不是作为入口包这种必要的情况下,确实没有使用独立分包的需求。


PS:一个小程序里可以有多个独立分包


独立分包有版本兼容问题吗


有滴,但你不用这个兼容问题直接让你报错
在低于 6.7.2 版本的微信中运行时,独立分包视为普通分包处理,不具备独立运行的特性。


所以,即使在低版本的微信中,也只是会编译成普通分包而已。


注意!!! 这里有一个可能会遇到的,就是如果你在独立分包中使用了app.wxss或者app.less这些小程序层面的公共css文件,那么在低版本(<6.7.2)进行兼容的时候,你就会发现,独立分包的页面会被这些全局的CSS影响。因为那时候独立分包被编译成了普通分包。而普通分包是适用全局公共文件的。


分包预下载


首先我们需要了解,分包是基本功能是,在下程序打包的时候不去加载分包,然后在进入当前分包页面的时候才开始下载分包。一方面目的是为了加快小程序的响应速度。另一方面的原因是避开微信小程序本身只能上传2M的限制。


这里有一个问题,就是我在首次跳转某个分包的某个页面的时候,出现短暂的白屏怎么办?(下载分包的时间+运行接口的时间+渲染视图的时间)。


后两者没法彻底避免,只能优化代码,第一个下载分包的时间可以使用分包预下载功能解决。


我们可以通过分包预下载在进入分包页面之前就开始下载分包,来减少进入分包页面的时间。


如何配置分包预下载


当前的分包预下载只能在app.config(Taro)或者app.json(原生小程序)通过preloadRule字段去配置。


preloadRule字段是一个对象,key是页面的路径,value是进行预加载的分包name或者key,__APP__代表主包


上案例


image.png


通过preloadRule字段去配置


”packageB/pages/user/index“是key


packages:["packageA"]是value


案例上的意思是当进入packageA分包的时候,开始下载分包packageB


如果要某一个分包在加载主包的就开始下载,那么就设置packages:["APP"]即可。


总结



  1. 分包是为了解决小程序超过2m无法上传的问题

  2. 分包依赖于主包,进入分包页面,主包必然需要优先被加在

  3. 主包和分包同时引用一个依赖或资源,则当前依赖或资源会被打入到主包

  4. 两个分包使用了同一个依赖或资源,则该依赖和资源会被打入到主包

  5. 某资源或依赖只在某一个分包中使用,则该资源和依赖会被打入到该分包中

  6. 独立分包的配置相对于普通分包只是多了一个independent字段,设置为true

  7. 独立分包无需依赖主包,可独立加载。

  8. 独立分包中谨慎使用全局属性,最好别用,可能获取不到

  9. 分包可以被预加载,用于解决进入分包页面时才开始加载分包导致页面可能出现的(取决于加载速度)短暂白屏的问题。


分包官方文档


分包官方分包demo-小程序版


如果您认为对您有用的话,留个赞或收藏一下吧~


image.png


作者:工边页字
来源:juejin.cn/post/7321049399281958922
收起阅读 »

现代 CSS 解决方案:文字颜色自动适配背景色!

web
在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色。简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B。...
继续阅读 »

在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色

简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。

其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B

其功能能够涵盖:

完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

当然,今天我们不会一个一个去过这些功能,更多的时候,我们只需要知道我们能够实现这些功能。

本文,我们将从实际实用角度出发,基于实际的案例,看看 CSS 相对颜色,能够如何解决我们的一些实际问题。

快速语法入门

首先,我们通过一张图,一个案例,快速入门 CSS 相对颜色语法:

相对颜色语法的目标是允许从另一种颜色派生颜色。

上图显示了将原始颜色 green 转换为新颜色的颜色空间后,该颜色会转换为以 r、g、b 和 alpha 变量表示的各个数字,这些数字随后会直接用作新的 rgb() 颜色的值。

举个例子:

<p> CSS Relative Color p>
p {
color: rgb(255, 0, 0);
}

实现一个 color 为红色(rgb 值为 rgb(255, 0, 0))的字体:

基于上面的相对颜色语法,我如何通过一个红色生成绿色文字呢?示意如下:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}

效果如下,我们就得到绿色字体:

解释一下:

  1. 原本的红色颜色,我们把它设置为 CSS 变量 --color: rgb(255, 0, 0)
  2. 想通过红色得到绿色,对于红色的 rgb 值 rgb(255, 0, 0) 而言,需要转换成 rgb(0, 255, 0)
  3. 使用 CSS 相对颜色语法,就是 rgb(from var(--color) calc(r - 255) calc(g + 255) b)

通过这个 DEMO,我们把几个核心基础语法点学习一下:

  1. from 关键字

from 关键字,它是相对颜色的核心。它表示会将 from 关键字后的颜色定义转换为相对颜色!在 from 关键字后面,CSS 会期待一种颜色,即能够启发生成另一种颜色

  1. from 关键字 后的颜色表示,支持不同颜色表示或者是 CSS 变量

第二个关键点,from 后面通常会接一个颜色值,这个颜色值可以是任意颜色表示法,或者是一个 CSS 变量,下面的写法都是合法的:

p {
color: rgba(from #ff0000) r g b);
color: rgb(from rgb(255, 0, 0) r g b);
color: rgb(from hsl(0deg, 100%, 50%) r g b);
color: rgb(from var(--hotpink) r g b);
}
  1. 对转换后的变量使用 calc() 或其他 CSS 函数

另外一个非常重要的基础概念就是,我们可以对 (from color r g b) 后的转换变量 r g b 使用 calc() 或其他 CSS 函数。

就是我们上面的例子:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}
  1. 相对颜色语法支持,各种颜色表示函数:

相对颜色的基础的使用规则就是这样,它不仅支持 rgb 颜色表示法,它支持所有的颜色表示法:

使用 CSS 相对颜色,实现统一按钮点击背景切换

通常页面上的按钮,都会有 hover/active 的颜色变化,以增强与用户的交互。

像是这样:

最常见的写法,就是我们需要在 Normal 状态、Hover 状态、Active 状态下写 3 种颜色:

p {
color: #ffcc00;
transition: .3s all;
}
/* Hover 伪类下为 B 颜色 */
p:hover {
color: #ffd21f;
}
/** Active 伪类下为 C 颜色 **/
p:active {
color: #ab8a05;
}

在之前,我们介绍过一种利用滤镜 filter: contrast() 或者 filter: brightness() 的统一解决方案,无需写多个颜色值,可以根据 Normal 状态下的色值,通过滤镜统一实现更亮、或者更暗的伪类颜色。

在今天,我们也可以利用 CSS 相对颜色来做这个事情:

div {
--bg: #fc0;
background: var(--bg);
transition: .3s all;
}

div:hover {
background: hsl(from var(--bg) h s calc(l * 1.2));
}
div:active {
background: hsl(from var(--bg) h s calc(l * 0.8));
}

我们通过 hsl 色相、饱和度、亮度颜色表示法表示颜色。实现:

  1. 在 :hover 状态下,根据背景色,将背景亮度 l 调整为原背景色的 1.2 倍
  2. 在 :avtive 状态下,根据背景色,将背景亮度 l 调整为原背景色的 0.8 倍

在实际业务中,这是一个非常有用的用法。

完整的 DEMO,你可以戳这里:CodePen Demo -- https://codepen.io/Chokcoco/pen/KKEdOeb

使用 CSS 相对颜色,实现文字颜色自适应背景

相对颜色,还有一个非常有意思的场景 -- 让文字颜色能够自适应背景颜色进行展示。

有这么一种场景,有的时候,无法确定文案的背景颜色的最终表现值(因为背景颜色的值可能是后台配置,通过接口传给前端),但是,我们又需要能够让文字在任何背景颜色下都正常展现(譬如当底色为黑色时文字应该是白色,当背景为白色时,文字应该为黑色)。

像是这样:

在不确定背景颜色的情况下,无论什么情况,文字颜色都能够适配背景的颜色。

在之前,纯 CSS 没有特别好的方案,可以利用 mix-blend-mode: difference 进行一定程度的适配:

div {
// 不确定的背景色
}
p {
color: #fff;
mix-blend-mode: difference;
}

实操过这个方案的同学都会知道,在一定情况下,前景文字颜色还是会有一点瑕疵。并且,混合模式这个方案最大的问题是会影响清晰度

有了 CSS 相对颜色后,我们有了更多的纯 CSS 方案。

利用 CSS 相对颜色,反转颜色

我们可以利用相对颜色的能力,基于背景色颜色进行反转,赋值给 color。

一种方法是将颜色转换为 RGB,然后从 1 中减去每个通道的值。

代码非常简单:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: rgb(from var(--bg) calc(1 - r) calc(1 - g) calc(1 - b)); /** 基于背景反转颜色 **/
}

用 1 去减,而不是用 255 去,是因为此刻,会将 rgb() 表示法中的 0~255 映射到 0~1

效果如下:

配个动图,我们利用背景色的反色当 Color 颜色,适配所有背景情况:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

当然,这个方案还有两个问题:

  1. 如果颜色恰好是在 #808080 灰色附近,它的反色,其实还是它自己!会导致在灰色背景下,前景文字不可见;
  2. 绝大部分情况虽然可以正常展示,但是并不是非常美观好看

为了解决这两个问题,CSS 颜色规范在 CSS Color Module Level 6 又推出了一个新的规范 -- color-contrast()

利用 color-contrast(),选择高对比度颜色

color-contrast() 函数标记接收一个 color 值,并将其与其他的 color 值比较,从列表中选择最高对比度的颜色。

利用这个 CSS 颜色函数,可以完美的解决上述的问题。

我们只需要提供 #fff 白色和 #000 黑色两种可选颜色,将这两种颜色和提供的背景色进行比较,系统会自动选取对比度更高的颜色。

改造一下,上面的代码,它就变成了:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: color-contrast(var(--bg) vs #fff, #000); /** 基于背景色,自动选择对比度更高的颜色 **/
}

这样,上面的 DEMO 最终效果就变成了:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

此方案的优势在于:

  1. 可以限定前景 color 颜色为固定的几个色值,以保证 UI 层面的统一及美观
  2. 满足任何情况下的背景色

当然,唯一限制这个方案的最大问题在于,当前,color-contrast 还只是一个实验室功能,未大规模被兼容。

总结一下

到今天,我们可以利用 CSS 提供的各类颜色函数,对颜色有了更为强大的掌控力。

很多交互效果,不借助 JavaScript 的运算,也能计算出我们想要的最终颜色值。本文简单的借助:

  1. 使用 CSS 相对颜色,实现统一按钮点击背景切换
  2. 使用 CSS 相对颜色,实现文字颜色自适应背景

两个案例,介绍了 CSS 相对颜色的功能。但它其实还有更为广阔的应用场景,完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:Chokcoco
来源:juejin.cn/post/7321410822789742618
收起阅读 »

产品经理:“一个简单的复制功能也能写出bug?”

web
问题 刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。 我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在...
继续阅读 »

问题


刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。


我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在 hook 里没啥意义,但是乙方交付过来的代码好像特别喜欢把工具函数写成个 hook 来用),点进去查看就是简单的一个 navigator.clipboard.writeText()的方法,本地运行我又能复制成功。于是我怀疑是手机浏览器不支持这个 api 便去搜索了一下。


Clipboard


MDN 上的解释:


剪贴板 Clipboard APINavigator 接口添加了只读属性 clipboard,该属性返回一个可以读写剪切板内容的 Clipboard 对象。在 Web 应用中,剪切板 API 可用于实现剪切、复制、粘贴的功能。


只有在用户事先授予网站或应用对剪切板的访问许可之后,才能使用异步剪切板读写方法。许可操作必须通过取得权限 Permissions API (en-US)"clipboard-read" 和/或 "clipboard-write" 项获得。


浏览器兼容性


image.png


使用 document.execCommand() 降级处理


这里我也不清楚用户手机浏览器的版本是多少,那么这个 api 出现之前,是用的什么方法呢?总是可以 polyfill 降级处理的吧!于是我就查到了document.execCommand()这个方法:



  • document.execCommand("copy") : 复制;

  • document.execCommand("cut") : 剪切;

  • document.execCommand("paste") : 粘贴。


对比


Clipboard 的所有方法都是异步的,返回 Promise 对象,复制较大数据时不会造成页面卡顿。但是其支持的浏览器版本较新,且只允许 https 和 localhost 这些安全网络环境可以使用,限制较多。


document.execCommand() 限制较少,使用起来相对麻烦。但是 MDN 上提到该 api 已经废弃:


image.png


image.png


浏览器很可能在某个版本弃用该 api ,不过当前 2023/12/29 ,该复制 api 还是可以正常使用的。


具体代码修改


于是我修改了一下原来的 hook:


import Toast from "~@/components/Toast";

export const useCopy = () => {

const copy = async (text: string, toast?: string) => {

const fallbackCopyTextToClipboard = (text: string, toast?: string) => {
let textArea = document.createElement("textarea");
textArea.value = text;

// Avoid scrolling to bottom
textArea.style.top = "-200";
textArea.style.left = "0";
textArea.style.position = "fixed";
textArea.style.opacity = "0"

document.body.appendChild(textArea);
// textArea.focus();
textArea.select();
let msg;
try {
let successful = document.execCommand("copy");
msg = successful ? toast ? toast : "复制成功" : "复制失败";
} catch (err) {
msg = "复制失败";
}
Toast.dispatch({
content: msg,
});
document.body.removeChild(textArea);
};

const copyTextToClipboard = (text: string, toast?: string) => {
if (!navigator.clipboard || !window.isSecureContext) {
fallbackCopyTextToClipboard(text, toast);
return;
}
navigator.clipboard
.writeText(text)
.then(() => {
Toast.dispatch({
content: toast ? toast : "复制成功",
});
})
.catch(() => {
fallbackCopyTextToClipboard(text, toast)
});
};
copyTextToClipboard(text, toast);
};

return copy;
};

上线近一年,这个复制方法没出现异常问题。


作者:HyaCinth
来源:juejin.cn/post/7317577665014448167
收起阅读 »

MyBatis实战指南(一):从概念到特点,助你快速上手,提升开发效率!

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。

大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作而感到困扰?是否曾经因为SQL语句的编写而烦恼?那么,MyBatis或许就是你的救星。

接下来,让我们一起来了解一下MyBatis的概念与特点吧!

一、MyBatis基本概念

MyBatis 是一款优秀的半自动的ORM持久层框架,它支持自定义 SQL、存储过程以及高级映射。

MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。

MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

那么,什么是ORM?

要了解ORM,先了解下面概念:

持久化

把数据(如内存中的对象)保存到可永久保存的存储设备中。持久化的主要应用是将内存中的数据存储在关系型的数据库中,当然也可以存储在磁盘文件中、XML数据文件中等等。

持久层

即专注于实现数据持久化应用领域的某个特定系统的一个逻辑层面,将数据使用者和数据实体相关联。

ORM, 即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射。这样在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。

Description

总结:

  • 它是一种将内存中的对象保存到关系型数据库中的技术;

  • 主要负责实体对象的持久化,封装数据库访问细节;

  • 提供了实现持久化层的另一种模式,采用映射元数据(XML)来描述对象-关系的映射细节,使得ORM中间件能在任何一个Java应用的业务逻辑层和数据库之间充当桥梁。

Java典型的ORM框架:

  • hibernate:全自动的框架,强大、复杂、笨重、学习成本较高;

  • Mybatis:半自动的框架, 必须要自己写sql;

  • JPA:JPA全称Java Persistence API、JPA通过JDK 5.0注解或XML描述对象-表的映射关系,是Java自带的框架。

二、Mybatis的作用

Mybatis是一个Java持久层框架,它主要用于简化与数据库的交互操作。Mybatis的主要作用有以下几点:

  • 将Java对象与数据库表进行映射,通过配置XML文件实现SQL语句的定义和执行,使得开发者可以专注于业务逻辑的实现而无需编写繁琐的JDBC代码。

  • 提供了灵活的SQL映射功能,可以根据需要编写动态SQL,支持复杂的查询条件和更新操作。

  • 支持事务管理,可以确保数据的一致性和完整性。

  • 提供了缓存机制,可以提高数据库查询性能。

  • 可以与Spring、Hibernate等其他框架无缝集成,方便开发者在项目中使用。

Mybatis就是帮助程序员将数据存取到数据库里面。传统的jdbc操作,有很多重复代码块比如: 数据取出时的封装, 数据库的建立连接等等,通过框架可以减少重复代码,提高开发效率 。

MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) -->对象关系映射。

所有的事情,不用Mybatis依旧可以做到,只是用了它,会更加方便更加简单,开发更快速。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

三、MyBatis特点

1、定制化SQL

同为持久层框架的Hibernate,对操作数据库的支持方式较多,完全面向对象的、原生SQL的和HQL的方式。MyBatis只支持原生的SQL语句,这个“定制化”是相对Hibernate完全面向对象的操作方式的。

2、存储过程

储存过程是实现某个特定功能的一组sql语句集,是经过编译后存储在数据库中。当出现大量的事务回滚或经常出现某条语句时,使用存储过程的效率往往比批量操作要高得多。

MyBatis是支持存储过程的,可以看个例子。假设有一张表student:

create table student
(
id bigint not null,
name varchar(30),
sex char(1),
primary key (id)
);

有一个添加记录的存储过程:

create procedure pro_addStudent (IN id bigint, IN name varchar(30), IN sex char(1))
begin
insert into student values (id, name, sex);
end

此时就可以在mapper.xml文件中调用存储过程:

<!-- 调用存储过程 -->
<!-- 第一种方式,参数使用parameterType -->
<select id="findStudentById" parameterType="java.lang.Long" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(#{id,jdbcType=BIGINT,mode=IN})}
</select>

<parameterMap type="java.util.Map" id="studentMap">
<parameter property="id" mode="IN" jdbcType="BIGINT"/>
</parameterMap>

<!-- 调用存储过程 -->
<!-- 第二种方式,参数使用parameterMap -->
<select id="findStudentById" parameterMap="studentMap" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(?)}
</select>

3、高级映射

可以简单理解为支持关联查询。

4、避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。

使用Mybatis时,数据库的连接配置信息,是在mybatis-config.xml文件中配置的。同时,获取查询结果的代码,也是尽量做到了简洁。以模糊查询为例,需要做两步工作:

1)首先在配置文件中写上SQL语句,示例:

 <mapper namespace="com.test.pojo">
<select id="listCategoryByName" parameterType="string" resultType="Category">
select * from category_ where name like concat('%',#{0},'%')
</select>
</mapper>

2)在Java代码中调用此语句,示例:

        List<Category> cs = session.selectList("listCategoryByName","cat");
for (Category c : cs) {
System.out.println(c.getName());
}

5、Mybatis中ORM的映射方式也是比较简单的

"resultType"参数的值指定了SQL语句返回对象的类型。示例代码:
<mapper namespace="com.test.pojo">
<select id="listCategory" resultType="Category">
select * from category_
</select>
</mapper>

四、Mybatis的适用场景

MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。MyBatis因其简单易用、灵活高效的特点,广泛应用于各种Java项目中。

以下是一些常见的应用场景:

  • 数据查询:MyBatis可以执行复杂的SQL查询,返回Java对象或者结果集。

  • 数据插入、更新和删除:MyBatis可以执行INSERT、UPDATE和DELETE等SQL语句。

  • 存储过程和函数调用:MyBatis可以调用数据库的存储过程和函数。

  • 高级映射:MyBatis支持一对一、一对多、多对一等复杂关系的映射。

  • 懒加载:MyBatis支持懒加载,只有在真正需要数据时才会去数据库查询。

  • 缓存机制:MyBatis内置了一级缓存和二级缓存,可以提高查询效率。

为什么说Mybatis是半自动ORM映射工具

Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。

而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。

MyBatis作为半自动ORM映射工具与全自动ORM工具相比,有几个主要的区别点:

1.SQL的灵活性

MyBatis作为半自动ORM映射工具,开发人员可以灵活地编写SQL语句,充分发挥数据库的特性和优势。而全自动ORM工具通常会在一定程度上限制开发人员对SQL的灵活控制。

2.映射关系的可定制性

MyBatis允许开发人员通过配置文件(或注解)自定义对象和数据库表之间的映射关系,可以满足各种复杂的映射需求。而全自动ORM工具通常根据约定和规则自动生成映射关系,对于某些特殊需求无法满足。

3.SQL的可复用性

MyBatis支持SQL的可复用性,可以将常用的SQL语句定义为独立的SQL片段,并在需要的地方进行引用。而全自动ORM工具通常将SQL语句直接与对象的属性绑定在一起,缺乏可复用性。

4.性能调优的灵活性

MyBatis作为半自动ORM映射工具,允许开发人员对SQL语句进行灵活的调优,通过手动编写SQL语句和使用高级特性进行性能优化。而全自动ORM工具通常将性能优化的控制权交给框架,开发人员无法灵活地对SQL进行调优。

MyBatis作为一种半自动ORM映射工具,相对于全自动ORM工具具有更高的灵活性和可定制性。通过灵活的SQL控制、自定义的映射关系、可复用的SQL以及灵活的性能调优,MyBatis可以满足各种复杂的映射需求和性能优化需求。

虽然MyBatis相对于全自动ORM工具需要开发人员编写更多的SQL语句,但正是由于这种半自动的特性,使得MyBatis在某些复杂场景下更加灵活和可控。

因此,我们可以说MyBatis是一种半自动ORM映射工具,与全自动的ORM工具相比,它更适用于那些对SQL灵活性和性能调优需求较高的场景。

五、Mybatis的优缺点

Mybatis有以下优点:

1.基于SQL语句编程,相当灵活

SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。

2. 代码量少

与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接。

3.很好的与各种数据库兼容

4.数据库字段和对象之间可以有映射关系

提供映射标签,支持对象与数据库的ORM字段关系映射。

5.能够与Spring很好的集成

Mybatis有以下缺点:

1.SQL语句的编写工作量较大

尤其当字段多、关联表多时,SQL语句较复杂。

2.数据库移植性差

SQL语句依赖于数据库,不能随意更换数据库(可以通过在mybatis-config.xml配置databaseIdProvider来弥补)。

示例:

 <databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>

然后在xml文件中,就可以针对不同的数据库,写不同的sql语句。

3.字段映射标签和对象关系映射标签仅仅是对映射关系的描述,具体实现仍然依赖于sql。

示例:

public class Student{
String name;
List<Interest> interests;
}

public class Interest{
String studentId;
String name;
String direction;
}

<resultMap id="ResultMap" type="com.test.Student">
<result column="name" property="name" />
<collection property="interests" ofType="com.test.Interest">
<result column="name" property="name" />
<result column="direction" property="direction" />
</collection>
</resultMap>

在该例子中,如果查询sql中,没有关联Interest对应的表,则查询出数据映出的Student对象中,interests属性值就会为空。

4.DAO层过于简单,对象组装的工作量较大

即Mapper层Java代码过少,XxxMapper.xml文件中维护数据库字段和实体类字段的工作量较大。

5.不支持级联更新、级联删除

仍以上面的Student和Interest为例,当要更新/删除某个Student的信息时,需要在两个表进行手动更新/删除。

通过以上的介绍,相信大家对MyBatis已经有了更深入的了解。MyBatis是一个非常强大的持久层框架,它的灵活性、易用性、解耦性、高效性和全面性都使得它在Java开发中得到了广泛的应用。

收起阅读 »

啊?两个vite项目怎么共用一个端口号啊

web
问题: 最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后: ...
继续阅读 »

问题:


最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后:


image.png


该项目的端口号为5173,但是此时我再次通过vite的官方搭建一个react+ts+vite项目:npm create vite@latest react_demos -- --template react-ts,之后通过npm run dev启动项目,发现端口号并没有更新:


image.png


这是什么原因呢?


寻因:


查阅官方文档,我发现:


image.png


那么我主动在vite.config.ts中添加这个配置:


image.png


正常来说,会出现这个报错:


image.png


但是此时结果依然为:


image.png


我百思不得不得其解,于是再次查阅官方文档:


image.png
我寻思这也与文档描述不一致啊,于是我再次尝试,思考是不是vite版本号的问题,两个项目的版本号分别为:


image.png


image.png


我决定创建一个4版本的项目npm create vite@^4.1.4 react_demos3 -- --template react-ts


image.png


结果发现,还是有这个问题,跟版本号没有关系,于是我又耐心继续看官方文档,看到了这个配置:


image.png
我抱着试试的态度,在其中一个vite项目中添加这个配置:


image.png


发现,果然是这个配置的锅,当其中一个项目host配置为0.0.0.0时,vite不会自动尝试更新端口号


难道vite的端口监测机制与host也有关?


结果:


不甘心的我再次进行尝试,将两个项目的host都设置成:


image.png


image.png


vite会自动尝试更新端口号


原来如此,vite的端口号检测机制在对比端口号之前,会先对比host,由于我的微前端项目中设置了host,而新建的项目中没有设置host,新建的项目host默认值为localhost对比不成功,vite不会自动尝试下一个可用端口,而是共用一个端口


总结:


在遇到问题时,要多多去猜,去想各种可能,并且最重要的是去尝试各种可能,还要加上积极去翻阅官方文档,问题一定会得到解决的;哪怕不能解决,那也会在尝试中,学到很多东西


作者:进阶的鱼
来源:juejin.cn/post/7319699173740363802
收起阅读 »

终于搞懂了网盘网页是怎么唤醒本地应用了

web
写在前面 用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。 自定义协议 本身单凭浏览器是没有唤醒本地应用这个能力的,...
继续阅读 »

写在前面


用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。


自定义协议


本身单凭浏览器是没有唤醒本地应用这个能力的,不然随便一个网页都能打开你的所有应用那不就乱套了吗。但是电脑系统本身又可以支持这个能力,就是通过配置自定义协议。


举个例子,当你用浏览器打开一个本地的PDF的时候,你会发现上面是file://path/xxx.pdf,这就是系统内置的一个协议,浏览器可以调用这个协议进行文件读取。


那么与之类似的,windows本身也支持用户自定义协议来进行一些操作的,而这个协议就在注册表中进行配置。


配置自定义协议


这里我用VS Code来举例子,最终我要实现通过浏览器打开我电脑上的VS Code。


我们先编写一个注册表文件


Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\vscode]
@="URL:VSCode Protocol"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\vscode\shell]

[HKEY_CLASSES_ROOT\vscode\shell\open]

[HKEY_CLASSES_ROOT\vscode\shell\open\command]
@=""D:\VScode\Microsoft VS Code\Code.exe" "%1""

这里我逐行解释



  1. Windows Registry Editor Version 5.00 这行表明该文件是一个 Windows 注册表编辑器文件,这是标准的头部,用于告诉 Windows 如何解析文件。

  2. [HKEY_CLASSES_ROOT\vscode] 这是一个注册表键的开始。在这里,\vscode 表示创建一个名为 vscode 的新键。

  3. @="URL:VSCode Protocol"vscode 键下,这行设置了默认值(表示为 @ ),通过 "URL:VSCode Protocol" 对这个键进行描述。

  4. "URL Protocol"="" 这行是设置一个名为 URL Protocol 的空字符串值。这是代表这个新键是一个 URI 协议。

  5. [HKEY_CLASSES_ROOT\vscode\shell] 创建一个名为 shell 的子键,这是一个固定键,代表GUI界面的处理。

  6. [HKEY_CLASSES_ROOT\vscode\shell\open]shell 下创建一个名为 open 的子键。这耶是一个固定键,open 是一个标准动作,用来执行打开操作。

  7. [HKEY_CLASSES_ROOT\vscode\shell\open\command]open 下创建一个名为 command 的子键。这是一个固定键,指定了当协议被触发时要执行命令。

  8. @=""D:\VScode\Microsoft VS Code\Code.exe" "%1""command 键下,设置默认值为 VSCode 的路径。 "%1" 是一个占位符,用于表示传递给协议的任何参数,这里并无实际用处。


写好了注册表文件后,我们将其保存为 vscode.reg,并双击执行,对话框选择是,相应的注册表信息就被创建出来了。



可以通过注册表中查看。


浏览器打开VS Code


这时,我们打开浏览器,输入 vscode://open



可以看到,就像百度网盘一样,浏览器弹出了询问对话框,然后就可以打开VS Code了。


如果想要在网页上进行打开,也简单


<script>
function openVSCode() {
window.location.href = 'vscode://open/';
}
</script>
<button onclick="openVSCode()">打开 VSCode</button>

写一个简单的JS代码即可。


写在最后


至此,终于是了解了这方面的知识。这就是说,在网盘安装的过程中,就写好了这个注册表文件,自定义了网盘的唤醒协议,才可以被识别。


而我也找到了这个注册表



原来叫baiduyunguanjia协议(不区分大小写),使用 baiduyunguanjia://open 可以打开。


作者:银空飞羽
来源:juejin.cn/post/7320513026188460067
收起阅读 »

前端无感知刷新token & 超时自动退出

web
前端无感知刷新token&超时自动退出 一、token的作用 因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。 以oauth2.0授权码模式为例: 每次请求资...
继续阅读 »

前端无感知刷新token&超时自动退出


一、token的作用


因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。


以oauth2.0授权码模式为例:


oauth2授权码模式.png


每次请求资源服务器时都会在请求头中添加 Authorization: Bearer access_token 资源服务器会先判断token是否有效,如果无效或过期则响应 401 Unauthorize。此时用户处于操作状态,应该自动刷新token保证用户的行为正常进行。


刷新token:使用refresh_token获取新的access_token,使用新的access_token重新发起失败的请求。


二、无感知刷新token方案


2.1 刷新方案


当请求出现状态码为 401 时表明token失效或过期,拦截响应,刷新token,使用新的token重新发起该请求。


如果刷新token的过程中,还有其他的请求,则应该将其他请求也保存下来,等token刷新完成,按顺序重新发起所有请求。


2.2 原生AJAX请求


2.2.1 http工厂函数


function httpFactory({ method, url, body, headers, readAs, timeout }) {
   const xhr = new XMLHttpRequest()
   xhr.open(method, url)
   xhr.timeout = isNumber(timeout) ? timeout : 1000 * 60

   if(headers){
       forEach(headers, (value, name) => value && xhr.setRequestHeader(name, value))
  }
   
   const HTTPPromise = new Promise((resolve, reject) => {
       xhr.onload = function () {
           let response;

           if (readAs === 'json') {
               try {
                   response = JSONbig.parse(this.responseText || null);
              } catch {
                   response = this.responseText || null;
              }
          } else if (readAs === 'xml') {
               response = this.responseXML
          } else {
               response = this.responseText
          }

           resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) })
      }

       xhr.onerror = function () {
           reject(xhr)
      }
       xhr.ontimeout = function () {
           reject({ ...xhr, isTimeout: true })
      }

       beforeSend(xhr)

       body ? xhr.send(body) : xhr.send()

       xhr.onreadystatechange = function () {
           if (xhr.status === 502) {
               reject(xhr)
          }
      }
  })

   // 允许HTTP请求中断
   HTTPPromise.abort = () => xhr.abort()

   return HTTPPromise;
}

2.2.2 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

// 存放因token过期而失败的请求
let requests = []

function httpRequest(config) {
   let abort
   let process = new Promise(async (resolve, reject) => {
       const request = httpFactory({...config, headers: { Authorization: 'Bearer ' + cookie.load('access_token'), ...configs.headers }})
       abort = request.abort
       
       try {                            
           const { status, response, getResponseHeader } = await request

           if(status === 401) {
               try {
                   if (!isRefreshing) {
                       isRefreshing = true
                       
                       // 刷新token
                       await refreshToken()

                       // 按顺序重新发起所有失败的请求
                       const allRequests = [() => resolve(httpRequest(config)), ...requests]
                       allRequests.forEach((cb) => cb())
                  } else {
                       // 正在刷新token,将请求暂存
                       requests = [
                           ...requests,
                          () => resolve(httpRequest(config)),
                      ]
                  }
              } catch(err) {
                   reject(err)
              } finally {
                   isRefreshing = false
                   requests = []
              }
          }                        
      } catch(ex) {
           reject(ex)
      }
  })
   
   process.abort = abort
   return process
}

// 发起请求
httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })

2.3 Axios 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

let requests: ReadonlyArray<(config: any) => void> = []

// 错误响应拦截
axiosInstance.interceptors.response.use((res) => res, async (err) => {
   if (err.response && err.response.status === 401) {
       try {
           if (!isRefreshing) {
               isRefreshing = true
               // 刷新token
               const { access_token } = await refreshToken()

               if (access_token) {
                   axiosInstance.defaults.headers.common.Authorization = `Bearer ${access_token}`;

                   requests.forEach((cb) => cb(access_token))
                   requests = []

                   return axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${access_token}`,
                      },
                  })
              }

               throw err
          }

           return new Promise((resolve) => {
               // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
               requests = [
                   ...requests,
                  (token) => resolve(axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${token}`,
                      },
                  })),
              ]
          })
      } catch (e) {
           isRefreshing = false
           throw err
      } finally {
           if (!requests.length) {
               isRefreshing = false
          }
      }
  } else {
       throw err
  }
})

三、长时间无操作超时自动退出


当用户登录之后,长时间不操作应该做自动退出功能,提高用户数据的安全性。


3.1 操作事件


操作事件:用户操作事件主要包含鼠标点击、移动、滚动事件和键盘事件等。


特殊事件:某些耗时的功能,比如上传、下载等。


3.2 方案


用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该自动退出。所以需要标签页之间共享操作信息。这里我们使用 localStorage 来实现跨标签页共享数据。


在 localStorage 存入两个字段:


名称类型说明说明
lastActiveTimestring最后一次触发操作事件的时间戳
activeEventsstring[ ]特殊事件名称数组

当有操作事件时,将当前时间戳存入 lastActiveTime。


当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。


设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。


3.3 代码实现


const LastTimeKey = 'lastActiveTime'
const activeEventsKey = 'activeEvents'
const debounceWaitTime = 2 * 1000
const IntervalTimeOut = 1 * 60 * 1000

export const updateActivityStatus = debounce(() => {
   localStorage.set(LastTimeKey, new Date().getTime())
}, debounceWaitTime)

/**
* 页面超时未有操作事件退出登录
*/

export function timeout(keepTime = 60) {
   document.addEventListener('mousedown', updateActivityStatus)
   document.addEventListener('mouseover', updateActivityStatus)
   document.addEventListener('wheel', updateActivityStatus)
   document.addEventListener('keydown', updateActivityStatus)

   // 定时器
   let timer;

   const doTimeout = () => {
       timer && clearTimeout(timer)
       localStorage.remove(LastTimeKey)
       document.removeEventListener('mousedown', updateActivityStatus)
       document.removeEventListener('mouseover', updateActivityStatus)
       document.removeEventListener('wheel', updateActivityStatus)
       document.removeEventListener('keydown', updateActivityStatus)

       // 注销token,清空session,回到登录页
       logout()
  }

   /**
    * 重置定时器
    */

   function resetTimer() {
       localStorage.set(LastTimeKey, new Date().getTime())

       if (timer) {
           clearInterval(timer)
      }

       timer = setInterval(() => {
           const isSignin = document.cookie.includes('access_token')
           if (!isSignin) {
               doTimeout()
               return
          }

           const activeEvents = localStorage.get(activeEventsKey)
           if(!isEmpty(activeEvents)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }
           
           const lastTime = Number(localStorage.get(LastTimeKey))

           if (!lastTime || Number.isNaN(lastTime)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }

           const now = new Date().getTime()
           const time = now - lastTime

           if (time >= keepTime) {
               doTimeout()
          }
      }, IntervalTimeOut)
  }

   resetTimer()
}

// 上传操作
function upload() {
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, [...current, 'upload'])
   ...
   // do upload request
   ...
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))
}

作者:ww_怒放
来源:juejin.cn/post/7320044522910269478
收起阅读 »

解决扫码枪因输入法中文导致的问题

web
问题 最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题 思考 这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。...
继续阅读 »

问题


最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题


思考


这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。那我们可以针对这个来想解决方案。


方案一


首先想到的第一种方案是,监听keydown的键盘事件,创建一个字符串数组,将每一个输入的字符进行比对,然后拼接字符串,并回填到输入框中,下面是代码:


function onKeydownEvent(e) {
this.code = this.code || ''
const shiftKey = e.shiftKey
const keyCode = e.code
const key = e.key
const arr = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-']
this.nextTime = new Date().getTime()
const timeSpace = this.nextTime - this.lastTime
if (key === 'Process') { // 中文手动输入
if (this.lastTime !== 0 && timeSpace <= 30) {
for (const a of arr) {
if (keyCode === 'Key' + a) {
if (shiftKey) {
this.code += a
} else {
this.code += a.toLowerCase()
}
this.lastTime = this.nextTime
} else if (keyCode === 'Digit' + a) {
this.code += String(a)
this.lastTime = this.nextTime
}
}
if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething....
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
}
}
} else {
if (arr.includes(key.toUpperCase())) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
// 30ms以内来区分是扫码枪输入,正常手动输入时少于30ms的
this.code += key
}
this.lastTime = this.nextTime
} else if (arr.includes(key)) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += String(key)
}
this.lastTime = this.nextTime
} else if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething()
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
} else {
this.lastTime = this.nextTime
}
}
}


这种方案能解决部分问题,但是在不同的扫码枪设备,以及不同输入法的情况下,还是会出现丢失问题


方案二


使用input[type=password]来兼容不同输入的中文模式,让其只能输入英文,从而解决丢失问题


这种方案网上也有不少的参考

# 解决中文状态下扫描枪扫描错误

# input type=password 取消密码提示框


使用password密码框确实能解决不同输入法的问题,并且Focus到输入框,输入法会被强制切换为英文模式


添加autocomplete="off"autocomplete="new-password"属性


官方文档:
# 如何关闭表单自动填充


但是在Chromium内核的浏览器,不支持autocomplete="off",并且还是会出现这种自动补全提示:


image.png


上面的属性并没有解决浏览器会出现密码补全框,并且在输入字符后,浏览器还会在右上角弹窗提示是否保存:


image.png


先解决密码补全框,这里我想到了一个属性readonly,input原生属性。input[type=password]readonly
时,是不会有密码补全的提示,并且也不会弹窗提示密码保存。


那好,我们就可以在输入前以及输入完成后,将input[type=password]立即设置成readonly


但是需要考虑下面几种情况:



  • 获取焦点/失去焦点时

  • 当前输入框已focus时,再次鼠标点击输入框

  • 扫码枪输出完成最后,输入Enter键时,如果清空输入框,这时候也会显示自动补全

  • 清空输入框时

  • 切换离开页面时


这几种情况都需要处理,将输入框变成readonly


我用vue+element-ui实现了一份,贴上代码:


<template>
<div class="scanner-input">
<input class="input-password" :name="$attrs.name || 'one-time-code'" type="password" autocomplete="off" aria-autocomplete="inline" :value="$attrs.value" readonly @input="onPasswordInput">
<!-- <el-input ref="scannerInput" v-bind="$attrs" v-on="$listeners" @input="onInput"> -->
<el-input ref="scannerInput" :class="{ 'input-text': true, 'input-text-focus': isFocus }" v-bind="$attrs" v-on="$listeners">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
<!-- <slot slot="suffix" name="suffix"></slot> -->
</el-input>
</div>
</template>

<script>
export default {
name: 'WispathScannerInput',
data() {
return {
isFocus: false
}
},
beforeDestroy() {
this.$el.firstElementChild.setAttribute('readonly', true)
this.$el.firstElementChild.removeEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.removeEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.removeEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.removeEventListener('keydown', this.oPasswordKeyDown)
},
mounted() {
this.$el.firstElementChild.addEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.addEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.addEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.addEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.addEventListener('keydown', this.oPasswordKeyDown)

const entries = Object.entries(this.$refs.scannerInput)
// 解决ref问题
for (const [key, value] of entries) {
if (typeof value === 'function') {
this[key] = value
}
}
this['focus'] = this.$el.firstElementChild.focus.bind(this.$el.firstElementChild)
},
methods: {
onPasswordInput(ev) {
this.$emit('input', ev.target.value)
if (ev.target.value === '') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
onPasswordFocus(ev) {
this.isFocus = true
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
},
onPasswordBlur() {
this.isFocus = false
this.$el.firstElementChild.setAttribute('readonly', true)
},
// 鼠标点击输入框一瞬间,禁用输入框
onPasswordMouseDown() {
this.$el.firstElementChild.setAttribute('readonly', true)
},
oPasswordKeyDown(ev) {
// 判断enter键
if (ev.key === 'Enter') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
// 点击之后,延迟200ms后放开readonly,让输入框可以输入
onPasswordClick() {
if (this.isFocus) {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
}, 200)
}
},
onInput(_value) {
this.$emit('input', _value)
},
getList(value) {
this.$emit('input', value)
}
// onChange(_value) {
// this.$emit('change', _value)
// }
}
}
</script>

<style lang="scss" scoped>
.scanner-input {
position: relative;
height: 36px;
width: 100%;
display: inline-block;
.input-password {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0 16px;
font-size: 14px;
letter-spacing: 3px;
background: transparent;
color: transparent;
// caret-color: #484848;
}
.input-text {
font-size: 14px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
background-color: transparent;
::v-deep .el-input__inner {
// background-color: transparent;
padding: 0 16px;
width: 100%;
height: 100%;
}
}

.input-text-focus {
::v-deep .el-input__inner {
outline: none;
border-color: #1c7af4;
}
}
}
</style>


至此,可以保证input[type=password]不会再有密码补全提示,并且也不会再切换页面时,会弹出密码保存弹窗。
但是有一个缺点,就是无法完美显示光标。如果用户手动输入和删除,使用起来会有一定的影响。


我想到过可以使用模拟光标,暂时不知道可行性。有哪位同学知道怎么解决的话,可以私信我,非常感谢


作者:香脆又可口
来源:juejin.cn/post/7265666505102524475
收起阅读 »

年会了,公司想要一个离线PC抽奖应用

web
年会了,公司想要一个离线PC抽奖应用 背景 公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC...
继续阅读 »

年会了,公司想要一个离线PC抽奖应用


封面截图.png


背景


公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC抽奖应用,所有就有了这么一个主题。


程序需求


以下是领导从其他地方复制粘贴过来的,就是想实现类似的效果而已。



  • 1、支持数字、字母、手机号、姓名部门+姓名、编号、身-份-证号等任意组合的抽奖形式。

  • 2、支持名单粘贴功能,从EXCEL、WORD、TXT等任何可以复制的地方复制名单数据内容,粘贴至抽奖软件中作为名单使用,比导入更方便。

  • 3、支持标题、副标题、奖项提示信息、奖品图片等都可以通过拖拽更改位置。

  • 4、支持内定指定中奖者。

  • 5、支持万人抽奖,非常流畅,中奖机率一致,保证公平性。

  • 6、支持中奖不重复,软件自动排除已中奖人员,每人只有一次中奖机会不会出现重复中奖。

  • 7、支持临时追加奖项、补奖等功能支持自定义公司名称、自定义标题。

  • 8、背景图片,音乐等。

  • 9、支持抽奖过程会自动备份中奖名单(不用担心断电没保存中奖名单)。

  • 10、支持任意添加奖项、标题文字奖项名额,自由设置每次抽奖人数设置不同的字体大小和列数。

  • 11、支持空格或回车键抽奖。

  • 12、支持临时增加摇号/抽奖名单,临时删掉不在场人员名单。


目前未实现的效果


有几个还没实现的



  1. 关于人员信息的任意组合抽奖形式,这边只固定了上传模板的表头,需要组合只能通过改excel的内容。

  2. 对于临时不在场名单,目前只能通过改excel表再上传才能达到效果。


技术选型


由于给的时间不多,能有现成的最好;最终选择下面的开源项目进行集成和修改。


说明:由于之前没看到有electron-vite-vue这个项目,所有自己粗略了用vue3+vite+electron开发了 抽奖程序 , 所以现在就是迁移项目的说明。


github开源项目



根据仓库的说明运行起来


动画.gif


修改web端代码并集成到electron


I 拆分页面


组件说明拼图.png


II 补充组件


​ 本人根据自己想法加了背景图片、奖品展示、操作按钮区、展示全部中奖人员名单这几个组件以及另外9个弹窗设置组件。


III 页面目录结构


目录结构.png

IV 最后就是对开源的网页抽奖项目进行大量的修改了,这里就不详细说了;因为变化太多了,一时半会想不起来改了什么。


迁移项目


I 迁移静态资源


静态资源.png


​ 关于包资源说明,这边因为要做离线的软件,所以我把固定要使用的包保存到本地了;


1. 引入到index.html中

引入资源.png


2. 引入图片静态资源

功能代码调整.png


II 迁移electron代码


说明:由于我之前写的一版代码是用js而不是ts,如果一下子全改为ts需要一些时间;所以嫌麻烦,我直接引用js文件了,后期有时间可以再优化一下。


功能代码调整.png




  1. 这时候先运行一下,看下有没有问题



​ 问题一:


问题一.png


​ 这个是因为 我之前的项目一直是用require 引入的;所以要把里面用到require都改为import引入方式;(在preload.ts里面不能用ESM导入的形式,会报语法错误,要用回require导入)


​ 问题二:


问题二.png


​ __dirname不是ESM默认的变量;改为


import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url))

III 迁移前端代码



  • 目录说明


前端功能代码.png



  • 然后一顿复制粘贴,运行,最后报错;


问题三.png
按提示来改 如下:


修改1.png



  • 问题2:资源报错
    资源报错.png
    修复:


资源变化.png



  • 接下来运行看下是否有问题;
    抽奖运行动画.gif
    ​ 运行成功

  • 下一步试一下功能


功能执行动画.gif
​ 功能报错了



  • 看后台错误打印并修复问题
    保存位置错误.png
    修改:
    路径保存源头.png

  • 再次尝试功能 - 成功
    功能执行动画2.gif


IV 一个流程下来


待使用-删除帧后-运行抽奖一个流程动画-gif.gif


打包安装运行


I 运行“npm run build”之后 报错了


打包-js报错.png
这里再次说明一下;由于本人懒得把原本js文件的代码 改为ts;要快速迁移项目 所以直接使用了js;导致打包报错了,所以需要再 tsconfig.json配置一下才行:


  "compilerOptions": {
"allowJs": true // 把这段加上去
},

II 图标和应用名称错误


default Electron icon is used  reason=application icon is not set
building block map blockMapFile=release\28.0.0\YourAppName-Windows-28.0.0-Setup.exe.blockmap

找到打包的配置文件(electron-builder.json5)进行修改:


1. 更改应用名称
"productName": "抽奖程序",

2. 添加icon图标
"win": {
"icon": "electron/controller/data/img/lottery_icon.ico", // ico保存的位置
},

III 打包后运行;资源路径报错了


打包后资源报错.png
打包后资源路径查询不到.png
由于上面的原因,需要把程序涉及读写的文件目录暴露出来;


1. 在构建配置中加入如下配置,将应用要读写的文件目录暴露出来
"extraResources": [
{
"from": "electron/assets",
"to": "assets"
}
],

剩下的就是要重新调整打包后的代码路径了,保证能够找到读写路径;
路径查找纠正.png


最后打包成功,运行项目


删除帧后-一个完整的流程-gif.gif


总结: 主打的要快速实现,所以这个离线pc抽奖程序还有很多问题,希望大家多多包容;


最后附上github地址:github.com/programbao/…
欢迎大家使用


作者:宝programbao
来源:juejin.cn/post/7319795736153210895
收起阅读 »

前端服务框架调研:Next.js、Nuxt.js、Nest.js、Fastify

web
概述 这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。 最终选取了以下具有代表性的框架: Next.js、Nuxt.js:它们是分别与...
继续阅读 »

概述


这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。


最终选取了以下具有代表性的框架:



  • Next.js、Nuxt.js:它们是分别与特定前端技术 React、Vue 绑定的前端应用开发框架,有一定的相似性,可以放在一起进行调研对比。

  • Nest.js:是“Angular 的服务端实现”,基于装饰器。可以使用任何兼容的 http 提供程序,如 Express、Fastify 替换底层内核。可用于 http、rpc、graphql 服务,对提供更多样的服务能力有一定参考价值。

  • Fastify:一个使用插件模式组织代码且支持并基于 schema 做了运行效率提升的比较纯粹的偏底层的 web 框架。


Next.js、Nuxt.js


这两个框架的重心都在 Web 部分,对 UI 呈现部分的代码的组织方式、服务器端渲染功能等提供了完善的支持。



  • Next.js:React Web 应用框架,调研版本为 12.0.x。

  • Nuxt.js:Vue Web 应用框架,调研版本为 2.15.x。


功能


首先是路由部分:



  • 页面路由:

    • 相同的是两者都遵循文件即路由的设计。默认以 pages 文件夹为入口,生成对应的路由结构,文件夹内的所有文件都会被当做路由入口文件,支持多层级,会根据层级生成路由地址。同时如果文件名为 index 则会被省略,即 /pages/users 和 /pages/users/index 文件对应的访问地址都是 users。

    • 不同的是,根据依赖的前端框架的不同,生成的路由配置和实现不同:

      • Next.js:由于 React 没有官方的路由实现,Next.js 做了自己的路由实现。

      • Nuxt.js:基于 vue-router,在编译时会生成 vue-router 结构的路由配置,同时也支持子路由,路由文件同名的文件夹下的文件会变成子路由,如 article.js,article/a.js,article/b.js,a 和 b 就是 article 的子路由,可配合 组件进行子路由渲染。





  • api 路由:

    • Next.js:在 9.x 版本之后添加了此功能的支持,在 pages/api/ 文件夹下(为什么放在pages文件夹下有设计上的历史包袱)的文件会作为 api 生效,不会进入 React 前端路由中。命名规则相同,pages/api/article/[id].js -> /api/article/123。其文件导出模块与页面路由导出不同,但不是重点。

    • Nuxt.js:官方未提供支持,但是有其他实现途径,如使用框架的 serverMiddleware 能力。



  • 动态路由:两者都支持动态路由访问,但是命名规则不同:

    • Next.js:使用中括号命名,/pages/article/[id].js -> /pages/article/123。

    • Nuxt.js:使用下划线命名,/pages/article/_id.js -> /pages/article/123。



  • 路由加载:两者都内建提供了 link 类型组件(LinkNuxtLink),当使用这个组件替代 标签进行路由跳转时,组件会检测链接是否命中路由,如果命中,则组件出现在视口后会触发对对应路由的 js 等资源的加载,并且点击跳转时使用路由跳转,不会重新加载页面,也不需要再等待获取渲染所需 js 等资源文件。

  • 出错兜底:两者都提供了错误码响应的兜底跳转,只要 pages 文件夹下提供了 http 错误码命名的页面路由,当其他路由发生响应错误时,就会跳转到到错误码路由页面。


在根据文件结构生成路由配置之后,我们来看下在代码组织方式上的区别:



  • 路由组件:两者没有区别,都是使用默认导出组件的方式决定路由渲染内容,React 导出 React 组件,Vue 导出 Vue 组件:

    • Next.js:一个普普通通的 React 组件:
      export default function About() {
      return <div>About usdiv>
      }


    • Nuxt.js:一个普普通通的 Vue 组件:







在编译构建方面,两者都是基于 webpack 搭建的编译流程,并在配置文件中通过函数参数的方式暴露了 webpack 配置对象,未做什么限制。其他值得注意的一点是 Next.js 在 v12.x.x 版本中将代码压缩代码和与原本的 babel 转译换为了 swc,这是一个使用 Rust 开发的更快的编译工具,在前端构建方面,还有一些其他非基于 JavaScript 实现的工具,如 ESbuild。


在扩展框架能力方面,Next.js 直接提供了较丰富的服务能力,Nuxt.js 则设计了模块和插件系统来进行扩展。


Nest.js


Nest.js 是“Angular 的服务端实现”,基于装饰器。Nest.js 与其他前端服务框架或库的设计思路完全不同。我们通过查看请求生命周期中的几个节点的用法来体验下 Nest.js 的设计方式。


先来看下 Nest.js 完整的的生命周期:



  1. 收到请求

  2. 中间件

    1. 全局绑定的中间件

    2. 路径中指定的 Module 绑定的中间件



  3. 守卫

    1. 全局守卫

    2. Controller 守卫

    3. Route 守卫



  4. 拦截器(Controller 之前)

    1. 全局

    2. Controller 拦截器

    3. Route 拦截器



  5. 管道

    1. 全局管道

    2. Controller 管道

    3. Route 管道

    4. Route 参数管道



  6. Controller(方法处理器)

  7. 服务

  8. 拦截器(Controller 之后)

    1. Router 拦截器

    2. Controller 拦截器

    3. 全局拦截器



  9. 异常过滤器

    1. 路由

    2. 控制器

    3. 全局



  10. 服务器响应


可以看到根据功能特点拆分的比较细,其中拦截器在 Controller 前后都有,与 Koa 洋葱圈模型类似。


功能设计


首先看下路由部分,即最中心的 Controller:



  • 路径:使用装饰器装饰 @Controller 和 @GET 等装饰 Controller 类,来定义路由解析规则。如:
    import { Controller, Get, Post } from '@nestjs/common'

    @Controller('cats')
    export class CatsController {
    @Post()
    create(): string {
    return 'This action adds a new cat'
    }

    @Get('sub')
    findAll(): string {
    return 'This action returns all cats'
    }
    }

    定义了 /cats post 请求和 /cats/sub get 请求的处理函数。

  • 响应:状态码、响应头等都可以通过装饰器设置。当然也可以直接写。如:
    @HttpCode(204)
    @Header('Cache-Control', 'none')
    create(response: Response) {
    // 或 response.setHeader('Cache-Control', 'none')
    return 'This action adds a new cat'
    }


  • 参数解析:
    @Post()
    async create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat'
    }


  • 请求处理的其他能力方式类似。


再来看看生命周期中其中几种其他的处理能力:



  • 中间件:声明式的注册方法:
    @Module({})
    export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
    consumer
    // 应用 cors、LoggerMiddleware 于 cats 路由 GET 方法
    .apply(LoggerMiddleware)
    .forRoutes({ path: 'cats', method: RequestMethod.GET })
    }
    }


  • 异常过滤器(在特定范围捕获特定异常并处理),可作用于单个路由,整个控制器或全局:
    // 程序需要抛出特定的类型错误
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)

    // 定义
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()
    const status = exception.getStatus()

    response
    .status(status)
    .json({
    statusCode: status,
    timestamp: new Date().toISOString(),
    path: request.url,
    })
    }
    }
    // 使用,此时 ForbiddenException 错误就会被 HttpExceptionFilter 捕获进入 HttpExceptionFilter 处理流程
    @Post()
    @UseFilters(new HttpExceptionFilter())
    async create() {
    throw new ForbiddenException()
    }


  • 守卫:返回 boolean 值,会根据返回值决定是否继续执行后续声明周期:
    // 声明时需要使用 @Injectable 装饰且实现 CanActivate 并返回 boolean 值
    @Injectable()
    export class AuthGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
    return validateRequest(context);
    }
    }

    // 使用时装饰 controller、handler 或全局注册
    @UseGuards(new AuthGuard())
    async create() {
    return 'This action adds a new cat'
    }


  • 管道(更侧重对参数的处理,可以理解为 controller 逻辑的一部分,更声明式):

    1. 校验:参数类型校验,在使用 TypeScript 开发的程序中的运行时进行参数类型校验。

    2. 转化:参数类型的转化,或者由原始参数求取二级参数,供 controllers 使用:


    @Get(':id')
    findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
    // 使用 id param 通过 UserByIdPipe 读取到 UserEntity
    return userEntity
    }



我们再来简单的看下 Nest.js 对不同应用类型和不同 http 提供服务是怎样做适配的:



  • 不同应用类型:Nest.js 支持 Http、GraphQL、Websocket 应用,在大部分情况下,在这些类型的应用中生命周期的功能是一致的,所以 Nest.js 提供了上下文类 ArgumentsHostExecutionContext,如使用 host.switchToRpc()host.switchToHttp() 来处理这一差异,保障生命周期函数的入参一致。

  • 不同的 http 提供服务则是使用不同的适配器,Nest.js 的默认内核是 Express,但是官方提供了 FastifyAdapter 适配器用于切换到 Fastify。


Fastify


有这么一个框架依靠数据结构和类型做了不同的事情,就是 Fastify。它的官方说明的特点就是“快”,它提升速度的实现是我们关注的重点。


我们先来看看开发示例:


const routes = require('./routes')
const fastify = require('fastify')({
logger: true
})

fastify.register(tokens)

fastify.register(routes)

fastify.listen(3000, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
fastify.log.info(`server listening on ${address}`)
})

class Tokens {
constructor () {}
get (name) {
return '123'
}
}

function tokens (fastify) {
fastify.decorate('tokens', new Tokens())
}

module.exports = tokens

// routes.js
class Tokens {
constructor() { }
get(name) {
return '123'
}
}

const options = {
schema: {
querystring: {
name: { type: 'string' },
},
response: {
200: {
type: 'object',
properties: {
name: { type: 'string' },
token: { type: 'string' }
}
}
}
}
}

function routes(fastify, opts, done) {
fastify.decorate('tokens', new Tokens())

fastify.get('/', options, async (request, reply) => {
reply.send({
name: request.query.name,
token: fastify.tokens.get(request.query.name)
})
})
done()
}
module.exports = routes

可以注意到的两点是:



  1. 在路由定义时,传入了一个请求的 schema,在官方文档中也说对响应的 schema 定义可以让 Fastify 的吞吐量上升 10%-20%。

  2. Fastify 使用 decorate 的方式对 Fastify 能力进行增强,也可以将 decorate 部分提取到其他文件,使用 register 的方式创建全新的上下文的方式进行封装。


没体现到的是 Fastify 请求介入的支持方式是使用生命周期 Hook,由于这是个对前端(Vue、React、Webpack)来说很常见的做法就不再介绍。


我们重点再来看一下 Fastify 的提速原理。


如何提速


有三个比较关键的包,按照重要性排分别是:



  1. fast-json-stringify

  2. find-my-way

  3. reusify



  • fast-json-stringify:
    const fastJson = require('fast-json-stringify')
    const stringify = fastJson({
    title: 'Example Schema',
    type: 'object',
    properties: {
    firstName: {
    type: 'string'
    },
    lastName: {
    type: 'string'
    }
    }
    })

    const result = stringify({
    firstName: 'Matteo',
    lastName: 'Collina',
    })


    • 与 JSON.stringify 功能相同,在负载较小时,速度更快。

    • 其原理是在执行阶段先根据字段类型定义提前生成取字段值的字符串拼装的函数,如:
      function stringify (obj) {
      return `{"firstName":"${obj.firstName}","lastName":"${obj.lastName}"}`
      }

      相当于省略了对字段值的类型的判断,省略了每次执行时都要进行的一些遍历、类型判断,当然真实的函数内容比这个要复杂的多。那么引申而言,只要能够知道数据的结构和类型,我们都可以将这套优化逻辑复制过去。



  • find-my-way:将注册的路由生成了压缩前缀树的结构,根据基准测试的数据显示是速度最快的路由库中功能最全的。

  • reusify:在 Fastify 官方提供的中间件机制依赖库中,使用了此库,可复用对象和函数,避免创建和回收开销,此库对于使用者有一些基于 v8 引擎优化的使用要求。在 Fastify 中主要用于上下文对象的复用。


总结



  • 在路由结构的设计上,Next.js、Nuxt.js 都采用了文件结构即路由的设计方式。Ada 也是使用文件结构约定式的方式。

  • 在渲染方面 Next.js、Nuxt.js 都没有将根组件之外的结构的渲染直接体现在路由处理的流程上,隐藏了实现细节,但是可以以更偏向配置化的方式由根组件决定组件之外的结构的渲染(head 内容)。同时渲染数据的请求由于和路由组件联系紧密也都没有分离到另外的文件,不论是 Next.js 的路由文件同时导出各种数据获取函数还是 Nuxt.js 的在组件上直接增加 Vue options 之外的配置或函数,都可以看做对组件的一种增强。Ada 的方式有所不同,路由文件夹下并没有直接导出组件,而是需要根据运行环境导出不同的处理函数和模块,如服务器端对应的 index.server.js 文件中需要导出 HTTP 请求方式同名的 GET、POST 函数,开发人员可以在函数内做一些数据预取操作、页面模板渲染等;客户端对应的 index.js 文件则需要导出组件挂载代码。

  • 在渲染性能提升方面,Next.js、Nuxt.js 也都采取了相同的策略:静态生成、提前加载匹配到的路由的资源文件、preload 等,可以参考优化。

  • 在请求介入上(即中间件):

    • Next.js、Nuxt.js 未对中间件做功能划分,采取的都是类似 Express 或 Koa 使用 next() 函数控制流程的方式,而 Nest.js 则将更直接的按照功能特征分成了几种规范化的实现。

    • 不谈应用级别整体配置的用法,Nuxt.js 是由路由来定义需要哪个中间件,Nest.js 也更像 Nuxt.js 由路由来决定的方式使用装饰器配置在路由 handler、Controller 上,而 Next.js 的中间件会对同级及下级路由产生影响,由中间件来决定影响范围,是两种完全相反的控制思路。

    • Ada 架构基于 Koa 内核,但是内部中间件实现也与 Nest.js 类似,将执行流程抽象成了几个生命周期,将中间件做成了不同生命周期内功能类型不同的任务函数。对于开发人员未暴露自定义生命周期的功能,但是基于代码复用层面,也提供了服务器端扩展、Web 模块扩展等能力,由于 Ada 可以对页面路由、API 路由、服务器端扩展、Web 模块等统称为工件的文件进行独立上线,为了稳定性和明确影响范围等方面考虑,也是由路由主动调用的方式决定自己需要启用哪些扩展能力。



  • Nest.js 官方基于装饰器提供了文档化的能力,利用类型声明( 如解析 TypeScript 语法、GraphQL 结构定义 )生成接口文档是比较普遍的做法。不过虽然 Nest.js 对 TypeScript 支持很好,也没有直接解决运行时的类型校验问题,不过可以通过管道、中间件达成。

  • Fastify 则着手于底层细节进行运行效率提升,且可谓做到了极致。同时越是基于底层的实现越能够使用在越多的场景中。其路由匹配和上下文复用的优化方式可以在之后进行进一步的落地调研。

  • 除此之外 swc、ESBuild 等提升开发体验和上线速度的工具也是需要落地调研的一个方向。




作者:智联大前端
来源:juejin.cn/post/7030995965272129567
收起阅读 »

我的天!多个知名组件库都出现了类似的bug!

web
前言 首先声明,没有标题党哈! 以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库: 阿里系:ant design, fusion design, 字节系:arco design 腾讯...
继续阅读 »

前言


首先声明,没有标题党哈!


以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库:



本来字节还有一个semi design,结果我发现它没有Affix组件,也就是固钉组件,让他躲过一劫,他有这个组件我也觉得肯定会复现相同的bug。


Affix组件是什么,以及bug复现


Affix组件(固钉组件)能将页面元素钉在可视范围。如下图:


image.png


这个button组件,会在距离顶部80px的时候会固定在屏幕上(position: fixed),如下图:


image.png


如何复现bug


你在这个button元素任意父元素上,加上以下任意style属性



  • will-change: transform;

  • will-change: filter;

  • will-change: perspective;

  • transform 不为none

  • perspective不为none

  • 非safari浏览器,filter属性不为none

  • 非safari浏览器,backdrop-filter属性不为none

  • 等等


都可以让这个固定组件失效,就是原本是距离顶部80px固定。


我的组件库没有这个bug,哈哈


mx-design


目前组件不是很多,还在努力迭代中,不知道凭借没有这个bug的小小优点,能不能从你手里取一个star,哈哈


bug原因


affix组件无非都是用了fixed布局,我是如何发现这个bug的呢,我的组件库动画系统用的framer-motion,我本来是想在react-router切换路由的时候整点动画的,动画效果就是给body元素加入例如transform的变化。


然后我再看我的固钉组件怎么失效了。。。后来仔细一想,才发现想起来fixed布局的一个坑就是,大家都以为fixed布局相对的父元素是window窗口,其实是错误的!


真正的规则如下(以下说的包含块就是fixed布局的定位父元素):



  1. 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:



    1. transform 或 perspective 的值不是 none

    2. will-change 的值是 transform 或 perspective

    3. filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。

    4. contain 的值是 paint(例如:contain: paint;

    5. backdrop-filter 的值不是 none(例如:backdrop-filter: blur(10px);




评论区有很多同学居然觉的这不是bug?


其实这个问题本质是定位错误,在这些组件库里,同样使用到定位的有,例如Tooltip,Select,Popuver等等,明显这些组件跟写Affix组件的不是一个人,其他组件这个bug是没有的,只有Affix组件出现了,所以你说这是不是bug。


还有,如果因为引用了Affix组件,这个固定元素的任一父元素都不能用以上的css属性,我作为使用者,我用了动画库,动画库使用transfrom做Gpu加速,你说不让我用了,因为引起Affix组件bug,我心里想凭啥啊,明明加两行代码就解决了。


最后,只要做过定位组件的同学,其复杂度在前端算是比较高的了,这也是为什么有些组件库直接用第三方定位组件库(floating-ui,@popper-js),而不是自己去实现,因为自己实现很容易出bug,这也是例如以上组件库Tooltip为什么能适应很多边界case而不出bug。


所以你想想,这仅仅是定位组件遇到的一个很小的问题,你这个都解决不了,什么都怪css,你觉得用户会这么想吗,一有css,你所有跟定位相关的组件全部都不能用了,你们还讲理不?


总之一句话,你不能把定位组件的复杂度高怪用户没好好用,建议去看看floating-ui的源码,或者之前我写的@popper-js定位组件的简要逻辑梳理,你就会意识到定位组件不简单。边界case多如牛毛。


解决方案



  • 首先是找出要固定元素的定位元素(定位元素的判断逻辑上面写了),然后如果定位元素是window,那么跟目前所有组件库的逻辑一样,所以没有bug,如果不是window,就要求出相对定位父元素距离可视窗口顶部的top的值

  • 然后在我们原本要定位的值,比如距离顶部80px的时候固定,此时80px再减去上面说的定位父元素距离可视窗口顶部的top的值,就没有bug了


具体代码如下:



  • offsetParent固定元素的定位上下文,也就是相对定位的父元素

  • fixedTop是我们要触发固定的值,比如距离可视窗口顶部80px就固定



affixDom.style.top = `${isHTMLElement(offsetParent) ? (fixedTop as number) - offsetParent.getBoundingClientRect().top : fixedTop}px`;

如何找出offsetParent,也就是定位上下文


export function getContainingBlock(element: Element) {
let currentNode = element.parentElement;
while (currentNode) {
if (isContainingBlock(currentNode)) return currentNode;
currentNode = currentNode.parentElement;
}
return null;
}

工具方法,isContainingBlock如下:


import { isSafari } from './isSafari';

export function isContainingBlock(element: Element): boolean {
const safari = isSafari();
const css = getComputedStyle(element);

// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
return (
css.transform !== 'none' ||
css.perspective !== 'none' ||
(css.containerType ? css.containerType !== 'normal' : false) ||
(!safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false)) ||
(!safari && (css.filter ? css.filter !== 'none' : false)) ||
['transform', 'perspective', 'filter'].some((value) => (css.willChange || '').includes(value)) ||
['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value))
);
}


本文完毕,求关注,求star!!!对于react组件库感兴趣的小伙伴,欢迎加群一起交流哦!


作者:孟祥_成都
来源:juejin.cn/post/7265121637497733155
收起阅读 »

百度考题:反复横跳的个性签名

web
浅聊一下 在各种社交网站上,都会有个性签名一栏,当没有写入时,默认为 “这个人很懒,什么也没有留下” 当我们没有点击它时,默认为一个文本,当我们点击它时,又变成input框了,我们可以在里面添加自己的个性签名...本篇文章将带大家copy一个低配版的的个性签...
继续阅读 »

浅聊一下


在各种社交网站上,都会有个性签名一栏,当没有写入时,默认为 “这个人很懒,什么也没有留下”


image.png


当我们没有点击它时,默认为一个文本,当我们点击它时,又变成input框了,我们可以在里面添加自己的个性签名...本篇文章将带大家copy一个低配版的的个性签名组件

动手


我们要有JS组件思维,个性签名是很多地方都可以用到的,我们把它做成组件,用到的时候直接搬过去就好了。所以我们将使用原生JS将组件封装起来(大家也可以使用VUE)


EditInPlace 类


我们要使用这个JS组件,首先得将其方法和参数封装在一个类里,再通过类的实例化对象来展示。


function EditInPlace(id,parent,value){
this.id = id;
this.parent = parent;
this.value =value || "这个家伙很懒,什么都没有留下";
this.createElements()//动态装配html结点
this.attachEvents();
}


  1. 将传入的idparentvalue参数赋值给新创建的对象的对应属性。

  2. 如果没有提供value参数,则将默认字符串"这个家伙很懒,什么都没有留下"赋值给新对象的value属性。

  3. 调用createElements方法来动态装配HTML节点。

  4. 调用attachEvents方法来附加事件。


EditInPlace.prototype


在 EditInPlace 类中,我们调用了createElements() attachEvents()两个方法,所以我们得在原型上定义这两个方法


createElements


    createElements:function(){
this.containerElement = document.createElement('div');
this.containerElement.id= this.id;
//签名文字部分
this.staicElement = document.createElement('span');
this.staicElement.innerText = this.value
this.containerElement.appendChild(this.staicElement);
//输入框
this.fieldElement = document.createElement('input')
this.fieldElement.type = 'text'
this.fieldElement.value = this.value;
this.containerElement.appendChild(this.fieldElement);
//save 确认
this.saveButton = document.createElement('input');
this.saveButton.type = 'button'
this.saveButton.value = '保存'
this.containerElement.appendChild(this.saveButton)
//取消按钮
this.cancelButton = document.createElement('input')
this.cancelButton.type = 'button'
this.cancelButton.value = '取消'
this.containerElement.appendChild(this.cancelButton)


this.parent.appendChild(this.containerElement)
this.converToText();

}


  1. 创建一个<div>元素,并将其赋值给this.containerElement属性,并设置其id为传入的id

  2. 创建一个<span>元素,并将其赋值给this.staicElement属性,然后设置其文本内容为传入的value

  3. this.staicElement添加到this.containerElement中。

  4. 创建一个<input>元素,并将其赋值给this.fieldElement属性,设置其类型为"text",并将其值设置为传入的value

  5. this.fieldElement添加到this.containerElement中。

  6. 创建一个保存按钮(<input type="button">),并将其赋值给this.saveButton属性,并设置其值为"保存"。

  7. 将保存按钮添加到this.containerElement中。

  8. 创建一个取消按钮(<input type="button">),并将其赋值给this.cancelButton属性,并设置其值为"取消"。

  9. 将取消按钮添加到this.containerElement中。

  10. this.containerElement添加到指定的父元素this.parent中。

  11. 调用converToText方法。


这个方法主要是用于动态生成包含静态文本、输入框和按钮的编辑组件,并将其添加到指定的父元素中。也就是说我们在这里主要就是创建了一个div,div里面有一个text和一个input,还有保存和取消按钮


div和span的显示


我们怎么样实现点击一下,就让text不显示,input框显示呢?
就是在点击一下以后,让text的display为'none',让input框和按钮为 'inline'就ok了,同样的,在保存或取消时采用相反的方法就好


    converToText:function(){
this.staicElement.style.display = 'inline';
this.fieldElement.style.display = 'none'
this.saveButton.style.display = 'none'
this.cancelButton.style.display = 'none'
},
converToEdit:function(){
this.staicElement.style.display = 'none';
this.fieldElement.style.display = 'inline'
this.saveButton.style.display = 'inline'
this.cancelButton.style.display = 'inline'
}

attachEvents


当然,我们需要在text文本和按钮上添加点击事件


    attachEvents:function(){
var that = this
this.staicElement.addEventListener('click',this.converToEdit.bind(this))
this.cancelButton.addEventListener('click',this.converToText.bind(this))
this.saveButton.addEventListener('click',function(){
var value = that.fieldElement.value;
that.staicElement.innerText = value;
that.converToText();
})
}


  1. 通过var that = this将当前对象的引用保存在变量that中,以便在后续的事件监听器中使用。

  2. 使用addEventListener为静态元素(this.staicElement)添加了一个点击事件的监听器,当静态元素被点击时,会调用this.converToEdit.bind(this)方法,这样做可以确保在converToEdit方法中this指向当前对象。

  3. 为取消按钮(this.cancelButton)添加了一个点击事件的监听器,当取消按钮被点击时,会调用this.converToText.bind(this)方法,同样也是为了确保在converToText方法中this指向当前对象。

  4. 为保存按钮(this.saveButton)添加了一个点击事件的监听器,当保存按钮被点击时,会执行一个匿名函数,该函数首先获取输入框的值,然后将该值更新到静态元素的文本内容中,并最后调用converToText方法,同样使用了变量that来确保在匿名函数中this指向当前对象。


通过这些事件监听器的设置,实现了以下功能:



  • 点击静态元素时,将编辑组件转换为编辑状态。

  • 点击取消按钮时,将编辑组件转换为静态状态。

  • 点击保存按钮时,获取输入框的值,更新静态元素的文本内容,并将编辑组件转换为静态状态。


HTML


在html中通过new将组件挂载在root上


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>就地编辑-EditIntPlace</title>
</head>
<body>
<div id="root"></div>
<script src="./editor_in_place.js"></script>
<script>
// 为了收集签名,给个表单太重了 不好看
// 个性签名,就地编辑
// 面向对象 将整体开发流程封装 封装成一个组件
new EditInPlace('editor',document.getElementById('root'))//名字 挂载点
</script>
</body>
</html>

完整效果



结尾


写的不太美观,掘友们可以自己试试加上样式,去掉按钮,再加上一张绝美的背景图片...


作者:滚去睡觉
来源:juejin.cn/post/7315730050576367616
收起阅读 »

前端实习生如何提升自己

web
这几年就业形势很严峻,很多大学生都选择通过实习来提升自己毕业后的就业压力。 同时,国家也在大力鼓励学生们多去实习,一些高校甚至强制学生大三就要出去实习,用来换成学分,而且比重很大,比如从深圳大学分出来的深圳技术大学。最近,我们就从深圳技术大学招聘了多个前端实习...
继续阅读 »

这几年就业形势很严峻,很多大学生都选择通过实习来提升自己毕业后的就业压力。


同时,国家也在大力鼓励学生们多去实习,一些高校甚至强制学生大三就要出去实习,用来换成学分,而且比重很大,比如从深圳大学分出来的深圳技术大学。最近,我们就从深圳技术大学招聘了多个前端实习生。


正常来讲一个资深的研发,最多可以辅导两个实习生,但因为我们是初创阶段,为了降低成本,我们经常会出现需要同时辅导三个以上实习生的情况。


到网上搜了一圈,都没有系统讲解实习生如何提升自己的资料,本文将结合自己过去十年带人的经验,面向前端岗位,谈一谈前端实习生如何提升自己。


作者简介


先后就读于北京交大和某军校,毕业后任职于原北京军区38集团军,带过两次新兵连,最多时管理过120人左右的团队。


退役后,先进入外企GEMALTO做操作系统研发,后进入搜狐、易车和美团等公司做大前端开发和团队管理,最多时管理过30多人的大前端团队,负责过几百个产品的前端开发工作,做过十多个大型复杂前端项目的架构工作。


目前在艺培行业创业,通过AIGC技术提升艺培机构门店运营和招生推广的效率和降低成本。


如何选择实习?


我永远相信,选择比努力奋斗更重要。选择一家合适的实习单位或公司,可以达到事半功倍的效果。


大公司的优势


毫无疑问,很多大学生都会选择大公司作为自己的实习单位,尤其是一些上市公司,甚至为了求得一个实习机会,即使倒贴钱也愿意。


与小企业相比,大公司一般都会有相对完整的企业制度。从签实习协议开始,如薪资结算、工作安排等,完全按照相关的企业制度,以及法律法规进行,这样一定程度有利于保障实习生的劳动权益。


与此相比,一些小企业甚至连实习协议都不签,这对于实习生来说是非常不利的。


大公司一般都会有日历表和日常的作息时刻表。因此,在大公司工作一般都按照正常的作息时间上下班,不太会出现加班的情况。


而相对来说,小企业由于刚刚起步,工作量非常大,人手常常不够,因此加班加点是家常便饭了。


每个大公司都会具有良好的、独特的企业文化,从办公室的装饰、到公司员工午休时的娱乐活动,无不体现出自己的特性。因此,在大公司里实习,你能够亲身感受到大公司内部的企业文化。


而小企业,则不太可能会有自己的企业文化,即使有多数也是非常糟糕的。


小企业的特点


在大公司实习,由于大公司里本身就人员冗余,再加上对“实习生”的不信任,因此他们给实习生安排的工作常属于打杂性质的。可能你在大公司里呆了两个月,可是连它最基本的业务流程都没弄明白。


而在小企业里,由此大多处于起步,没有太多的职位分化,你可能一人需要兼多职,许多事情都得自己解决,能够提高你独立解决问题的能力。


大公司一般有完整的部门体制,上下级关系相对严格分明,因此,官僚作风比较明显,新人难有发言权。


而在小企业,这方面的阻力则小得多,使新人少受束缚,更自由地施展自己的能力、表达自己的想法。


小结


实习为了给将来就业铺路,因此,实习最重要的在于经验。当然,在大公司实习如果能在增加自己进该公司的概率那固然好,但如果只是为了在简历里加上“在某大公司里实习过”的虚名,那实在是没有必要。


结合未来职业选择,要实习的公司和岗位是和你的职业理想直接相关的,这个岗位的实习和这个公司的进入可以给你日后的职业发展加分。


选你能在实习期间出成果的项目。如果你选了个很大的项目,半年还没做完,这就说不清楚是你没做好,还是项目本身的问题了。


实习的时候要更深入的了解企业的实际情况和问题,结合理论和实际,要给企业解决实际问题,有一个具体的成果,让企业真正感觉到你的项目做好了


我个人建议,在正式工作前,大公司和小企业,最好都能争取去实习一段时间,但第一份实习最好去大公司。


通过实习,希望大家能够验证自己的职业抉择,了解目标工作内容,学习工作及企业标准,找到自身职业的差距。


如何做好实习


选择好合适的实习单位,只是走出了万里长征的第一步,要做好实习,大家可以关注以下四个方面:


端正态度


从实习生个人的角度来看,多数人的出发点都是学习提升,但如果我们只是抱着纯学习而不为企业创造价值的心态的话,实习工作就不容易长久。


我们可以从企业的角度来看:



  • 一是实习提供了观察一位潜在的长期员工工作情况的方法,为企业未来发展培养骨干技术力量与领导人;

  • 二手有利于廉价劳动力争夺人才,刚毕业的学员便于管理,这样不仅能降低成本,还能提高企业的知名度,有利于企业长远发展。


实习生只有表现出了企业看重的那些方面,企业才会投入时间和精力去好好培养他,教一些真本领。


不管是在美团,还是我自己创业的公司,我都是基于相同的原则,去评估是否留下某个实习,进行重点培养。


我个人看重以下几点:



  • 不挑活:即使不是自己喜欢或在工作范围的任务,也能积极去完成;

  • 爱思考:接到任务或经过指导后,能及时反馈存在的问题和提升改进方案;

  • 善沟通:遇到困难及时寻求帮助,工作不开心了不封闭自己、不拒绝沟通。


不管当年我去实习还是上班,我的心态都是先为企业创造价值,在此基础上再争取完成自己的个人目标。


注重习惯


有的人毕业10年都还没毕业3年的人混得好,不是因为能力不行,而是因为他没有养成良好的职场习惯,以至于总是吃亏。


开发人员培养好的习惯至关重要。编写好的代码,持续学习,逐步提高代码质量。


主动反馈和同步信息


这个习惯非常重要,可以避免大家做很多无用功,大大提升大家日常工作的效率,而且也特别容易获得大家的好感与信任。


经常冒烟自测


对于自己负责的功能或产品,不管是测试阶段还是正式发布,只要需要其他人使用,都最好冒烟自测一遍,避免低级错误,进而影响大家对你专业度的信任。


良好的时间管理


先每天花几分钟确定好我今天要办的最重要的三到六件事情,如此能帮助提高整体的效率;把今天要做的所有事情一一写下来,按轻重缓急排序,可以按照要事第一的原则;每做完一件事情就把它划掉,在今天工作结束之前检查我是不是把整个list的工作都完成了。


避免重复的错误


不要犯了错误之后就草草了事,花一点时间深度挖掘清楚我犯错的底层原因是什么,再给自己一些小提示,比如做一个便利贴做提醒,在电脑桌面上面放一个警告,避免相同的错误重复发生。


构建知识体系


在信息爆炸的年代,碎片化的知识很多,系统学习的时间越来越少,如果没有自己的知识体系,很容易被淹没在知识的海洋中,而且难以记忆。


100分程序员推荐的做法,通过Wiki或者其他知识管理工具构建一个知识框架,大的分类可以包括软技能、架构、语言、前端、后端等,小的分类可以更细化。


培养大局观


程序员比较容易陷入的困境是专注于自己的一亩三分地,不关心团队的进度和业绩,也不关心软件的整体架构和其他模块。


这种状态长期下去没有好处,特别是在大公司中,逐渐成长为一颗螺丝钉。100分程序员会在工作之余,多看看其他在做什么,看看团队的整体规划,看看软件系统的架构和说明文档。


对自己的工作更理解,而且知道为什么这个产品应该这样设计,为什么领导应该这样做规划,这种大局观非常有利于自己的职业生涯。


技能提升


通过实习阶段,你才会知道专业知识在工作中的具体运用,才能切身体会专业知识的重要性,这样也让自己在以后的学习中更加的努力和勤奋和有所侧重的安排各科目的学习时间。


对于前端实习生的技能学习,除了打牢HTML、CSS和JS这些基础的前端知识以及熟练掌握vue、react这些框架开发技术栈,建议大家还需要选中自己感兴趣的前端技术领域进行持续深入的学习实践,具体请看我之前总结的文章:前端学哪些技能饭碗越铁收入还高


关系处理


大学生在实习过程中最难适应的就是人际关系,同时还要了解企业组织运行的特点、规章制度,了解企业文化对员工的影响等等,知道职场中和领导及同事该如何沟通,企业对员工有着什么样的需求等。


这些也只有在实习时,让自己处于团队之中,才能切身的体会和参与,只有这样大学生才会对社会生活有深刻的理解,对将来就业才有益处。


不论是对同事,还是对客户,还是对合作伙伴,都不要吝啬你的赞美之词。善于夸赞他人的员工,更容易获得他人的好评,有时还能让你成功避免一些不必要的麻烦。


当然,赞美他人,不要过于浮夸,要能说出赞美的理由,以及哪些是你表示认同的地方。比如,我们在称赞他人方案做得好的时候,不要只是说:“这方案不错”。


可以这么说:“这方案在市场推广方面的内容写得很好,逻辑清晰,数据充分,还能有同行的案例借鉴,十分值得我学习。”


要让别人感受到你是真心的在夸奖他,而不是讨好他。


我们常常很难控制激动时的情绪,发怒时更是。往往这个时候表达出的观点都是偏激的,事后再后悔就来不及了。


如果你与同事之间发生争执而发怒,请在这个时间段保持沉默。给自己找个地方安静一会儿,等到情绪稳定以后,再向他人表达你的观点,慢慢地,你就会更好地控制自己的情绪。


在职场中切忌过于情绪化,学会管理好自己的情绪是一个职场人必备的技能之一。坏情绪并不会帮助你解决任何问题,过多的抱怨只会使你成为负能量的传播者。


协作在软件开发中至关重要。一个好的开发人员应该能够与他人很好地合作,并了解如何有效地协作。这意味着对反馈持开放态度,积极寻求团队成员的意见,并愿意作为团队的一部分从事项目。


协作会带来更好的沟通、更高效的工作流程和更好的产品。


总结


社会实践对大学生的就业有着很大的促进作用,是大学生成功就业的前提和基础。


实习的目的因人而异,可以千差万别,而你实习的真正目的也只有自己才最清楚。只有在开始实习之前首先明确自己的目的,后面的路才会变得清晰明了。


作者:文艺码农天文
来源:juejin.cn/post/7319181229520568371
收起阅读 »

前端学哪些技能饭碗越铁收入还高

web
随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。 但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深...
继续阅读 »

随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。


但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深耕。


本文,我们就聊聊,掌握了哪些技能,能让前端同学,收入高且稳定。


端智能


首推的是端智能,很多行业大咖都认为,随着ChatGPT的横空出世,开启了第四次工业革命,很多产品都可以用大模型重做一遍。当前,我创业的方向,也和大模型有关。


当前的大模型主要还跑在云端,但云端的成本高,大模型的未来在端智能,这也是小米创始人雷军在今年一次发布会上提出的观点。


在2023年8月14日的雷军年度演讲中,雷军宣布小米已经在手机端跑通13亿参数的大模型,部分场景效果媲美云端。


目前,端上大模型的可行性和前景已经得到了业内的普遍认可,国内外各个科技大厂在大模型的端侧部署领域均开始布局,目前大量工程已在PC端、手机端实现大模型的离线部署,更有部分App登陆应用商店,只需下载即可畅通无阻地对话。


我们相信,在不久的将来,端上大模型推理将会成为智能应用的重要组成部分,为用户带来更加便捷、智能的体验。


我在美团从零研发了web端智能推理引擎,当时立项时,就给老板画饼,美团每天的几百亿推理调用,如果有一半用端智能替代的话,每年能为公司节省上亿元。


要想掌握端智能,需要学习深度学习的基本知识,还要掌握图形学和C++编程,通过webgl或webassembly 技术实现在Web端执行深度学习算法。


图形学


前面提到的端智能,只是涉及到了图形学中的webgl计算,但图形学的未来在元宇宙,通过3D渲染,实现VR、AR、XR等各种R。


计算机图形学是一门快速发展的领域,涵盖了三维建模、渲染、动画、虚拟现实等众多技术和应用。在电影、广告、游戏等领域中,计算机图形学的应用已经非常广泛。


熟练使用threejs开发各种3D应用,只能算是入门。真正的图形学高手,不仅可以架构类似3D家装软件的大型应用,而且能掌握渲染管线的底层原理,熟练掌握各种模型格式和解决各种软件,进行模型转换遇到的各种兼容问题。


随着计算机硬件和算法的不断进步,计算机图形学正迎来新的发展趋势。


首先是实时渲染与逼真度提升



  • 实时渲染技术:随着游戏和虚拟现实的兴起,对实时渲染的需求越来越高。计算机图形学将继续致力于研发更高效的实时渲染算法和硬件加速技术,以实现更逼真、流畅的视觉效果。

  • 光线追踪与全局照明:传统的实时渲染技术在光照模拟方面存在挑战。计算机图形学将借助光线追踪等技术,实现更精确的全局照明效果,提升场景的真实感和细节表现。


其次是虚拟与增强现实的融合



  • 混合现实技术:计算机图形学将与传感器技术、机器视觉等相结合,推动虚拟现实与增强现实的融合发展。通过实时感知和交互,用户可以在真实世界中与虚拟对象进行互动,创造更沉浸式的体验。

  • 空间感知与虚拟对象定位:计算机图形学将致力于解决空间感知和虚拟对象定位的挑战。利用深度学习、摄像头阵列等技术,实现高精度的空间感知和虚实融合,为虚拟与增强现实应用带来更自然、精确的交互方式。


再次是计算机图形学与人工智能的融合



  • 生成对抗网络(GAN)在图形生成中的应用:GAN等人工智能技术为计算机图形学带来了新的创作手段。通过训练模型生成逼真的图像和场景,计算机图形学能够更便捷地创建大量内容,并提供个性化的用户体验。

  • 计算机图形学驱动的虚拟人物与角色生成:结合计算机图形学和人工智能技术,研究人员正在努力开发高度逼真的虚拟人物和角色生成方法。这将应用于游戏、影视等领域,带来更具情感表达和交互性的虚拟角色。


最后是可视化分析与科学研究。



  • 一是大数据可视化:随着大数据时代的到来,计算机图形学在可视化分析方面扮演着关键角色。通过创新的可视化方法和交互技术,研究人员能够更深入地理解和分析庞大而复杂的数据集,揭示潜在的模式和趋势。

  • 二是科学数据可视化:计算机图形学在科学研究中的应用也日益重要。通过将科学数据转化为可视化形式,研究人员能够更直观地理解复杂的数据模式和关系,加快对科学问题的洞察和发现。这种可视化分析有助于领域如天文学、生物学、气象学等的研究进展。


工程提效


其实,过去三年我在美团的工作,至少有一半的精力是做和工程提效相关的事情。当然,也做了降本的事情,从零搭建外包团队。


就像我之前总结的文章:我在美团三年的前端工程化实践回顾 中提到那样,前端工程提效,一般会按照工具化、标准化、平台化和体系化进行演进。


相比前面的端智能和图形学,除了建设低代码平台和WebIDE有点技术难度,其他更多需要的是沟通、整合资源的能力,既要有很强的项目管理能力,又要人产品思维。


前两个方向做好了我们一般称为技术专家,而工程提效则更偏管理者,未来可以成为高管或自己创业。


总结


端智能和图形学,我在美团都尝试过进行深耕,但自己性格外向,很难坐得住。工程提效做得也一般,主要是因为需要换城市而换了部门,没有机缘继续推进,在美团很难往上走,所以只能尝试自己创业。


当前我创业的公司,也需要用到很多端智能的技术,比如我们使用yolov8实现了在web端进行智能抠图(后面计划使用segment anything大模型实现);也需要用到很多图形学的技能,比如开发3D美术馆为艺培机构招生引流,通过3D展示火箭、太阳系、空间站等,提升教学效果。


未来,我们需要招聘很多端智能及图形学方向的技术人才,欢迎大家加入。


作者:三一习惯
来源:juejin.cn/post/7310143510103064585
收起阅读 »

说一说css的font-size: 0?

web
平常我们说的font-size:0;就是设置字体大小为0对吧,但是它的用处不仅仅如此哦,它还可以消除子行内元素间额外多余的空白! 问题描述? 是否出现过当多个img标签平铺的时候,会出现几个像素的间距?就像这样👇(为了醒目加了个红色的框框) 是什么原因造成...
继续阅读 »

平常我们说的font-size:0;就是设置字体大小为0对吧,但是它的用处不仅仅如此哦,它还可以消除子行内元素间额外多余的空白



问题描述?


是否出现过当多个img标签平铺的时候,会出现几个像素的间距?就像这样👇(为了醒目加了个红色的框框)


image.png


是什么原因造成的呢?


大家都知道img是行内元素,比如当我们的标签换行的时候,回车符会解析一个空白符,所以这是造成会有间距的原因之一。


当然喽,不仅仅是img,包括其他的一些常见的行内元素,比如span👇标签回车换行的效果,同样也会间隙,当然如果是缩进、空格等字符同样也会产生空白间隙,导致元素间产生多余的间距


image.png


    <span>背景图</span>
<span>背景图</span>
<span>背景图</span>
<span>背景图</span>
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >

如何解决呢?


那我们首先想到取消换行、空格...


既然是因为标签换行了引起的,那么我们就取消换行、空格等试一试。


image.png


<span>背景图</span><span>背景图</span><span>背景图</span><span>背景图</span>
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >

证明方法还是有用的~ 那还有没有其他的方法解决呢,那这个时候可以借助font-size:0来用一用。


如何使用font-size: 0 解决呢?


利用font-size:0消除子行内元素间额外多余的空白,需要在父元素上添加font-size:0


image.png


是不是就解决了呀?


看一个完整的完整demo效果


image.png
当然需要注意一下



设置font-size: 0时,子元素必须指定一个font-size大小,否则文本内容不会显示哦



示例代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
<style>
/*************************css代码👇***********************/
ul {
margin: 20px;
display: flex;
gap: 20px;
}
.item {
width: 300px;
height: 200px;
padding: 20px;
border-radius: 10px;
background: #fff;
overflow: hidden;
font-size: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)
}
.item-img {
width: 100%;
height: 175px;
object-fit: cover;
border-radius: 5px;
}
.item-text {
color: #333;
font-size: 14px;
}
span {
background-color: red;
padding: 10px;
}
</style>
</head>
<body>


<ul>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
</ul>
</body>
</html>


作者:是小西瓜吖
来源:juejin.cn/post/7260752483055878204
收起阅读 »

一个左侧导航栏的选中状态把我迷得颠三倒四

web
事情是这样的 👇 前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EAS...
继续阅读 »

事情是这样的 👇


前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EASY吗😎


我用的是vue版本的,说白了就一个知识点——路由和菜单。凭借我的聪明才智,肯定一看就会。


路由


首先,需要先了解一下路由表的配置。基本的路由配置请参阅 Vue-Router 官方文档


// 在本例子中,页面最终路径为 /dashboard/workplace
export default {
path: 'dashboard',
name: 'dashboard', // 路由名称
component: () => import('@/views/dashboard/index.vue'),
meta: {
locale: 'menu.dashboard',
requiresAuth: true,
icon: 'icon-dashboard',
},
children: [
{
path: 'workplace',
name: 'workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
roles: ['admin'],
hideInMenu: false,
},
},
],
};

路由 Meta 元信息


参数名说明类型默认值
roles配置能访问该页面的角色,如果不匹配,则会被禁止访问该路由页面string[]-
requiresAuth是否需要登录鉴权booleanfalse
icon菜单配置iconstring-
locale一级菜单名(语言包键名)string-
hideInMenu是否在左侧菜单中隐藏该项boolean-
hideChildrenInMenu强制在左侧菜单中显示单项boolean-
activeMenu高亮设置的菜单项string-
order排序路由菜单项。如果设置该值,值越高,越靠前number-
noAffix如果设置为true,标签将不会添加到tab-bar中boolean-
ignoreCache如果设置为true页面将不会被缓存boolean-

hideInMenu 控制菜单显示, requiresAuth 控制是否需要登录,没错,以上知识点完全够我用了🤏🤏🤏🤏


三下五除二就开发完成了,正当我沉浸在成功的喜悦时,测试给我提了个bug。


说导航栏目切换,选中状态有俩个正常,俩个不正常,切换页面导航没选中,再点一次才会选中。我擦👏👏👏👏👏,我傻眼了😧😧😧


到底哪里出了问题,我哪里写错了呢,人家官方肯定没问题,于是我开始寻找。是路由名称重复了,还是那个组件写的有问题,还好有俩个正常的,我仔细比对一下不就好了吗,我可真是个小机灵鬼。


就这样,我又一次为我的自大付出了汗水,对比了一天,我感觉好像真不是我写的有问题,不过还是有收获的,我发现requiresAuth 设置为true的导航正常,requiresAuth设置为false的不正常。抱着怀疑的态度我开始找原来模板项目中处理requiresAuth的代码。最后在components》menu》index.vue文件下发现一个方法:


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if (requiresAuth && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

这... ... 我人麻了,这不是只有requiresAuth 为true的时候才会有效吗?他为啥这么写呢?还是我复制项目的时候复制错了?然后我改成了


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if ((requiresAuth === true || requiresAuth === false) && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

🙈🙈🙈🙈🙈看出来我改哪里了吗,考考你们的眼力,我改的方法是不是很nice。
反正好使了,下班!!!!


作者:一路向北京
来源:juejin.cn/post/7317277887567151145
收起阅读 »

【日常总结】解决el-select数据量过大的3种方法

web
背景 最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。 想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,...
继续阅读 »

背景


最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。


image.png


想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,前端搞也是可以的。


这个故事还有个后续


image.png


过了一周上线后,发现有一个下拉框的数据有30w+!!!加载都加载不出来,哈哈哈哈,接口直接超时报错了,所以又cuocuocuo改了一遍,最后改成了:



  1. 接口翻页请求

  2. 前端使用自定义指令实现上拉加载更多,搜索直接走的后端接口


方案


通过一顿搜索加联想总结了3种方法,以下方法都需要支持开启filterable支持搜索。


标题具体问题
方案1只展示前100条数据,这个的话配合filter-method每次只返回前100条数据。限制展示的条数可能不全,搜索需要多搜索点内容
方案2分页方式,通过指令实现上拉加载,不断上拉数据展示数据。仅过滤加载出来的数据,需要配合filterMethod过滤数据
方案3options列表采用虚拟列表实现。成本高,需要引入虚拟列表组件或者自己手写。经掘友指点,发现element-plus提供了对应的实现,如果是plus,则可以直接使用select-v2

方案一(青铜段位) filterMethod直接过滤数据量


<template>
<el-select
v-model="value"
clearable filterable
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options.slice(0, 100)"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>

</template>
export default {
name: 'Demo',
data() {
return {
options: [],
value: ''
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
for (let i = 0; i < 25000; i++) {
this.options.push({label: "选择"+i,value:"选择"+i});
}
},
filterMethod(val) {
console.log('filterMethod', val);
this.options = this.options.filter(item => item.value.indexOf(val) > -1).slice(0, 100);
},
visibleChange() {
console.log('visibleChange');
}
}
}

方案二(白银段位) 自定义滚动指令,实现翻页加载


写自定义滚动指令,options列表滚动到底部后,再加载下一页。但这时候筛选出来的是已经滚动出来的值。


这里如果直接使用filterable来搜索,搜索出来的内容是已经滑动出来的内容。如果想筛选全部的,就需要重写filterMethod方法来自定义过滤功能。可以根据情况选择是否要重写filterMethod。


image.png


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>
<script>
export default {
name: 'Demo',
data() {
return {
options: [],
value: '',
pageNo: 0
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
},
filterMethod(val) {
this.data = val ? this.allData.filter(item => item.label.indexOf(val) > -1) : this.allData;
this.getPageList();
}
},
directives:{
'el-select-loadmore':(el, binding) => {
// 获取element-ui定义好的scroll父元素
const wrapEl = el.querySelector(".el-select-dropdown .el-select-dropdown__wrap");
if(wrapEl){
wrapEl.addEventListener("scroll", function () {
/**
* scrollHeight 获取元素内容高度(只读)
* scrollTop 获取或者设置元素的偏移值,
* 常用于:计算滚动条的位置, 当一个元素的容器没有产生垂直方向的滚动条, 那它的scrollTop的值默认为0.
* clientHeight 读取元素的可见高度(只读)
* 如果元素滚动到底, 下面等式返回true, 没有则返回false:
* ele.scrollHeight - ele.scrollTop === ele.clientHeight;
*/

if (this.scrollTop + this.clientHeight >= this.scrollHeight) {
// binding的value就是绑定的loadmore函数
binding.value();
}
});
}
},
},
}
</script>

</script>

方案三(黄金段位) 虚拟列表


引入社区的vue-virtual-scroll-list 支持虚拟列表。但这里想的自己再实现一遍虚拟列表,后续再写吧。


另外,element-plus提供了对应的实现,如果是使用的是plus,则可以直接使用 select-v2组件


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable >

<virtual-list
class="list"
style="height: 360px; overflow-y: auto;"
:data-key="'value'"
:data-sources="data"
:data-component="item"
:estimate-size="50"
/>

</el-select>
</div>

</template>
<script>
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item';
export default {
name: 'Demo',
components: {VirtualList, Item},
data() {
return {
options: [],
data: [],
value: '',
pageNo: 0,
item: Item,
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
}
}
}
</script>


// item组件
<template>
<el-option :label="source.label" :value="source.value"></el-option>
</template>


<script>
export default {
name: 'item',
props: {
source: {
type: Object,
default() {
return {}
}
}
}
}
</script>


<style scoped>
</style>



总结


最后我们项目中使用的虚拟列表,为啥,因为忽然发现组件库支持select是虚拟列表,那就直接使用这个啦。


最后的最后


没有用虚拟列表,因为接口数据量过大(你见过返回30w+的接口吗🙄。。),后端接口改成分页,前端支持自定义指令上拉加载,引用的参数增加了remote、remote-method设置为远端的方法。


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
remote
:remote-method="remoteMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>

参考文章:



作者:searchop
来源:juejin.cn/post/7278238985448341544
收起阅读 »

从 Vue 2 迁移到 Svelte

web
大家好,这里是大家的林语冰。 本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。 在使用 Vue 2 作为我们的前端框架近 2 ...
继续阅读 »

大家好,这里是大家的林语冰。


本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。


在使用 Vue 2 作为我们的前端框架近 2 年后,我们宣布此支持将不再维护,因此我们决定迁移到新框架。但该选谁呢:Vue 3 还是 Svelte 呢


粉丝请注意,我们迁移后的目标也是改善 DX(开发体验),尤其是类型检查、性能和构建时间。我们没有考虑 React,因为它需要投资一大坨时间成本来学习,而且与 Vue 和 Svelte 不同,它没有提供开箱即用的解决方案。此外,后者共享相同的 SFC(单文件组件)概念:同一文件中的逻辑(JS)、结构(HTML)和样式(CSS)。


Svelte vs Vue 3


Svelte 的留存率更高。对于我们的新前端,我们必须从市场上可用的 2 个框架权衡,即 Svelte 和 Vue 3。下面是过去 5 年不同框架留存率的图示(留存率 = 会再次使用/(会再次使用 + 不会再次使用))。JS 现状调查汇编了该领域开发者的数据,如你所见,Svelte 排名第二,而 Vue 3 排名第四。


01-state.jpg


这启示我们,过去使用过 Svelte 的开发者愿意再次使用它的数量比不愿使用它的要多。


Svelte 的类型体验更棒


Vue 2Vue 3Svelte
组件级类型YesYesNo
跨组件类型NoYesYes
类型事件NoNoYes

Svelte 通过更简单的组件设计流程和内置类型事件提供了更好的类型体验,对我们而言十分用户友好。


全局变量访问限制。使用 Svelte,可以从其他文件导入枚举,并在模板中使用它们,而 Vue 3 则达咩。


02-benchmark.png


语法。就我个人而言,私以为 Svelte 语法比 Vue 更优雅和用户友好。您可以瞄一下下面的代码块,并亲自查看它们。


Svelte:


03-svelte.png


Vue:


04-vue.png


没有额外的 HTML div <template>。在 Svelte 中您可以直接编写自己的 HTML。


样式在 Svelte 中会自动确定作用域,这对于可维护性而言是一个优点,且有助于避免 CSS 副作用。每个组件的样式独立,能且仅能影响该组件,而不影响其父/子组件。


更新数据无需计算属性。在 Svelte 中,感觉更像在用纯 JS 撸码。您只需要专注于编写一个箭头函数:


const reset = () => {
firstName = ''
lastName = ''
}

Svelte 中只需单个括号:


//Svelte
{fullName}

//Vue
{{fullName}}

添加纯 JS 插件更简单。此乃使用 Svelte 和 Prism.js 的语法高亮集成用例,如下所示:


05-prism.png


无需虚拟 DOM 即可编译代码。Svelte 和 Vue 之间的主要区别是,减少了浏览器和 App 之间的层数,实现更优化、更快的任务成果。


自动更新。诉诸声明变量的辅助,Svelte 可以自动更新您的数据。这样,您就不必等待变更反映在虚拟结构中,获得更棒的 UX(用户体验)。


Svelte 也有短板


理所当然,Svelte 也有短板,比如社区相对较小,因为它是 2019 年才诞生的。但随着越来越多的开发者可能会认识到其质量和用户友好的内容,支持以及社区未来可能会不断发展壮大。


因此,在审查了此分析的结果后,尽管 SvelteKit 在迁移时仍处于积极开发阶段,我们决定使用 Svelte 和 Svelte Kit 砥砺前行。


06cons.png


如何处理迁移呢?


时间:我们选择在 8 月份处理迁移,当时该 App 用户较少。


时间长度:我们花了 2 周时间将所有文件从 Vue 迁移到 Svelte。


开发者数量:2 名前端开发者全职打工 2 周,另一名开发者全职打工 1 周,因此涉及 3 名开发人员。


工作流:首先,我们使用 Notion 工具将我们的凭据归属于团队的开发者。然后,我们开始在 Storybook 中创建新组件,最后,每个开发者都会奖励若干需要在 Svelte 中重写的页面。


作为一家初创公司,迁移更简单,因为我们没有 1_000 个文件需要重写,因此我们可以快速执行。虽然但是,当 SvelteKit 仍处于积极开发阶段时,我们就冒着风险开始迁移到 SvelteKit,这导致我们在迁移仅 1 个月后就不得不做出破坏性更新。但 SvelteKit 专业且博大精深的团队为我们提供了一个命令(npx svelte-migrate routes),以及一个解释清晰的迁移指南,真正帮助我们快速适应新的更新。


此外,9 月份,SvelteKit 团队宣布该框架终于进入候选版本阶段,这意味着,它的稳定性现在得到了保证!


文件和组件组织


SvelteKit 的“文件夹筑基路由”给我们带来了很多。我们可以将页面拆分为子页面,复用标准变量名,比如 loading/submit 等等。此外,布局直接集成到相关路由中,由于树内组织的增加,访问起来更简单。


那么我们得到了什么?


除了上述好处之外,还值得探讨其他某些关键因素:


性能提高且更流畅。编译完成后,我们可以体会到该 App 的轻量级。与其他框架相比,这提高了加载速度,其他框架在 App 的逻辑代码旁嵌入了“运行时”。


DX 更棒。SvelteKit 使用 Vite 打包器,此乃新一代 JS 构建工具,它利用浏览器中 ES 模块的可用性和编译为原生(compile-to-native)的打包器,为您带来最新 JS 技术的最佳 DX。


代码执行更快。它没有虚拟 DOM,因此在页面上变更时,需要执行的层数少了一层。


启动并运行 SSR(服务器端渲染)。如果最终用户没有良好的互联网连接或启用 JS,平台仍将在 SSR 的帮助下高效运行,因为用户仍能加载网页,同时失去交互性。


代码简洁易懂。Svelte 通过将逻辑(JS)、结构(HTML)和样式(CSS)分组到同一文件中,可以使用更具可读性和可维护性的面向组件的代码。黑科技在于所有这些元素都编译在 .svelte 文件中。


固定类型检查。自从我们迁移到 Svelte 以来,我们已经成功解决了类型检查的最初问题。事实上,我们以前必须处理周期性的通知,而如今时过境迁。不再出现头大的哨兵错误。(见下文)


07-error.jpg


粉丝请注意,此博客乃之前的迁移测评,其中某些基准测试见仁见智,尤大还亲自码字撰写博客布道分享,我们之后会继续翻译 Vue 官方博客详细说明。



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Migrating from Vue 2 to Svelte



您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎关注地球猫猫教。谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7317222425384714294
收起阅读 »

看我如何用JDBC数据库连接池,轻松解决大量并发请求问题!

我们已经知道JDBC是Java语言中用来规范客户端程序如何访问数据库的应用程序接口,也是大多数Java开发者与数据库打交道的必备工具。但是,你是否知道,JDBC在处理大量并发请求时,可能会遇到一些问题?这就是我们今天要讨论的主题——JDBC数据库连接池。首先,...
继续阅读 »

我们已经知道JDBC是Java语言中用来规范客户端程序如何访问数据库的应用程序接口,也是大多数Java开发者与数据库打交道的必备工具。

但是,你是否知道,JDBC在处理大量并发请求时,可能会遇到一些问题?这就是我们今天要讨论的主题——JDBC数据库连接池。

首先,让我们来了解一下什么是数据库连接池。

一、数据库连接池简介

JDBC连接池,全称为Java多线程数据库连接池,是一种用于管理数据库连接的技术。其主要作用是减少每次请求时创建和释放数据库连接的开销,以此提高系统性能。

在应用程序和数据库之间,JDBC连接池会建立一个连接池,当需要访问数据库时,无需每次都重新创建连接,而是直接从池中获取已有的连接。

Description

总结一下就是:

  • 数据库连接池是个容器,负责分配、管理数据库连接(Connection)

  • 它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。

  • 释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。

那么,为什么我们需要JDBC数据库连接池呢?

这主要有以下几个原因:

1.提高性能: 频繁地创建和销毁数据库连接会消耗大量的系统资源,而通过使用连接池,可以大大减少这部分开销,提高系统的性能。

2.提高稳定性: 在高并发的情况下,如果直接使用JDBC创建数据库连接,可能会出现系统无法创建更多的数据库连接的情况,导致系统崩溃。而通过使用连接池,可以有效地控制并发请求的数量,保证系统的稳定性。

3.提高数据库的响应速度: 通过使用连接池,可以减少等待数据库连接的时间,从而提高系统的响应速度。

之前我们代码中使用连接是没有使用都创建一个Connection对象,使用完毕就会将其销毁。这样重复创建销毁的过程是特别耗费计算机的性能的及消耗时间的。

而数据库使用了数据库连接池后,就能达到Connection对象的复用,如下图:

Description

  • 连接池是在一开始就创建好了一些连接(Connection)对象存储起来。用户需要连接数据库时,不需要自己创建连接;

  • 而只需要从连接池中获取一个连接进行使用,使用完毕后再将连接对象归还给连接池。

这样就可以起到资源重用,也节省了频繁创建连接销毁连接所花费的时间,从而提升了系统响应的速度。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


二、数据库连接池实现

1、标准接口:

javax.sql.DataSource。

官方(SUN公司)为数据库连接池提供了一套标准接口,由第三方组织实现此接口。

  • 核心方法:Connection getConnection(),获取连接。

Description

2、常见的数据库连接池:

JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由第三方来实现。

市面上有很多开源的JDBC数据库连接池,如C3P0、DBCP、Druid等,它们都有各自的特点和优势。

C3P0数据库连接池: 速度相对较慢(只是慢一丢丢),但是稳定性很好,Hibernate,Spring底层用的就是C3P0。

DBCP数据库连接池: 速度比C3P0快,但是稳定性差。

Proxool数据库连接池: 有监控连接池状态的功能,但稳定性仍然比C3P0差一些。

BoneCP数据库连接池: 速度较快。

Druid数据库连接池(德鲁伊连接池): 由阿里提供,集DBCP,Proxool,C3P0连接池的优点于一身,是日常项目开发中使用频率最高的数据库连接池。

三、Durid(德鲁伊连接池)的使用

Druid使用步骤:

  • 导入jar包 druid-1.1.12.jar。

  • 定义配置文件。

  • 加载配置文件。

  • 获取数据库连接池对象。

  • 获取连接。

druid.properties配置文件:

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc_test?useSSL=false&useServerPrepStmts=true
username=root
password=123456
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 最大等待时间
maxWait=3000

代码示例:


package com.green.druid;


import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.util.Properties;

public class DruidDemo {

public static void main(String[] args) throws Exception {
//1、导入jar包


//2、定义配置文件


//3、加载配置文件
Properties prop = new Properties();
prop.load(new FileInputStream("jdbc-demo/src/druid.properties"));
//System.out.println(System.getProperty("user.dir")); //当前文件目录 D:\code\JDBC


//4、获取连接池对象
DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);


//5、获取数据库连接 Connection
Connection conn = dataSource.getConnection();


System.out.println(conn);


}
}

以上就是JDBC数据连接池的简介与常见连接池的基本使用,希望对你有所帮助。在未来的开发过程中,不妨尝试使用JDBC数据库连接池,让你的应用性能更上一层楼!

收起阅读 »

js跨标签页通信

web
一、为什么要跨标签页通信 在web开发中,有时会有这样的情况,A页面中打开了B页面,B页面中操作了一些内容,再回到A页面时,需要看到更新后的内容。这种场景在电商、支付、社交等领域经常出现。 二、实现跨标签页通信的几种方式 2.1 localStorage 打...
继续阅读 »

一、为什么要跨标签页通信


在web开发中,有时会有这样的情况,A页面中打开了B页面,B页面中操作了一些内容,再回到A页面时,需要看到更新后的内容。这种场景在电商、支付、社交等领域经常出现。


二、实现跨标签页通信的几种方式


2.1 localStorage


image.png


打开A页面,可以看到localStorage和sessionStorage中都存储了testA:


image.png
image.png

B页面中,可以获取到localStorage,但是无法获取到sessionStorage:


image.png

2.2 BroadcastChannel


BroadcastChannel允许同源下浏览器不同窗口订阅它,postMessage方法用于发送消息,message事件用于接收消息。


A页面:


      const bc = new BroadcastChannel('test')

bc.postMessage('不去上班行吗?')

B页面:


      const bc = new BroadcastChannel('test')

bc.onmessage = (e) => {
console.log(e)
}

动画.gif


2.3 postMessage(跨源通信)


image.png


2.3.1 iframe跨域数据传递


parent.html


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h1>主页面</h1>
<iframe id="child" src="http://10.7.9.69:8080"></iframe>
<div>
<h2>主页面跨域接收消息区域</h2>
<div id="message"></div>
</div>
</body>
<script>
// 传递数据到子页面
window.onload = function () {
// 第二个参数表示哪个窗口可以接收到消息
document.getElementById('child').contentWindow.postMessage('不上班行不行', 'http://10.7.9.69:8080')
}
// 接受子页面传递过来的数据
window.addEventListener('message', function (event) {
document.getElementById('message').innerHTML = '收到' + event.origin + '消息:' + event.data
})
</script>
</html>


App.vue


<template>
<div class="app">
<div id="message"></div>
</div>

</template>
<script>
export default {
created() {
// 接收父页面传过来的数据
window.addEventListener('message', function (event) {
// 处理addEventListener执行两次的情况,避免获取不到data
// 因此判断接收的域是否是父页面
console.log('event', event)
if (event.origin.includes('http://127.0.0.1:5501')) {
document.getElementById('message').innerHTML = '收到' + event.origin + '消息:' + event.data
// 把数据传递给父页面 window.parent === top
window.parent.postMessage('不上班你养我啊', 'http://127.0.0.1:5501')
}
})
},
}
</script>


image.png


注:



  1. http://127.0.0.1:5501/ 是使用 Open with Live Server打开后的地址

  2. http://10.7.9.69:8080/ 是启动vue后的地址


2.3.2 postMessage在window.open()中的使用


作者:蓝色海岛
来源:juejin.cn/post/7315354087829536803
收起阅读 »

【前端考古】没有await,如何处理“回调地狱”

web
太长不看 不要嵌套使用函数。给每个函数命名并把他们放在你代码的顶层 利用函数提升。先使用后声明。 处理每一个异常 编写可以复用的函数,并把他们封装成一个模块 什么是“回调地狱”? 异步Javascript代码,或者说使用callback的Javascrip...
继续阅读 »

太长不看



  • 不要嵌套使用函数。给每个函数命名并把他们放在你代码的顶层

  • 利用函数提升。先使用后声明。

  • 处理每一个异常

  • 编写可以复用的函数,并把他们封装成一个模块


什么是“回调地狱”?


异步Javascript代码,或者说使用callback的Javascript代码,很难符合我们的直观理解。很多代码最终会写成这样:


fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})

看到上面金字塔形状的代码和那些末尾参差不齐的 }) 了吗?这就是广为人知的回调地狱了。

人们在编写JavaScript代码时,误认为代码是按照我们看到的代码顺序从上到下执行的,这就是造成回调地狱的原因。在其他语言中,例如C,Ruby或者Python,第一行代码执行结束后,才会开始执行第二行代码,按照这种模式一直到执行到当前文件中最后一行代码。随着你学习深入,你会发现JavaScript跟他们是不一样的。


什么是回调(callback)?


某种使用JavaScript函数的惯例用法的名字叫做回调。JavaScript语言中没有一个叫“回调”的东西,它仅仅是一个惯例用法的名字。大多数函数会立刻返回执行结果,使用回调的函数通常会经过一段时间后才输出结果。名词“异步”,简称“async”,只是意味着“这将花费一点时间”或者说“在将来某个时间发生而不是现在”。通常回调只使用在I/O操作中,例如下载文件,读取文件,连接数据库等等。


当你调用一个正常的函数时,你可以向下面的代码那样使用它的返回值:


var result = multiplyTwoNumbers(5, 10)
console.log(result)
// 50 gets printed out

然而使用回调的异步函数不会立刻返回任何结果。


var photo = downloadPhoto('http://coolcats.com/cat.gif')
// photo is 'undefined'!

在这种情况下,上面那张gif图片可能需要很长的时间才能下载完成,但你不想你的程序在等待下载完成的过程中中止(也叫阻塞)。


于是你把需要下载完成后运行的代码存放到一个函数中(等待下载完成后再运行它)。这就是回调!你把回调传递给downloadPhoto函数,当下载结束,回调会被调用。如果下载成功,传入photo给回调;下载失败,传入error给回调。


downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)

function handlePhoto (error, photo) {
if (error) console.error('Download error!', error)
else console.log('Download finished', photo)
}

console.log('Download started')

人们理解回调的最大障碍在于理解一个程序的执行顺序。在上面的例子中,发生了三件事情。



  1. 声明handlePhoto函数

  2. downloadPhoto函数被调用并且传入了handlePhoto最为它的回调

  3. 打印出Download started


请大家注意,起初handlePhoto函数仅仅是被创建并被作为回调传递给了downloadPhoto,它还没有被调用。它会等待downloadPhoto函数完成了它的任务才会执行。这可能需要很长一段时间(取决于网速的快慢)。


这个例子意在阐明两个重要的概念:



  1. handlePhoto回调只是一个存放将来进行的操作的方式

  2. 事情发生的顺序并不是直观上看到的从上到下,它会当某些事情完成后再跳回来执行。


怎样解决“回调地狱”问题?


糟糕的编码习惯造成了回调地狱。幸运的是,编写优雅的代码不是那么难!


你只需要遵循三大原则


1. 减少嵌套层数(Keep your code shallow)


下面是一堆乱糟糟的代码,使用browser-request做AJAX请求。


**


var form = document.querySelector('form')
form.onsubmit = function (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, function (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}

这段代码包含两个匿名函数,我们来给他们命名。


var form = document.querySelector('form')
form.onsubmit = function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}

如你所见,给匿名函数一个名字是多么简单,而且好处立竿见影:



  • 起一个一望便知其函数功能的名字让代码更易读

  • 当抛出异常时,你可以在stacktrace里看到实际出异常的函数名字,而不是"anonymous"

  • 允许你合理安排函数的位置,并通过函数名字调用它


现在我们可以把这些函数放在我们程序的顶层。


document.querySelector('form').onsubmit = formSubmit

function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse)
}

function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
}

请大家注意,函数声明在程序的底部,但是我们在函数声明之前就可以调用它。这是函数提升的作用。


2.模块化(Modularize)


任何人都有有能力创建模块,这点非常重要。写出一些小模块,每个模块只做一件事情,然后把他们组合起来放入其他的模块做一个复杂的事情。只要你不想陷入回调地狱,你就不会。让我们把上面的例子修改一下,改为一个模块。


下面是一个名为formuploader.js的新文件,包含了我们之前使用过的两个函数。


module.exports.submit = formSubmit

function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse)
}

function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
}

module.exports是node.js模块化的用法。现在已经有了 formuploader.js 文件,我们只需要引入它并使用它。请看下面的代码:


var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit

我们的应用只有两行代码并且还有以下好处:



  1. 方便新开发人员理解你的代码 -- 他们不需要费尽力气读完formuploader函数的全部代码

  2. formuploader可以在其他地方复用


3.处理每一个异常(Handle every single error)


有三种不同类型的异常:语法异常,运行时异常和平台异常。语法异常通常由开发人员在第一次解释代码时捕获,运行时异常通常在代码运行过程中因为bug触发,平台异常通常由于没有文件的权限,硬盘错误,无网络链接等问题造成。这一部分主要来处理最后一种异常:平台异常。


前两个大原则意在提高代码可读性,但是第三个原则意在提高代码的稳定性。在你与回调打交道的时候,你通常要处理发送请求,等待返回或者放弃请求等任务。任何有经验的开发人员都会告诉你,你从来不知道哪里回出现问题。所以你有必要提前准备好,异常总是会发生。


把回调函数的第一个参数设置为error对象,是Node.js中处理异常最流行的方式。


var fs = require('fs')

fs.readFile('/Does/not/exist', handleFile)

function handleFile (error, file) {
if (error) return console.error('Uhoh, there was an error', error)
// otherwise, continue on and use `file` in your code
}

把第一个参数设为error对象是一个约定俗成的惯例,提醒你记得去处理异常。如果它是第二个参数,你更容易把它忽略掉。


作者:Max力出奇迹
来源:juejin.cn/post/7294166986195533843
收起阅读 »

做了几年前端,别跟我说没配置过webpack

web
引言 webpack中文官网:webpack.docschina.org/concepts/ webpack是一个javascript常用的静态模块打包工具,通过配置webpack,可以实现包括压缩文件,解析不同文件格式等强大的功能,作者发现很多同学对webp...
继续阅读 »

引言


webpack中文官网:webpack.docschina.org/concepts/


webpack是一个javascript常用的静态模块打包工具,通过配置webpack,可以实现包括压缩文件,解析不同文件格式等强大的功能,作者发现很多同学对webpack的认知都停留在入口出口以及简单的loader和plugin配置上,对webpack的核心原理都一知半解。本文期望通过更深层的解读,让读者能更彻底地理解这个打包工具的来龙去脉。


为什么要用webpack


在webpack等打包工具出世之前,我们普通的H5项目是怎么处理错综复杂的脚本呢?
第一种方式:引用不同的脚本去使用不同的功能,但脚本太多的时候会导致网络瓶颈
第二种方式:使用一个大型js文件去引入所有代码,但这样会严重影响可读性,可维护性,作用域。


举个栗子:
由于浏览器不能直接解析less文件,我们可通过引入转换的插件(file watcher)把less实时转换为css并引入,但项目里面会多出一个map跟css文件,造成项目文件的臃肿。


官方文档的说法:


node.js诞生可以让Javasrcipt在浏览器环境之外使用,而webpack运行在node.js中。CommonJS的require机制允许在文件中引用某个模块,如此一来就可以解决作用域的问题。


    const HtmlWebpackPlugin = require('html-webpack-plugin')

webpack 关心性能和加载时间;它始终在改进或添加新功能,例如:异步地加载 chunk 和预取,以便为你的项目和用户提供最佳体验


核心概念


webpack有7个核心概念:



  1. 入口(entry)

  2. 输出(output)

  3. loader

  4. 插件(plugin)

  5. 模式(mode)

  6. 浏览器兼容性(brower compatibility)

  7. 环境(environment)


新建一个build文件夹,里面新建一个webpack.config.js


入口entry


这是打包的入口文件,所有的脚本将从这个入口文件开始


单入口


    const path = require('path')
module.exports = {
entry: path.resolve(__dirname, '../src/main.js')
}

多入口


使用对象语法配置,更好扩展,例如一个项目有前台网页跟后台管理两个项目可用多入口管理。


    entry: {
app: path.resolve(__dirname, '../src/main.js'),
admin: path.resolve(__dirname, '../src/admin.js'),
},

输出output


打包后输出的文件,[name]跟[hash:8]表示文件名跟入口的保持一致但后面加上了hash的后缀让每次生成的文件名是唯一的。


单入口


module.exports = {
output: {
filename: '[name].[hash:8].js',
path: path.resolve(__dirname, '../dist'),
},
}

多入口


module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
},
output: {
filename: '\[name].js',
path: \_\_dirname + '/dist',
},
};
// 写入到硬盘:./dist/app.js, ./dist/admin.js

loader转化器


用于模块的源码的转换,也是同学们在配置webpack的时候频繁接触的一个配置项。


举个例子,加载typescript跟css文件需要用到ts-loader跟css-loader、style-loader,
如果没有对应的loader,打包会直接error掉。


image.png


我们可以这么配置:先 npm i css-loader style-loader


module: {
rules: [
{
test: /\.css\$/,
use: ['style-loader','css-loader']
},
{
test: /\.ts\$/,
use: 'ts-loader'
}

必须留意的是,loader的执行是从右到左,就是css-loader执行完,再交给style-loader执行,


plugin插件


这是webpack配置的核心,有一些loader无法实现的功能,就通过plugin去扩展,建立一个规范的插件系统,能让你每次搭建项目的时候省去很多成本。


举个例子,我们会使用HtmlWebpackPlugin这个插件去生成一个html,其中会引入入口文件main.js。
假设不用这个插件,会发生什么?


当然是不会生成这个html,因此HtmlWebpackPlugin插件也是webpack的必备配置之一


const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
}),
],

模式mode


mode一共有production,development,node三种,如果没有设置,会默认为production


不同的mode对于默认优化的选项有所不同,环境变量也不同,具体需要了解每个插件的具体使用


选项描述
development会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development
production会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPlugin 和 TerserPlugin
none没优化选项

module.exports = {
mode: 'production'
}

source-map 的解读


Sourcemap 本质上是一个信息文件,里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。 Sourcemap 解决了在打包过程中,代码经过压缩,去空格以及 babel 编译转化后,由于代码之间差异性过大,造成无法debug的问题


当mode为development时,devtool默认为‘eval’,当mode为production时,devtool默认为false。


sourceMap的分类



  • source-map:外部。可以查看错误代码准确信息和源代码的错误位置。

  • inline-source-map:内联。只生成一个内联 Source Map,可以查看错误代码准确信息和源代码的错误位置

  • hidden-source-map:外部用于生产环境。可以查看错误代码准确信息,但不能追踪源代码错误,只能提示到构建后代码的错误位置。

  • eval-source-map:内联。每一个文件都生成对应的 Source Map,都在 eval 中,可以查看错误代码准确信息 和 源代码的错误位置。

  • nosources-source-map:外部。可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息。

  • cheap-source-map:外部。可以查看错误代码准确信息和源代码的错误位置,只能把错误精确到整行,忽略列。

  • cheap-module-source-map:外部用于生产环境。可以错误代码准确信息和源代码的错误位置,module 会加入 loader 的 Source Map。

  • eval-cheap-module-source-map: 内联,用于开发环境,构建跟热更新比较快


内联和外部的区别: 外部生成了文件(.map),内联没有。内联构建速度更快。


笔者用的两种配置分为是


// webpack.dev.js
devtool: 'eval-cheap-module-source-map',

// webpack.prod.js
devtool: 'cheap-module-source-map'

浏览器兼容性 brower compatibility


Webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。webpack 的 import() 和 require.ensure() 需要 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载 polyfill


环境 enviroment


本文使用的是webpack5 ,要求Node.js V在10.13.0+


Loader的汇总


笔者汇总了一部分常用的Loader以及其配置事项



  1. 浏览器兼容性:babel-loader

  2. css相关: css/style/less/postcss-loader

  3. vue: vue-loader



在配置loader前,先了解一下基本的配置



  • test: 匹配的文件,多用正则匹配

  • use: 使用loader,多用数组

  • exclude: 调整Loader解析的范围,包括某个路径下的文件,不如node_modules

  • include: 调整Loader解析的范围,包括某个路径下的文件


解决浏览器兼容性:babel


转义语法的babel-loader


譬如把const转为浏览器认识的var,虽然现在大部分主流浏览器都认识ES5之后的语法。


npm i babel-loader @babel/preset-env @babel/core

在rules配置:


{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
exclude: /node_modules/
}

转义ES API的babel/polyfill


如果只有babel-loader,浏览器并不能识别出新的API(promise,proxy,includes),如图:


image.png


因此还需要配置一个babel/polyfill,在入口里面:


// npm i @babel/polyfill

entry: ["@babel/polyfill",path.resolve(__dirname, '../src/main.js')],

解析vue的vue-loader


vue-loader: 解析vue
vue-template-compiler: 编译vue模板


npm i vue-loader vue-template-compiler vue-style-loader
npm i vue

在rules跟plugins配置:


const { VueLoaderPlugin } = require('vue-loader') // vue3的引入跟vue2路径不同

rules:{
{
test: /\.vue$/,
use: ['vue-loader']
}
},
plugins:[
...
new VueLoaderPlugin()
...
]

配置完成后,vue文件就可以正常解析了


image.png


解析CSS文件


需要引入的Loader不止一个



  • 引入的基本Loader: style-loader,css-loader,如有less还需要less-loader

  • postcss-loader 添加不同浏览器的css前缀: 解决部分css语法在不同浏览器的写法不同的弊端


modules.exports = {
modules: {
rules: [{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
] // 解析css文件必须的style-loader,css-loader
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env' // 解决css不同浏览器兼容性
],
],
},
}
}, 'less-loader'
]
},
}
}

拆分css


mini-css-extract-plugin: 把css拆分出来用外链的形式引入css文件,然后会在dist生成css文件,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件,
ps: 使用该插件不能重复使用style-loader


··· 
plugins: [
...
new MiniCssExtractPlugin({
filename: devMode ? "[name].css" : "[name].[hash].css",
chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
}),
...
],
module:{
rules: [{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader',
'postcss-loader'
] // 解析css文件必须的style-loader,css-loader
}]
}

打包图片,字体,媒体等文件


file-loader: 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件url),并将文件移动到输出的目录中


url-loader 一般与file-loader搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中



{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/image')],
exclude: /node_modules/
},
{
test: /\.(jpg|png|gif)$/i,
use: [{
loader: 'url-loader',
options: {
limit: 10240, // KB
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/image')],
exclude: /node_modules/
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/icon')],
exclude: /node_modules/
},

使用多线程提升构建速度


js是单线程的工程,在构建工程的过程中,要消耗大量的时间在Loader的转换过程中,为了提升构建速度,这里使用了thread-loader将任务拆分为多个线程去处理。 其原理是把任务分配到各个worker线程中,之前多数人会使用happyPack,但webpack官方使用了thread-loader取代happypack。


...
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', ],
cacheDirectory: true,
}
},
{
loader: 'thread-loader', // 多线程解析模块
options: {
workers: 3, // 开启几个 worker 进程来处理打包,默认是 os.cpus().length - 1
}
}
],
exclude: /node_modules/
}
...

必须使用的插件Plugins


配置plugins必须注意的是,由于我们的模式(mode)区分为development跟production,因此plugins也需要按照实际需要,在config(公用),dev,prod三个配置文件分开加入。


首先先配置公用部分的plugins


公用plugins


清除打包残留文件


每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹


const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')



用外链的形式引入css


当一个html文件里面的css太多,全部把css添加到html中会显得很臃肿,那我们可以用mini-css-extract-plugin 把css拆分成外链引入,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件。


需要留意的是不能跟style-loader同时使用,下面用了hash


const MiniCssExtractPlugin = require("mini-css-extract-plugin");
....
plugins: [
new MiniCssExtractPlugin({
filename: devMode ? "[name].css" : "[name].[hash].css",
chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
}, {
filename: devMode ? "[name].css" : "[name].[hash].less",
chunkFilename: devMode ? "[id].css" : "[id].[hash].less",
}),
]
....

module:{
rules:[{
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader',
'postcss-loader'
]
},
}]
}
....

生产打包后的html


wepback必备的插件之一,上述举例也有提到。
主要是生产打包后的html, 同时由于main.js文件会随机生成新的hash名字,html在引入main.js文件时频繁改名字会很浪费时间,此插件会自动同步改文件名


const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
}),
],

开发环境Dev


热更新: webpack-dev-server


当我们修改文件的内容时,要重新build一次才能看到变化,这样对开发的效率不友好。


需要留意的是,webpack-dev-server只是在开发环境搭建一个服务帮助开发人员提高开发效率,实现了实时更新的功能,在生产环境并不会用到这一个插件.


同时注意在plugins中加入webpack自带的HotModuleReplacementPlugin。


webpack-dev-server这个插件功能十分强大,官方文档有详细的记录
(webpack.docschina.org/configurati…)


npm i webpack-dev-server --save-dev

module.exports = {
devServer: {
// 基本目录
static: {
directory: path.join(__dirname, 'dist'),
},
// 自动压缩代码
compress: true,
port: 9000,
// 自动打开浏览器
open: true,
// 热加载,默认是true
hot: true,
},
plugins: [
new Webpack.HotModuleReplacementPlugin()
]
}

生产环境Prod


由于生产环境对性能的要求跟开发不同,需要引入的插件比较丰富,也更需要对项目构建有更高的熟悉程度


压缩Js文件


webpack mode设置production的时候会自动压缩js代码。原则上不需要引入terser-webpack-plugin进行重复工作。但是optimize-css-assets-webpack-plugin压缩css的同时会破坏原有的js压缩,所以这里我们引入terser-webpack-plugin进行压缩


option很多,使用了dropconsole去除打印的内容


const TerserPlugin = require("terser-webpack-plugin");

...

optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress:{
drop_console: true
}
}
}),
],
},
...


压缩CSS


前面有使用mini-css-extract-plugin的插件去拆分css,但这个插件并不能压缩CSS体积,
使用css-minimizer-webpack-plugin 可以压缩css的体积,但不同的是它是被加入到optimization的minimizer中,跟上述的js压缩插件共同作用


const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
...
optimization: {
minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin({
terserOptions: {
compress:{
drop_console: true
}
}
}),
],
},
...


抽离第三方模块


使用DllReferencePlugin把不需要经常变更的静态文件抽离出来,譬如element-ui组件,这样每次打包的时候就不会再去重新打包选中的静态文件了。


如此一来,当我们修改代码后,webpack只需要打包项目的代码而不需要重复去编译没有发生改变的第三方库。这样当我们没有升级第三方库时,webpack就不会再对这些库进行打包,从而提升项目构建的速度。


首先我们在同级目录下新建文件webpack.dll.config.js,在entry的vendor里面配置了vue跟element-ui。


// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
// 每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')

module.exports = {
mode: 'production',
// 你想要打包的模块的数组
entry: {
vendor: ['vue','element-plus']
},
output: {
path: path.resolve(__dirname, '../public/vendor'), // 打包后文件输出的位置,要在静态资源里面避免被打包转义
filename: '[name].dll.js',
library: 'vendor_library'
// 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: 'vendor_library',
context: __dirname
})
]
};



同时在packake.json里面配置dll的命令


"scripts":{
"dll": "webpack --config build/webpack.dll.config.js",
}

最后在webpack.prod.js 加入配置项


···
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
···

随后执行命令npm run dll
在public/vendor会出现一个vendor.dll.js文件,我们需要在html文件引入这个文件.


<body>
<!-- dll插件的配置路径,注意是打包后的 -->
<script src="./vendor/vendor.dll.js"></script>
<!-- <img src="../assets/image/logo192.png" alt=""> -->
<!-- <img src="../assets/image/loginbg.png" alt=""> -->

<div id="app"></div>
</body>

配置完毕,这样我们在不需要更新第三方包的时候可以不用执行npm run dll,然后直接执行npm run build/dev的时候就会发现构建速度有所提高。


分析打包后的文件


使用webpack-bundle-analyzer,启动项目后会打开一个展示各个包的大小。从图中可以看出来,es6.promise.js这个包



  • stat size: webpack 从入口文件打包递归到的所有模块体积

  • parsed size: 解析与代码压缩后输出到dist目录的体积

  • gzipped size: 开启Gzip之后的体积


image.png


总结


webpack身为前端必备的一项技能,各位在学会基础的配置之后,千万别忘了因地制宜,看看哪些插件更适合自己的项目哦


作者:广州交租公
来源:juejin.cn/post/7277490138518159379
收起阅读 »

亲测实战大屏项目的适配方法

web
背景 想必在平常开发中,遇到要适配各种不同尺寸的屏幕、分辨率,甚至一套代码要兼容PC端到手机端对前端来说是比较头疼和麻烦的事情。 如果要在时间很紧迫的情况下去一套代码,要在大屏、PC和移动端都可运行,且界面不乱,也不超出范围;确实挺头疼。 我这里倒是有一个亲测...
继续阅读 »

背景


想必在平常开发中,遇到要适配各种不同尺寸的屏幕、分辨率,甚至一套代码要兼容PC端到手机端对前端来说是比较头疼和麻烦的事情。

如果要在时间很紧迫的情况下去一套代码,要在大屏、PC和移动端都可运行,且界面不乱,也不超出范围;确实挺头疼。

我这里倒是有一个亲测且实践的一个特别简单的方法,不用换算,设计给多少px前端就写几px,照着UI设计无头脑的搬就可,而且在大屏、电脑pc端、手机移动端,一套代码,就可运行,保证界面不乱,不超出屏幕范围。

这个方法是在我第一次写大屏项目时,用上的,当时时间也是很紧迫,但是项目上线后,对甲方来说很完美无缺了,一次过。

最后发现它在手机端也能看。

如果好奇想知道究竟是什么方法可以这么的丝滑去兼容大屏乃至任何尺寸任何分辨率的设备的小伙伴们,不妨接着往下看。


常见兼容各尺寸的方法


css媒体查询


@media screen

eg:


@media screen and (max-width: 1700px){
//屏幕尺寸小于1700的样式
}

这种方式,太麻烦,如果要求高,就要分的越细,几乎每个尺寸都要兼顾,写进去,实在实在是太太麻烦了。


viewport


<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">

网页头部加入这段代码,网页宽度自动适应屏幕的宽度。

这种方式大屏中不太适合,这种更加适合移动端。

亲测,大屏不太适合。


rem


要换算,而且每次要根据屏幕尺寸的变化而去重新给一个新的根节点字体大小值。

还是麻烦,嗯,也是不能完美适合大屏。


vw和vh


把整个屏幕宽高分成一百分,一个vw就是1%的宽,一个vh就是10的高。

也是麻烦,而且也不能完美适配大屏。


百分百%


同vw,vh一样也不能完美适配大屏。


重点来了


最终我用了缩放,完美解决了大屏适配问题。

它不仅可以在任何尺寸任何分辨率下,去相对完美的展示,在PC甚至移动端也是可以看的。


如何?


jsx页面里:


 useEffect(() => {
//全屏模式
fullScreens();
//首次加载应用,设置一次
setScale();
//调试比例
window.addEventListener('resize', Listen(setScale, 100));
}, []);
//调试比例
// 监听手机窗口变化,重新设置
function Listen(fn, t) {
const delay = t || 100;
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
const context = this;
timer = setTimeout(() => {
timer = null;
fn.apply(context, args);
}, delay);
};
}

// 获取放大缩小比例
function getScale() {
const w = window.innerWidth / 1920;
const h = window.innerHeight / 1080;
return w < h ? w : h;
}

// 设置比例
function setScale() {
const bigMainDom = document.getElementById('bigMain');
bigMainDom && (bigMainDom.style.transform = 'scale(' + getScale() + ') translate(-50%, -50%)');
}

css:


 width:1920px;
height:1080px;
transform-origin: 0 0;
position: absolute;
left: 50%;
top: 50%;

想要哪个页面去做这种兼容,就可在哪个页面去这样设置;

利用查找id的方式找到对应的页面元素(这里只要能查找到元素就行,怎么写随自己),然后给对应的元素添加样式 transform ,去设置比例


getScale方法里面:

先求一下宽高的比例,因为设计稿一般使用的分辨率是1920和1080,所以这里使用这个取比例;

最后返回比例的时候,取比例比较小的,这样可以保证都显示出来;

只不过相对小的尺寸会出现空白,大的尺寸那个占满全屏;

js这里的translate(-50%, -50%) 和css里面的position: absolute; left:50%;top: 50%;transform-origin:0 0可保证画面在最中间


注意点:transform-origin:0 0这个很重要哦,用来设置动画的基点,没有这个,怎么都不会居中正常显示的哦。


否则就会出现以下这几个情况:


image.png


image.png


而:


// 设置比例
function setScale() {
const bigMainDom = document.getElementById('bigMain');
bigMainDom && (bigMainDom.style.transform = 'scale(' + getScale() + ') translate(0, 0)');
}

则会:


image.png
Listen方式,是专门设备窗口大小变化时,就会自动调用setScale方法,我设置的是每100秒监听一次,相当于做了一个自适应的处理;

setScale在页面加载时调用了一次,然后也加了一个resize方法;

宽度、长度也设置一样,我写的是1920 * 1080


出现问题,两边有空白?


屏幕没有铺满,左右有空白,这是因为设备上面有地址栏,而导致的;

因为地址栏占了一定的高度,但是设计稿把这块高度没有算进去,因此,实际的整体高度会比设计稿的大,而宽度是一样的,为了能够全部展示,就以大的为主,因此,宽度相对比就会出现空白啦;

image.png


解决


设置成全屏就好啦;


image.png

完美;

因为我的笔记本电脑分辨率高,所以,电脑默认缩放设置的是125%,而不是100%,默认设置为100%整体字体什么都都会变小,观看都不高,所以电脑默认设置为125%啦, 无论电脑默认是多少百分比,只要设置成全屏,都是全部铺满的。


image.png


设置全屏的代码


进入全屏、退出全屏、当前是否全屏模式的代码:

commonMethod.js:


//进入全屏
static fullScreen = (element) => {
if (window.ActiveXObject) {
var WsShell = new ActiveXObject('WScript.Shell');
WsShell.SendKeys('{F11}');
}
//HTML W3C 提议
else if (element.requestFullScreen) {
element.requestFullScreen();
}
//IE11
else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
// Webkit (works in Safari5.1 and Chrome 15)
else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
// setTimeout(()=>{
// console.log(isFullScreen());
// },100)
}
// Firefox (works in nightly)
else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
}
};
//退出全屏
static fullExit = () => {
//IE ActiveXObject
if (window.ActiveXObject) {
var WsShell = new ActiveXObject('WScript.Shell');
WsShell.SendKeys('{F11}');
}
//HTML5 W3C 提议
else if (element.requestFullScreen) {
document.exitFullscreen();
}
//IE 11
else if (element.msRequestFullscreen) {
document.msExitFullscreen();
}
// Webkit (works in Safari5.1 and Chrome 15)
else if (element.webkitRequestFullScreen) {
document.webkitCancelFullScreen();
}
// Firefox (works in nightly)
else if (element.mozRequestFullScreen) {
document.mozCancelFullScreen();
}
};
//当前是否全屏模式
static isFullScreen = () =>{
return document.fullScreen ||
document.mozFullScreen ||
document.webkitIsFullScreen ||
document.msFullscreenElement;
}

这是react项目页面里引用的封装好的方法:


 //全屏
const fullScreens = () => {
if (!CommonMethod.isFullScreen()) {
Modal.info({
content: '开启全屏模式,体验更佳',
okText: '确定',
maskClosable: true,
mask: false,
// centered:true,
width: '200px',
height: '100px',
className: 'fullScreenModal',
onOk() {
CommonMethod.fullScreen(document.getElementsByTagName('html')[0]);
},
onCancel() {
CommonMethod.fullScreen(document.getElementsByTagName('html')[0]);
},
});
}
};

然后大屏页面第一次加载时,调用一次全屏模式方法即可,fullScreens


useEffect(() => {
//全屏模式
fullScreens();
}


注意点


如果在使用transform,缩放的地方,就不能使用其他定位,你会发现使用其他定位(绝对定位、相对定位、固定定位等)会失效;

使用antd里的模态框,就会失效哦。


完结


设置缩放的方法可以试试,并且在不同设备尺寸和分辨率下,大屏、pc、手机端都可以试一波。

回头我把自己上面那个项目在不同设备下运行的截图整理一下,补发出来。

对移动端有点要求的其实还会需要重新写一套代码的,我上面这种方式移动端只是样式没有乱也没有超出屏幕而已,毕竟这个只是专门给大屏做的代码,那么大的尺寸怎么的在手机端看也不会很符合的


作者:浅唱_那一缕阳光
来源:juejin.cn/post/7232229178278903865
收起阅读 »

Vue 2 最后之舞“天鹅挽歌”

web
大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本&nb...
继续阅读 »

大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。

圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本 Vue@2.7.16 正式发布,版本代号“Swan Song”(天鹅挽歌)。

01-swan.png

地球人都知道,去年 Vue 2 官宣了最后一个次版本 Vue@2.7.x,如今 Vue 2 官宣最后一个补丁版本 Vue@2.7.16,也算是为 Vue 2 的最后之舞画上惊叹号!此去经年,再无 Vue 2。

虽然但是,前端踏足之地,Vue 亦生生不息,此乃“Vue 之意志”。故本期《前端翻译计划》一起来重温 Vue@2.7 的官方博客,为 Vue 生态的未来规划未雨绸缪。

00-wall.png

今天我们十分鸡冻地官宣,Vue 2.7(版本代号“火影忍者”)正式发布!

尽管 Vue 3 现在已经是默认版本,但我们特别理解,由于依赖兼容性、浏览器支持的要求或带宽不足无法升级,仍然有一大坨用户被迫滞留在 Vue 2。在 Vue 2.7 中,我们向后移植了 Vue 3 中的某些特色功能,Vue 2 爱好者也可以有福同享。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Vue 2.7 "Naruto" Released

向后移植的功能

  • 组合式 API
  • SFC 
收起阅读 »

前端部署真的不简单

web
现在大部分的中小型公司部署前端代码都是比较简单的,主要步骤如下: 首先,通过脚手架提供的命令npm run build打包前端代码,生成dist文件夹; 最后,将dist文件夹丢给后台开发人员放在他们的工程里面,随后台一起部署;现在普遍是前后端分开部署,因此,...
继续阅读 »

现在大部分的中小型公司部署前端代码都是比较简单的,主要步骤如下:


首先,通过脚手架提供的命令npm run build打包前端代码,生成dist文件夹;


最后,将dist文件夹丢给后台开发人员放在他们的工程里面,随后台一起部署;现在普遍是前后端分开部署,因此,利用nginx起一个web服务器,将dist文件夹放到指定的路径下,配置下nginx访问路径,对于请求接口使用proxy_pass进行转发,解决跨域的问题。


更加高端一点的操作,是利用CI/CD + Docker进行自动化部署。


但是,你是否真的想过前端部署真的就这么简单吗?


这其实是一个非常严肃且复杂的问题,因为这关系到线上生产环境的稳定


有一天,从自知乎上看到一篇张云龙大佬在2014年写的文章,非常有启发,即使这篇文章距离现在有快10年了,但是其中的思想仍然熠熠生辉。


因为写的真的是太好了,为了让更多的人看到,所以大部分内容直接就照搬过来,为了让自己加深印象。如果想看原文,原文网址在这里。


那让我们从原始的前端开发讲起。


下图是一个 index.html 页面和它的样式文件 a.css,无需编译,本地预览,丢到服务器,等待用户访问。


image.png


哇,前端这么简单,门槛好低啊。这也是前端有太多人涌入进来的原因。


接着,我们访问页面,看到效果,再查看一下网络请求,200!不错,太完美了!


image.png


那么,研发完成。。。。了么?


等等,这还没完呢!


对于像 BAT 这种公司来说,那些变态的访问量和性能指标,将会让前端一点也不好玩。


看看那个 a.css 的请求,如果每次用户访问页面都要加载,是不是很影响性能,很浪费带宽啊,我们希望最好这样:


image.png


利用304,让浏览器使用本地缓存。


但,这样也就够了吗?


不够!


304叫协商缓存,这玩意还是要和服务器通信一次,我们的优化级别是变态级,所以必须彻底灭掉这个请求,要变成这样:


image.png


强制浏览器使用本地缓存(cache-control/expires),不要和服务器通信。


好了,请求方面的优化已经达到变态级别,那问题来了:你都不让浏览器发资源请求了,这缓存咋更新


很好,相信有人想到了办法:通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源


像这样:


image.png


下次上线,把链接地址改成新的版本,这就更新资源了。


问题解决了么?当然没有,思考这种情况:


image.png


页面引用了3个 css 文件,而某次上线只改了其中的a.css,如果所有链接都更新版本,就会导致b.cssc.css的缓存也失效,那岂不是又有浪费了?


不难发现,要解决这种问题,必须让url的修改与文件内容关联,也就是说,只有文件内容变化,才会导致相应url的变更,从而实现文件级别的精确缓存控制


什么东西与文件内容相关呢?


我们会很自然的联想到利用数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。


OK,那我们把 url 改成带摘要信息的:


image.png


这回再有文件修改,就只更新那个文件对应的 url 了,想到这里貌似很完美了。你觉得这就够了么?


图样图森破!


现代互联网企业,为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径:


image.png


好了,当我要更新静态资源的时候,同时也会更新 html 中的引用吧,就好像这样:


image.png


这次发布,同时改了页面结构和样式,也更新了静态资源对应的url地址。现在重点来了,现在要发布代码上线,亲爱的前端研发同学,你来告诉我,咱们是先上线页面,还是先上线静态资源



这里的静态资源不仅仅包括css文件,也包括图片,以及不怎么经常变的资源。




  1. 先部署动态页面,再部署静态资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。

  2. 先部署静态资源,再部署动态页面:在部署时间间隔之内,有旧版本资源本地缓存的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地缓存,这种情况下页面展现正常;但没有本地缓存或者缓存过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。


好的,上面一坨分析想说的就是:先部署谁都不成!都会导致部署过程中发生页面错乱的问题。


所以,访问量不大的项目,可以让研发同学苦逼一把,等到半夜偷偷上线,先上静态资源,再部署页面,看起来问题少一些。这也是很多公司的部署方案。


但是,大公司超变态,没有这样的绝对低峰期,只有相对低峰期。


所以,为了稳定的服务,还得继续追求极致啊!


这个奇葩问题,起源于资源的 覆盖式发布,用待发布资源覆盖已发布资源,就有这种问题。


解决它也好办,就是实现 非覆盖式发布


image.png


看上图,用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。


因为很多前端开发同学不怎么接触部署,对灰度部署不太熟悉,下面将介绍下什么是灰度部署。


软件开发一般都是一个版本一个版本的迭代。新版本上线前都会经过测试,但就算这样,也不能保证上线了不出问题。


所以,在公司里上线新版本代码一般都是通过灰度系统。灰度系统可以把流量划分成多份,一份走新版本代码,一份走老版本代码。


image.png


而且灰度系统支持设置流量的比例,比如可以把走新版本代码的流程设置为 5%,没啥问题了再放到 10%,50%,最后放到 100% 全量。这样可以把出现问题的影响降到最低。


不然一上来就全量,万一出了线上问题,那就是大事故。


另外,灰度系统不止这一个用途,比如,产品不确定某些改动是不是有效的,就要做 AB 实验,也就是要把流量分成两份,一份走 A 版本代码,一份走 B 版本代码。


那这样的灰度系统是怎么实现的呢?其实很多都是用 nginx 实现的。


nginx 是一个反向代理的服务,用户请求发给它,由它转发给具体的应用服务器。


image.png


它的过程如下图所示:


image.png


首先,需要对流量进行染色,即对这个用户进行标注,让这个用户访问服务1,另外的用户访问服务2。染色的方式有很多,可以通过cookie来完成。不同的用户携带的cookie是不同的。第一染色的时候,所有的用户都访问服务1。


然后,第二次访问的时候,nginx根据用户携带的cookie进行转发到不同的服务,这样就完成了灰度访问。


好了,灰度部署就介绍到这里,回到原文讲的先全量部署静态资源,再灰度部署页面,这是什么意思呢?


首先,部署静态资源的时候,不要删除原来的静态资源,而是把新的静态资源发复制过去,因为文件名用摘要算法重命名的,所以不会发生重名的问题。


其次,灰度部署动态页面,也就是一部分用户访问老的页面,一部分用户访问新的页面。访问老页面的用户请求的还是老资源,直接使用缓存。访问新页面的用户访问新资源,此时新资源已经部署完成,所以不会访问老的资源,导致页面出现错误。


最后,根据访问情况,利用灰度系统,逐渐把访问老页面的用户过渡到访问新页面上。


所以,大公司的静态资源优化方案,基本上要实现这么几个东西:



  1. 配置超长时间的本地缓存:节省带宽,提高性能

  2. 采用内容摘要作为缓存更新依据:精确的缓存控制

  3. 静态资源CDN部署:优化网络请求

  4. 更资源发布路径实现非覆盖式发布:平滑升级


全套做下来,就是相对比较完整的静态资源缓存控制方案了,而且,还要注意的是,静态资源的缓存控制要求在前端所有静态资源加载的位置都要做这样的处理


是的,所有!


什么js、css自不必说,还要包括js、css文件中引用的资源路径,由于涉及到摘要信息,引用资源的摘要信息也会引起引用文件本身的内容改变,从而形成级联的摘要变化,大概就是:


image.png


到这里本文结束了,我们已经了解了前端部署中关于静态资源缓存要面临的优化和部署问题,新的问题又来了:这™让工程师怎么写码啊!!!


这又会扯出一堆有关模块化开发、资源加载、请求合并、前端框架等等的工程问题。


妈妈,我再也不玩前端了。。。。


作者:小p
来源:juejin.cn/post/7316202725330796571
收起阅读 »

封装一个丝滑的评分组件

web
效果 实现 找到喜欢的图标 http://www.iconfont.cn/ 打开 iconfont,随便搜个点赞,仅需一个点赞即可,节省内存,另一个用CSS完成 下载解压 取出如下文件,在项目入口导入 iconfont.css,或者统一放入一个文件导入也...
继续阅读 »

效果


score.gif


实现


找到喜欢的图标


http://www.iconfont.cn/


打开 iconfont,随便搜个点赞,仅需一个点赞即可,节省内存,另一个用CSS完成


image.png


下载解压


image.png


取出如下文件,在项目入口导入 iconfont.css,或者统一放入一个文件导入也行


image.png


编写组件


一个点赞,就叫Like


一个点踩,就叫Hate


Hate点踩组件


先写属性,就几个,不用注释也明白


分别是



  • 绑定值 modelValue

  • 默认颜色 defaultColor

  • 激活颜色 activeColor

  • 大小 size


image.png


HTML给个容器,再加个固定格式(xxx-container)的容器类名,防止样式穿透


再把iconfont样式加上,图标就出来了


这些用VSCode自定义代码片段都能自动生成


image.png


现在是这样的图标,也就是点赞


image.png


那么怎样让它变成点踩手势呢?答案是rotate旋转


加上下图类名有用吗? 答案是没用,他并不会旋转


image.png


为什么呢?因为i标签是行盒模型,必须变成块盒才行


方式有很多,不过我就爱flex,优点很多,这里就不赘述了


image.png


这不就转起来了吗


image.png


动效


加个鼠标移入变色效果,考虑到一会还有另一个点赞组件,那就写个通用的sass


接收一个激活颜色和默认颜色参数


image.png


导入并使用,下面用了个v-bind绑定当前被点击的颜色,下面来实现逻辑


image.png


上面接收了一个modelValue,类型是布尔值,当他为真时就把颜色改为激活颜色


于是就理所当然的使用计算属性


image.png


事件


当被点击或hover时,就激活图标颜色,现在还差点击


点击事件要做 3 件事



  1. 改变父组件的值

  2. 改变颜色

  3. 实现开头的丝滑动画


image.png


这里用一个showAnimation变量控制动画展示


再改变父组件的值,父组件的值一变,自己的颜色也会跟着变


还差个动画,只要让动画在其中一段不停反复横跳,即可实现国际友好手势


image.png


但这样有问题,只有第一次触发动画才有动效,后续需要改变showAnimation的值


那么怎么知道动画什么时候结束呢?


答案是事件onanimationend,只要在这个事件把动画关掉,点击时开启即可


image.png


ok,实现最主要的组件了,另一个点赞就是复制改东改西,大家都会


score.gif


组合组件


接下来需要把点赞和点踩组合一起,那就叫Gr0up


需要实现逻辑



  1. 根据父组件的值,动态展示

  2. 点击时传递一个事件,告诉父组件,究竟是点赞还是点踩

  3. 提供一个值,锁定按钮,因为点完之后一般都不能反悔

  4. 提供样式设置,传递到子组件


类型


传递一个固定类型的值,才能分辨是什么操作


image.png


null就是初始状态,无操作,另外俩见名知义


那么现在需要编写自身的状态


image.png



  • wasChoose是否有按钮被选中

  • disabled当被选中,同时父组件完成(done),就禁用


样式


image.png


初始化


建议写代码都提供一个入口,后面有空出一期如何像写诗一般写代码


image.png


事件


当被点击时,改变两个状态,没什么好说的,完成


image.png


测试


test.gif



源码: gitee.com/cjl2385/dig…



作者:寅时码
来源:juejin.cn/post/7316321509857034280
收起阅读 »