注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

为什么同一表情'🧔‍♂️'.length==5但'🧔‍♂'.length==4?本文带你深入理解 String Unicode UTF8 UTF16

web
背景 为什么同样是男人,但有的男人'🧔‍♂️'.length === 5,有的男人'🧔‍♂'.length === 4呢? 这二者都是JS中的字符串,要理解本质原因,你需要明白JS中字符串的本质,你需要理解 String Unicode UTF8 UTF16 ...
继续阅读 »

背景


为什么同样是男人,但有的男人'🧔‍♂️'.length === 5,有的男人'🧔‍♂'.length === 4呢?


这二者都是JS中的字符串,要理解本质原因,你需要明白JS中字符串的本质,你需要理解 String Unicode UTF8 UTF16 的关系。本文,深入二进制,带你理解它!


从 ASCII 说起


各位对这张 ASCII 表一定不陌生:


image.png


因为计算机只能存储0和1,如果要让计算机存储字符串,还是需要把字符串转成二进制来存。ASCII就是一直延续至今的一种映射关系:把8位二进制(首位为0)映射到了128个字符上。


从多语言到Unicode


但是世界上不止有英语和数字,还有各种各样的语言,计算机也应该能正确的存储、展示它们。


这时候,ASCII的128个字符,就需要被扩充。有诸多扩充方案,但思路都是一致的:把一个语言符号映射到一个编号上。有多少个语言符号,就有多少个编号。


至今,Unicode 已经成为全球标准。



The Unicode Consortium is the standards body for the internationalization of software and services. Deployed on more than 20 billion devices around the world, Unicode also provides the solution for internationalization and the architecture to support localization.


Unicode 联盟是软件和服务国际化的标准机构。 Unicode 部署在全球超过 200 亿台设备上,还提供国际化解决方案和支持本地化的架构。



Unicode是在ASCII的128个字符上扩展出来的。


例如,英文「z」的Unicode码是7A(即十进制的122,跟ASCII一致)。


Unicode中80(即128号)字符是€,这是ASCII的128个字符(0-127)的后一个字符。


汉字「啊」的Unicode码是554A


Emoji「🤔」的Unicode码是1F914


从Unicode到Emoji


随着时代发展,人们可以用手机发短信聊天了,常常需要发送表情,于是有人发明了Emoji。Emoji其实也是一种语言符号,所以Unicode也收录了进来。


image.png


Unicode一共有多少


现在,Unicode已经越来越多了,它的编码共计111万个!(有实际含义的编码并没这么多)


目前的Unicode字符分为17组编排,每组称为平面(Plane),而每平面拥有65536(即2^4^4=2^16)个代码点。目前只用了少数平面。


平面始末字符值中文名称英文名称
0号平面U+0000 - U+FFFF基本多文种平面Basic Multilingual Plane,简称BMP
1号平面U+10000 - U+1FFFF多文种补充平面Supplementary Multilingual Plane,简称SMP
2号平面U+20000 - U+2FFFF表意文字补充平面Supplementary Ideographic Plane,简称SIP
3号平面U+30000 - U+3FFFF表意文字第三平面Tertiary Ideographic Plane,简称TIP
4号平面 至 13号平面U+40000 - U+DFFFF(尚未使用)
14号平面U+E0000 - U+EFFFF特别用途补充平面Supplementary Special-purpose Plane,简称SSP
15号平面U+F0000 - U+FFFFF保留作为私人使用区(A区)Private Use Area-A,简称PUA-A
16号平面U+100000 - U+10FFFF保留作为私人使用区(B区)Private Use Area-B,简称PUA-B

以前只有ASCII的时候,共128个字符,我们统一用8个二进制位(因为log(2)128=7,取整得8),就一定能存储一个字符。


现在,Unicode有16*65536=1048576个字符,难道必须用log(2)1048576=20 向上取整24位(3个字节)来表示一个字符了吗?


那样的话,字母z就是00000000 00000000 01111010了,而之前用ASCII的时候,我们用01111010就可以表示字母z。也就是说,同样一份纯英文文件,换成Unicode后,扩大了3倍!1GB变3GB。而且大部分位都是0。这太糟糕了!


因此,Unicode只是语言符号和一些自然数的映射,不能直接用它做存储。


UTF8如何解决「文本大小变3倍问题」


答案就是:「可变长编码」,之前我在文章《太卷了!开发象棋,为了减少40%存储空间,我学了下Huffman Coding》提到过。


使用「可变长编码」,每个字符不一定都要用统一的长度来表示,针对常见的字符,我们用8个二进制位,不常见的字符,我们用16个二进制位,更不常见的字符,我们用24个二进制位。


这样,能够减少大部分场景的文件体积。这也是哈夫曼编码的思想。


要设计一套高效的「可变长编码」,你必须满足一个条件:它是「前缀码」。即通过前缀,我就能知道这个字符要占用多少字节。


而UTF8,就是一种「可变长编码」。


UTF8的本质



  1. UTF8可以把2^21=2097152个数字,映射到1-4个字节(这个范围能够覆盖所有Unicode)。

  2. UTF8完全兼容ASCII。也就是说,在UTF8出现之前的所有电脑上存储的老的ASCII文件,天然可以被UTF8解码。


具体映射方法:



  • 0-127,用0xxxxxxx表示(共7个x)

  • 128-2^11-1,用110xxxxx 10xxxxxx表示(共11个x)

  • 2^11-2^16-1,用1110xxxx 10xxxxxx 10xxxxxx表示(共16个x)

  • 2^16-2^21-1,用11110xxx 10xxxxxx 10xxxxxx 10xxxxxx表示(共21个x)


不得不承认,UTF8确实有冗余,还有压缩空间。但考虑到存储不值钱,而且考虑到解析效率,它已经是最优解了。


UTF16的本质


回到本文开头的问题,为什么'🧔‍♂️'.length === 5,但'🧔‍♂'.length === 4呢?


你需要知道在JS中,字符串使用了UTF16编码(其实本来是UCS-2,UTF16是UCS-2的扩展)。



为什么JS的字符串不用UTF8?


因为JS诞生(1995)时,UTF8还没出现(1996)。



UTF16不如UTF8优秀,因为它用16个二进制位或32个二进制位映射一个Unicode。这就导致:



  1. 它涉及到大端、小端这种字节序问题。

  2. 它不兼容ASCII,很多老的ASCII文件都不能用了。


UTF16的具体映射方法:


16进制编码范围(Unicode)UTF-16表示方法(二进制)10进制码范围字节数量
U+0000 - U+FFFFxxxxxxxx xxxxxxxx (一共16个x)0-655352
U+10000 - U+10FFFF110110xx xxxxxxxx 110111xx xxxxxxxx (一共20个x)65536-11141114


细心的你有没有发现个Bug?UTF16不是前缀码? 遇到110110xx xxxxxxxx 110111xx xxxxxxxx,怎么判断它是1个大的Unicode字符、还是2个连续的小的Unicode字符呢?


答案:其实,在U+0000 - U+FFFF范围内,110110xx xxxxxxxx110111xx xxxxxxxx都不是可见字符。也就是说,在UTF16中,遇到110110一定是4字节UTF16的前2字节的前缀,遇到110111一定是4字节UTF16的后2字节的前缀,其它情况,一定是2字节UTF16。这样,通过损失了部分可表述字符,UTF16也成为了「前缀码」。



JS中的字符串


在JS中,'🧔‍♂️'.length算的就是这个字符的UTF16占用了多少个字节再除以2。


我开发了个工具,用于解析字符串,把它的UTF8二进制和UTF16二进制都展示了出来。


工具地址:tool.hullqin.cn/string-pars…


我把2个男人,都放进去,检查一下他们的Unicode码:


image.png


image.png


发现区别了吗?


长度为4的,是1F9D4 200D 2642;长度为5的,是1F9D4 200D 2642 FE0F


都是一个Emoji,但是它对应了多个Unicode。这是因为200D这个零宽连字符,一些复杂的emoji,就是通过200D,把不同的简单的emoji组合起来,展示的。当然不是任意都能组合,需要你字体中定义了那个组合才可以。


标题中的Emoji,叫man: beard,是胡子和男人的组合。


末尾的FE0F变体选择符,当一个字符一定是emoji而非text时,它其实是可有可无的。


于是,就有的'🧔‍♂️'长,有的'🧔‍♂'短了。



作者:HullQin
来源:juejin.cn/post/7165859792861265928
收起阅读 »

URL刺客现身,竟另有妙用!

web
工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。 先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。 刺客介绍 1. iOS WKWebview 刺客 此类刺客手段单一,只会影响 iOS WK...
继续阅读 »

工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。


先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。


刺客介绍


1. iOS WKWebview 刺客


此类刺客手段单一,只会影响 iOS WKWebview



  • 空格


运营人员由于在通讯工具中复制粘贴,导致前面多了一个空格,没有仔细检查,直接录入了后台管理系统。



  • 中文


运营人员为了方便自身统计,直接在url中加入中文,录入了后台管理系统。


现象均为打开一个空白页,常见的处理手段如下:



  • 将参数里的中文URIEncode

  • 去掉首尾空格


const safeUrl = (url: string) => {
const index = url.indexOf('?');

if (index === -1) return url.trim();

// 这行可以用任意解析参数方法替代,仅代表要拿到参数,不考虑兼容性的简单写法
const params = new URLSearchParams(url.substring(index));
const paramStr = Object.keys(params)
.map((key: string) => {
return `${key}=${encodeURIComponent(params[key])}`;
})
.join('&');

const formatUrl = url.substring(0, index + 1) + paramStr;

return formatUrl.trim();
};

可以看到虽然这里提出了一个 safeUrl 方法,但如果业务中大量使用 window.location.href , window.location.replace, 之类的方法进行跳转,替换起来会比较繁琐.


再比如在 Hybrid App 的场景中,虽然都是跳转,打开新的 webview ,还是在本页面跳转会是不同的实现,所以在业务内提取一个公共的跳转方法更有利于健壮性和拓展性。


值得注意的是,如果链接上的中文可能是用于统计的,在上报打点时,应该将其值(前端/服务端处理均可)进行 URIDecode,否则运营人员会在后台看到一串串莫名其妙的 %XX ,会非常崩溃(别问我怎么知道的,可能只是伤害过太多运营)


2. 格式刺客


格式刺客指的是,不管何种原因,不知何种场景,就是不小心配错了,打错了,漏打了等。


比如:https://www.baidu.com 就被打成了 htps://www.baidu.com、www.baidu.com 等。


// 检查URL格式是否正确
function isValidUrl(url: string): boolean {
const urlPattern = new RegExp(
"^(https?:\/\/)?" + // 协议
"(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})" + // 域名
"(:[0-9]{1,5})?" + // 端口号
"(\/.*)?$", // 路径
"i"
);
return urlPattern.test(url);
}

以上是一个很基础的判断,但是实际的应用场景中,有可能会需要填写相对路径,或者自定义的 scheme ,比如 wx:// ,所以检验的宽松度可以自行把握。


在校验到 url 配置可能存在问题时,可以上报到 sentry 或者其他异常监控平台,这样就可以比用户上报客服更早的发现潜在问题,避免长时间的运营事故。


3. 异形刺客


这种刺客在视觉上让人无法察觉,只有在跳转后才会让人疑惑不已。他也是最近被产品同学发现的,以下是当时的现场截图:



一段平平无奇的文本,跟着一段链接,视觉上无任何异常。


经过对跳转后的地址进行分析,发现了前面居然有一个这样的字符%E2%80%8B,好奇的在控制台中进行了尝试。




一个好家伙,这是什么,两个单引号吗?并不是,对比了很常用的 '%2B' ,单引号是自带的,那么我到底看到了什么,魔鬼嘛~


在进行了一番检索后知道了这种字符被称为零宽空格,他还有以下兄弟:



  • \u202b-\u202f

  • \ufeff

  • \u202a-\u202e


具体含义可以看看参考资料,这一类字符完全看不见,但是却对程序的运行产生了恶劣的影响。


可以使用这个语句去掉


str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

刺客的妙用


头一天还被刺客气的瑟瑟发抖。第二天居然发现刺客的妙用。


场景:




  • 产品要求在微信环境隐藏标题




我方前端工程师:



  • 大手一挥,发功完毕,准备收工


document.title = '';

测试:




  • 来看看,页面A标题隐藏不了




我方前端工程师:


啊?怎么回事,本地调试还是好的,发上去就不行了,为什么页面A不可以,另外一个页面B只是参数变了变就行。


架构师出手:


页面A包含了开放标签,导致设置空Title失效,空,猛然想起了刺客,快用起来!


function setTitle(title: string) {
if (title) {
document.title = title;
} else {
document.title = decodeURIComponent('%E2%80%8B');
}
}

果然有效,成功解决了一个疑难杂症,猜测是微信里有不允许设置标题为空的机制,会在某些标签存在的时候被触发。(以上场景在 Android 微信 Webview 中可复现)


小结


以上只是工作中碰到 url 异常的部分场景和处理方案,如果小伙伴们也有类似的经历,可以在评论区中分享,帮助大家避坑,感谢朋友们的阅读,笔芯~


参考资料:


零宽字符 - 掘金LvLin


什么零宽度字符,以及零宽度字符在JavaScript中的应用 - 掘金whosmeya


作者:windyrain
来源:juejin.cn/post/7225133152490094651
收起阅读 »

Linux当遇到kill -9杀不掉的进程怎么办?

web
前言 在Linux中,我们经常使用kill或者kill -9来杀死特定的进程,但是有些时候,这些方法可能无法终止某些进程。本文将详细解释为什么会出现这种情况,以及如何处理这种问题。 无法被杀死的进程: 首先,我们来理解一下为什么有些进程无法被杀死。通常,这是因...
继续阅读 »

前言


在Linux中,我们经常使用kill或者kill -9来杀死特定的进程,但是有些时候,这些方法可能无法终止某些进程。本文将详细解释为什么会出现这种情况,以及如何处理这种问题。


无法被杀死的进程:


首先,我们来理解一下为什么有些进程无法被杀死。通常,这是因为这些进程处于以下两种状态之一:


僵尸进程(Zombie Process):


当一个进程已经完成了它的运行,但是其父进程还没有读取到它的结束状态,那么这个进程就会成为僵尸进程。僵尸进程实际上已经结束了,所以你无法使用kill命令来杀掉它。



内核态进程:


如果一个进程正在执行某些内核级别的操作(即进程处在内核态),那么这个进程可能无法接收到kill命令发送的信号。


查找和处理僵尸进程:


如果你怀疑有僵尸进程存在,你可以使用以下命令来查找所有的僵尸进程:


ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'

这个命令实际上是由两个命令通过管道(|)连接起来的。管道在Linux中的作用是将前一个命令的输出作为后一个命令的输入。命令的两部分是 ps -A -ostat,ppid,pid,cmd 和 grep -e '^[Zz]'。



  • ps -A -ostat,ppid,pid,cmd:这是ps命令,用来显示系统中的进程信息。

    • -A:这个选项告诉ps命令显示系统中的所有进程。

    • -o:这个选项允许你定义你想查看的输出格式。在这里,你定义的输出格式是stat,ppid,pid,cmd。这会让ps命令输出每个进程的状态(stat)、父进程ID(ppid)、进程ID(pid)以及进程运行的命令(cmd)。



  • grep -e '^[Zz]':这是grep命令,用来在输入中查找匹配特定模式的文本行。

    • -e:这个选项告诉grep命令接下来的参数是一个正则表达式。

    • '^[Zz]':这是你要查找的正则表达式。^符号表示行的开始,[Zz]表示匹配字符“Z”或者“z”。因此,这个正则表达式会匹配所有以“Z”或者“z”开头的行。在ps命令的输出中,状态为“Z”或者“z”的进程是僵尸进程。




因为僵尸进程已经结束了,所以你无法直接杀掉它。但是,你可以试图杀掉这些僵尸进程的父进程。杀掉父进程之后,僵尸进程就会被init进程(进程ID为1)接管,然后被清理掉。


你可以使用以下命令来杀掉父进程:


kill -HUP [父进程的PID]

请注意,在杀掉父进程之前,你需要确定这样做不会影响到系统的其他部分。另外,这个方法并不保证能够清理掉所有的僵尸进程。


查找和处理内核态进程:


如果一个进程处在内核态,那么这个进程可能无法接收到kill命令发送的信号。在这种情况下,你需要首先找到这个进程的父进程,然后试图杀掉父进程。你可以使用以下命令来查找进程的父进程:


cat /proc/[PID]/status | grep PPid

这个命令会输出进程的父进程的ID,由两个独立的命令组成,通过管道(|)连接起来。我会分别解释这两个命令,然后再解释整个命令:



  • cat /proc/[PID]/status :

    • 这是一个cat命令,用于显示文件的内容。在这个命令中,它用于显示一个特殊的文件/proc/[PID]/status。

    • /proc是一个特殊的目录,它是Linux内核和用户空间进行交互的一种方式。在/proc目录中,每个正在运行的进程都有一个与其PID对应的子目录。每个子目录中都包含了关于这个进程的各种信息。

    • /proc/[PID]/status文件包含了关于指定PID的进程的各种状态信息,包括进程状态、内存使用情况、父进程ID等等;



  • grep PPid :

  • 这是一个grep命令,用于在输入中查找匹配特定模式的文本行。在这个命令中,它用于查找包含PPid的行。在/proc/[PID]/status文件中,PPid一行包含了这个进程的父进程的PID;
    然后,你可以使用以下命令来杀掉父进程:


kill -9 [父进程的PID]

同样,你需要在杀掉父进程之前确定这样做不会影响到系统的其他部分。另外,这个方法并不保证能够杀掉所有的内核态进程。


结论:


在Linux系统中,处理无法被杀死的进程可以是一项挑战,尤其是当你无法确定进程状态或者无法影响父进程的时候。以上的方法并不保证能够解决所有问题。如果你尝试了所有的方法,但问题仍然存在,或者你不确定如何进行,那么你可能需要联系系统管理员,或者寻求专业的技术支持。


总的来说,处理无法被杀死的进程需要对Linux的进程管理有深入的理解,以及足够的耐心和谨慎。希望这篇文章能够帮助你更好地理解这个问题,以及如何解决这个问题。


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

h5调用手机摄像头踩坑

web
1. 背景 一般业务也很少接触摄像头,有也是现成的工具库扫个二维码。难得用一次,记录下踩坑。 2.调用摄像头的方法 2.1. input <!-- 调用相机 --> <input type="file" accept="image/*" ca...
继续阅读 »

1. 背景


一般业务也很少接触摄像头,有也是现成的工具库扫个二维码。难得用一次,记录下踩坑。


2.调用摄像头的方法


2.1. input


<!-- 调用相机 -->
<input type="file" accept="image/*" capture="camera">
<!-- 调用摄像机 -->
<input type="file" accept="video/*" capture="camcorder">
<!-- 调用录音机 -->
<input type="file" accept="audio/*" capture="microphone">

这个就不用多说了,缺点就是没办法自定义界面,它是调用的系统原生相机界面。


2.2. mediaDevices


由于我需要自定义界面,就像下面这样:
image.png


所以我选择了这个方案,这个api使用起来其实很简单:


<!-- 创建一个video标签用来播放摄像头的视屏流 -->
<video id="video" autoplay="autoplay" muted width="200px" height="200px"></video>
<button onclick="getMedia()">开启摄像头</button>

async getMedia() {
// 获取设备媒体的设置,通常就video和audio
const constraints = {
// video配置,具体配置可以看看mdn
video: {
height: 200,
wdith: 200,
},
// 关闭音频
audio: false
};
this.video = document.getElementById("video");
// 使用getUserMedia获取媒体流
// 媒体流赋值给srcObject
this.video.srcObject = await window.navigator.mediaDevices.getUserMedia(constraints);
// 直接播放就行了
this.video.play();
}

image.png
可以看到这个效果。


这个api的配置可以参考MDN


// 截图拍照
takePhoto() {
const video = document.getElementById("video");
// 借助canvas绘制视频的一帧
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext('2d');
ctx.drawImage(this.video, 0, 0, 300, 300);
},
// 停止
stopMedia() {
// 获取媒体流
const stream = this.video.srcObject;
const tracks = stream.getTracks();
// 停止所有轨道
tracks.forEach(function (track) {
track.stop();
})
this.video.srcObject = null;
}

3.坑


如果你复制我的代码,在localhost上肯定能运行,但是你想在手机上试试的时候就会发现很多问题。


3.1. 需要https


由于浏览器的安全设置,除了localhosthttps连接,你都没办法获取到navigator.mediaDevices,打印出来是undefined。如果要在手机上测试,你要么用内网穿透代理一个https,要么部署在https域名的服务器上测试。


3.2. 设置前后摄像头


默认是使用user设备,也就是前摄像头,想要使用后摄像头也是有配置的,


async getMedia() {
// ...
let constraints = {
video: {
height: 200,
wdith: 200,
// environment设备就是后置
facingMode: { exact: "environment" },
},
audio: false
};
// ...
}

3.3. 设置显示区域大小


我的需求是铺满整个设备,所以我想当然的直接把video样式宽高设置成容器大小:


#video {
width: 100%;
height: 100%;
}

async getMedia() {
// ....
// 将宽高设置成容器大小
const pageSize = document.querySelector('.page').getBoundingClientRect()
let constraints = {
video: {
height: pageSize.height,
width: pageSize.width,
facingMode: { exact: "environment" },
},
audio: false
};
//....
}

image.png
发现这个视频横着而且没有铺满屏幕。


通过输出video的信息可以看到,设备返回的视频流宽高是反的:


image.png


所以配置换一下就行了:


    let constraints = {  
video: {
height: pageSize.width,
width: pageSize.height,
},
};

作者:头上有煎饺
来源:juejin.cn/post/7287965561035210771
收起阅读 »

实现转盘抽奖功能

web
1、实现转盘数据动态配置(可通过接口获取) 2、背景色通过分隔配置 3、转动速度慢慢减速,最后停留在每一项的中间,下一次开始从本次开始 4、当动画停止后在对应事件中自定义生成中奖提示。 5、本次中奖概率随机生成,也可自定义配置 实现代码 html <te...
继续阅读 »

1、实现转盘数据动态配置(可通过接口获取)


2、背景色通过分隔配置


3、转动速度慢慢减速,最后停留在每一项的中间,下一次开始从本次开始


4、当动画停止后在对应事件中自定义生成中奖提示。


5、本次中奖概率随机生成,也可自定义配置


实现代码


html


<template>
<div class="graph-page">
<div class="plate-wrapper" :style="`${bgColor};`">
<div class="item-plate" :style="plateCss(index)" v-for="(item, index) in plateList" :key="index" >
<img :src="item.pic" alt="">
<p>{{item.name}}</p>
</div>
</div>
<div @click="handleClick" class="btn"></div>
</div>
</template>


css


<style lang="less" scoped>
.graph-page {
width: 540px;
height: 540px;
margin: 100px auto;
position: relative;
}
.plate-wrapper {
width: 100%;
height: 100%;
border-radius: 50%;
border: 10px solid #98d3fc;
overflow: hidden;
}
.item-plate {
position: absolute;
left: 0;
right: 0;
top: -10px;
margin: auto;
}
.item-plate img {
width: 30%;
height: 20%;
margin: 40px auto 10px;
display: block;
}
.item-plate p {
color: #fff;
font-size: 12px;
text-align: center;
line-height: 20px;
}
.btn {
width: 160px;
height: 160px;
background: url('https://www.jq22.com/demo/jquerylocal201912122316/img/btn_lottery.png') no-repeat center / 100% 100%;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
cursor: pointer;
}
.btn::before {
content: "";
width: 41px;
height: 39px;
background: url('https://www.jq22.com/demo/jquerylocal201912122316/img/icon_point.png') no-repeat center / 100% 100%;
position: absolute;
left: 0;
right: 0;
top: -33px;
margin: auto;
}
</style>


js


其中背景色采用间隔配置,扇形背景采用锥形渐变函数conic-gradient可实现。


每个转项的宽度和高度可参照以下图片,所有奖品的div都定位在圆心以上,根据圆心转动,所以旋转点为底部中心,即:transform-origin: 50% 100%;


可采用监听transitionend事件判断动画是否结束,可自定义中奖提示。


lADPJwKt5iekh_DNA1bNBJI_1170_854.jpg_720x720g.jpg


<script>
export default {
data() {
return {
plateList: [],
isRunning: false, //判断是否正在转动
rotateAngle: 0, //转盘每项角度
baseRunAngle: 360 * 5, //总共转动角度,至少5圈
totalRunAngle: 0, //要旋转的总角度
activeIndex: 0, //中奖index
wrapDom: null //转盘dom
}
},
computed: {
bgColor(){ //转盘的每项背景
let len = this.plateList.length
let color = ['#5352b3', '#363589']
let colorVal = ''
this.plateList && this.plateList.forEach((item, index)=>{
colorVal += `${color[index % 2]} ${(360/len)*index}deg ${(360/len)*(index+1)}deg,`
})
return `background: conic-gradient(${colorVal.slice(0, -1)})`
},
plateCss(){ //转盘的每项样式
if(this.plateList && this.plateList.length){
return (i) => {
return `
width: ${Math.floor(2 * 270 * Math.sin(this.rotateAngle / 2 * Math.PI / 180))}px;
height: 270px;
transform: rotate(${this.rotateAngle * i + this.rotateAngle / 2}deg);
transform-origin: 50% 100%;
`

}
}
return ()=>{''}
},
},
created(){
this.plateList= [
{ name: '手机', pic: 'https://bkimg.cdn.bcebos.com/pic/3801213fb80e7bec54e7d237ad7eae389b504ec23d9e' },
{ name: '手表', pic: 'https://img1.baidu.com/it/u=2631716577,1296460670&fm=253&fmt=auto&app=120&f=JPEG' },
{ name: '苹果', pic: 'https://img2.baidu.com/it/u=2611478896,137965957&fm=253&fmt=auto&app=138&f=JPEG' },
{ name: '棒棒糖', pic: 'https://img2.baidu.com/it/u=576980037,1655121105&fm=253&fmt=auto&app=138&f=PNG' },
{ name: '娃娃', pic: 'https://img2.baidu.com/it/u=4075390137,3967712457&fm=253&fmt=auto&app=138&f=PNG' },
{ name: '木马', pic: 'https://img1.baidu.com/it/u=2434318933,2727681086&fm=253&fmt=auto&app=120&f=JPEG' },
{ name: '德芙', pic: 'https://img0.baidu.com/it/u=1378564582,2397555841&fm=253&fmt=auto&app=120&f=JPEG' },
{ name: '玫瑰', pic: 'https://img1.baidu.com/it/u=1125656938,422247900&fm=253&fmt=auto&app=120&f=JPEG' }
]
this.rotateAngle = 360 / this.plateList.length
this.totalRunAngle = this.baseRunAngle + 360 - this.activeIndex * this.rotateAngle - this.rotateAngle / 2
},
mounted(){
this.$nextTick(()=>{
this.wrapDom = document.getElementsByClassName('plate-wrapper')[0]
})
},
beforeDestroy(){
this.wrapDom.removeEventListener('transitionend', this.stopRun)
},
methods:{
handleClick(){
if(this.isRunning) return
this.isRunning = true
const ind = Math.floor(Math.random() * this.plateList.length)//通过随机数返回奖品编号
this.activeIndex = ind
this.startRun()
},
startRun(){
// 设置动画
this.wrapDom.setAttribute('style', `
${this.bgColor};
transform: rotate(${this.totalRunAngle}deg);
transition: all 4s ease;
`
)
this.wrapDom.addEventListener('transitionend', this.stopRun) // 监听transition动画停止事件
},
stopRun(){
this.isRunning = false
this.wrapDom.setAttribute('style', `
${this.bgColor};
transform: rotate(${this.totalRunAngle - this.baseRunAngle}deg);
`
)
}
}
}
</script>

参考来源:juejin.cn/post/718031…


作者:李某某的学习生活
来源:juejin.cn/post/7287125076369801279
收起阅读 »

聊聊2023年怎么入局小游戏赛道?

web
一、微信小游戏赛道发展史 第一阶段:轻度试水期,2017~2019年 微信小游戏于2017年底上线,初期以轻度休闲为主,例如棋牌、合成消除以及益智相关游戏类型。一是开发门槛不高,产品可以快速上线; 二是大部分厂商并无计划投入过多资金,仅试水。在变现方式上,极大...
继续阅读 »

一、微信小游戏赛道发展史


第一阶段:轻度试水期,2017~2019年


微信小游戏于2017年底上线,初期以轻度休闲为主,例如棋牌、合成消除以及益智相关游戏类型。一是开发门槛不高,产品可以快速上线;


二是大部分厂商并无计划投入过多资金,仅试水。在变现方式上,极大部分以IAA为主。


第二阶段:官方孵化期,2019~2021年


2019年官方推出“游戏优选计划",为符合标准的产品提供全生命周期服务,包括前期产品的立项和调优,以及后期的增长、变现等。


20050414514227.jpg


出现了一批《三国全明星》、《房东模拟器》、《乌冬的旅店》等这样的精品游戏。


第三阶段:快速爆发期,2022年至今


在官方鼓励精品化下,手游大厂开始进入,产品逐渐开始偏向中重度化。三国、仙侠、神话、西游以及传奇等传统中重度游戏占比逐渐加大。


全流量拓展投放,库存近百亿。腾讯全域流量、字节系、快手、百度、B站等基本全部渠道均可进行买量,真正进入前所未有的爆发期!


WechatIMG3428.jpg


二、该赛道持续高增长的原因


1、小游戏的链路相比于APP更加顺畅,无需下载,点击即玩。游戏买量中用户损失最大的部分就是“点击-下载-激活"。而在小游戏的链路中,用户可一键拉起微信直达小游戏登录页面,无需等待,导流效率极高。

2、微信生态提供的统一的实名认证底层能力。

3、小游戏链路可以绕开IOS的IDFA获取率不足的问题,实现IOS平台的高效精准归因。

4、各大游戏开发引擎特别是unity对小游戏平台的优化和适配能力提升。

5、顺畅的微信支付链路。

6、高效开放的社交裂变自然流量来源和社群运营能力。


三、小游戏和app游戏的买量核心差异


1、买量技术框架


小游戏在多数广告平台的技术链路都是从H5改进而来的。APP付费游戏在安卓常见的SDK数据上报在小游戏链路因为无法进行分包而彻底被API上报所取代。


API上报不同于SDK上报,有着成熟且空间巨大的广告主自主策略优化玩法。


2、买量目标


小游戏买首次付费、每次付费的比例要高于买 ROI。这一点和APP游戏也有明显不同,小游戏品类分散,人群宽泛且行业刚起步缺乏历史数据,对广告系统来说ROI买量的达成难度要高,效果相对不稳定。


3、素材打法


APP游戏大盘消耗以重度为主,素材中洗重度用户的元素较多;小游戏则是轻中度玩法为主,素材多面向泛人群,更注重对真实核心玩法的表现。


四、广告买量为什么在小游戏赛道中很重要


1、买量红利巨大,再好的产品都要靠买量宣发


微信小游戏链路在转化率上有着明显的优势。这会让小游戏产品在相同的前端出价上,要比同类型的APP产品买量竞争力更强。


而小游戏的研发成本并不算高,一旦跑出一款好产品,跟进者众多。在产品同质化比较严重时,快速买量拿量能力就决定了产品和业务的规模,除非大家有信心做出一款不怕被抄的爆品中的精品。


2、技术能力及格不难,做好很难


小游戏的买量技术相关的问题,如果只想将就用,可能一两个研发简单做一个月就能做到跑通。


但是如果想把买量技术能力做完善,这里依然有很大的门槛,而且会成为拉开规模差距的核心能力之一。这里我们给出几个细节,篇幅原因不具体展开。


归因方式


不同于APP生态已经比较成熟统一的设备ID和IP+ UA模糊匹配,小游戏链路因为微信生态、平台能力和开发成本不同,在不同平台存在多种归因方式,主要有平台自采集,启动参数,监测匹配等。


有效触点


因为小游戏不用去应用商店或者落地页下载,因而看广告但是不直接点击,而是选择去微信自己搜索启动的流量占比要高一些。为了适应这一情况,有些媒体平台会选择在小游戏链路将之前 APP的默认有效触点前置到播放或者视频浏览上。这里会让监测归因方式需要处理的数据提升两个数量级,对归因触点识别的难度也会加大。


效果衡量


因为支付通道的原因,腾讯系的平台和小游戏后台都只能收集安卓的付费数据,不能收集ios的数据,导致IAP类型的产品追踪完整ROI需要自建中台或者采买三方,打通归因和付费埋点数据。


数据口径


因为数据 采集来源不同,时间统计口径不同,小游戏链路下数据分析对运营和投放人员有着较高的要求,需要科学成熟的数据分析工具作为辅助。


3、渠道分散且需要掌控节奏


因为小游戏更为顺畅的用户链路,导致其转化率要比APP链路更高。因此小游戏在一些腰部平台甚至尾部平台都能有很好的跑量能力。APP游戏很多规模不大的产品可能只需要在巨量、腾讯和快手进行买量,现在小游戏完全可以尝试在百度信息流、B站、微博甚至是知乎等平台进行买量。


除了大家熟知的流量平台以外,长尾流量渠道往往是很多小游戏能闷声发财的致胜法宝。比如:陌陌、番茄、书旗等具有大量用户流量的非主流流量平台,一方面这些流量渠道取决于发行商的商务能力,另一方面也需要具备相应的技术能力。以业内新晋的小游戏发行技术 FinClip 来说,以嵌入SDK的方式,就可以让任何APP流量平台具备运行微信小游戏的能力。这意味着,小游戏在平台无需做任何跳转,用户转化链路降到最短。当然,腰尾部流量平台对小游戏在落地页资产、微信数据授权、链路技术支持等方面都还不是完全成熟,还属于比较小众的渠道方式。


小游戏发行领域,达人营销和自然裂变也是重要的渠道手段。通过合适的技术手段,达人营销和裂变也可以做到精准的效果追踪和按效果付费。


五、怎么入局小游戏赛道?


小游戏=变现方式游戏品质玩法受众裂变运营买量能力


以IAP或者混合变现的形式入局成功率会更大一些。


游戏品质主要和研发成本正相关:



  • 50万成本以下的小游戏往往因为玩法过于休闲,长线留存天花板低,美术品质不够,同质化竞争过于严重等原因导致很难获得预期的规模。

  • 200万成本以上的游戏又会因为试错成本太高,研发周期过长,不够紧跟市场热点等原因不被看好。

  • 因此,一般推荐50万到200万的成本,通过自有产研团队从APP转型,或者与稳定合作CP定制的方式获取第一款试水的产品。


具体的玩法和受众:



  • 一些在APP赛道被验证的轻中度的合成、抽卡和挂机类玩法都是在小游戏领域被广泛看好证的。

  • 在APP受限于受众规模小和付费渗透率低的小众玩法,如女性向Q萌养成,解密等玩法都有着亮眼的表现。

  • 整个小游戏的生态从开发者侧也更偏向于中长尾,多种垂直品类共存发展的趋势。


小游戏有着顺畅的玩家加群和裂变分享路径:



  • 持续运营私域群流量可以显著拉升核心用户的留存活跃,配合节日礼包等活动也可以提升付费率。

  • 小游戏无需应用商店下载,也不会有H5官网下载被微信拦截的情况,配合一些魔性和话题性的分享引导,很容易在已有一定用户规模的前提下实现比APP更快的自然增长,让用户规模更上一层。


作者:Finbird
来源:juejin.cn/post/7287494827701682176
收起阅读 »

用代码预测未来买房后的生活

web
背景 最近家里突然计划买房,打破了我攒钱到财务自由的规划。所以开始重新计算自己背了房贷之后的生活到底如何。 一开始通过笔记软件来进行未来收入支出推算。后来发现太过麻烦,任何一项收入支出的改动,都会影响到后续结余累计值的计算。 所以干脆发挥传统艺能,写网页! 逻...
继续阅读 »

背景


最近家里突然计划买房,打破了我攒钱到财务自由的规划。所以开始重新计算自己背了房贷之后的生活到底如何。


一开始通过笔记软件来进行未来收入支出推算。后来发现太过麻烦,任何一项收入支出的改动,都会影响到后续结余累计值的计算。


所以干脆发挥传统艺能,写网页!


逻辑



  • 假设当前年收入稳定不变,在 50 岁之后收入降低。

  • 通过 上一年结余 + 收入-房贷-生活支出-特殊情况支出 的公式得到累加计算每年的结余资金。

  • 通过修改特使事件来模拟一些如装修、买车的需求。

  • 最后预测下 30 年后的生活结余,从而可知未来的生活质量。


实现


首先,创建一个 HTML 文件 feature.html,然后咔咔一顿写。


<!DOCTYPE html>
<html lang="zh-CN" dir="ltr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="https://cn.vuejs.org/logo.svg" />
<title>生涯模拟</title>
<meta name="description" content="人生经费模拟器" />

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<style>
body {
margin: 0;
padding: 0;
}

.content {
background: #181818;
height: 100vh;
}

.time-line {
height: 100%;
overflow: auto;
}

.time-line-item {
position: relative;
padding: 10px 40px;
}

.flex-wrap {
display: flex;
flex-direction: row;
align-items: center;
}

.tli-year {
line-height: 24px;
font-size: 18px;
font-weight: bold;
color: #e5eaf3;
}

.tli-amount {
font-size: 14px;
color: #a3a6ad;
margin: 0 20px;
}

.tli-description {
margin-top: 6px;
line-height: 18px;
font-size: 12px;
color: #8d9095;
}

.tli-description-event {
color: #f56c6c;
}
</style>
</head>
<body>
<div id="app">
<div class="content">
<div class="time-line">
<div v-for="item in data" :key="item.year" class="time-line-item">
<div class="flex-wrap">
<span class="tli-year">{{ item.year }}年</span>
<span class="tli-amount">¥{{ item.ammount / 10000 }} 万</span>
</div>
<div
v-for="desc in item.descriptions"
class="tli-description flex-wrap"
:class="desc.normal ? '' : 'tli-description-event'">

<span style="margin-right: 20px">{{ desc.name }}</span>
<span v-show="desc.ammount">{{ desc.ammount }}</span>
</div>
</div>
</div>
</div>
</div>

<script>
const { createApp, ref, onMounted } = Vue;

const config = {
price: 6000000, // 房价
startAmount: 1850000, // 启动资金
income: 26000 * 12, // 年收入
loan: 15700 * 12, // 年贷款
live: 7000 * 12, // 年支出
startYear: 2023, // 开始还贷年份
// 生活事件
events: [
{ year: 2024, ammount: 0, name: "大女儿一年级" },
{ year: 2026, ammount: 0, name: "小女儿一年级" },
{ year: 2028, ammount: 0, name: "老爸退休" },

{ year: 2027, ammount: -300000, name: "装修" },
{ year: 2031, ammount: -300000, name: "买车" },
{ year: [2028, 2036], ammount: 7500 * 12, name: "老房子房租" },
{ year: 2036, ammount: 3500000, name: "老房子卖出" },
],
};

createApp({
setup() {
const data = ref([]);

onMounted(() => {
genData();
});

function genData() {
const arr = [];
const startYear = config.startYear;
const endYear = startYear + 30;

for (let year = startYear; year < endYear; year++) {
if (year === startYear) {
arr.push({
year,
ammount: config.startAmount - config.price * 0.3,
descriptions: [
{
name:
"开始买房,房价" +
config.price / 10000 +
"万,首付" +
(config.price * 0.3) / 10000 +
"万",
ammount: 0,
},
],
});
} else {
const latestAmount = arr[arr.length - 1].ammount;

const filterDescs = config.events.filter((item) => {
if (Array.isArray(item.year)) {
return item.year[0] <= year && item.year[1] >= year;
}
return item.year === year;
});

let descAmount = 0;
if (filterDescs.length > 0) {
descAmount = filterDescs
.map((item) => item.ammount)
.reduce((acc, val) => acc + val);
}

const income = config.income;

arr.push({
year,
ammount:
latestAmount +
income -
config.loan -
config.live +
descAmount,
descriptions: [
{
name: "月收入",
ammount: income / 12,
normal: true,
},
{
name: "月贷款",
ammount: -config.loan / 12,
normal: true,
},
{
name: "月支出",
ammount: -config.live / 12,
normal: true,
},
{
name: "月结余",
ammount: (income - config.loan - config.live) / 12,
normal: true,
},
...filterDescs,
],
});
}
}

data.value = arr;
}

return {
data,
};
},
}).mount("#app");
</script>
</body>
</html>


PS: 之所以用 vue 呢是因为写起来顺手且方便(小工具而已,方便就行。不必手撕原生 JS DOM)。


效果


通过修改 config 中的参数来定义生活中收支的大致走向。外加一些标注和意外情况的支出。得到了下面这个图。


image.png


结论



  • 倘若过上房贷生活,那么家里基本一直徘徊在没钱的边缘,需要不停歇的工作,不敢离职。压力真的很大。30 年后除了房子其实没剩下多少积蓄了。

  • 修改配置,将房贷去掉,提高生活支出,那么 30 年后大概能存下 500w 的收入。


以上没有算通货膨胀和工资的上涨,这个谁也说不准。只是粗浅的计算。


所以,感觉上买房真的是透支了未来长期生活质量和资金换来的。也不知道买房的决定最终会如何。


作者:VioletJack
来源:juejin.cn/post/7287144390601244672
收起阅读 »

一篇文章让你的网站拥有CDN级的访问速度,告别龟速个人服务器~

web
通常来说,前端加快页面加载的手段无非是缩小文件、减少请求等几种常见的方式,但如果说页面加载慢的本质原因是因为没有CDN服务和服务器带宽限制这些非前端代码因素,那么前端代码再怎么优化,加载速度还是会差强人意。 最常见的就是我们在各大云平台白嫖的新人专享的服务器或...
继续阅读 »

通常来说,前端加快页面加载的手段无非是缩小文件、减少请求等几种常见的方式,但如果说页面加载慢的本质原因是因为没有CDN服务和服务器带宽限制这些非前端代码因素,那么前端代码再怎么优化,加载速度还是会差强人意。


最常见的就是我们在各大云平台白嫖的新人专享的服务器或者是那种配置很低的服务器,虽说能用,但是用个IP访问网站就算了,关键是还是很慢,一个1M的JS文件都能加载几秒钟。


关于彻底解决这个问题,我有一个一劳永逸的办法……


首先我们要明确的是,访问速度慢是因为服务器带宽限制以及没有CDN的支持,带宽限制就是从服务器获取资源的最大速度,CDN就是内容分发网络,简单理解就是你在世界上任意位置访问某个CDN资源,通过CDN服务就可以从离你最近的一台CDN服务器上获取资源,简单粗暴地优化远距离访问导致的物理延迟的问题。


CDN前后对比


首先我们来看一个小网站直接部署在一个某云平台最基础的服务器上访问的速度:


image.png
可以看到的是加载速度惨不忍睹,这还只是一个页面的网站,如果再大一点加上没有浏览器缓存的第一次访问,网站的响应速度应该随随便便破10秒。


接着我们再看看经过CDN加速的网站访问速度:


image.png


可以看到的是速度有了极大的提升,而且我们访问的资源除了index.html,也就是上图中的第一行请求是直接访问我们自己的服务器获取的,其他都是走的CDN服务。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="http://static.admin.rainbowinpaper.cn/logo.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>纸上的彩虹-管理端</title>
<script type="module" crossorigin src="http://static.admin.rainbowinpaper.cn/assets/index.f1217c6c.js"></script>
<link rel="stylesheet" href="http://static.admin.rainbowinpaper.cn/assets/index.a5fafcaf.css">
</head>
<body>
<div id="main-app"></div>
</body>
</html>

首先我们访问的地址是:admin.rainbowinpaper.cn,而网站中所有资源的加载地址是:static.admin.rainbowinpaper.cn,所以后者就是一个映射到CDN服务的地址。


准备域名


在我们准备把自己的项目接入CDN之前我们首先要注册一个域名并且备案好,关于域名如何注册备案的问题,我这里不过多赘述,你可以去的买服务器的云平台搜索域名注册,随便买个几块钱一年的便宜域名,然后按照平台提示的备案流程完成后续操作,我这里从准备好域名说起。


有了域名后我们就可以先把自己用IP访问服务器改用域名访问,操作方法也很简单,就是在你所购买的平台的域名管理里面加一行解析:


image.png


如图所示,类型为A,将域名指向ipv4地址,注意打开解析域名必须要备案,不然会被屏蔽访问


现在试试直接用域名能不能访问到你的网站。


准备CDN


网上提供CDN服务的平台有很多,我这里以七牛云作为CDN服务平台,毕竟免费的CDN服务真的很香。


首先我们去七牛云注册一个账号,然后新建一个存储空间:


image.png
然后绑定自定义域名:


image.png


这里我们可以随便写一个二级域名,比如我们的域名是rainbowinpaper.cn,那我们的加速域名就可以填写img.rainbowinpaper.cn


其他的保持默认,我们直接创建,当我们在七牛云新建域名的时候需要验证你对当前域名的所有权,所以需要按照七牛云的提示去管理你域名的平台加一条解析记录,这一条仅作为验证所有权,无实际作用,大致如下:


image.png


当七牛云验证成功后,你需要再加一条域名的解析记录,就是解析你刚才在七牛云填写的加速域名:


image.png


注意值那一行,是七牛云提供的CNAME。关于如何配置,七牛云也有帮助文档可以查看,都很简单。


当我们配置好了再回七牛云域名管理就能看到如下的状态:


image.png


现在我们可以去刚刚创建的空间里面上传一张图片,查看详情里面的链接是否能访问,如果访问到我们刚才上传的图片,就说明成功了。


image.png


到此为止我们的准备工作都完成了,准备上代码!


自动化上传打包文件


前面我提到了,访问网站除了index.html是从服务器获取的其他文件都是从CDN服务器上获取的,其原理就是修改了项目打包时的base值(图中所示的是vite项目的配置,其他打包工具请自行兼容),让所有引入的静态文件指向CDN的加速域名,而不是从源服务器去获取。


image.png


到这里指向变了,但是我们不可能每次更新项目都要手动上传打包文件到七牛云里面,所以我们需要写一个脚本自动将打包文件上传到七牛云。话不多说直接上代码:


/* eslint-disable no-console */
const path = require('path');
const fs = require('fs');
const qiniu = require('qiniu');
const chalk = require('chalk');

const { ak, sk, bucket } = {
ak: '你的ak',
sk: '你的sk',
bucket: '你刚才创建的存储空间名',
};

const mac = new qiniu.auth.digest.Mac(ak, sk);

const config = new qiniu.conf.Config();
// 你创建空间时选择的存储区域
config.zone = qiniu.zone.Zone_z2;
config.useCdnDomain = true;

const bucketManager = new qiniu.rs.BucketManager(mac, config);

/**
* 上传文件方法
* @param key 文件名
* @param file 文件路径
* @returns {Promise<unknown>}
*/

const doUpload = (key, file) => {
console.log(chalk.blue(`正在上传:${file}`));
const options = {
scope: `${bucket}:${key}`,
};
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra();
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
return new Promise((resolve, reject) => {
formUploader.putFile(uploadToken, key, file, putExtra, (err, body, info) => {
if (err) {
reject(err);
}
if (info.statusCode === 200) {
resolve(body);
} else {
reject(body);
}
});
});
};

const getBucketFileList = (callback, marker, list = []) => {
!marker && console.log(chalk.blue('正在获取空间文件列表'));
const options = {
limit: 100,
};
if (marker) {
options.marker = marker;
}
bucketManager.listPrefix(bucket, options, (err, respBody, respInfo) => {
if (err) {
console.log(chalk.red(`获取空间文件列表出错 ×`));
console.log(chalk.red(`错误信息:${JSON.stringify(err)}`));
throw err;
}
if (respInfo.statusCode === 200) {
// 如果这个nextMarker不为空,那么还有未列举完毕的文件列表,下次调用listPrefix的时候,
// 指定options里面的marker为这个值
const nextMarker = respBody.marker;
const { items } = respBody;
const newList = [...list, ...items];
if (!nextMarker) {
console.log(chalk.green(`获取空间文件列表成功 ✓`));
console.log(chalk.blue(`需要清理${newList.length}个文件`));
callback(newList);
} else {
getBucketFileList(callback, nextMarker, newList);
}
} else {
console.log(chalk.yellow(`获取空间文件列表异常 状态码${respInfo.statusCode}`));
console.log(chalk.yellow(`异常信息:${JSON.stringify(respBody)}`));
}
});
};

const clearBucketFile = () =>
new Promise((resolve, reject) => {
getBucketFileList(items => {
if (!items.length) {
resolve();
return;
}
console.log(chalk.blue('正在清理空间文件'));
const deleteOperations = [];
// 每个operations的数量不可以超过1000个,如果总数量超过1000,需要分批发送
items.forEach(item => {
deleteOperations.push(qiniu.rs.deleteOp(bucket, item.key));
});
bucketManager.batch(deleteOperations, (err, respBody, respInfo) => {
if (err) {
console.log(chalk.red(`清理空间文件列表出错 ×`));
console.log(chalk.red(`错误信息:${JSON.stringify(err)}`));
reject();
} else if (respInfo.statusCode >= 200 && respInfo.statusCode <= 299) {
console.log(chalk.green(`清理空间文件成功 ✓`));
resolve();
} else {
console.log(chalk.yellow(`获取空间文件列表异常 状态码${respInfo.deleteusCode}`));
console.log(chalk.yellow(`异常信息:${JSON.stringify(respBody)}`));
reject();
}
});
});
});

const publicPath = path.join(__dirname, '../../dist');

const uploadAll = async (dir, prefix) => {
if (!prefix){
console.log(chalk.blue('执行清理空间文件'));
await clearBucketFile();
console.log(chalk.blue('正在读取打包文件'));
}
const files = fs.readdirSync(dir);
if (!prefix){
console.log(chalk.green('读取成功 ✓'));
console.log(chalk.blue('准备上传文件'));
}
files.forEach(file => {
const filePath = path.join(dir, file);
const key = prefix ? `${prefix}/${file}` : file;
if (fs.lstatSync(filePath).isDirectory()) {
uploadAll(filePath, key);
} else {
doUpload(key, filePath)
.then(() => {
console.log(chalk.green(`文件${filePath}上传成功 ✓`));
})
.catch(err => {
console.log(chalk.red(`文件${filePath}上传失败 ×`));
console.log(chalk.red(`错误信息:${JSON.stringify(err)}`));
console.log(chalk.blue(`再次尝试上传文件${filePath}`));
doUpload(file, filePath)
.then(() => {
console.log(chalk.green(`文件${filePath}上传成功 ✓`));
})
.catch(err2 => {
console.log(chalk.red(`文件${filePath}再次上传失败 ×`));
console.log(chalk.red(`错误信息:${JSON.stringify(err2)}`));
throw new Error(`文件${filePath}上传失败,本次自动化构建将被强制终止`);
});
});
}
});
};

uploadAll(publicPath).finally(() => {
console.log(chalk.green(`上传操作执行完毕 ✓`));
console.log(chalk.blue(`请等待确认所有文件上传成功`));
});


代码逻辑就是获取存储空间所有文件后删除,然后获取本地打包文件后上传,这样存储空间的文件不会一直堆积,所以这个存储空间只能存放项目的静态文件。


其中需要注意的是,需要在七牛云的秘钥管理中生成一对密钥写入代码中。


package.json中写入上传指令:


image.png


运行指令,打印日志如下:


image.png


这时候再到七牛云的空间看下看见文件是否已经存在,这时候再访问下网站,如果能正确加载网站,说明就大功告成了。


说在后面


我之前在做自动化部署的时候发现自己的网站总是访问的很慢,但又是因为不想花更多的钱买更好的服务器,所以就被迫去研究到底哪些方法可以立竿见影的让网站加快访问速度,于是就有了本文。


总而言之,实践是检验真理的唯一标准,网上关于加快网页加载的文章一大堆,不是说它们没用,只是我们都是在前人的经验上去直接照搬的,这样就缺少了自己实践成功的那种成就感,关于这些技术点的由来可能还是一知半解,所以看过别人的文章,不如自己亲自实验一番。


最后,如有问题欢迎评论区讨论。


作者:纸上的彩虹
来源:juejin.cn/post/7283682738498273317
收起阅读 »

我的发!地表最强扫一扫

web
在很久很久以前,我亲爱的同事们在对接二维码扫描业务的时候,都是使用的微信官方自带的扫一扫,比如这样 wx.scanQRCode({ needResult: 0, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果, scanType: ["qrC...
继续阅读 »

在很久很久以前,我亲爱的同事们在对接二维码扫描业务的时候,都是使用的微信官方自带的扫一扫,比如这样


wx.scanQRCode({
needResult: 0, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
scanType: ["qrCode","barCode"], // 可以指定扫二维码还是一维码,默认二者都有
success: function (res) {
var result = res.resultStr; // 当needResult 为 1 时,扫码返回的结果
}
});

所以我扫码就一定得依赖微信,在普通的浏览器中打开就GG,并且还要绑定公众号,烦的一批。


然后我就在想,扫码不就是靠摄像头捕捉图像进行解码出内容嘛,那肯定会有原生的解决方案。


Google Google Google Google ......


果然是有的,Web API中也提供了一个实验性的功能,Barcode Detection API


image.png


它提供了一个detect方法,可以接收图片元素、图片二进制数据或者是ImageData,最终返回一个包含码信息的Promise对象。


但是呢,这个功能的浏览器兼容性比较差,看了caniuse,心凉了一半。


image.png


但我相信大神们肯定有自己的解决方案,继续Google呗。


Google Google Google Google ......


还真有这么一个库,html5-qrcode,它在zxing-js的基础之上,又增加了对多种码制的解码支持,站在巨人的肩膀上又跟高了一层。


html5-qrcode支持的码有:


CodeExample
QR Codeimage.png
AZTECimage.png
CODE_39image.png
CODE_93image.png
CODE_128image.png
ITFimage.png
EAN_13image.png
EAN_8image.png
PDF_417image.png
UPC_Aimage.png
UPC_Eimage.png
DATA_MATRIXimage.png
MAXICODE*
RSS_14*
RSS_EXPANDED*image.png

我个人觉得非常够用了,平时用的最多的还是二维码、条形码,其他的码也都少见。


关键是人家还支持了各种浏览器,可以说已经是很良心了(什么UC浏览器的,其实我都瞧不上,不支持就不支持,无所吊谓)


image.png


来看看官方提供的demo效果


chrome-capture-2023-8-27.gif


好好好,很棒。但是他们没有提供框架支持,那么我又可以站在巨人的肩膀上的巨人的肩膀上造轮子了。


先来看看我自己封装的React组件


demo.gif


使用方法也简单


function App() {
const scanCodeRef = useRef();
const [scanResult, setScanResult] = useState('');

function startScan() {
scanCodeRef.current?.initScan();
}

return (
<div>
<button onClick={startScan}>扫一扫</button>
<p>扫描结果: {scanResult}</p>
<ScanQrCodeH5
ref={scanCodeRef}
scanTips="请一定要对准二维码哦~"
onScanSuccess={(text) =>
{
setScanResult(text);
}}
// onScanError={(err) => {
// console.log(err);
// }}
/>
</div>

);
}

三二一,上链接,rc-qrcode-scan


这次的版本没有加入从相册选择图片进行解码,下个版本将会加入,希望能帮到掘友们。


2023-09-28更新,掘友们我把从相册选择加进去了。


作者:AliPaPa
来源:juejin.cn/post/7283080455852359734
收起阅读 »

Web 版 PS 用了哪些前端技术?

web
经过 Adobe 工程师多年来的努力,并与 Chrome 等浏览器供应商密切合作,通过 WebAssembly + Emscripten、Web Components + Lit、Service Workers + Workbox 和新的 Web API 的支...
继续阅读 »

经过 Adobe 工程师多年来的努力,并与 Chrome 等浏览器供应商密切合作,通过 WebAssembly + Emscripten、Web Components + Lit、Service Workers + Workbox 和新的 Web API 的支持,终于在近期推出了 Web 版 Photoshop(photoshop.adobe.com),这在实现高度复杂和图形密集型软件在浏览器中运行方面具有重大意义!


图片


本文就来看看 Photoshop 所使用的 Web 能力、进行的性能优化以及未来可能的发展方向。


愿景:在浏览器中使用 Photoshop


Adobe 的愿景就是将 Photoshop 带到浏览器中,让更多的用户能够方便地使用它进行图像编辑和平面设计。过去几十年,Photoshop一直是图像编辑和平面设计的黄金标准,但它只能在桌面上运行。现在,通过将它移植到浏览器中,就打开一个全新的世界。


Web 版 Photoshop 承诺了无处不在、无摩擦的访问体验。用户只需打开浏览器,就能即时开始使用 Photoshop 进行编辑和协作,而不需要安装任何软件。而且,由于Web是一个跨平台的运行环境,它可以屏蔽底层操作系统的差异,使Photoshop 能够在不同的平台上与用户进行互动。


图片


另外,通过链接的功能,共享工作流变得更加方便。Photoshop文档可以通过URL直接访问。这样,创作者可以轻松地将链接发送给协作者,实现更加便捷的合作。


但是,实现这个愿景面临着重大的技术挑战,要求重新思考像Photoshop这样强度大的应用如何在Web上运行。


使用新的 Web 能力


最近几年出现了一些新的 Web 平台能力,可以通过标准化和实现最终使类似于Photoshop这样的应用成为可能。Adobe工程师们创新地利用了几个关键的下一代API。


使用 OPFS 实现高性能本地文件访问


Photoshop 操作涉及读写可能非常大的PSD文件。这要求有效访问本地文件系统,新的Origin Private File System API (OPFS) 提供了一个快速、特定于源的虚拟文件系统。



Origin Private File System (OPFS) 是一个提供了快速、安全的本地文件系统访问能力的 Web API。它允许Web应用以原生的方式读取和写入本地文件,而无需将文件直接暴露给Web环境。OPFS通过在浏览器中运行一个本地代理和使用特定的文件系统路径来实现文件的安全访问。



 
const opfsRoot = await navigator.storage.getDirectory();

使用 OPFS 可以快速创建、读取、写入和删除文件。例如:


 
// 创建文件
const file = await opfsRoot.getFileHandle('image.psd', { create: true });

// 获取读写句柄
const handle = await file.createSyncAccessHandle();

// 写入内容

handle.write(buffer);

// 读取内容
handle.read(buffer);

// 删除文件
await file.remove();

为了实现绝对快的同步操作,可以利用Web Workers获取 FileSystemSyncAccessHandle


这个本地高性能文件系统在浏览器中实现Photoshop所需的高要求文件工作流程非常关键。它能够提供快速而可靠的文件读写能力,使得Photoshop能够更高效地处理大型文件。这种优化的文件系统为用户带来更流畅的图像编辑和处理体验。


释放WebAssembly的强大潜力


WebAssembly是重新在JavaScript中实现Photoshop计算密集型图形处理的关键因素之一。为了将现有的 C/C++ 代码库移植到 JavaScript 中,Adobe使用了Emscripten编译器生成WebAssembly模块代码。


在此过程中,WebAssembly具备了几个至关重要的能力:



  • SIMD:使用SIMD向量指令可以加速像素操作和滤波。

  • 异常处理:Photoshop的代码库中广泛使用C++异常。

  • 流式实例化:由于Photoshop的WASM模块大小超过80MB,因此需要进行流式编译。

  • 调试:Chrome浏览器在DevTools中提供的WebAssembly调试支持是非常有用的

  • 线程:Photoshop使用工作线程进行并行执行任务,例如处理图像块:


 
// 线程函数
void* tileProcessor(void* data) {
// 处理图像块数据
return NULL;
}

// 启动工作线程
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, tileProcessor, NULL);
pthread_create(&thread2, NULL, tileProcessor, NULL);

// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

利用 P3 广色域


P3色域比sRGB色域更广阔,能够显示更多的颜色范围。然而长时间以来,在 Web 上sRGB一直是唯一的色域标准,其他更宽广的色域如P3并没有被广泛采用。


图片


Photoshop利用新的color()函数和Canvas API来充分发挥P3色域的鲜艳度,从而实现更准确的颜色呈现。通过使用这些功能,Photoshop能够更好地展示P3色域所包含的更丰富、更生动的颜色。


 
color: color(display-p3 1 0.5 0)

Web Components 提供UI的灵活性


Photoshop是 Adobe Creative Cloud 生态系统中的一部分。通过使用基于 Lit[1] 构建的标准化 Web Components 策略,可以实现应用之间 UI 的一致性。



Lit 是一个构建快速、轻量级 Web Components 库。它的核心是一个消除样板代码的组件基础类,它提供了响应式状态、作用域样式和声明性模板系统,这些系统都非常小、快速且具有表现力。



图片


Photoshop 的 UI 元素来自于Adobe 的 Web Components 库:Spectrum[2],该库实现了Adobe的设计系统。


Spectrum Web Components 具有以下特点:



  • 默认支持无障碍访问:开发时考虑到现有和新兴浏览器规范,以支持辅助技术。

  • 轻量级:使用 Lit Element 实现,开销最小。

  • 基于标准:基于 Web Components 标准,如自定义元素和 Shadow DOM 构建。

  • 框架无关:由于浏览器级别的支持,可以与任何框架一起使用。


此外,整个 Photoshop 应用都是使用基于 Lit 的 Web Components 构建的。Lit的模板和虚拟DOM差异化使得UI更新效率高。当需要时,Web Components 的封装性也使得轻松地集成其他团队的 React 代码成为可能。


总体而言,Web Components 的浏览器原生自定义元素结合Lit的性能,为Adobe构建复杂的 Photoshop UI 提供了所需的灵活性,同时保持了高效性。


优化 Photoshop 在浏览器中的性能


尽管新的 Web Components 提供了基础,但像Photoshop这样的密集型桌面应用仍然需要进行广泛的跟踪和性能优化工作,以提供一流的在线体验。


图片


使用 Service Workers 缓存资源和代码


Service Workers 可以让 Web 应用在用户首次访问后将其代码和资源等缓存到本地,以便在后续加载时可以更快地呈现。尽管 Photoshop 目前还不支持完全离线使用,但它已经利用了 Service Workers 来缓存其 WebAssembly 模块、脚本和其他资源,以提高加载速度。


图片


Chrome DevTools Application 面板 > Cache storage 展示了 Photoshop 预缓存的不同类型资源,包括在Web上进行代码拆分后本地缓存的许多JavaScript代码块。这些被本地缓存的JavaScript代码块使得后续的加载非常快速。这种缓存机制对于加载性能有着巨大的影响。在第一次访问之后,后续的加载通常非常快速。


Adobe 使用了 Workbox[3] 库,以更轻松地将 Service Worker 缓存集成到构建过程中。


当资源从Service Worker缓存中返回时,V8引擎使用一些优化策略:



  • 安装期间缓存的资源会被立即进行编译,并立即进行代码缓存,以实现一致且快速的性能表现。

  • 通过Cache API 进行缓存的资源,在第二次加载时会经过优化的缓存处理,比普通缓存更快速。

  • V8能够根据资源的缓存重要性进行更积极的编译优化。


这些优化措施使得 Photoshop 庞大的缓存 WebAssembly 模块能够获得更高的性能。


图片


流式编译和缓存大型WebAssembly模块


Photoshop的代码库需要多个大型的WebAssembly模块,其中一些大小超过80MB。V8和Chrome中的流式编译支持高效处理这些庞大的模块。


此外,当第一次从 Service Worker 请求 WebAssembly 模块时,V8会生成并存储一个优化版本以供缓存使用,这对于 Photoshop 庞大的代码尺寸至关重要。


并行图形操作的多线程支持


在 Photoshop 中,许多核心图像处理操作(如像素变换)可以通过在多个线程上进行并行执行来大幅提速。WebAssembly 的线程支持能够利用多核设备进行计算密集型图形任务。


这使得 Photoshop 可以将性能关键的图像处理函数移植到 WebAssembly,并使用与桌面端相同的多线程方法来实现并行处理。


通过 WebAssembly 调试优化


对于开发过程中的诊断和解决性能瓶颈来说,WebAssembly 调试支持非常重要。Chrome DevTools 具备分析 WASM 代码、设置断点和检查变量等一系列功能,这使得WASM的调试与JavaScript有着相同的可调试性。


图片


将设备端机器学习与 TensorFlow.js 集成


Photoshop 最近的 Web 版本包括了使用 TensorFlow.js[4] 提供 AI 功能的能力。在设备上运行模型而不是在云端运行,可以提高隐私、延迟和成本效益。



TensorFlow.js 是一款面向JavaScript开发者的开源机器学习库,能够在浏览器客户端中运行。它是 Web 机器学习方案中最成熟的选项,支持全面的 WebGL 和 WebAssembly 后端算子,并且未来还将可选用WebGPU后端以实现更快的性能,以适应新的Web标准。



“选择主题”功能利用机器学习技术,在图像中自动提取主要前景对象,大大加快了复杂选区的速度。


下面是一幅日落的插图,想将它改成夜晚的场景。使用了"选择主题"和 AI prompt 来尝试选择最感兴趣的区域以进行更新。


图片


Photoshop 能够根据 AI prompt 生成一幅更新后的插图:


图片


根据 AI prompt,Photoshop 生成了一幅基于此的更新插图:


图片


该模型已从 TensorFlow 转换为 TensorFlow.js 以启用本地执行:


 
// 加载选择主题模型
const model = wait tf.loadGraphModel('select_subject.json');

// 对图像张量运行推理
const {mask, background} = model.execute(imgTensor);

// 从掩码中细化选择

Adobe 和 Google 合作通过为 Emscripten 开发代理 API 来解决 Photoshop 的 WebAssembly 代码和 TensorFlow.js 之间的同步问题。这使的框架之间可以无缝集成。



由于Google团队通过其各种支持的后端(WebGL,WASM,Web GPU)改进了 TensorFlow.js 的硬件执行性能,这使模型的性能提高了30%到200%,在浏览器中能够实现接近实时的性能。



关键模型针对性能关键的操作进行了优化,例如Conv2D。Photoshop 可以根据性能需求选择在设备上还是在云端运行模型。


Photoshop 未来在 Web 上的发展


Photoshop 在 Web 上的普遍应用是一个巨大的里程碑,但这只是可能性的冰山一角。


随着浏览器厂商不断发展和完善标准和性能,Photoshop 将继续在 Web 上扩展,通过渐进增强来上线更多功能。而且,Photoshop 只是一个开始。Adobe计划在网络上积极构建其整个 Creative Cloud 套件,在浏览器中解锁更多复杂的设计应用。


Adobe 与浏览器工程师的合作将持续推动 Web 平台的进步,通过提升标准和改进性能,开发出更具雄心的应用。前方等待着我们的,是充满无限可能性的未来!



Photoshop 网页版目前可以在以下桌面版浏览器上使用:



  • Chrome 102+

  • Edge 102+

  • Firefox 111+



作者:QdFe
来源:juejin.cn/post/7285942684174778431
收起阅读 »

对不起 localStorage,现在我爱上 localForage了!

web
前言 前端本地化存储算是一个老生常谈的话题了,我们对于 cookies、Web Storage(sessionStorage、localStorage)的使用已经非常熟悉,在面试与实际操作之中也会经常遇到相关的问题,但这些本地化存储的方式还存在一些缺陷,比较明...
继续阅读 »

前言


前端本地化存储算是一个老生常谈的话题了,我们对于 cookies、Web Storage(sessionStorage、localStorage)的使用已经非常熟悉,在面试与实际操作之中也会经常遇到相关的问题,但这些本地化存储的方式还存在一些缺陷,比较明显的缺点如下:



  1. 存储量小:即使是web storage的存储量最大也只有 5M

  2. 存取不方便:存入的内容会经过序列化,当存入非字符串的时候,取值的时候需要通过反序列化。


当我们的存储量比较大的时候,我们一定会想到我们的 indexedDB,让我们在浏览器中也可以使用数据库这种形式来玩转本地化存储,然而 indexedDB 的使用是比较繁琐而复杂的,有一定的学习成本,但第三方库 localForage 的出现几乎抹平了这个缺陷,让我们轻松无负担的在浏览器中使用 indexedDB


截止今天,localForage 在 github 的 star 已经22.8k了,可以说 localForageindexedDB 算是相互成就了。


什么是 indexedDB


IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象)。


存取方便


IndexedDB 是一个基于 JavaScript 的面向对象数据库。IndexedDB 允许你存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。


之前我们使用 webStorage 存储对象或数组的时候,还需要先经过先序列化为字符串,取值的时候需要经过反序列化,那indexedDB就比较完美的解决了这个问题,可以轻松存取对象或数组等结构化克隆算法支持的任何对象。


stackblitz.com/ 网站为例,我们来看看对象存到 indexedDB 的表现



异步存取


我相信你肯定会思考一个问题:localStorage如果存储内容多的话会消耗内存空间,会导致页面变卡。那么 IndexedDB 存储量过多的话会导致页面变卡吗?


不会有太大影响,因为 IndexedDB 的读取和存储都是异步的,不会阻塞浏览器进程。


庞大的存储量


IndexedDB 的储存空间比LocalStorage 大得多,一般可达到500M,甚至没有上限。


But.....关于 indexedDB 的介绍就到此为止,详细使用在此不再赘述,因为本篇文章我重点想介绍的是 localForage!


什么是 localForage


localForage 是基于 indexedDB 封装的库,通过它我们可以简化 IndexedDB 的使用。



兼容性


想必你一定很关注兼容性问题吧,我们可以看下 localStorage 与 indexedDB 兼容性比对,两者之间还是有一些小差距。


image.png


但是你也不必太过担心,因为 localforage 已经帮你消除了这个心智负担,它有一个优雅降级策略,若浏览器不支持 IndexedDB 则使用 WebSQL ,如果不支持 WebSQL 则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。


localForage 的使用



  1. 下载


import localforage from 'localforage'




  1. 创建一个 indexedDB


const myIndexedDB = localforage.createInstance({
name: 'myIndexedDB',
})


  1. 存值


myIndexedDB.setItem(key, value)


  1. 取值


由于indexedDB的存取都是异步的,建议使用 promise.then() 或 async/await 去读值


myIndexedDB.getItem('somekey').then(function (value) {
// we got our value
}).catch(function (err) {
// we got an error
});

or


try {
const value = await myIndexedDB.getItem('somekey');
// This code runs once the value has been loaded
// from the offline store.
console.log(value);
} catch (err) {
// This code runs if there were any errors.
console.log(err);
}


  1. 删除某项


myIndexedDB.removeItem('somekey')


  1. 重置数据库


myIndexedDB.clear()


以上是本人比较常用的方式,细节及其他使用方式请参考官方中文文档localforage.docschina.org/#localforag…



VUE 推荐使用 Pinia 管理 localForage


如果你想使用多个数据库,建议通过 pinia 统一管理所有的数据库,这样数据的流向会更明晰,数据库相关的操作都写在 store 中,让你的数据库更规范化。


// store/indexedDB.ts
import { defineStore } from 'pinia'
import localforage from 'localforage'

export const useIndexedDBStore = defineStore('indexedDB', {
state: () => ({
filesDB: localforage.createInstance({
name: 'filesDB',
}),
usersDB: localforage.createInstance({
name: 'usersDB',
}),
responseDB: localforage.createInstance({
name: 'responseDB',
}),
}),
actions: {
async setfilesDB(key: string, value: any) {
this.filesDB.setItem(key, value)
},
}
})

我们使用的时候,就直接调用 store 中的方法


import { useIndexedDBStore } from '@/store/indexedDB'
const indexedDBStore = useIndexedDBStore()
const file1 = {a: 'hello'}
indexedDBStore.setfilesDB('file1', file1)

后记


以上就是本篇文章的所有内容,感谢观看,欢迎留言讨论。


作者:阿李贝斯
来源:juejin.cn/post/7275943591410483258
收起阅读 »

你网站的网速是很快,但是在没有网络的情况下你怎么办?🐒🐒🐒

web
在现代的网络世界里,5G 网络的普及,我们可以访问一个网站或者使用一个 App 的速度极其快,但是在没有网络的情况下你啥都看不了,只能大眼瞪小眼了。 离线应用是指通过离线缓存技术,让资源在第一次被加载后缓存在本地,下次访问它时就直接返回本地的文件,就算没有网络...
继续阅读 »

在现代的网络世界里,5G 网络的普及,我们可以访问一个网站或者使用一个 App 的速度极其快,但是在没有网络的情况下你啥都看不了,只能大眼瞪小眼了。


离线应用是指通过离线缓存技术,让资源在第一次被加载后缓存在本地,下次访问它时就直接返回本地的文件,就算没有网络连接。


通过离线应用,主要有以下几个优点:



  1. 在没有网络的情况下也能打开网页。

  2. 由于部分被缓存的资源直接从本地加载,对用户来说可以加速网页加载速度,对网站运营者来说可以减少服务器压力以及传输流量费用。


离线应用的核心是离线缓存技术,要实现这种方式,我们可以使用 Service Worker 来实现这种缓存技术。


什么是 Service Worker


Service Worker 服务器和浏览器之间的之间的桥梁或者中间人。


Service Worker 运行在一个与页面 JavaScript 主线程独立的线程上,并且无权访问 DOM 结构。但是它能拦截当前网站所有的请求,对请求使用相应的逻辑进行判断,如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器。从而大大提高浏览体验。


注册 Service Worker


要使用 Service Worker,首先我们要判断浏览器是否支持 Service Worker,具体代码逻辑如下:


if (navigator.serviceWorker) {
window.addEventListener("DOMContentLoaded", function () {
navigator.serviceWorker.register("/worker.js");
});
}

这段代码的主要目的是在支持 Service Worker 的浏览器中,当页面加载完成后注册一个指定的 Service Worker 脚本。这个传入的 worker.js 就是 Service Worker 的运行环境。


这个脚本被安装到浏览器中后,就算用户关闭了当前网页,它仍会存在。 也就是说第一次打开该网页时 Service Workers 的逻辑不会生效,因为脚本还没有被加载和注册,但是以后再次打开该网页时脚本里的逻辑将会生效。


Service Worker 安装和激活


注册完成后,worker.js 文件会自动下载、安装,然后激活。它提供了一些 API 给我们做一些监听事件:


self.addEventListener("install", function (e) {
console.log("Service Worker 安装成功");
});

self.addEventListener("fetch", function (event) {
console.log("service worker is fetch");
});

当 install 完成并且成功激活之后,就能够监听 fetch 操作了,如上代码所示,输出结构如下图所示:


20230918074308


使用 Service Workers 实现离线缓存


在上面的内容我们已经知道了 Service Workers 在注册成功后会在其生命周期中派发出一些事件,通过监听对应的事件在特点的时间节点上做一些事情。


在 Service Workers 安装成功后会派发出 install 事件,需要在这个事件中执行缓存资源的逻辑,实现代码如下:


// 当前缓存版本的唯一标识符,用当前时间代替
const cacheKey = new Date().toISOString();

// 需要被缓存的文件的 URL 列表
const cacheFileList = ["/index.html", "/index.js", "/index.css"];

// 监听 install 事件
self.addEventListener("install", function (event) {
// 等待所有资源缓存完成时,才可以进行下一步
event.waitUntil(
caches.open(cacheKey).then(function (cache) {
// 要缓存的文件 URL 列表
return cache.addAll(cacheFileList);
})
);
});

在 install 阶段我们就已经指定了要被缓存的内容了,那么就可以在 fetch 阶段中听网络请求事件去拦截请求,复用缓存,代码如下:


self.addEventListener("fetch", function (event) {
event.respondWith(
// 去缓存中查询对应的请求
caches.match(event.request).then(function (response) {
// 如果命中本地缓存,就直接返回本地的资源
if (response) {
return response;
}
// 否则就去用 fetch 下载资源
return fetch(event.request);
})
);
});

通过上面的操作,创建和添加了一个缓存的库,如下图所示:


20230918080142


缓存更新


线上的代码有时需要更新和重新发布,如果这个文件被离线缓存了,那就需要 Service Workers 脚本中有对应的逻辑去更新缓存。


这可以通过更新 Service Workers 脚本文件做到,浏览器针对 Service Worker 有如下机制:



  1. 每次打开接入了 Service Workers 的网页时,浏览器都会去重新下载 Service Workers 脚本文件,如果发现和当前已经注册过的文件存在字节差异,就将其视为新服务工作线程。

  2. 新 Service Workers 线程将会启动,且将会触发其 install 事件。

  3. 当网站上当前打开的页面关闭时,旧 Service Workers 线程将会被终止,新 Service Workers 线程将会取得控制权。

  4. 新 Service Workers 线程取得控制权后,将会触发其 activate 事件。


新 Service Workers 线程中的 activate 事件就是最佳的清理旧缓存的时间点,代码如下:


var cacheWhitelist = [cacheKey];

self.addEventListener("activate", function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
// 不在白名单的缓存全部清理掉
if (cacheWhitelist.indexOf(cacheName) === -1) {
// 删除缓存
return caches.delete(cacheName);
}
})
);
})
);
});

这样能确保只有那些我们需要的文件会保留在缓存中,我们不需要留下任何的垃圾,毕竟浏览器的缓存空间是有限的,手动清理掉这些不需要的缓存是不错的主意。


参考资料



总结


Service Worker 作为服务器和浏览器两者之间的桥梁,它并且可以缓存技术,通过这种方式,在断网的时候,去获取缓存中相应的数据以展示给客户显示。


当断网之后,直接给他页面返回一个俄罗斯方块让他玩足一整天。


作者:Moment
来源:juejin.cn/post/7279321729462616121
收起阅读 »

我入职了

web
前言 从5月底离职到现在,一个半月的时间,通过内推+BOSS直聘,前前后后约到了10家面试,终于拿到了一个满意的offer,一家做saas系统的上市公司。 本文就跟大家分享下我这段时间找工作的心路历程,欢迎各位感兴趣的开发者阅读本文。 无所畏惧 6月1号,裸辞...
继续阅读 »

前言


从5月底离职到现在,一个半月的时间,通过内推+BOSS直聘,前前后后约到了10家面试,终于拿到了一个满意的offer,一家做saas系统的上市公司。


本文就跟大家分享下我这段时间找工作的心路历程,欢迎各位感兴趣的开发者阅读本文。


无所畏惧


6月1号,裸辞的第一天,制定了接下来的每日计划,终于可以全身心投入做自己喜欢的事情啦。



  • 06:30,起床、洗漱、蒸包子

  • 07:00,日常学英语

  • 08:00,吃早餐,顺便刷一下BOSS直聘

  • 08:30,日常学算法、看面试题

  • 11:40,出门吃饭,午休

  • 14:00,维护开源项目

  • 18:00,出门吃饭,去附近的湖边逛一圈,放松下心情

  • 20:30,将当天所学做一个总结,归纳成文章

  • 23:00,洗澡睡觉,充实的一天结束


image-20230717212836044


be9877e8144d437c9a2f9ea9b188c7fe


内推情况


通过在掘金、V站和技术群发的文章,为我带来了20多个内推,从大厂到中厂到小厂,约到面试的只有4个。其他的技术部认可我,但是HR卡学历(统招本科)。


image-20230717214830630


image-20230717215823196


image-20230717215836796


image-20230718195936071


无响应式网站开发经验被拒


这是一家杭州的公司,可以远程办公,跟我约了线上面试。做完自我介绍后,他对我的开源项目比较感兴趣,问了我:



  • 你为什么会选择写一个聊天工具来作为开源项目?

  • 你的截图功能是怎么实现的?


行,那我们来聊几个技术问题吧。



  • 讲一下webpack的打包流程

  • webpack的热更新原理是怎样的?

  • 讲一下你对webpack5模块联邦的理解


这些问题回答完后,他问我你有做过响应式网站开发吗?


我:我知道怎么写一个响应式网站,在工作中我没接触过这方面的业务。


面试官:行,那你讲一下要怎么实现一个响应式网站?


我:用css3的媒体查询来实现,如果移动端跟PC端布局差异很大的话,就写两套页面,对应两个域名,服务端根据http请求头判断设备类型来决定是否要重定向到移动端。


面试官:还有其他方案吗?


我:嗯...,应该没有了吧,我只了解过这两种方式。


面试官:好吧,在seo优化方面,前端要从哪些点去考虑?


我:标签语义化、ssr服务端渲染、img标签添加alt属性来、在head中添加meta标签、优化网站的加载速度,提高搜索引擎的排名。


面试官:我的问题问完了,你有什么想了解的?


我:团队人员配比是怎么样的?


面试官:我们这个团队,前端的话有4个人,有2个后端。然后,前端有时候需要用node写一些接口。


我:如果我进去的话,主要负责哪块业务的开发?


面试官:负责一些响应式网站业务的开发,再就是负责我们内部系统的一个开发。


我:行,我的问题就这些。


面试官:OK,那今天的面试就先到这。



大概过了3天时间,也没有给我答复。因为这个是他们老板在v站看到了我的文章,觉得我还不错,加了微信,让他们技术面的我,我也不好意思问结果。


很大可能是因为我没有响应式网站的实际开发经验,所以拒了我吧。😔



期望太高被拒


这是一家上海的公司,他们的主要业务是做产品包装。有自己品牌的网站、小程序、app。他们公司一个负责公司内部事务的人加了我微信,跟我简单聊了下,让我体验下他们的产品,看看有没有什么我能帮到他们的地方。


image-20230718161603524


image-20230718161614565


image-20230718161740495


image-20230718161655844


聊完后,他一直没有主动联系我,我也没有约到其他面试,我就主动出击了,看能不能确定下来,约个面试。


image-20230718162410852


image-20230718162435259


image-20230718162606997


我整理了一套方案,发到了他的邮箱,期望薪资我写了20k,过了两天,他给了我答复,告诉我期望太高。我说薪资可以商量的,但无济于事。


image-20230718164059392


白嫖劳动力


这家公司是做物流的,是一个群友曾经面过的公司,但是最后没去。看到hr在朋友圈发了招聘信息,在招高级前端,就推给我了,约了线下面试。


到公司后,按照惯例填了一张表,写了基本信息。过了一会,一个男的来面我,让我做了自我介绍,顺着我的回答提问了公司的规模以及业务。


提问完成后,他说我看你期望薪资写了15k,你上家才12k,为什么涨幅会这么高?


我:因为我经过两年的努力以及公司业务的积累,自己的技术水平有显著提升。我对这一行很喜欢,平常80%的业余时间都用来学习了。


面试官:好,我让技术来面下你,看看你实力如何。


等了5分钟左右,他来了告诉我说:技术在开会,我先带你做一下机试吧。你把这两个页面(后台管理系统登陆页与后台首页)画出来就行。


我把页面画出来后,又过来一个人看我做的,他说 你就把页面画出来了?我说:对啊,刚才带我过来那个人说让我画页面出来的。


他说,那可能是他没说清楚,那这样肯定是不行的,你要自己重新建项目,把页面画出来后,要调接口的,把整个流程走通才行的。现在已经11点40多了,你下午再过来继续弄吧。


我直接满脸问号,把整个流程走通只是时间问题,你们这个机试到底想考察啥呢?


他说,页面在我们这里不重要,调接口,走通整个流程才重要。


我直接无语了,就说 抱歉,我下午有其他安排了,我就先走了。


image-20230718172136268


焦虑不安


时间来到6月20日,已经好多天没有约到面试了,逐渐焦虑起来了,虽然兜里余粮还有很多,但始终无法静下心来做事情,充满了对未知的恐惧。


就在这时,我还迎来了别人的嘲讽。他成功让我生气了,我努力的平复心情,告诉自己不要把这件事放在心上,通过让自己忙起来转移注意力,通过学习来克制焦虑。


image-20230718191713122


image-20230718191749187



白天我可以通过学习来缓解焦虑,但是一到晚上躺在床上,我就会开始胡思乱想。想着自己一直找不到工作怎么办,难道我真的不适合吃这碗饭吗,我怎么这么差劲,连个面试都约不到...唉,怎么会这样,我明明已经很努力了,为什么结果会是这样...



完善打招呼语


内推无望,BOSS直聘发消息也是送达、已读未回。这个时候,有个网友建议我把招呼语改改,hr不懂什么开源不开源的,他们只会关键词匹配,只要包含了,就会收你简历,于是我就把打招呼语改成了:


image-20230718195551550



招呼语改完后,效果好了一些,终于有HR愿意收我简历了🥳



学历歧视、贬低、pua、拒了offer


改完打招呼语后,我在BOSS直聘上约到了第一家面试,这家公司是做可视化VR编辑器的,团队有30来个人,BOSS直聘的薪资范围是20K~25K。


我经历了五轮面试,拿到了offer,给了18K,但是最终还是拒绝了,本章节就跟大家分享下这段故事。


技术面


技术面是去线下的,按照惯例做完自我介绍,面试官提问了我:



  • 你刚才说你写了个web端的截图插件,你能讲一下你是怎么实现的吗?

  • 我看你上家公司是做动画编辑器的,你在做这个项目的时候有遇到过哪些难点吗?

  • 你刚才提到了你为编辑器做了一些性能优化,你都做了哪些优化?

  • 你刚才说你还实现了svg类型的文本组件搜索功能,你能讲讲你是如何实现的吗?


问完这些后,他说我的问题问完了,你有什么想要了解的吗?


我:团队人员配比是怎么样的?


面试官:我们这边是重前端的,因为是做编辑器嘛,难点在前端这块,目前有4个前端,计划再招3个,再就是有几个做算法的、做c++的,1个产品经理,2个后端,2个UI,3个测试。


我:如果我进去的话,是做哪方面的项目?


面试官:你进来的话,主要是负责VR编辑器项目的,这个项目刚开始做。目前的话,比较累,会加班,基本上是早9晚8,有时候可能要10点才能走。再就是,我们这边是大小周,你能接受的吧?
我:哦哦 明白了,我可以接受


面试官:那行,你稍等下,我让我们的产品经理面下你。


产品经理面


过了一会儿,产品经理过来了。他说:我们的技术对你的评价很高,我再来面面你,你先做个自我介绍吧。做完自我介绍后,产品经理顺着我的介绍进行了提问:



  • 你刚才说你这个截图插件Gitee的产品经理在网上看到了,是码云官方的吗?

  • 我看你上家公司也是做编辑器的,你们这个产品主要面向的用户群体是哪些?

  • 你们这个产品啥时候上线的,你主要负责的是什么?

  • 你们的团队配比是怎么样的?

  • 你们在开发项目时,是如何管理git分支的?


问完这些后,他让我稍等下,让HR来面下我。


过了3分钟左右,他过来说:我们HR这会儿太忙了,抽不开身,这样,你今晚有空吧,我让她跟你电话聊聊。我回答说,7点后我都有空。


HR电话面


因为约了晚上7点的电话面试,所以我就随便吃了点,就匆匆忙忙回家等电话了。我等到了晚上9点,也没电话打过来,我就在boss直聘问了下,对方说:可能是HR忙忘了,我让她明天给你打。


晚上躺床上睡觉的时候,不出意外,我又开始胡思乱想了,心想:我这煮熟的鸭子该不会飞了吧,会不会是面试表现的不好人家婉拒我了呢,会不会是...,又焦虑了。


到了第二天下午2点多的时候,HR终于给我打了电话,问我期望薪资多少。我说22k,她问我上家薪资多少,我说12k。不出意外,她很震惊:你这涨幅也太大了吧,能说说原因吗?我说:你们这里是大小周,工作强度比较大,而且做的项目也是较为复杂的,我看BOSS直聘标的价格也是20k~25k。


她说:我们这个岗位是中、高级前端都招聘的,你这边最低能接受的薪资是多少呢?
我说:20k


她说:行,了解了,我再跟面试官对接下,晚些时候我加你微信聊。


又过了一天,她加了我微信,跟我说:我只匹配他们的中级开发岗位,让架构师再跟我聊聊。


image-20230718210811195


前端架构师面


跟架构师约的是电话面试,做完自我介绍后,他提问了我:



  • 讲一下webpack的打包原理

  • 讲一下webpack的loader和plugin

  • 讲一下webpack5的模块联邦

  • 讲一下Babel的原理,讲一下AST抽象语法树

  • 讲一下你所知道的设计模式

  • 讲一下浏览器的垃圾回收机制

  • 讲一下浏览器的渲染流程

  • 讲一下浏览器多进程的渲染优势

  • 谈谈你对浏览器架构的理解


我回答完之后,他说:我大概知道你的技术水平了。你现在的水平还不到P6,也就P5多一点,远远不及P7。


我刚才问你的问题,你每回答完一个我都问你有没有要补充的,你都说没有,我从你嘴里没听到任何性能优化相关的东西,这些知识现在还都不是你的,你只知道这么个东西,缺乏实践。就好比,我刚问了你垃圾回收机制,你回答的是chrome的,那火狐呢?edge呢?


你对你未来的规划是怎么样的?


我说:我还是以技术为主,我会继续学习来充实自己,未来如果有机会的话,希望能做到技术管理的位置。


面试官冷笑了下说:你一个大专怎么做管理?


我沉默了一会儿说:未来我会把自己的学历提升下的


面试官:你要认清自己的地位,你要想一下你的价值是什么?你能给我们公司带来什么?我们要用到three.js,你只是学过它,没有落地项目做支撑,你进来后我们还是要给你时间来熟悉项目的,跟没学过的人没啥两样。就好比,我问你three.js的坐标系用的是啥,你都不知道。
我:这个我知道,它用的是右手坐标系


面试官楞了一下说:你知道这个也没啥的,这很简单的,我们这边随便拉一个人都会这些,而且比你厉害。


我继续保持沉默。


面试官:我对你的评价就这么多,你在我们这边是能学到很多东西的,你多想想我今天跟你说的,我不知道你的业务能力怎么样,回头我再跟其他面试官聊聊,今天的面试就先到这。


第二天,HR联系我了,跟我说薪资在16k~18k左右,跟我约了下午1点30的面试。


image-20230718215534222


image-20230718215606136


老板面


到公司后,HR直接带我进了老板办公室,跟我说这个是X总,你们聊吧。 跟老板聊了一个多小时,聊的内容大概是谈人生、理想,大概能记得起的一些问题有:



  • 你觉得你是一个什么样的人?

  • 你有哪些优点?

  • 你想成为一个什么样的人?

  • 你觉得你的技术水平怎么样?

  • 如果让你给自己打标签,你会打什么标签?

  • 回看你的过往人生,你后悔吗?


考虑再三 终拒offer


从公司回来后的第二天,HR告诉我面试结束了,最终给我定的薪资是17k,发了offer。


image-20230718222724254


发了offer后,我本该高兴的,但是我却高兴不起来,那一晚我想了很多,觉得早9晚8,大小周。这个钱还是太少了,而且那个前端架构师说的话让我很不舒服,pua的气息太重了。入职后,跟这种人一起工作,我也不会开心。思考再三后,我最终还是拒掉了这个offer。


image-20230718222252549


image-20230718222337117


比较钟意的小外企


这是我在BOSS直聘约到的第二家面试(15k~20k),面试体验很好。到公司后,接待我的人很有礼貌,告诉我前端是技术总监来面的,他还没来,你先坐着等他一会儿。


等了一会儿后,看到了技术总监,主动跟我握了个手。然后说:他临时有个会开,让我稍等下他,然后安排我在会议室坐了会儿,倒了一杯水给我。


我在会议室坐了40多分钟,他会开完了,喊我去办公室聊,按照惯例做完自我介绍后。他问我:



  • 你刚才提到了你做了编辑器的性能优化,你具体是怎么做的?

  • 你们这个编辑器前端编辑的应该是dom吧,最后生成的视频是怎么生成的?

  • 我看你的项目经验都是vue,你应该对vue全家桶都很熟了吧?


问完这些问题后,他用笔记本打开了我简历上的项目,边看边问我这块你是怎么实现的,有没有遇到过啥问题,你是怎么解决的。项目看完后,他说你技术没问题,我了解完了。我跟你介绍下我们这边的项目,我们在做...。介绍完了后,他问了我离职原因,以及我的期望薪资。


我说了20k,他说,站在客观角度来说,你的学历是大专,在我们这里拿到这个数很难,我们也不是什么特别有钱的公司。但是,我们的产品是很有发展前景的,已经拿了一轮800w美金的融资了,这个岗位我在boss直聘挂了1个月了,收到了300多份简历,有很多大厂出来的,但是我都不太满意,偶然间看到你的简历,觉得你是一个爱学习、肯钻研的人,就约你来面试了。你是我面的第一个前端。


我听他这么说后,我就说:那薪资17、18也可以。


他说:行,明白了,我回头跟老板说说,尽量帮你争取。我们这边工作氛围很棒,团队是一支很精湛的团队组成的,我们这边做算法的是麻省理工毕业的,这边的一个后端是之前抖音短视频架构组出来的。你在这里也能学到很多前端之外的东西,我们是早上10点上班,晚上6点30下班,不打卡,双休。


我听他这么说后,觉得很不错,就说:那15k也行。


他说:你也不用太勉强,不然你进来了也不开心,我们这里发展空间很大的,未来拿到更多的融资,你在这里是可以涨薪的。那今天我们就先到这里,后天就是端午节了,这样,我端午节后的那周给你具体的答复。


就这样,我又进入了焦灼的等待期。


端午节后的第2天,那边还没答复,我就主动问了下,他给我的答复是:


image-20230721214840273


又过了3天,一直没约到面试,焦虑的很。我就又厚着脸皮问了下情况,得来的答复是他们还没找到合适的产品经理。(这个时候,心里很难受到极点了,泪水在眼珠里打转,我焦虑到哭了😔)


image-20230721215034742



晚上躺在床上又开始胡思乱想了,觉得老天很不公平,为什么好运总是不能降临到我头上。唉...就这样想着想着,不知想了多久,也不知道自己睡着了没,只记得手机的闹钟响了,关了闹钟继续睡去了...



随遇而安


又浑浑噩噩的过了几天,时间来到7月3日,BOSS直聘有人跟我约面试了,一天下来约了3个面试,都是很多天之前联系的,今天才收了我简历,我的心情终于好了一些。


做物联网的公司


这家公司距离我住的地方很近,步行1.1公里就能到。BOSS直聘标的价格是(15k~18k),到了公司后,前台让我扫二维码关注他们的公众号,填写面试登记表(基本信息、期望薪资、上家公司薪资)。


填写完后,前台带我进了公司,等了5分钟左右,面试官来了,按照惯例做完自我介绍后,他问了我:



  • 你讲一下vue双向绑定的原理

  • 讲一下vue3相比vue2,它在diff算法上做了哪些优化?

  • Vue2为什么要对数组的常用方法进行重写?

  • Vue的nextTick是怎么实现的?

  • 讲一下你对EventLoop的理解吧

  • 讲一下webpack5的模块联邦


这里我讲一下EventLoop这个问题吧,我回答完之后,他反问我:你确定宏任务先执行的吗?我很确信的说,是的,宏任务先执行的。(之所以这么自信是因为我之前特意研究了这方面的知识,写了大量的用例做验证,写了文章做总结,绝对错不了)


那你意思是,setTimeoutPromise().then()先执行,


我回答:是的。


面试官:你回去再查查资料吧,看一看到底是哪个先执行吧。我的问题问完了,你有什么想问我的吗?


我问了他部门做的产品是什么、团队情况、如果我进来的话负责的是哪块的东西。了解完之后,他让我稍等下。


过了3分钟左右,HR过来了,她问我觉得这场面试咋样,刚才面你的人职级在我们这里算是比较高的了,然后她就跟我介绍了她们公司的情况以及福利制度。介绍完之后,她问我说:我对你写的这个期望薪资比较好奇,我看你上家薪资是12k,怎么期望薪资写了18k呢?涨幅这么高。


我说了理由后,她说:今年市场很差,求职者很多,很多公司都在降低成本,你要是放在互联网红利的时候,你这个涨幅没问题,但2023年这个大环境,你这个涨幅是不可能的。你这边最低期望薪资是多少?


我说:16k,她在求职表上用笔写了下。随后她说,那行,今天的面试就先到这,后面我们电话联系。


回到家后,我立马查了我写的那篇事件循环的文章,验证下我有没有记错。看完之后我发现我并没有记错,于是我又问了下AI,他给我的答案是:


image-20230722182035941


我就纳闷儿了,于是我说宏任务先执行的吧,它的回答是:


image-20230722182223460


它还在嘴硬,我就反问了句,你确定?它终于改变口风了。


image-20230722182301304



这家公司是7月5号面的,等了3天都没联系我,看来是有人要价比我低🌚



做交易所的公司


这家公司是在一个技术交流群看到的招聘信息,公司在海外,远程办公的方式,给的薪资是20k~25k。按照惯例做完自我介绍后他问我:



  • 讲一下vue的生命周期

  • 讲一下computed与watch的区别

  • 讲一下vue的双向绑定和原理

  • 讲一下vue3相比vue2有哪些提升

  • 你有开发过不用脚手架的项目吗?

  • seo优化有了解过吗?讲一下你的见解

  • 响应式网站开发你知道哪些方案?


回答完这些问题后,按照惯例我问了他团队的人员情况以及项目情况,就结束了这场面试。他问的问题也很简单,我回答的也不错。但是,过了3天,最终还是没下文。


做工具软件的公司


这家公司是朋友内推的,经历了三轮面试,我看了下BOSS直聘标价是15k~25k。先是用腾讯会议,让打开屏幕共享和摄像头,做一份笔试题。内容是填空题、判断题、代码题。填空跟判断就是一些简单的问题,代码题是:



  • 观察一组数列,写一个方法求出第31个数字是什么?(通过观察后,发现那是一组斐波那契数列

  • 实现一个深拷贝函数

  • 写一个通用的方法来获取地址栏的某个参数对应的值,不能使用正则表达式。


线上技术面


笔试题做完发给HR后,等待了半个小时,面试官进入了腾讯会议,按照惯例做完自我介绍后他问我:



  • vue3的diff算法做了哪些改进

  • vue双向绑定的原理是什么

  • 假设要设计一个全局的弹窗组件你会怎么设计?

  • 如果这个弹窗组件可以弹出多个,消息会垂直排列,新消息会把旧消息顶起来,每个消息都可以设置一个停留时间,到了时间后就会消失,这一块你会怎么设计?

  • 你了解堆这种数据结构吗?讲一讲你对它的理解


回答完这些问题后,我按照惯例问了他项目情况以及我进去后所负责的模块,就结束了这场线上面试,第二天收到了一面通过的答复。


image-20230722234026788


线下总监面


时间来到7月6日,本来是7月5日面试的,但是面试官临时有事改了时间。


image-20230722234450217


这家公司在林和西地铁站这边,地处CBD,公司应该是很有钱的。到了公司后,HR接待了我,带我进了会议室,等了3分钟左右,技术总监过来了,做完自我介绍后,他问我:



  • 挑一个你最拿手的项目讲一下吧

  • 看你写了很多开源项目,是个爱捣鼓的人,讲一下你的开源项目吧

  • 你会Java,是用的SpringBoot吗?你讲一下你这个开源项目的后端服务是怎么设计的吧

  • 你都知道哪些数据库?进行SQL查询时,你有哪些优化手段来优化查询效率

  • 你讲下vue3和vue2的一个区别吧

  • 你觉得你跟别人相比,你的优势是什么?


回答完这些问题后,我问了他团队的规模以及公司的人员情况,他跟我说:我们公司总共有52个人,很大一部分都是程序员,他们都是全能的,任何一个人拉出来,前端、后端、运维都能做,就好比你让运维来写前端的业务代码他也能写,你也看到了,我们目前不缺人,是想招一个优秀的人做候补。我们这边的技术栈是vue和Electron,你进来的话,负责前端页面以及一些node后端服务的编写。你稍等下,我让我们的HR来面下你。


线下HR面


等了4分钟左右,HR来了,她带我去到了另一个会议室聊,她问了我:



  • 你的离职原因是什么?

  • 你对新工作的期望是怎么样的?

  • 如果公司让你休年假,你必须要做一件事情,你会做什么事情?


问完这些问题后,她问了我期望薪资,我说了20k,她说了一些其他的东西,大概意思就是给不到的话你最低期望是多少,我说18k。


她说:行,了解了,我们这边要做一下横向对比,尽快给你答复,你放心无论结果如何,我们都会给你一个答复的。


面试完的第二天,那个hr跟我发消息说结果还没定。


image-20230723002131979


进入新的一周后,她给我发来了感谢信。


image-20230723002232232



只能感叹卷王太多了,全干工程师的价格已经被你们打到18k以下了👍



做旅游的公司


这是一家在BOSS直聘上约到的面试(11k~17k),到了公司后,HR先让我做了一份笔试题,这份笔试题全是八股文,我把答案短的都写了,比较长的就写了面试时候讲。


做完笔试题后,她带我进了会议室,是两个人面我,一个是前端负责人,另一个是他的领导,做完自我介绍后,那个前端负责人说:我之前在网上看到过你的截图插件,写的很不错。我相信你的技术肯定没问题的,他和他的领导交叉问了我问题:



  • vue3相比vue2做了哪些提升?

  • 讲一下vue的diff算法吧

  • 讲一下V8的垃圾回收机制

  • 讲一下chrome是如何渲染一个网页的

  • 大文件分块上传以及断点续传,你会怎么实现


回答问这些问题后,他们让我稍等下,找来了HR跟我聊,HR问了我期望薪资,我说17K,她也惊讶的说,你上家才给你12k,你怎么一下子要求涨幅这么多,是出于什么考虑呢?我说了理由后,她说:结合我们公司的情况和制度,我们这边给不到你这么多。


我:那大概能给到多少呢?


HR:15k,有些事情我要提前跟你说清楚,我们这边试用期是一个月,现在项目组比较忙,是需要加班的,基本上是996,大概要忙到9月份,项目第一期做好后,就可以按照正常时间上下班了。忙的这段时间是可以累积调休的。试用期不缴纳社保,我们只有五险,没有公积金。


我听了这些后,头皮发麻,一时不知道说啥,我就说了:哦哦 好


HR:如果你能接受的话,我这边是没问题的。


我:我要考虑考虑,晚些时候给你答复。


到了第二天,HR在boss直聘上给我发了消息,问我考虑的如何了,我拒绝了她。


image-20230723004628907


做saas系统的上市公司


这家公司是我6月13号在BOSS直聘上沟通的,6月27号收了我简历,7月3号跟我约了面试,一直持续到7月14号,经历了三轮面试,最终拿到了offer。


HR面(线上)


按照惯例做完自我介绍后,HR让我介绍下公司的产品,以及我在公司的一个职位,技术水平在公司排第几,为什么离职,职业规划和一些其他问题:


HR:你能接受出差吗?


我:这个看情况,如果距离不是很远,出差时间不超过1周,交通、住宿这些都能报销的话,我是接受的。


HR:交通、住宿这些肯定都报销,不然谁愿意出差,我们除了这个外,每天还有一个xxx块的补贴。你在广州这边,出差的话就是去深圳,一般也就去个3、4天,你是前端,几乎不怎么出差。


我:哦哦 那可以的


HR:你对加班是怎么看的?


我:加班的话,如果是项目比较急,我是没问题的,但是如果是其他原因的一些强迫加班,我就不太能接受了


HR:我们这边加班的话,是项目比较急的时候才会,加班不会太频繁。如果加班的话,是可以1:1兑换成调休的,法定节假日加班的话,我们会按照法律规定发放3倍工资


我:哦哦 行


HR:你这边是在广州,如果面试通过的话,是广州的编制。我们广州分部在xx,距离这块的话,你能接受吧?


我:我有查过公司的位置,从我住的这边过去也挺近的,40分钟左右就到了,我可以接受


HR:那行,今天的面试就先到这,后面会安排我们的技术面下你。


技术面(线上)


HR面完后,过了一天,跟我约了技术面。


image-20230723083059122


时间来到7月5号,一男一女,两个人一起面的我。按照惯例做完自我介绍后,他们问了我:



  • 我看你写了很多开源项目和技术文章,这是一个很好的习惯,能很多年坚持做一件事,并且能把这件事情做好,你很厉害。

  • 刚才听你自我介绍说你会Java,你Java目前是一个什么水平?

  • 我看你们公司项目是做web动画编辑器的,你在这个项目中担任的角色是什么?有没有什么印象比较深刻的难题,你是如何解决的?

  • 我看你简历上还写了一个海外项目的重构经验,你能介绍下这个项目吗?以及你在这里面担任的角色是什么?

  • 我看你简历上的项目都是以Vue为主的,那你应该对Vue很熟悉,你讲一下watch与computed的区别

  • vue中组件通信都有哪些方式?

  • vuex刷新后数据会丢失,除了把数据放本地存储外,你还知道其他什么方法吗?

  • 我看你写的那个截图的开源项目用到了canvas,你应该对canvas很熟悉了吧,有这样一个场景:超市中的货架,上面有很多商品。现在要把这个货架用canvas画出来,商品需要支持一些交互,调整大小,移动位置,你会怎么实现?


问完这些问题后,按照惯例,我问了下他们的团队情况以及所做的业务,我进去后所负责的模块,就结束了这场面试。


事业部总经理面(线上)


过了一天,告知我技术面通过了,跟我约了第二天的面试,我看到她说:总经理同时面我跟其他两位候选人。我就压力有点大,从业4年了,第一次遇到这种大场面😂


image-20230723084854849


image-20230723085150444


到了约定好的面试时间,我跟其他两位候选人都进入了会议,过了10分钟,总经理还是没有进来,我就私聊问了下HR。过了一会儿,HR进入了会议。她说:总经理临时有点事情,要换个时间约面试了,真不好意思。


image-20230723085623543


时间来到7月10号,总经理进入腾讯会议后,他先让我们轮流做自我介绍,然后抛出问题,让我们挨个回答,最后他做了总结,给我们三个人做了评价:



  • A(1号面试者):你的组织协调能力应该不错

  • B(我):我看了你在掘金上发的文章以及个人网站,能看出来你的技术实力是最强的。

  • C(3号面试者):你的业务能力应该不错


说完这些后,总经理说晚上会抽时间再单独打电话给我们再聊聊,到了第二天早上我一直没等到电话,我就问了下HR。


image-20230723090532956


过了半个小时左右,电话打来了,他问了我离职原因和两个场景题:



  • 前端的框架有很多,当有新项目的时候,你会通过哪些方面来考虑应该使用哪个框架?

  • 有一个上线的项目它是vue2写的,如果想升级到vue3,但是没有太多的专用时间来做这件事,此时你会怎么做?


回答完这些问题后,挂断了电话,下午1点40多的时候,HR联系我说面试通过了,开始走发offer流程了,到时候会有她的另一个同事联系我。


时间来到7月14号,第一面面我的那个人打电话给我了,跟我聊了薪资、福利制度和五险一金,她说我们公司的五险一金是按照实际工资进行缴纳的,没有绩效,有季度奖和年终奖,会按照公司的盈利情况以及你的工作表现进行发放,后面还有其他问题的话,你随时联系加你微信的那个HR,她是华南区域的负责人。


电话挂断后,过了2小时左右吧,HR联系我说发offer了,我突然想到忘记问上下班时间了,我就确认了下(BOSS直聘标记了时间)。


image-20230723093034336


image-20230723092444819



截止发文时间,我已经入职这家公司很多天了,团队氛围很棒。入职的第一天下午,我接到了我们主管的电话,他让我第二天去一趟武汉,事业部的总经理是在武汉分部的,他要见一下你,那边也有前端在,跟你讲解下业务,熟悉熟悉团队的人。


广州这边的后端架构师同事告诉我出差是不需要自己花钱的,公司内部有一个平台可以直接在上面定高铁票和酒店,我的内部OA和钉钉账号后,他教了我怎么操作。


来武汉后,跟这边的团队成员熟悉了下,聊了下业务,主管告诉我说大概7月26号左右就可以回广州了。我们是双休,我入职后的第一个周六、日是在武汉过的,在这边跟群友面了基,逛了下附近的粮道街,去了玫瑰街、黄鹤楼等地方🥳



作者:神奇的程序员
来源:juejin.cn/post/7258952063219384376
收起阅读 »

一个古诗文起名工具

web
大家好,我是 Java陈序员,我们常常会为了给孩子取名而烦恼,取名不仅要好听而且要规避大众化。其实,我们中华文化博大精深,可以借鉴先辈文人们留下的经典诗词中的文字来起名。今天,给大家介绍一个古诗文起名的工具。 这个工具支持从《诗经》、《楚辞》、《唐诗》、《宋词...
继续阅读 »

大家好,我是 Java陈序员,我们常常会为了给孩子取名而烦恼,取名不仅要好听而且要规避大众化。其实,我们中华文化博大精深,可以借鉴先辈文人们留下的经典诗词中的文字来起名。今天,给大家介绍一个古诗文起名的工具。


这个工具支持从《诗经》、《楚辞》、《唐诗》、《宋词》、《乐府诗集》、《古诗三百首》、《著名辞赋》等经典中来生成不同的名字。


Img


我们可以根据自己的姓氏来生成名字,例如《陈》姓:
Img


一次性可以生成六个姓名,并有对应的诗句来源说明,是不是很nice呢!


再比如,《李》姓:
Img


当然了,这个项目没有任何人工智能, 没有判断名字价值的目标函数,所以都是随机生成的。因此可以孕育出一些惊艳、惊鸿一瞥的名字,反之也会生成智障、搞笑的名字,大家可自行甄别。


大家如果对于这个项目感兴趣的话,也可自行下载代码到本地运行:


# 克隆代码
git clone https://github.com/holynova/gushi_namer.git

# 安装依赖
npm install

# 本地调试
npm start

# 编译
npm run build

或者直接使用线上地址:


http://xiaosang.net/gushi_namer/

线上地址也是完美支持移动端的。


Img


大家快把这个地址收藏到收藏夹吃灰吧,以免需要的时候找不到!


最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7282692430100201535
收起阅读 »

百分百空手接大锅

web
背景 愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅...
继续阅读 »

背景


愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅,都怪我们前端,没有做好前端监控,导致线上问题持续两天才发现。原本以为运营会把推辞一下说不,锅是她们的,可惜人家不太懂人情世故,这锅就扣在了技术部头上。虽然但是,我还是静下心来把前端异常监控搞了出来,下次一定不要主动接锅,希望看到本文的朋友们也不要随便心软接锅^_^


监控


因为之前基于sentry做了埋点处理,基础已经打好,支持全自动埋点、手动埋点和数据上报。相关的原理可以参考之前的一篇文章如何从0-1构建数据平台(2)- 前端埋点。本次监控的数据上报也基于sentry.js。那么如何设计整个流程呢。具体步骤如下:




  1. 监控数据分类




  2. 监控数据定义




  3. 监控数据收集




  4. 监控数据上报




  5. 监控数据输出




  6. 监控数据预警




数据分类


我们主要是前端的数据错误,一般的异常大类分为逻辑异常和代码异常。基于我们的项目,由于涉及营收,我们就将逻辑错误专注于支付异常,其他的代码导致的错误分为一大类。然后再将两大异常进行细分,如下:




  1. 支付异常


    1.1 支付成功


    1.2 支付失败




  2. 代码异常


    2.1 bindexception


     2.1.1  js_error

    2.1.2 img_error

    2.1.3 audio_error

    2.1.4 script_error

    2.1.5 video_error



  3. unhandleRejection


    3.1 promise_unhandledrejection_error


    3.2 ajax_error




  4. vueException




  5. peformanceInfo




数据定义


基于sentry的上报数据,一般都包括事件与属性。在此我们定义支付异常事件为“page_h5_pay_monitor”,定义代码异常事件为“page_monitor”。然后支付异常的属性大概为:



pay_time,

pay_orderid,

pay_result,

pay_amount,

pay_type,

pay_use_coupon,

pay_use_coupon_id,

pay_use_coupon_name,

pay_use_discount_amount,

pay_fail_reason,

pay_platment


代码异常不同的错误类型可能属性会有所区别:



// js_error

monitor_type,

monitor_message,

monitor_lineno,

monitor_colno,

monitor_error,

monitor_stack,

monitor_url

// src_error

monitor_type,

monitor_target_src,

monitor_url

// promise_error

monitor_type,

monitor_message,

monitor_stack,

monitor_url

// ajax_error

monitor_type,

monitor_ajax_method,

monitor_ajax_data,

monitor_ajax_params,

monitor_ajax_url,

monitor_ajax_headers,

monitor_url,

monitor_message,

monitor_ajax_code

// vue_error

monitor_type,

monitor_message,

monitor_stack,

monitor_hook,

monitor_url

// peformanceInfo 为数据添加 loading_time 属性,该属性通过entryTypes获取

try {

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

if (entry.entryType === 'paint') {

sa.store.set('loading_time', entry.startTime)

}
}

})

observer.observe({ entryTypes: ['paint'] })

} catch (err) {

console.log(err)

}


数据收集


数据收集通过事件绑定进行收集,具体绑定如下:


import {

BindErrorReporter,

VueErrorReporter,

UnhandledRejectionReporter

} from './report'

const Vue = require('vue')


// binderror绑定

const MonitorBinderror = () => {

window.addEventListener(

'error',

function(error) {

BindErrorReporter(error)

},true )

}

// unhandleRejection绑定 这里由于使用了axios,因此ajax_error也属于promise_error

const MonitorUnhandledRejection = () => {

window.addEventListener('unhandledrejection', function(error) {

if (error && error.reason) {

const { message, code, stack, isAxios, config } = error.reason

if (isAxios && config) {

// console.log(config)

const { data, params, headers, url, method } = config

UnhandledRejectionReporter({

isAjax: true,

data: JSON.stringify(data),

params: JSON.stringify(params),

headers: JSON.stringify(headers),

url,

method,

message: message || error.message,

code

})

} else {

UnhandledRejectionReporter({

isAjax: false,

message,

stack

})

}

}

})

}

// vueException绑定

const MonitorVueError = () => {

Vue.config.errorHandler = function(error, vm, info) {

const { message, stack } = error

VueErrorReporter({

message,

stack,

vuehook: info

})

}

}

// 输出绑定方法

export const MonitorException = () => {

try {

MonitorBinderror()

MonitorUnhandledRejection()

MonitorVueError()

} catch (error) {

console.log('monitor exception init error', error)

}

}


数据上报


数据上报都是基于sentry进行上报,具体如下:



/*

* 异常监控库 基于sentry jssdk

* 监控类别:

* 1、window onerror 监控未定义属性使用 js资源加载失败问题

* 2、window addListener error 监控未定义属性使用 图片资源加载失败问题

* 3、unhandledrejection 监听promise对象未catch的错误

* 4、vue.errorHandler 监听vue脚本错误

* 5、自定义错误 包括接口错误 或其他diy错误

* 上报事件: page_monitor

*/


// 错误类别常量

const ERROR_TYPE = {

JS_ERROR: 'js_error',

IMG_ERROR: 'img_error',

AUDIO_ERROR: 'audio_error',

SCRIPT_ERROR: 'script_error',

VIDEO_ERROR: 'video_error',

VUE_ERROR: 'vue_error',

PROMISE_ERROR: 'promise_unhandledrejection_error',

AJAX_ERROR: 'ajax_error'

}

const MONITOR_NAME = 'page_monitor'

const PAY_MONITOR_NAME = 'page_h5_pay_monitor'

const MEMBER_PAY_MONITOR_NAME = 'page_member_pay_monitor'

export const BindErrorReporter = function(error) {

if (error) {

if (error.error) {

const { colno, lineno } = error

const { message, stack } = error.error

// 过滤

// 客户端会有调用calljs的场景 可能有一些未知的calljs

if (message && message.toLowerCase().indexOf('calljs') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else if (error.target) {

const type = error.target.nodeName.toLowerCase()

const monitorType = type + '_error'

const src = error.target.src

sa.track(MONITOR_NAME, {

//属性

})

}

}

}

export const UnhandledRejectionReporter = function({

isAjax = false,

method,

data,

params,

url,

headers,

message,

stack,

code

}
) {

if (!isAjax) {

// 过滤一些特殊的场景

// 1、自动播放触发问题

if (message && message.toLowerCase().indexOf('user gesture') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else {

sa.track(MONITOR_NAME, {

//属性

})

}

}

export const VueErrorReporter = function({ message, stack, vuehook }) {

sa.track(MONITOR_NAME, {

//属性

})

}

export const H5PayErrorReport = ({

isSuccess = true,

amount = 0,

type = -1,

couponId = -1,

couponName = '',

discountAmount = 0,

reason = '',

orderid = 0,

}
) => {

// 事件名:page_member_pay_monitor

sa.track(PAY_MONITOR_NAME, {

//属性

})

}


以上,通过sentry的sa.track进行上报,具体不作展开


输出与预警


数据被上报到大数据平台,被存储到hdfs中,然后我们直接做定时任务读取hdfs进行一定的过滤通过钉钉webhook输出到钉钉群,另外如果有需要做数据备份可以通过hdfs到数据仓库再到kylin进行存储。


总结


数据监控对于大的,特别是涉及营收的平台是必要的,我们在设计项目的时候一定要考虑到,最好能说服服务端,让他们服务端也提供相应的代码监控。ngnix层或者云端最好也来一层。严重的异常可以直接给你打电话,目前云平台都有相应支持。这样有异常及时发现,锅嘛,接到手里就可以精准扔出去了。


作者:CodePlayer
来源:juejin.cn/post/7244363578429030459
收起阅读 »

跨浏览器兼容性指南:解决常见的前端兼容性问题

跨浏览器兼容性是前端开发中至关重要的概念。由于不同浏览器(如Chrome、Firefox、Safari等)在实现Web标准方面存在差异,网页在不同浏览器上可能会呈现不一致的结果。因此,确保网页在各种浏览器上都能正确显示和运行,是提供良好用户体验、扩大受众范围以...
继续阅读 »

跨浏览器兼容性是前端开发中至关重要的概念。由于不同浏览器(如Chrome、Firefox、Safari等)在实现Web标准方面存在差异,网页在不同浏览器上可能会呈现不一致的结果。因此,确保网页在各种浏览器上都能正确显示和运行,是提供良好用户体验、扩大受众范围以及增强网站可访问性的关键。



兼容性测试工具和方法


自动化测试工具的使用 自动化测试工具能够帮助开发者更快速、高效地进行浏览器兼容性测试,以下是一些常用的自动化测试工具:


  1. Selenium:Selenium是一个流行的自动化测试框架,用于模拟用户在不同浏览器上的交互。它支持多种编程语言,并提供了丰富的API和工具,使开发者可以编写功能测试、回归测试和跨浏览器兼容性测试。
  2. TestCafe:TestCafe是一款基于JavaScript的自动化测试工具,用于跨浏览器测试。它不需要额外的插件或驱动程序,能够在真实的浏览器中运行测试,并支持多个浏览器和平台。
  3. Cypress:Cypress是另一个流行的自动化测试工具,专注于现代Web应用的端到端测试。它提供了简单易用的API,允许开发者在多个浏览器中运行测试,并具有强大的调试和交互功能。
  4. BrowserStack:BrowserStack是一个云端跨浏览器测试平台,提供了大量真实浏览器和移动设备进行测试。它允许开发者在不同浏览器上同时运行测试,以检测网页在不同环境中的兼容性问题。

手动测试方法和技巧 除了自动化测试工具,手动测试也是重要的一部分,特别是需要验证用户体验和视觉方面的兼容性。以下是几种常用的手动测试方法和技巧:


  1. 多浏览器测试:在不同浏览器(如Chrome、Firefox、Safari)上手动打开网页,并检查布局、样式和功能是否正常。特别关注元素的位置、尺寸、颜色和字体等。
  2. 响应式测试:使用浏览器的开发者工具或专门的响应式测试工具(如Responsive Design Mode)来模拟不同设备的屏幕尺寸和方向,确保网页在不同设备上呈现良好。
  3. 用户交互测试:模拟用户操作,例如点击按钮、填写表单、滚动页面和使用键盘导航,以确保网页在各种用户交互场景下都能正常运行。
  4. 边界条件测试:测试极端情况下的表现,例如超长文本、超大图片、无网络连接等。确保网页在异常情况下具备良好的鲁棒性和用户友好性。

设备和浏览器的兼容性测试 为了确保网页在不同设备和浏览器上的兼容性,以下是一些建议的测试方法:

  1. 设备兼容性测试:

    • 使用真实设备:将网页加载到不同类型的设备上进行测试,例如桌面电脑、笔记本电脑、平板电脑和智能手机等。
    • 使用模拟器和仿真器:利用模拟器或仿真器来模拟不同设备的环境,并进行测试。常用的模拟器包括Android Studio自带的模拟器和Xcode中的iOS模拟器。
  2. 浏览器兼容性测试:

    • 考虑常见浏览器:测试网页在主流浏览器(如Chrome、Firefox、Safari、Edge)的最新版本上的兼容性。
    • 旧版本支持:如果目标受众使用旧版浏览器,需要确保网页在这些浏览器上也能正常运行。可以使用Can I Use(caniuse.com)等工具来查找特定功能在不同浏览器上的兼容性。
  3. 定期更新测试设备和浏览器:随着时间的推移,新的设备和浏览器版本会发布,因此建议定期更新测试设备和浏览器,以保持兼容性测试的准确性。


常见的前端兼容性问题


我在下面列举了一些常见的兼容性问题,以及解决办法。

  • 浏览器兼容性问题:

    • 不同浏览器对CSS样式的解析差异:使用CSS预处理器(如Less、Sass)可以减少浏览器间的差异,并使用reset.css或normalize.css来重置默认样式。
    • JavaScript API的差异:使用polyfill或Shim库(如Babel、ES5-Shim)来填补不同浏览器之间JavaScript API的差异。
    1. 响应式布局兼容性问题:

      • 媒体查询失效:确保正确使用CSS媒体查询,并对不支持媒体查询的旧版浏览器提供备用样式。
      • 页面在不同设备上的布局错乱:使用弹性布局(Flexbox)、网格布局(Grid)和CSS框架(如Bootstrap)可以有效解决布局问题。
    2. 图片兼容性问题:

      • 不支持的图片格式:使用WebP、JPEG XR等现代图片格式,同时提供备用格式(如JPEG、PNG)以供不支持的浏览器使用。
      • Retina屏幕显示问题:使用高分辨率(@2x、@3x)图片,并通过CSS的background-size属性或HTML的srcset属性适应不同屏幕密度。
    3. 字体兼容性问题:

      • 不支持的字体格式:使用Web字体(如Google Fonts、Adobe Fonts)或@font-face规则,并提供备用字体格式以适应不同浏览器。
      • 字体加载延迟:使用字体加载器(如Typekit、Font Face Observer)来优化字体加载,确保页面内容在字体加载完成前有一致的显示。
    4. JavaScript兼容性问题:

      • 不支持的ES6+特性:使用Babel等工具将新版本的JavaScript代码转换为旧版本的代码,以兼容不支持最新特性的浏览器。
      • 缺乏对旧版浏览器的支持:根据目标用户群体使用的浏览器版本,选择合适的JavaScript库或Polyfill进行填充和修复。
    5. 表单兼容性问题:

      • 不同浏览器对表单元素样式的差异:使用CSS样式重置或规范化库来保证表单元素在各个浏览器上显示一致。
      • HTML5表单元素的不完全支持:使用JavaScript库(如Modernizr)来检测并补充HTML5表单元素的功能支持。
    6. Ajax和跨域请求问题:

      • 浏览器安全策略导致的Ajax跨域问题:通过设置CORS(跨域资源共享)或JSONP(仅适用于GET请求)来解决跨域请求问题。
      • IE浏览器对XMLHttpRequest的限制:使用自动检测并替代方案(如jQuery的AJAX方法),或考虑使用现代的XMLHttpRequest Level 2 API(如fetch)。

    CSS常见的兼容性问题


    CSS兼容性问题是在不同浏览器中,对CSS样式的解析和渲染会存在一些差异。以下是一些常见的CSS兼容性问题以及对应的解决方案:




    1. 盒模型:



      • 问题:不同浏览器对盒模型的解析方式存在差异,导致元素的宽度和高度计算结果不一致。

      • 解决方案:使用CSS盒模型进行标准化,通过设置box-sizing: border-box;来确保元素的宽度和高度包括边框和内边距。




    2. 浮动和清除浮动:



      • 问题:浮动元素可能导致父元素的塌陷问题(高度塌陷)以及与其他元素的重叠问题。

      • 解决方案:可以使用清除浮动的技巧,如在容器元素末尾添加一个空的<div style="clear: both;"></div>元素来清除浮动,或者使用clearfix类来清除浮动(如.clearfix:after { content: ""; display: table; clear: both; })。




    3. 绝对定位和相对定位:



      • 问题:绝对定位和相对定位的元素在不同浏览器中的表现可能存在差异,特别是在z轴上的堆叠顺序。

      • 解决方案:明确设置定位元素的position属性(position: relative;position: absolute;),并使用z-index属性来控制元素的堆叠顺序。




    4. 样式重置与规范化:



      • 问题:不同浏览器对默认样式的定义存在差异,导致页面在不同浏览器中显示效果不一致。

      • 解决方案:引入样式重置或规范化的CSS文件,如Eric Meyer's Reset CSS 或 Normalize.css。这些文件通过将默认样式置为一致的基准值,使页面在各个浏览器上的显示效果更加一致。




    5. 不同浏览器对CSS盒模型的解析差异:



      • 解决方案:使用box-sizing: border-box;样式来确保元素的宽度和高度包括内边距和边框。




    6. CSS选择器差异:



      • 解决方案:避免使用过于复杂的选择器,尽量使用普通的类名、ID或标签名进行选择。如果需要兼容旧版浏览器,请使用Polyfill或Shim库。




    7. 浮动元素引起的布局问题:



      • 解决方案:使用清除浮动(clear float)技术,例如在容器的末尾添加一个具有clear: both;样式的空元素或使用CSS伪类选择器(如:after)清除浮动。




    8. CSS3特性的兼容性问题:



      • 解决方案:使用CSS前缀来适应不同浏览器支持的CSS3属性和特效。例如,-webkit-适用于Chrome和Safari,-moz-适用于Firefox。




    除了以上问题,还可能存在字体、渐变、动画、弹性盒子布局等方面的兼容性问题。在实际开发中,可以使用CSS预处理器(如Less、Sass)来减少浏览器间的差异,并借助Autoprefixer等工具自动添加浏览器前缀,以确保在各种浏览器下的一致性。


    JavaScript常见的兼容性问题


    以下是几个常见的 JavaScript 兼容性问题及其解决方案:

  • 不支持ES6+语法和新的API:(上面有提到)

    • 问题:旧版本的浏览器可能不支持ES6+语法(如箭头函数、let和const等)和新的JavaScript API。
    • 解决方案:使用Babel等工具将ES6+代码转换为ES5语法,以便在旧版本浏览器中运行,并使用polyfill或shim库来提供缺失的JavaScript API支持。
    1. 缺乏对新JavaScript特性的支持:

      • 问题:某些浏览器可能不支持最新的JavaScript特性、方法或属性。
      • 解决方案:在编写代码时,可以检查特定的JavaScript特性是否受支持,然后使用适当的替代方法或实现回退方案。可以使用Can I use (caniuse.com) 等网站来查看浏览器对特定功能的支持情况。
    2. 事件处理程序兼容性问题:

      • 问题:不同浏览器对事件处理程序的绑定、参数传递和事件对象的访问方式存在差异。
      • 解决方案:使用跨浏览器的事件绑定方法(例如addEventListener),正确处理事件对象,并避免依赖事件对象的特定属性或方法。
    3. XMLHttpRequest兼容性问题:

      • 问题:旧版本的IE浏览器(< IE7)使用ActiveX对象而不是XMLHttpRequest。
      • 解决方案:检查浏览器是否支持原生的XMLHttpRequest对象,如果不支持,则使用ActiveX对象作为替代方案。
    4. JSON解析兼容性问题:

      • 问题:旧版本的浏览器可能不支持JSON.parse()JSON.stringify()方法。
      • 解决方案:使用json2.js等JSON解析库来提供对这些方法的支持,或者在必要时手动实现JSON的解析和序列化功能。
    5. DOM操作兼容性问题:

      • 问题:不同浏览器对DOM操作方法(如getElementByIdquerySelector等)的实现方式存在差异。
      • 解决方案:使用跨浏览器的DOM操作库(如jQuery、prototype.js)或使用feature detection技术来检测浏览器对特定DOM方法的支持,并根据情况使用不同的解决方案。
    6. 跨域请求限制:

      • 问题:浏览器的同源策略限制了通过JavaScript进行的跨域请求。
      • 解决方案:使用JSONP、CORS(跨源资源共享)、服务器代理或 WebSocket等技术来绕过跨域请求限制。

    总结


    跨浏览器兼容性是网站和应用程序开发中至关重要的一环。由于不同浏览器对CSS和JavaScript的解析和渲染存在差异,如果不考虑兼容性问题,可能会导致页面在不同浏览器上显示不正确、功能不正常甚至完全无法使用的情况。这将严重影响用户体验,并可能导致流失用户和损害品牌声誉。


    作者:狗头大军之江苏分军
    链接:https://juejin.cn/post/7267409589066498106
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    组件阅后即焚?挂载即卸载!看完你就理解了

    web
    前言 上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode生成二维码,然后在用html2canvas导出成图片嘛。 由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob就可以了,非常的easy啊...
    继续阅读 »

    前言


    上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode生成二维码,然后在用html2canvas导出成图片嘛。
    由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob就可以了,非常的easy啊。


    开始动手


    思路没啥问题,但第一步就犯了难,用过react框架或者其他MVVM框架的都知道,这种类型的框架都是数据驱动视图,也就是说一般情况下,必须先获得数据,然后根据数据才能得到视图。


    但是问题是,html2canvas也是必须需要获取真实dom的快照然后转换成canvas对象。


    听着好像不冲突,诶,我先获取数据,然后渲染出视图,在依次通过html2canvas来生成图片不就完事了嘛!但是想归想,却不能这么做。


    原因主要有两个,一个原因呢是交互逻辑上就行不太通,也不友好。你不能“啪”点一下导出按钮,然后获取数据之后再去等所有数据渲染出对应组件之后,再去延迟处理导出逻辑。(耗时太长)


    另一个原因呢,主要是跟html2canvas这个工具库有关系了,它的原理简单来说呢,就是复制你期望获取截图的那个dom的渲染树,然后根据这个渲染树在当前页面生成一个你看不见的canvas dom对象来。那么问题来了,因为是批量下载,所以肯定会有大量的数据,那么如果不做处理,就会有大量的canvas对象存在当前页面。


    canvas标签是会占用内存的,那么当同时存在过多的canvas时,就会出现一个问题,页面卡顿甚至崩溃。所以,这是第二个原因。


    那么这篇文章主要是解决第一个原因所带来的问题的。


    编程!启动!


    第一步


    那么先简单的随便生成一个组件好了,因为是公司源码嘛,大家懂的都懂。


    interface IProps {
    qrCode: string
    studentName: string
    className: string
    }

    const SaveQRCode = (props: IProps) => {
    const divRef = React.useRef<HTMLDivElement>(null)
    // 具体怎么渲染看你们需求了
    return (
    <div ref={divRef}>XXXXXX</div>
    )
    }

    看到代码,用过html2canvas的小伙伴应该知道ref是干嘛用的了,html2canvas()这个方法的参数是HTMLElement,传统一点的办法呢,可以通过document.getXXXXX这个方法来获取真实的dom元素。那么Ref就是替代前者的,它可以直接通过react框架获取真实的dom元素。


    第二步


    那么最简单的组件我们已经写好了,接下来就是如何动态的挂载这个组件,并且在挂载完之后就立刻卸载它。


    那么先来理一下思路:
    1、动态地挂载这个组件,且不能被用户肉眼观察到
    2、挂载动作执行完立刻执行html2canvas获取canvas对象
    3、通过canvas对象转换成blob对象并返回,或者直接通过回调函数返回canvas对象
    4、组件卸载,清空dom


    那么根据上面几点,可以得出:从外部获取的肯定是有组件这个东西,而挂载的位置则有要求,但并不一定需要从外部获取。


    为了不被样式影响,我们直接在body标签下,再挂载一个div标签,来进行组件的动态渲染和卸载,同时也避免了影响之前dom树的结构。


    思路就说到这了,接下来直接抛出代码:


    const AsyncMountComponent = (
    getElement: (onUnmount: () => void) => ReactNode,
    container: HTMLElement,
    ) => {
    const root = createRoot(container)
    const element = getElement(() => {
    root.unmount()
    container.remove()
    })
    root.render(<Suspense fallback={null}>{element}</Suspense>)
    }

    这里我因为想做的更加通用一点,所以把根节点让外部进行处理,如果希望更加业务一点,比如当前这个场景必然不会让用户可见,可以直接改成


    const AsyncMountComponent = (getElement: (onUnmount: () => void) => ReactNode) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    const root = createRoot(div)
    const element = getElement(() => {
    root.unmount()
    container.remove()
    })
    root.render(<Suspense fallback={null}>{element}</Suspense>)
    }

    这里的隐藏方式看个人喜好,无所谓。但有一点要注意的是,一定要可见,不然的话html2canvas生成不了图片,这里是最简单粗暴的方式,直接偏移left


    第三步


    那么地基打好了,我们该怎么用这两个东西呢


    interface IProps {
    qrCode: string
    studentName: string
    className: string
    // 这里自然就是获取blob和canvas对象的地方了
    onConfirm?: (data: { canvas: HTMLCanvasElement, blob: Blob }) => void
    // 这里是卸载的地方,由外部决定何时卸载节点,更加自由
    onUnmount?: () => void
    }

    const SaveQRCode = (props: IProps) => {
    const divRef = React.useRef<HTMLDivElement>(null)
    useEffect(() => {
    if (divRef.current && props.onConfirm) {
    html2canvas(divRef.current).then((canvas) => {
    canvas.toBlob((blob) => {
    props.onConfirm!({canvas, blob: blob!})
    props.onUnmount!()
    })
    })
    }
    }, [])
    // 具体怎么渲染看你们需求了
    return (
    <div ref={divRef}>XXXXXX</div>
    )
    }

    首先我们对组件进行修改,因为我的方案是第一种,没有太业务向,所以说一些业务逻辑必然是要到组件层面去处理的,所以添加两个参数,一个获取blobcanvas对象,另一个用来卸载节点。


    至于useEffect就很容易理解了,挂载后用html2canvas处理组件顶层div获取截图,然后返回数据,并卸载节点。


    组件改造完毕了,那我们接下来把这两个组合一下


    const getQRCodeBlobCanvas = async (props: IProps): Promise<{
    canvas: HTMLCanvasElement, blob: Blob
    }> => {
    return new Promise((resolve) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    asyncMountComponent(
    (dispose) => (<SaveQRCode {...props} onConfirm={resolve} onUnmount={dispose}/>),
    div
    )
    })
    }

    那么一个简单的动态阅后即焚组件就完成了,且可以直接通过方法的形式使用,完美适配批量导出功能,当然也包括单个导出,至于批量导出的细节我就不写了,非常的简单。


    升级V2


    我只提供了最通用一种方式来做这么个阅后即焚组件,之后我闲着无聊,又把它做了一次业务向升级,获得了V2版本


    这个版本呢,你只需要传入一个组件进去,且不用关心何时卸载,它是最真实的阅后即焚。至于数据,会通过Promise的方式返回给用户。


    const Wrapper = ({callback, children}: {  
    callback: (data: { blob: Blob,canvas: HTMLCanvasElement }) => void,
    children: ReactNode
    }
    ) => {
    const divRef = useRef<HTMLDivElement>(null)
    useEffect(() => {
    if (divRef.current) {
    html2canvas(divRef.current).then((canvas) => {
    canvas.toBlob((blob) => {
    callback({canvas, blob: blob!})
    })
    })
    }
    }, [])
    return <div ref={divRef}>
    {children}
    </div>

    }

    const getComponentSnapshotBlobCanvas = (getElement: () => ReactNode): Promise<{canvas:HTMLCanvasElement, blob: Blob}> => {
    return new Promise((resolve) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    const root = createRoot(div)
    root.render((
    <Wrapper
    callback={(values) =>
    {
    root.unmount()
    div.remove()
    resolve(values)
    }}
    >
    {getElement()}
    </Wrapper>

    ))
    })
    }

    其实也没啥特别的,无非就是把业务层公共的东西封装进了方法里,思路还是上面那个思路。


    那么这篇博客就到这里了,感谢阅读!


    作者:寒拾Ciao
    来源:juejin.cn/post/7278512641781334051
    收起阅读 »

    求你了,别再说不会JSONP了

    JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带您深入了解JSONP的工作原理、使用场景及安全注意事项,让您轻松掌握JSONP。 JSONP是什么? JSONP,全称JS...
    继续阅读 »

    JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带您深入了解JSONP的工作原理、使用场景及安全注意事项,让您轻松掌握JSONP。


    JSONP是什么?


    JSONP,全称JSON with Padding,是一项用于在不同域之间进行数据交互的技术。这项技术的核心思想是通过在页面上动态创建<script>标签,从另一个域加载包含JSON数据的外部脚本文件,然后将数据包裹在一个函数调用中返回给客户端。JSONP不仅简单而且强大,尤其在处理跨域数据请求时表现出色。


    JSONP的工作原理


    JSONP的工作流程如下:


    • 客户端请求数据:首先,客户端会创建一个<script>标签,向包含JSON数据的远程服务器发出请求。这个请求通常包括一个名为callback的参数,用来指定在数据加载完毕后应该调用的JavaScript函数的名称。
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSONP Example</title>
    </head>
    <body>
    <h1>JSONP Example</h1>
    <div id="result"></div>

    <script>
    // 定义JSONP回调函数
    function callback(data) {
    const resultDiv = document.getElementById('result');
    resultDiv.innerHTML = `Name: ${data.name}, Age: ${data.age}`;
    }

    // 创建JSONP请求
    const script = document.createElement('script');
    script.src = 'http://localhost:3000/data?callback=callback';
    document.body.appendChild(script);
    </script>
    </body>
    </html>

    • 服务器响应:服务器收到请求后,将JSON数据包装在指定的回调函数中,并将其返回给客户端。响应的内容类似于:
    const Koa = require('koa');
    const Router = require('koa-router');

    const app = new Koa();
    const router = new Router();

    // 定义一个简单的JSON数据
    const jsonData = {
    name: 'John',
    age: 30,
    };

    // 添加路由处理JSONP请求
    router.get('/data', (ctx) => {
    const callback = ctx.query.callback;
    if (callback) {
    ctx.body = `${callback}(${JSON.stringify(jsonData)})`;
    } else {
    ctx.body = jsonData;
    }
    });

    // 将路由注册到Koa应用程序
    app.use(router.routes()).use(router.allowedMethods());

    // 启动Koa应用程序
    const port = 3000;
    app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
    });


    • 客户端处理数据:在客户端的页面中,我们必须事先定义好名为callback的函数,以便在响应被加载和执行时被调用。这个函数会接收JSON数据,供我们在页面中使用。

    JSONP使用场景


    跨域请求:JSONP主要用于解决跨域请求问题,尤其适用于无法通过CORS或代理等方式实现跨域的情况。
    数据共享:在多个域名之间共享数据,可以利用JSONP实现跨域数据共享。
    第三方数据获取:当需要从第三方网站获取数据时,可以使用JSONP技术。


    使用JSONP注意事项


    JSONP的简单性和广泛的浏览器支持使其成为跨域数据交互的强大工具。然而,我们也必须谨慎使用它,因为它存在一些安全考虑,我们分析下它的优缺点:


    优点


    • 简单易用:JSONP非常容易实现和使用,无需复杂的配置。
    • 跨浏览器支持:几乎所有现代浏览器都支持JSONP。
    • 绕过同源策略:JSONP帮助我们绕过了同源策略的限制,轻松获取跨域数据。

    安全考虑


    • XSS风险:JSONP未经过滤的数据可能会引起XSS攻击,因此需要对返回的数据进行过滤和验证。
    • CSRF攻击:使用JSONP时要注意防范CSRF攻击,可以通过添加随机数等方式增强安全性。
    • 仅支持GET请求:JSONP只支持GET请求,不适用于POST等其他HTTP方法。
    • 难以处理HTTP错误:JSONP难以有效处理HTTP错误,在请求失败时的异常处理比较困难。

    随着技术的发展,JSONP已不再是首选跨域解决方案,但了解它的工作原理仍然有助于我们更深入地理解跨域数据交互的基本原理。在实际项目中,根据具体需求和安全考虑,建议优先选择CORS或代理服务器方式处理跨域问题。


    作者:小飞棍
    链接:https://juejin.cn/post/7280435879548715067
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    为什么日本的网站看起来如此不同

    快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器,ChatGPT4 已上线 cube.waixingyun.cn/home 该篇文章讨论了日本网站外观...
    继续阅读 »

    快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器,ChatGPT4 已上线 cube.waixingyun.cn/home


    该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。


    文章还讨论了日本网站的信息密集型布局,这种布局适应了日本语言的特点,使得页面能够容纳大量文字和图像。此外,文章提到了日本网站的功能丰富性,如弹出式窗口和互动元素,以及这些元素在用户体验方面的作用。


    作者强调了日本网站在技术和创新方面的进步,尽管在过去存在技术限制。最后,文章提出了一些关于如何将日本网站设计的元素应用到其他文化中的建议。


    下面是正文~~~


    多年来,我朋友与日本的网站有过许多接触——无论是研究签证要求、计划旅行,还是简单地在线订购东西。而我花了很长时间才适应这些网站上的大段文字、大量使用鲜艳颜色和10多种不同字体的设计,这些网站就像是直接冲着你扔过来的。




    虽然有许多网站都采用了更简约、易于导航的设计,适应了西方网站的用户,但是值得探究的是为什么这种更复杂的风格在日本仍然盛行。


    只是为了明确起见,这些不是过去的遗迹,而是维护的网站,许多情况下,它们最后一次更新是在2023年。




    我们可以从几个角度来分析这种设计方法:


    • 字体和前端网站开发限制
    • 技术发展与停滞
    • 机构数字素养(或其缺乏)
    • 文化影响

    与大多数话题一样,很可能没有一个正确的答案,而是这个网站设计是随着时间的推移而相互作用的各种因素的结果。


    字体和前端网站开发限制


    对于会一些基本排版知识、掌握适当软件并有一些空闲时间的人来说,为罗马化语言创造新字体可能是一项有趣的挑战。然而,对于日语来说,这是一个完全不同层次的努力。


    要从头开始创建英文字体,需要大约230个字形——字形是给定字母的单个表示(A a a算作3个字形)——或者如果想覆盖所有基于拉丁字母表的语言,则需要840个字形。对于日语而言,由于其三种不同的书写系统和无数的汉字,需要7,000至16,000个字形甚至更多。因此,在日语中创建新字体需要有组织的团队合作和比其拉丁字母表的同行们更多的时间。



    这并不令人意外,因此中文和(汉字)韩文字体也面临着类似的工作量,这导致这些语言通常被称为CJK字体所覆盖。



    由于越来越少的设计师面对这个特殊的挑战,建立网站时可供选择的字体也越来越少。再加上缺乏大写字母和使用日文字体会导致加载时间较长,因为需要引用更大的库,这就不得不采用其他方式来创建视觉层次。


    以美国和日本版的星巴克主页为例:


    美国的:




    日本的




    就这样,我们就可以解释为什么许多日本网站倾向于用文字较多的图片来表示内容类别了。有时,你甚至会看到每个磁贴都使用自己定制的字体,尤其是在限时优惠的情况下。




    技术发展/停滞与机构数字素养


    如果你对日本感兴趣,你可能对现代与过时技术之间的鲜明对比有所了解。在许多地方,先进的技术与完全过时的技术并存。作为世界机器人领导者之一的国家,在台场人工岛上放置了一座真人大小的高达雕像,却仍然依赖软盘和传真机,面对2022年Windows资源管理器关闭时感到恐慌。




    在德国,前总理安格拉·默克尔在2013年称互联网为“未知领域”后,遭到全国范围的嘲笑。然而,这在2018年被前网络安全部长樱田义孝轻易地超越,他声称自己从未使用过电脑,并且在议会被问及USB驱动器的概念时,他被引述为“困惑不解”(来源)。


    对于那些尚未有机会窥探幕后幻象的人来说,这可能听起来很奇怪,但日本在技术素养方面严重落后于更新计划。因此,可以推断这些问题也在阻碍日本网站设计的发展。而具体来说,日本的网页设计正面临着这一挑战——只需在谷歌或Pinterest上搜索日本海报设计,就能看到一个非常不同和现代化的平面设计水平。




    文化影响


    在分析任何设计选择时,不应低估文化习俗、倾向、偏见和偏好的影响。然而,“这是文化”的说法可能过于简单化,并被用作为各种差异辩解的借口。而且,摆脱自己的观点偏见是困难的,甚至可能无法完全实现。


    因此,从我们的角度来看,看这个网站很容易..




    感觉不知所措,认为设计糟糕,然后就此打住。因为谁会使用这个混乱不堪的网站呢?


    这就是因为无知而导致有趣的见解被忽视的地方。现在,我没有资格告诉你日本文化如何影响了这种设计。然而,我很幸运能够从与日本本土人士的交谈中获得启发,以及在日本工作和生活的经验。


    与这个分析相关的一次对话实际上不是关于网站,而是关于YouTube的缩略图 - 有时候它们也同样令人不知所措。




    对于习惯了许多西方频道所采用的极简和时尚设计——只有一个标题、重复的色彩搭配和有限的字体——上面的缩略图确实有些难以接受。然而,当我询问一个日本本土人士为什么许多极受欢迎频道的缩略图都是这样设计时,他对这种设计被视为令人困惑的想法感到惊讶。他认为日本的设计方法使视频看起来更加引人入胜,提供了一些信息碎片,从而使我们更容易做出是否有趣的明智决策。相比之下,我给他看的英文视频缩略图在他看来非常模糊和无聊。


    也许正是这种寻求信息的态度导致了我们的观念如此不同。在日本,对风险的回避、反复核对和对迅速做出决策的犹豫明显高于西方国家。这与更加集体主义的社会心态紧密相连——例如,在将文件发送给商业伙伴之前进行两次(或三次)检查可能需要更长时间,但错误的风险显著降低,从而避免了任何参与者丢面子的情况发生。


    尽管有人认为这只适用于足够高的赌注,而迷惑外国游客似乎不符合条件——搜索一下“Engrish”这个词,然后感谢我吧。


    回到网站设计,这种文化角度有助于解释为什么在线购物、新闻和政府网站在外部观察者看来常常是“最糟糕的罪犯”。毕竟,这些正是需要大量细节直接对应于做出良好购买决策、高效地保持最新信息或确保你拥有某个特定程序的所有必要信息的情况。


    有趣的是,关于美国人和中国/日本人如何感知信息,也有相当多的研究。几项研究的结果似乎表明,例如,日本人更加整体地感知信息,而美国人倾向于选择一个焦点来引导他们的注意力(来源)。这可能给我们提供了另一个线索,解释为什么即使在日语能力较高的情况下,西方人对这类网站也感到困难。


    后但并非最不重要的是,必须说的是,网站并不是在一个在线真空中存在。而且,各种媒体,从小册子或杂志到地铁广告,也使用了尽可能多地压缩信息的布局,人们可能已经习惯了这种无处不在的方式,以至于没有人曾经想过质疑它。


    长话短说,这并不是为了找到标题问题的绝对答案,也不是为了加强日本人独特性的观点,就像日本人论一样。相反,尤其是在看到了几次关注一个解释为“真正答案”的讨论之后,我想展示科技、历史和文化影响的广度,这些最终塑造了这种差异。


    作者:王大冶
    链接:https://juejin.cn/post/7272290608655941651
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    uCharts 小程序地图下钻功能

    web
    uCharts 小程序地图下钻功能 最近在研究小程序图表,其中提到了一个地图下钻的功能,感觉挺有意思的,分享一下共同学习! 项目简介 这个Uni-App项目旨在提供一个可交互的地图,允许用户在中国地图的不同层级之间自由切换。用户可以从国家地图开始,然后深入到各...
    继续阅读 »

    uCharts 小程序地图下钻功能


    最近在研究小程序图表,其中提到了一个地图下钻的功能,感觉挺有意思的,分享一下共同学习!


    项目简介


    这个Uni-App项目旨在提供一个可交互的地图,允许用户在中国地图的不同层级之间自由切换。用户可以从国家地图开始,然后深入到各省份地图,最终进入城市地图,点击不同区域/县级市查看详细信息。


    下面是最终效果图👇👇


    1695175245503.png


    文档地址



    准备工作


    在开始之前,请确保你已经安装了Vue.js和Uni-App


    并且准备好了模拟地图数据。这些数据将用于绘制地图。


    地图数据遵循geoJson地图数据交换格式。如果你不熟悉geoJson,可以参考这里


    绘制中国地图


    // 首先引入我们的mock数据
    import mockData from '../../mock/index'

    // onLoad中调用 drawChina 方法来绘制中国地图
    drawChina() {
    uni.setNavigationBarTitle({
    title: '中国地图'
    });
    setTimeout(() => {
    let series = mockData.china.features;
    // 这里循环一下series,把需要的数据增加到serie的属性中,fillOpacity是根据数据来显示的颜色层级透明度
    for (var i = 0; i < series.length; i++) {
    // 这里模拟了随机数据,实际开发中请根据实际情况修改
    series[i].value = Math.floor(Math.random() * 1000)
    series[i].fillOpacity = series[i].value / 1000
    series[i].color = "#0D9FD8"
    }
    // 这里把series赋值给chartData,这样就可以在页面中渲染出来了
    this.chartData = {
    series: series
    };
    }, 100);
    }

    uCharts组件使用


    插件导入后在uni_modules中,命名规则符合easyCom,可以直接在页面中使用


    <qiun-data-charts
    type="map"
    canvas2d=""
    :chartData="chartData"
    :opts="opts"
    :inScrollView="true"
    :pageScrollTop="pageScrollTop"
    tooltipFormat="mapFormat"
    @getIndex="getIndex"
    @complete="complete"
    />

    注释说明:



    • chartData 包含地图数据

    • opts 是我们在 data 中定义的配置项

    • tooltipFormat 类型为字符串,需要指定为 config-ucharts.jsconfig-echarts.js 中 formatter 下的属性值,
      这里我们使用了 mapFormat,可以在 config-ucharts.js 中查看

    • 在页面中必须传入 pageScrollTop,并将 inScrollView 设置为 true,否则可能导致某些地图事件无法触发


    事件说明:



    • @complete 事件是地图绘制完成后触发的事件,我们可以在这个事件中获取地图的实例,
      然后可以调用地图的方法进行进一步操作。

    • @getIndex 事件是地图点击事件,我们可以获取到点击的地图信息,
      根据这个信息来判断是否需要进行下钻操作,如果需要下钻,可以替换 chartData 并重新绘制地图。


    下钻操作


      // 点击地图获取点击的索引
    getIndex(e) {
    console.log('点击地图', e);
    if (e.currentIndex > -1) {
    switch (this.layout) {
    case 'china':
    this.layout = 'province';
    break;
    case 'province':
    this.layout = 'city';
    break;
    case 'city':
    this.layout = 'area';
    break;
    default:
    uni.showModal({
    title: '提示',
    content: '当前已经是最后一级地图,点击空白回到中国地图',
    success: () => {

    }
    });
    break;
    }

    this.drawNext(e.currentIndex);
    } else {
    this.layout = 'china';
    this.drawChina();
    }
    }

    以上代码中,我们通过 currentIndex 来判断当前点击的是哪个地图,然后根据 layout 的值来判断是否需要进行下钻操作。
    如果需要下钻,我们就调用 drawNext 方法来绘制下一级地图。


    这个demo中,我们只模拟了中国地图、省级地图、市级地图和区县级地图,如果在开发中我们需要根据adcode请求后端接口来获取地图数据


    具体代码:git仓库地址


    作者:养乐多多多
    来源:juejin.cn/post/7278945628905226275
    收起阅读 »

    某律师执业诚信信息公示平台字体加密解决思路

    web
    本文章只做技术探讨,请勿用于非法用途。 目标网站 为持续加深对律法的学习, 我们需要再来收集一些数据。 本文来解决 credit.acla.org.cn/ 这个网站的字体加密问题。 网站分析 网站反爬 这个网站的各种反爬措施还挺多的, 接口加密啊, 验证码啊...
    继续阅读 »

    本文章只做技术探讨,请勿用于非法用途。



    目标网站


    为持续加深对律法的学习, 我们需要再来收集一些数据。


    本文来解决 credit.acla.org.cn/ 这个网站的字体加密问题。


    网站分析


    网站反爬


    这个网站的各种反爬措施还挺多的, 接口加密啊, 验证码啊(每个页面都有), 无限 debugger 啊, 什么的, 还是挺烦的, 如果不要求效率的话可以考虑用 selenium 来过掉, 这里重点来解决一下字体加密的问题。


    加密分析


    首先来看下字体加密什么样子。


    image.png


    如图, 为律所详情页的截图, 可以看到啊, 这个 标签下的字体为加密字体, 这个网站他大多数数据信息都会像这样来做一个加密。


    开整


    首先来说下解决的方法。




    1. 找到字体文件。




    2. 确定文件字体与网站字体的映射关系。




    3. 替换网站字体。




    字体文件获取


    image.png


    刷新页面, 勾选字体栏即可看到返回的页面, 直接下载下来即可。



    有些网站可能会返回多个字体文件来迷惑你, 这时候可以全局搜索 ttf 等字体文件的关键词, 来读相关代码来找到前端页面解密时用的具体是字体文件。



    字体文件解析


    字体文件处理可以用 TTFont 工具, 我们先将文件解析成可读的 xml 格式来看下这到底是什么个东西。


    image.png


    下载字体文件保存为本地 font.ttf, 然后解析为 font.xml。


    image.png


    可以看到文件里是一些映射关系, 和一些字形的信息, 如果是简单的数字加密或是很少的字体加密的话, 这一步直接拿到映射关系就可以用了, 但是这个网站他每次的字体文件都不一样, 所以这种简单的映射关系不可用。


    image.png


    在字体编辑软件里也可以看到是对哪些字体进行了修改加密, windows 可以用 font creator , mac 上我用的是 FontForge 来解析的。


    映射关系获取


    上一步我们拿到了字形信息, 这里来生成提供一个通用的方法来做映射关系。



    font.ttf 文件中通过 unicode 码来标记对应的字体的字形信息, 我们也可以用同样的方式, 获取加密字体对应的原字体的字形信息(固定不变的), 以此为 key 来设计映射关系。



    image.png


    这里定义了一个全局变量 font_map 来存储映射关系, 通过 PIL 的 ImageFont 对象来将字体的字形信息复现出来, 然后通过 ocr 技术得到字的原型, 完成解密。将解密过的字存入 font_map, 随着收录的字越来越多, 解析效率会越来越高。


    加密字体替换


    image.png


    这里没什么难度, 做一个简单的替换就好。


    结语


    这个也是我首次接触这种麻烦些的字体加密, 就想写出来权当分享, 思路也是借鉴于之前看到的一个帖子(找不见了。。)。 文中有些东西需要自己去调试后可能会理解更深些, 因为写的过程中被其他事情打断了几次, 之前整理的思路乱掉了, 写的可能不太顺畅, 大家哪里不懂的话可以留言讨论吧, 或者有什么更好的思路也欢迎来交流。


    作者:Glommer
    来源:juejin.cn/post/7272399042091909131
    收起阅读 »

    各位微信小程序的开发者们,你们还好吗?

    web
    前言 最近微信小程序隐私指引这波骚操作实在是太好玩了,剧情跌宕起伏,让人不由得直呼周星驰在《唐伯虎点秋香》里的那句名言“人生大起大落得太快,实在是太刺激了”。 我自己因为有几个小程序需要做适配,所以全程体验了整个剧情,到今天感觉这过程实在是有点离谱,所以决定...
    继续阅读 »

    前言


    最近微信小程序隐私指引这波骚操作实在是太好玩了,剧情跌宕起伏,让人不由得直呼周星驰在《唐伯虎点秋香》里的那句名言“人生大起大落得太快,实在是太刺激了”。


    3583c74475ac4036932cf1421e1abafd.jpeg


    我自己因为有几个小程序需要做适配,所以全程体验了整个剧情,到今天感觉这过程实在是有点离谱,所以决定记录一下。以飨读者。


    起因


    让我们把时钟回拨到一个月前的8月14日,微信默默的发了一个足以影响所有小程序的公告 《关于小程序隐私保护指引设置的公告》。大概意思就是对于某些隐私接口,如相册,地理位置啥的,要给用户弹个窗,需要用户点同意以后才能调用。公告内容很多,总结起来有两个重点:



    1. 这个弹窗要你们自己做哟。

    2. 9月15号生效呦,在此之前要是没做弹窗,你的小程序就死定了呦。


    然后小伙伴们不敢怠慢,纷纷开始研究怎么个改法,社区论坛里乱成了一锅粥。


    有看文档看不懂的:


    Screen Shot 2023-09-15 at 12.46.41 AM.png


    有没看到公告早上上班突然发现接口都出错一脸懵的:


    6a5f760e492e69d02606d9339c33ea8a.jpeg


    Screen Shot 2023-09-15 at 12.56.09 AM.png


    还有先知先觉的:


    Screen Shot 2023-09-15 at 1.05.15 AM.png


    骂街的我就不贴了,以防不能过审。


    发展


    由于推出的过于仓促,阻碍了开发调试,引起开发者不满,官方很快又默默回滚了此次更新。并且鉴于大家都看不懂文档,也不知道怎么改代码。在小伙伴们的强烈呼吁下


    Screen Shot 2023-09-15 at 1.17.53 AM.png
    官方答应尽快推出demo。


    终于在8天之后的8月22日,距离9月15日大限还有24天的时候,大家翘首以盼的官方demo终于出现了。而且不是1个,是3个(后来又多了1个,一共4个)。这下一脸懵变成三(四)脸懵了。


    3d769c68d1e39eb2cfc670d8f47af595.jpeg


    你让我用哪个???


    demo都有了,那就开干吧,还有二十几天,来得及。于是天真的小伙伴们开始高ma高ma兴lie兴lie的写bug。这次改动不仅仅涉及代码层面,还需要后台配置,前台配置,更新缓存,uni/taro框架,基础库版本,第三方开发等等各种问题,大家也是磕磕绊绊的慢慢的都摸清楚了该怎么做。


    由于此次改动涉及到所有的小程序,影响面太大,随着9月15号大限的临近,更多的问题渐渐浮出水面。


    首先就是那些维护着几十上百个小程序的小朋友,改动看似不多,但每一个都要走开发测试发版流程,而且必须在这短短的不到一个月的时间内完成,听起来就头大有木有?改不完,根本改不完,加班加点不睡觉也改不完。


    然后就是那些在线上跑了好几年没更新的小程序,有的是源代码都找不到了,有的原来的开发者早就提桶跑路了,直接欲哭无泪。


    还有后知后觉的,剩下几天就到大限的时候来社区里问这个隐私协议是个啥?这么重要的事情平台为什么没有早点通知我???


    最后,就是审核变的奇慢无比,原来几个小时就有结果,而现在有小程序审核了好几天了还是没有反应。这其实也是可以预见的,这种影响所有小程序的改动必然会导致版本发布量大增,微信这是人为的自己给自己制造了个审核DDOS,但后果都是开发者承担。就问你急不急吧


    Screen Shot 2023-09-15 at 2.20.05 AM.png
    就剩几天的时候,有小伙伴被逼的没办法了,不得不使用了一年一次平时打死都不敢用的加急审核。


    当时间来到今天,也就是9.15大限前最后一天的时候,上了车的暗自庆幸,没上车的放弃治疗,尘埃即将落定,大家各安天命的时候,意想不到的转折又来了。。。


    高潮


    9月14号晚上20点38分,也就是距离9月15号0点还有3个多小时的时候,微信官方又发布了一条公告:


    Screen Shot 2023-09-15 at 2.49.07 AM.png
    总结下来就两点:



    1. 原来说大限是还剩3个小时的9月15号,新的大限推迟到10月17号。

    2. 代码不改也没关系,平台自己会弹窗。


    啊???????????????


    啥???????????????


    就剩3个小时了你给我来个180度大转弯???????????


    不瞒你们说,我晚上看到这个公告的开始的表情是:


    96282a4cc8aa3522a78448bd660be56a.jpeg
    然后是


    210235eea23d7fcc4867ae9bbccb3df9.jpeg


    这下原本上了车的直接被180度逮虾户给甩出去了,合着我吭哧吭哧费劲巴拉的折腾了一个月眼看就剩3个小时了你告诉我都是白干???你平台为什么不一开始就把方案定成你们自己处理,非要折磨我们开发者???我加班都是白加了?我的用掉的加急审核次数能不能退给我??我怎么觉得有一种被人耍了的感觉?


    离最后期限3个小时啊,不是3周,也不是3天,是3个小时啊。你们官方人员业余都是玩赛车的还是兼职开渣土车的啊,平时上下班开车是不是都是漂移过弯啊。


    未完待续


    这事就这么完了吗? no no no,我觉得还没完,接下来这几天社区里估计又要炸锅了,大家喜欢看热闹的没事可以去围观一下。这不又延期了一个月吗?再出什么幺蛾子我是一点都不会感到意外了。


    最后,我觉得这件事印证了我之前听过的一句话,“这个世界就是个巨大的草台班子”。


    最后的最后,拿社区里最经典的一张动图镇楼


    0-2.gif


    作者:ad6623
    来源:juejin.cn/post/7278517841884266536
    收起阅读 »

    环信web、uniapp、微信小程序sdk报错详解---登录篇

    项目场景:记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40 (一)登录用户报400原因分析:从console控制台输出...
    继续阅读 »

    项目场景:
    记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40

     (一)
    登录用户报400



    原因分析:
    从console控制台输出及network请求返回入手分析
    可以看到报错描述invalid password,密码无效,这个时候就需要去排查一下该用户密码填写是否正确


    排查思路:
    因为环信不保存用户的密码,可以在console后台或者调用修改密码的restapi来修改一下密码再重新登录(修改密码目前只有这两种方式)



    (二)
    登录用户报404




    原因分析:
    从console控制台输出及network请求返回入手分析
    可以看到报错描述user not found,这个时候就需要去排查一下该用户是否存在于该项目使用的appkey下了

    排查思路:
    可以看一下console后台拥有这个用户的appkey和自己项目初始化时用的是否是同一个,若在console后台并没有查到该用户,就要注意这个用户是否真的没有注册




    (三)
    登录用户报40、401




    原因分析:
    报错40或者401一般都是token的问题,需要排查一下token是否还在有效期,token是否是当前用户的用户token
    40的报错还有一种情况,用户名密码登录需要排查用户名及密码传参是否都是string类型


    注:此处需要注意用户token和apptoken两种概念
    用户token指的是该用户的token,一般只用于该用户在客户端使用环信 token 登录和鉴权
    app token指的是管理员权限 token,发送 HTTP 请求时需要携带 app token
    token较为私密,一般不要暴露出去

    排查思路:
    排查用户名及密码传参是否都是string类型,这个可以直接将option传参打印出来取一下数据类型看看是否是string
    关于token排查,现在没有合适的办法直接查询token是否还在有效期或者是不是当前用户的token,只能通过api调用看是否报错401,可以在console后台直接获取新的用户token来测试一下


    是不是当前用户的token也可以找环信的技术支持帮忙查,但在不在有效期他们也查不了


    话外
    有人遇到为什么已经open成功了但是还会报错

    这里要注意open只能证明获取到了token,证明不了已经建立了websocket连接,只有触发onOpened或者onConnected回调 只有onOpened或者onConnected回调触发,才算真正与环信建立连接。所以也不能在open返回的success或者.then中做任何逻辑处理,此外还要注意监听回调一定要放在调用api之前,在调用任何一个api时都要保证监听挂载完毕,包括open


    如何判断自己是否在登录状态

    可以用以下三种方法中的一种判断当前用户是否在登录状态~
    1、WebIM.conn方法下有一个logOut字段,该字段为true时表明未登录状态,该字段为false时表明登录;
    2、WebIM.conn.isOpened () 方法有三个状态,undefined为未登录状态,true为已登录状态,false为未登录状态,可以根据这三个状态去判断是否登录;
    3、通过onOpened 这个回调来判断,只要执行了就说明登录成功了,输出的话,输出的是undefined
    三者选其一判断登录状态

    收起阅读 »

    中秋节,我只想回家😭

    web
    前言 中秋节马上就要到啦,中秋节作为刻入我们DNA里面的团圆佳节,我想大家关心的肯定是团圆了吧 对于普通人回家跟家人团圆可能也就是一张车票而已 但是今年~我就想知道28 29号的票都被谁买去了,我现在连30号的都没买到!!! 这让我怎么团圆啊!!! 忆中秋 还...
    继续阅读 »

    前言


    中秋节马上就要到啦,中秋节作为刻入我们DNA里面的团圆佳节,我想大家关心的肯定是团圆了吧


    对于普通人回家跟家人团圆可能也就是一张车票而已


    但是今年~我就想知道28 29号的票都被谁买去了,我现在连30号的都没买到!!!


    这让我怎么团圆啊!!!


    忆中秋


    还记得小时候过中秋,就跟过年一样。记忆中的中秋,各家各户外出的人都会从天南地北赶回来,一家人团聚在一起;记忆中的月亮,又圆又亮,坐在门前的小院子里,一起吃着月饼赏着月,已经很开心了。


    小时候
    听着嫦娥的故事
    心里却惦记着月饼

    长大了
    手里捧着月饼
    心里却想着嫦娥

    中秋节到了
    愿你重拾童年的快乐
    点缀幸福的生活

    程序员怎么过中秋呢?


    当然是以代码来庆祝一下中秋啦,正好还可以参加下中秋创意大赛!但是很难有一些新奇的创意了,那就做一个猜灯谜的小游戏供大家消遣吧哈哈哈。话不多说,开始今天的主题,制作灯谜小游戏,码上掘金会有源码哦,很基础的~


    1、游戏简介



    • 玩家在提交答案后,游戏将根据玩家的回答情况给予相应的提示信息。如果答案正确,将显示回答正确的提示,并增加相应的得分;如果答案错误,将显示回答错误的提示,并扣除相应的分数。同时,游戏会记录玩家的最高分数,以便玩家挑战自己的最好成绩。

    • 玩家可以选择继续猜下一道题目,直到回答完所有题目或不再继续。游戏结束后,将显示玩家的得分和最高分数,并提供重新开始和退出游戏的选项。


    2、游戏规则



    游戏包括多道灯谜题目,每个题目都有一个对应的答案。


    玩家需要在输入框中输入自己的答案,并点击提交按钮进行确认。


    如果答案正确,将显示相应的提示信息,表示回答正确;如果答案错误,将显示错误提示信息并扣除相应分数。


    游戏根据玩家的回答情况给予评分,并记录最高分数。


    玩家可以选择重置游戏重新开始,或者退出游戏。



    3、游戏设计



    • 定义题目和答案数组,每个元素包含一个题目和对应的答案。

    • 初始化游戏数据,包括当前题目索引、得分和最高分数。

    • 显示当前题目,将题目显示在页面上供用户查看。

    • 用户输入答案后,点击提交按钮。

    • 检查用户答案是否正确,如果正确则增加得分,显示回答正确的提示;如果错误则显示回答错误的提示。

    • 更新最高分数,如果当前得分超过最高分数,则更新最高分数。

    • 显示当前得分和最高分数。

    • 清空输入框,准备接受下一题答案。

    • 判断是否回答完所有题目,若回答完所有题目则显示游戏结束的提示信息,并禁用提交按钮;若未完成则显示下一题。

    • 提供重新开始游戏的功能,重置游戏数据并重新显示第一题。

    • 提供退出游戏的功能,显示退出游戏的提示信息。


    4、功能实现


    题目和答案的存储



    题目和答案的存储可以使用数组来实现,每个元素表示一道题目和对应的答案。例如:



    // 定义题目和答案
    const questions = [

    { question: '中秋佳节结良缘 (打一城市名)', answer: '重庆' },
    { question: '中秋鼓励消费 (打一成语)', answer: '月下花前' },
    { question: '中秋遥知兄弟赏光处 (打一唐诗目)', answer: '望月怀远' },
    { question: '木兰迷恋中秋夜 (打一成语)', answer: '花好月圆' },
    { question: '中秋渡蜜月 (打一成语)', answer: '喜出望外' }
    ];

    每个元素都是一个对象,包含两个属性:question表示题目,answer表示答案。可以根据实际需要修改题目和答案的内容和数量。


    5、游戏展示


    话不多说直接上效果 !


    作者:优秀稳妥的Zn
    来源:juejin.cn/post/7280747221510733878
    收起阅读 »

    前端监控究竟有多重要?

    web
    为什么要有前端监控? 一个很现实的原因是bug是不可能被全部测试出来的,由于成本和上线档期的考虑,测试无法做到“面面俱到”,即使时间充裕也总会有这样或那样的bug埋藏在某个角落。 所以一个可靠的前端监控系统可以帮助我们化被动为主动,不再被动的等待客服来找,而是...
    继续阅读 »

    为什么要有前端监控?


    一个很现实的原因是bug是不可能被全部测试出来的,由于成本和上线档期的考虑,测试无法做到“面面俱到”,即使时间充裕也总会有这样或那样的bug埋藏在某个角落。


    所以一个可靠的前端监控系统可以帮助我们化被动为主动,不再被动的等待客服来找,而是在问题出现时开发人员可以第一时间知道并解决。并且我们还可以通过监控系统获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产品优化的方向。


    常见的前端监控


    前端监控系统大体可以分为四部分



    • 异常监控

    • 用户数据监控

    • 性能监控

    • 异常报警


    用户数据监控



    数据监控,就是监听用户的行为,可以帮助我们评估和改进用户在使用网站时的体验:




    • PV:PV(page view):即用户访问特定页面的次数,也可以说是页面的浏览量或点击量,

    • UV:访问网站的不同个体或设备数量,而不是页面访问次数

    • 新独立访客:当日的独立访客中,历史上首次访问网站的访客为新独立访客。

    • 跳出次数:跳出指仅浏览了1个页面就离开网站的访问(会话)行为。跳出次数越多则访客对网站兴趣越低或站内入口质量越差。

    • 来访次数:由该来源进入网站的访问(会话)次数。

    • 用户在每一个页面的停留时间

    • 用户通过什么入口来访问该网页

    • 用户在相应的页面中触发的行为

    • 网站的转化率

    • 导航路径分析


    统计这些数据是有意义的,我们可以清晰展示前端性能的表现,并依据这些监控结果来进一步优化前端性能。例如,我们可以改善动画效果以在低版本浏览器上兼容,或者采取措施加快首屏加载时间等。这些优化措施不仅可以提高转化率,因为快速加载的网站通常具有更高的转化率,还可以确保我们的网站在多种设备和浏览器上都表现一致,以满足不同用户的需求。最终达到,改善用户体验,提供更快的页面加载时间和更高的性能,增强用户满意度,降低跳出率的目的。


    性能监控



    性能监控是一种用于追踪和评估网站和性能的方法。它专注于用户在浏览器中与网站互时的性能体验




    • 首次绘制(FP): 全称 First Paint,标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点

    • 首次内容绘制(FCP):全称 First Contentful Paint,标记的是浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 <canvas> 元素。

    • 首次有效绘制(FMP):全称 First Meaningful Paint,标记的是页面主要内容绘制的时间点,例如视频应用的视频组件、天气应用的天气信息、新闻应用中的新闻条目。

    • 最大内容绘制(LCP):全称 Largest Contentful Paint,标记在可视区“内容”最大的可见元素开始绘制在屏幕上的时间点。

    • 白屏时间

    • http 等请求的响应时间

    • 静态资源整体下载时间

    • 页面渲染时间

    • 页面交互动画完成时间


    异常监控



    由于产品的前端代码在客户端的执行过程中也会发生异常,因此需要引入异常监控。及时的上报异常情况,这样可以避免线上故障的发生。虽然大部分异常可以通过 try catch 的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获。



    常见的需要监控的异常包括:



    • Javascript 的异常监控:捕获并报告JavaScript代码中的错误,如未定义的变量、空指针引用、语法错误等

    • 数据请求异常监控:监控Ajax请求和其他网络请求,以便识别网络问题、服务器错误和超时等。

    • 资源加载错误:捕获CSS、JavaScript、图像和其他资源加载失败的情况,以减少页面加载问题。

    • 跨域问题:识别跨域请求导致的问题,如CORS(跨源资源共享)错误。

    • 用户界面问题:监控用户界面交互时的错误,如用户界面组件的不正常行为或交互问题


    通过捕获和报告异常,开发团队可以快速响应问题,提供更好的用户体验,减少客户端问题对业务的不利影响


    异常报警



    前端异常报警是指在网站中检测和捕获异常、错误以及问题,并通过各种通知方式通知开发人员或团队,以便他们能够快速诊断、分析和解决问题。



    常见的异常报警方式




    • 邮件通知:通过邮件将异常信息发送给相关人员,通常用于低优先级的问题。




    • 短信或电话通知:通过短信或电话自动通知相关人员,通常用于紧急问题或需要立即处理的问题。




    • 即时消息:使用即时通讯工具如企业微信 飞书或钉钉发送异常通知,以便团队及时协作。




    • 日志和事件记录:将异常信息记录到中央日志,或者监控中台系统,以供后续分析和审计。




    报警级别和策略:


    异常报警通常有不同的级别和策略,根据问题的紧急性和重要性来确定通知的方式和频率。例如,可以定义以下报警级别:




    • 紧急报警:用于严重的问题,需要立即处理,通常通过短信或电话通知。




    • 警告报警:用于中等级别的问题,需要在短时间内处理,可以通过即时消息或邮件通知。




    • 信息报警:用于一般信息和低优先级问题,通过邮件或即时消息通知。




    • 静默报警:用于临时性问题或不需要立即处理的问题,可以记录到日志而不发送通知。




    异常报警是确保系统稳定性和可用性的重要机制。它能够帮助组织及时发现和解决问题,减少停机时间,提高系统的可靠性和性能,从而支持业务运营。异常报警有助于快速识别和响应问题,减少停机时间,提高系统的可用性和性能


    介绍完了前端监控的四大部分,现在就来聊聊前端监控常见的几种监控方式。


    SDK设计(埋点方案)


    前端埋点是一种用于收集和监控网站数据的常见方法


    image.png


    手动埋点:


    手动埋点也称为代码埋点,是通过手动在代码中插入埋点代码(SDK 的函数)的方式来实现数据收集。像腾讯分析(Tencent Analytics)、百度统计(Baidu Tongji)、诸葛IO(ZhugeIO)等第三方数据统计服务商大都采用这种方案,这种方法的优点是:



    • 灵活:开发人员可以根据需要自定义属性和事件,以捕获特定的用户行为和数据。

    • 精确:可以精确控制埋点位置,以确保收集到关键数据。


    然而,手动埋点的缺点包括:



    • 工作量大:需要在代码中多次插入埋点代码,工程量较大。

    • 沟通成本高:需要开发、产品和运营之间的频繁沟通,容易导致误差和延迟。

    • 更新迭代成本高:每次有埋点更新或漏埋点都需要重新发布应用程序,成本较高。


    可视化埋点:


    可视化埋点通过提供可视化界面,允许用户在不编写代码的情况下进行添加埋点。这种方法的优点是:



    • 简单方便:非技术人员也可以使用可视化工具添加埋点,减少了对技术团队的依赖。

    • 实时更新:可以实时更新埋点配置,无需重新上传网站。


    然而,可视化埋点的缺点包括:



    • 可定制性受限:可视化工具通常只支持有限的埋点事件和属性,无法满足所有需求。

    • 对控件有限制:可视化埋点通常只适用于特定的UI控件和事件类型。


    无埋点:


    无埋点是一种自动收集所有用户行为和事件的方法,然后通过后端过滤和分析以提取有用的数据。这种方法的优点是:



    • 全自动:无需手动埋点,数据自动收集,降低了工程量,而且不会出现漏埋和误埋等现象。

    • 全面性:捕获了所有用户行为,提供了完整的数据集。


    然而,无埋点的缺点包括:



    • 数据量大:数据量庞大,需要后端过滤和处理,可能增加服务器性能压力。

    • 数据处理复杂:需要处理大量原始数据,提取有用的信息可能需要复杂的算法和逻辑。


    作者:zayyo
    来源:juejin.cn/post/7280430881964638262
    收起阅读 »

    闲来无事,拜拜电子财神

    web
    最近在刷抖音的时候,经常能刷到类似下面这种手机桌面,通过手机小组件功能,搭了一个电子供台。。。    由于最近闲来无事儿,就在想可不可以制作一个类似的网页,功能点有以下这些: 1.类似手机小组件一样的布局 2.点击木鱼一次,可以显示功德加一并且带音效 3.随着...
    继续阅读 »

    最近在刷抖音的时候,经常能刷到类似下面这种手机桌面,通过手机小组件功能,搭了一个电子供台。。。


      


    由于最近闲来无事儿,就在想可不可以制作一个类似的网页,功能点有以下这些:


    1.类似手机小组件一样的布局


    2.点击木鱼一次,可以显示功德加一并且带音效


    3.随着功德点击,香炉上方会有烟雾飘散的效果


    4.统计不同省份的功德数据


    5.心愿墙功能,


    于是说干就干,就开始了开发工作;


    经过了 2 个下午的忙碌,完成了前三个功能,有了大概的雏形,就是下面这个样子



    开发的过程中也遇到了一些问题


    1.在手机上连续点击木鱼时,会导致网页放大


    在网上找了一些解决办法,设置 meta 属性


    无效,在 ios 的浏览器上没有效果


    这个方法类似于写个节流函数,不过这样做就没有连续敲击木鱼的快感了,所以也不行。


    最后让我找到了一个插件 fastClick.js,完美解决了问题。只要正常引入,然后加入以下代码即可。


    if ("addEventListener" in document) {            document.addEventListener(                "DOMContentLoaded",                function () {                    FastClick.attach(document.body);                },                false            );        }

    2.播放木鱼音效延迟问题


    通过document.createElement('audio')方式创建 audio 组件,代码如下


    var audio = document.createElement('audio') //生成一个audio元素
    audio.controls = true //这样控件才能显示出来
    audio.src = 'xxxxx' //音乐的路径
    document.body.appendChild(audio) //把它添加到页面中
    audio.play()

    声音是能播放出来了,但是延迟很高,点一下木鱼,过几秒钟后才有音效,所以这个方式 pass 了。还有说可以通过AudioContext API 来播放音效,但是看了一下,感觉写起来有些复杂,也 pass 掉了,最后也是找到了一款合适的插件解决了这个问题。



    使用方式也是异常简单


    var sound = new Howl({
    src: ['sound.mp3']
    });

    sound.play();

    由于有个功能是敲击木鱼后,页面香炉的位置会生成烟雾,自己不太会写,于是又找到了可以一个模拟烟雾的插件,可以在页面任意位置生成烟雾动画


    使用时先创建一个 canvas 标签


    <canvas id="smoke"></canvas>

    然后初始化


    let canvas = document.getElementById("smoke");let ctx = canvas.getContext("2d");canvas.width = window.innerWidth;canvas.height = window.innerHeight;party = SmokeMachine(ctx, [230, 230, 230]); // 数组里是颜色 rgb 值

    点击木鱼一次,创建一次播放动画


    party.start();party.addSmoke(    window.innerWidth / 2,    //烟雾生成的位置,x    window.innerHeight * 0.4, //烟雾生成的位置,y    10 //烟雾大小);

    至此烟雾效果就完美实现了。


    体验url:财神爷.我爱你


    没错,是纯中文域名,中国的神仙就要用中文域名。


    未完待续......


    作者:yibeicha
    来源:juejin.cn/post/7280435142245285946
    收起阅读 »

    前端又出新框架了,你还学得动吗?

    web
    最近前端又出来一个新框架/库,名为nue.js。一周前的9.13号提交了第一个commit,到今天已超过2000个star。 翻译一下: Nue 是一个强大的 React、Vue、Next.js、Vite 和 Astro 替代品。它可能会改变您的web开发...
    继续阅读 »

    最近前端又出来一个新框架/库,名为nue.js。一周前的9.13号提交了第一个commit,到今天已超过2000个star。


    官网首页截图


    翻译一下:



    Nue 是一个强大的 React、Vue、Next.js、Vite 和 Astro 替代品。它可能会改变您的web开发方式。



    What is Nue JS?


    Nue JS 是一个非常小的(压缩后 2.3kb)JavaScript 库,用于构建 Web 界面。 它是即将推出的 Nue 生态系统的核心。 它就像 Vue.js、React.js 或 Svelte,但没有hooks, effects, props, portals, watchers, provides, injects, suspension 这些抽象概念。了解 HTML、CSS 和 JavaScript 的基础知识,就可以开始了。


    用更少的代码构建用户界面


    它表示,Nue 最大的好处是你需要更少的代码来完成同样的事情:


    同样一个listBox组件,react需要2537行,vue需要1913行,svelte需要1286行,Nue只需要208行,比react小10倍。





    仅仅是HTML


    Nue 使用基于 HTML 的模板语法:


    <div @name="media-object" class="{ type }">
    <img src="{ img }">
    <aside>
    <h3>{ title }</h3>
    <p :if="desc">{ desc }</p>
    <slot/>
    </aside>
    </div>

    React 和 JSX 声称是“Just JavaScript”,但 Nue 可以被认为是“Just HTML”


    按比例构建


    Nue 具有出色扩展性的三个原因:



    1. 关注点分离,易于理解的代码比“意大利面条代码”更容易扩展

    2. 极简主义,一百行代码比一千行代码更容易扩展

    3. 人才分离,当 UX 开发人员专注于前端,而 JS/TS 开发人员专注于前端后端时,团队技能就会达到最佳平衡:



    解耦样式


    Nue不提倡使用 Scoped CSS、样式属性、Tailwind 或其他 CSS-in-JS 体操:



    1. 更多可重用代码:当样式未硬编码到组件时,同一组件可能会根据页面或上下文而看起来有所不同。

    2. 没有意大利面条式代码:纯 HTML 或纯 CSS 比混合意大利面条式代码更容易阅读

    3. 更快的页面加载:通过解耦样式,可以更轻松地从辅助 CSS 中提取主 CSS,并将 HTML 页面保持在关键的14kb 限制以下。


    反应式和同构


    Nue拥有丰富的组件模型,它允许您使用不同类型的组件创建各种应用程序:



    1. 服务器组件在服务器上呈现。它们可以帮助您构建以内容为中心的网站,无需 JavaScript 即可加载速度更快,并且可以被搜索引擎抓取。

    2. 反应式组件在客户端上呈现。它们帮助您构建动态岛或单页应用程序。

    3. 混合组件部分在服务器端呈现,部分在客户端呈现。这些组件可帮助您构建响应式、SEO 友好的组件,例如视频标签或图片库。

    4. 通用组件在服务器端和客户端上使用相同的方式。


    UI库文件


    Nue允许您在单个文件上定义多个组件。这是将相关组件组合在一起并简化依赖关系管理的好方法。


    <!-- shared variables and methods -->
    <script>
    import { someMethod } from './util.js'
    </script>

    <!-- first component -->
    <article @name="todo">
    ...
    </article>

    <!-- second component -->
    <div @name="todo-item">
    ...
    </div>

    <!-- third component -->
    <time @name="cute-date">
    ...
    </time>

    使用库文件,您的文件系统层次结构看起来更干净,并且您需要更少的样板代码将连接的部分连接在一起。他们帮助为其他人打包库。


    更简单的工具


    Nue JS带有一个简单的render服务器端渲染功能和一个compile为浏览器生成组件的功能。不需要 WebpackVite 等复杂的捆绑程序来控制您的开发环境。只需将 Nue 导入到项目中即可。


    如果应用程序因大量依赖项而变得更加复杂,可以在业务模型上使用打包器。Bunesbuild是很棒的高性能选择。


    用例


    Nue JS是一款多功能工具,支持服务器端和客户端渲染,可帮助您构建以内容为中心的网站和反应式单页应用程序。



    1. UI 库开发:为反应式前端或服务器生成的内容创建可重用组件。

    2. 渐进式增强:Nue JS 是一个完美的微型库,可通过动态组件或“岛”增强以内容为中心的网站

    3. 静态网站生成器:只需将其导入您的项目即可准备渲染。不需要捆绑器。

    4. 单页应用程序:与即将推出的Nue MVC项目一起构建更简单、更具可扩展性的应用程序。

    5. Template Nue:是一个用于生成网站和 HTML 电子邮件的通用工具。


    本文参考资料



    作者:xintianyou
    来源:juejin.cn/post/7280747833371705405
    收起阅读 »

    为什么5.225.toFixed(2)!=5.23,令人摸不着头脑的银行家舍入法

    web
    前言 很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。 今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题) 什么是银行家舍入法 银行家舍...
    继续阅读 »

    前言


    很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。

    今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题)


    什么是银行家舍入法


    银行家舍入法,也称为四舍六入五留双或四舍六入五成双,是一种在计算机科学和金融领域广泛使用的舍入方法。


    具体操作步骤如下:



    1. 如果被修约的数字小于5,则直接舍去;

    2. 如果被修约的数字大于5,则进行进位;

    3. 如果被修约的数字等于5,则需要查看5前面的数字。如果5前面的数字是奇数,则进位;如果5前面的数字是偶数,则舍去5,即修约后末尾数字都成为偶数。特别需要注意的是,如果5的后面还有不为0的任何数,则无论5的前面是奇数还是偶数,均应进位。


    以上可以看出银行家舍入法得规则,当为5时,并不是所有得都会向前进一位,所以就可以知道5.225.toFixed(2)为什么不等于5.23了


    举例


    在浏览器的控制台中,我们可以试着打印一下


    image.png
    这个时候我们可以看到,哎,好像是符合我们的所认知得四舍五入法了,但是紧接着


    image.png
    这里看出,怎么又变成这样的了,这还是银行家舍入法呀,为了更严谨再试一下5前面为奇数时得结果


    image.png
    这里结果又变了,反而是整数大于等于4得正常了,但是小于4得又有些失常了,反而整数为1得总是按照咱们预想的结果在进行,这种结果让我大脑一片混乱,所以这到底是什么原因,导致结果不像是银行家舍入法,也不像是四舍五入法


    在我掉了一花西币的头发后,终于想通了,是程序中的精度问题,我们所写的数字并不是表面那么纯粹,再次打印一下看看


    image.png
    现在可以清楚看出,我们所写的简单的数字后面并不见简单,之所以1.235和1.225使用toFiexd的时候都准确的四舍五入了,都是因为他的后面是多出来了0.0000000000几的数字,然而2.235就没有那么幸运了,所以2.235的0.005就被舍弃了!


    解决方法


    先说一种可行但不完全可行的解决方法,就是使用Math.round()
    首先这个方法确实是js中提供的真正含义上的四舍五入的方法。


    image.png
    哎,这么一看,确实可行,既然简单的可以,那我们就试着进行复杂运算一下,再保留一下两位小数试试看


    image.png
    呕吼,错了,按我们正常来算应该是9.77,但却得到了9.76。

    要知道程序中存在着精度问题,再我们算来这个式子的结果应该是9.765,但是在程序看来


    image.png
    可以说是无限趋近于9.765但还没有达到,然后就在Math.round这个方法中给舍弃掉了,这个方法似乎不完全可行


    那么另外一招就是可行但有隐式风险的方式,就是在我们所算出来的结果后面添加0.0000000001,这样再让我们看一下结果


    image.png
    这样可以看出,无论使用哪种方法,都能达到我们所需的结果了,即使使用toFixed有了银行家舍入法的规则,依旧可以按我们所想的一样进行四舍五入,因为当我们加了0.000000001后,即使最后一位等于5了,5后面还有数字,它就会向前进一位,那如果说加了这0.000000001正好等于5然后又触发了银行家舍入法的规则,那只能说算你倒霉,这就是我说为什么会有隐式风险,有风险但很小。


    当然还有一个方法就是自己写一个方法来解决这个问题


    //有的时候也许传的参数就是计算过后的,无线趋近于5的数,可以根据需求来判断是否传入第二个参数
    Number.prototype.myToFixed = function (n, d) {
    //进来之后转为字符串 字符串不存在精度问题
    const str = this.toString();
    const dotIndex = str.indexOf(".");
    //如果没有小数点传进来的就是整数,直接使用toFixed传出去
    if (dotIndex === -1) {
    return this.toFixed(n);
    }
    //当为小数的时候
    const intStr = str.substring(0, dotIndex);
    const decStr = str.substring(dotIndex + 1, str.length).split("");
    //当大于5时,就进一
    if (decStr[n] >= 5) {
    decStr[n - 1] = Number(decStr[n - 1]) + 1;
    const dec = decStr.slice(0, n).join("");
    return `${intStr}.${dec}`;
    } else {
    //否则小于五时 先判断是否有第二个参数
    if (d) {
    //如果有就截取到第二个参数的位置
    const newDec = decStr.splice(n, n + d);
    let nineSum = 0;
    //遍历循环有多少个9
    for (let index = 0; index < newDec.length; index++) {
    if (index != 0 && newDec[index] == 9) {
    nineSum++;
    }
    }
    //判断四舍五入后面的位置 是否为四 并且是否除了4之后全是9 或者 9的位数大于第二个传的参数
    if (newDec[0] == 4 && (nineSum >= newDec.length - 2 || nineSum >= d)) {
    //条件成立 就按5进一
    decStr[n - 1] = Number(decStr[n - 1]) + 1;
    const dec = decStr.slice(0, n).join("");
    return `${intStr}.${dec}`;
    } else {
    //不成立则舍一
    const dec = decStr.slice(0, n).join("");
    return `${intStr}.${dec}`;
    }
    } else {
    //没有第二个参数,小于五直接舍一
    const dec = decStr.slice(0, n).join("");
    return `${intStr}.${dec}`;
    }
    }
    };

    我们再进行测试一下


    image.png


    image.png
    这样就是我们想要的结果了


    总结


    在程序中,银行家舍入法和数字的精度问题很多时候都会遇见,不论前端还是后端,然而处理这些数据也是比较头疼的事,我所讲的这些也许不能满足所有情况,但大多数情况都是可以处理的。


    如果是相对于银行里这种对数字比较敏感的环境,这些参数的处理还需要更加谨慎的处理


    写的如有问题,欢迎提出建议


    作者:iceCode
    来源:juejin.cn/post/7280430881952759862
    收起阅读 »

    看完这位小哥的GitHub,我沉默了

    就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
    继续阅读 »

    就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



    而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



    出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


    即:在网页端实现了Windows 12的UI界面和交互效果。


    这里也放几张图片感受一下。

    登录页面

    开始菜单

    资源管理器

    设置

    终端命令行

    AI Copilot

    其他应用



    这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


    可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



    项目包含:

    • 精美的UI设计
    • 流畅丰富的动画
    • 各种高级的功能(相较于网页版)

    不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。


    • 项目规划


    • 项目畅想


    刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


    作者出生于2009年,在成都上的小学和初中,目前刚上初三。


    这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


    从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



    作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



    文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


    聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


    而14岁的我,当年在干嘛呢?


    我想了又想。。


    额,我好像在网吧里玩红警。。(手动doge)


    作者:CodeSheep
    链接:https://juejin.cn/post/7275978708644151354
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    硬盘坏了,一气之下用 js 写了个恢复程序

    web
    硬盘坏了,一气之下写了个恢复程序 师傅拯救无望 硬盘已经寄过去超过一周了,一问竟然是还没开始弄??? 再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置? 那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个...
    继续阅读 »

    硬盘坏了,一气之下写了个恢复程序


    师傅拯救无望


    硬盘已经寄过去超过一周了,一问竟然是还没开始弄???


    2023-03-24-14-15-16.png


    再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置?


    2023-03-24-14-18-50.png


    2023-03-24-14-19-30.png


    2023-03-24-14-20-05.png


    那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个星期的缓解,心情已经平复了很多,就像时光,回不来了就是回不来了。


    自救之路


    在把硬盘寄过去的时间里,等待师傅的修复结果的时间里,我并没有闲着(在摸鱼)。


    经过调研,数据恢复方法通常有:



    • 硬件损坏,对坏的盘进行修复

    • 误删或逻辑错误等,文件扫描修复

    • git 重置恢复


    很明显,这些都不适用于我现在的场景。因为师傅能不能修好是未知的,我只是数据盘没了,系统盘还在。由于 vscode 的数据目录空间占比较小,就没有搬迁到数据盘里,这刚好可以为恢复代码提供了可能。


    这是因为新版 vscode 有一个时间线功能,这个时间线数据是默认存储在用户目录下的。


    我从 C:/Users/love/AppData/Roaming/Code/User/History 目录中确实找到了很多名为 entries.json 的文件,结构如下:


    {
    // 配置版本
    "version": 1,
    // 原来文件所在位置
    "resource": "file:///d%3A/git2/cloudcmd/.madrun.mjs",
    // 文件历史
    "entries": [
    {
    // 历史文件存储的名称
    "id": "YFRn.mjs",
    "source": "工作区编辑",
    // 修改的时间
    "timestamp": 1656583915880
    },
    {
    "id": "Vfen.mjs",
    "timestamp": 1656585664751
    },
    ]
    }

    通过上面的文件大概可以看到,每一个时间点的文件都保存在另一个随机命名的文件里。而网上的方法基本都是自己一个个手动到目录里去根据最新的 id 去找对应的文件内容,然后创建文件并把内容复制出来。


    这个过程恢复一两个文件还好,但我这可是要恢复整个 git 工作区,大概有几十个项目上千个文件。


    这时候当然是在网上找找有没有什么 vscode 数据恢复 相关的工具,很遗憾找了大半天都没有找到。


    气死我了,一气之下就自己写个!


    恢复程序开发步骤


    毕竟只要数据在磁盘上,无非就是一个文件读取操作的问题,还要拿在这水文章,见谅见谅。


    首先考虑需求:



    • 我要实现一个自动扫描 vscode 数据目录

    • 然后以原始的目录结构还原出来,不需要我自己去创建文件夹和文件

    • 如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择

    • 扫描出来有N个项目时,我可以指定只还原某此项目

    • 我可以搜索文件、目录名或文件内容进行还原

    • 为了方便,我还要一个看起来不太丑的操作界面


    大概就上面这些吧。


    然后考虑实现:


    我要实现一个自动扫描 vscode 数据目录


    要的就是我自己连数据目录和恢复地址也不需要填写,就能自动恢复的那种。那么就让程序来自动查找数据目录。经过调研,各版本的 vscode 的数据目录一般保存在这些地方:


    参考: stackoverflow.com/a/72610691


      - win -- C:\Users\Mark\AppData\Roaming\Code\User\History
    - win -- C:\Users\Mark\AppData\Roaming\Code - Insiders\User\History
    - /home/USER/.config/VSCodium/User/History/
    - C:\Users\USER\AppData\Roaming\VSCodium\User\History

    大概有上面这些路径,当然不排除使用者故意把默认位置修改掉这种边缘情况,或者使用者就只想扫描某个数据目录的情况,所以我也要支持手动输入目录:


      let { historyPath, toDir } = req.body
    const homeDir = os.userInfo().homedir
    const pathList = [
    historyPath,
    `${homeDir}/AppData/Roaming/Code/User/History/`,
    `${homeDir}/AppData/Roaming/Code - Insiders/User/History/`,
    `${homeDir}/AppData/Roaming/VSCodium/User/History`,
    `${homeDir}/.config/VSCodium/User/History/`,
    ]
    historyPath = (() => {
    return pathList.find((path) => path && fs.existsSync(path))
    })()
    toDir = toDir || normalize(`${process.cwd()}/re-store/`)

    然后以原始的目录结构还原出来……


    这就需要解析扫描到的时间线文件 entries.json 了。我们先把解析结果放到一个 list 中,以下是一个完整的解析方法。


    然后再把列表转换为树型,与硬盘上的状态对应起来,这样便于调试数据和可视化。


    function scan({ historyPath, toDir } = {}) {
    const gitRoot = `${historyPath}/**/entries.json`

    fs.existsSync(toDir) === false && fs.mkdirSync(toDir, { recursive: true })
    const globbyList = globby.sync([gitRoot], {})

    let fileList = globbyList.map((file) => {
    const data = require(file)
    const dir = path.parse(file).dir
    // entries.json 地址
    data.from = file
    data.fromDir = dir
    // 原文件地址
    data.resource = decodeURIComponent(data.resource).replace(
    /.*?\/\/\/(.*$)/,
    `$1`
    )
    // 原文件存储目录
    data.resourceDir = path.parse(data.resource).dir
    // 恢复后的完整地址
    data.rresource = `${toDir}/${data.resource.replace(/:\//g, `/`)}`
    // 恢复后的目录
    data.rresourceDir = `${toDir}/${path
    .parse(data.resource)
    .dir.replace(/:\//g, `/`)}
    `

    const newItem = [...data.entries].pop()
    // 创建文件所在目录
    fs.mkdirSync(data.rresourceDir, { recursive: true })
    const binary = fs.readFileSync(`${dir}/${newItem.id}`, {
    encoding: `binary`,
    })
    fs.writeFileSync(data.rresource, binary, { encoding: `binary` })
    return data
    })

    const tree = pathToTree(fileList, { key: `resource` })
    return tree
    }

    为了方便,我还要一个看起来不太丑的操作界面


    我们要把文件树的形式展示出来,还要方便切换。后面决定使用 macos 的文件管理器风格,大概如下。


    image.png


    如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择


    理论上这里应该要做一个像 vscode 对比文件那样,有代码高亮功能,并且把有差异的字符高亮出来。


    实际上,这个需求得加钱。


    2023-03-24-15-09-25.png


    由于界面是在浏览器里的,需要自动打开,浏览器与系统交互需要一个接口,所以我们使用 opener 来自动打开浏览器。


    使用 get-port 来自动生成接口服务的端口,避免使用时出现占用。


      const opener = require(`opener`)
    const { portNumbers, default: getPort } = await import(`get-port`)
    const port = await getPort({ port: portNumbers(3000, 3100) })
    const server = express()
    server.listen(port, `0.0.0.0`, () => {
    const link = `http://127.0.0.1:${port}`
    opener(link)
    })

    封装成工具,我为人人


    理论上我根本不需要什么 UI 界面,也不需要配置,因为我的文件都恢复出来了我还花时间去搞毛线?


    实际上,万一别人也有这个恢复文件的需要呢?那么他只要运行下面这条命令代码就能立刻恢复到当前目录啦!


    npx vscode-file-recovery

    这就是恢复后的文件在硬盘里的样子啦:


    2023-03-24-15-22-23.png


    所有代码位于:



    建议收藏,以备不时之需。/手动狗头


    作者:程序媛李李李李李蕾
    来源:juejin.cn/post/7213994684262826040
    收起阅读 »

    跨域漏洞,我把前端线上搞崩溃了

    web
    最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!! 很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经...
    继续阅读 »

    最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!!


    WX20230807-141353@2x.png


    很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经正常运行很久了,理论上不应该出现跨域问题。而且更奇怪的是,这个问题只出现在某个 CSS 文件上。


    建议大家在阅读本文时结合目录一起查看。本文详细介绍了从跨域问题发现到跨域问题解决的整个过程,文章还简要提到了前端资源链路。结合上下文来看,对处理前端跨域问题具有一定的参考价值。希望对大家有所帮助。


    什么是跨域问题?


    跨域及其触发条件


    跨域是指在 web 开发中,一个网页的源(origin)与另一个网页的源不同,即它们的协议、域名或端口至少有一个不同。跨域问题是由于浏览器的同源策略而产生的,它限制了一个网页中加载的资源只能来自同一个源,以防止恶意网站在未经允许的情况下访问其他网站的数据。


    以下情况会触发跨域问题:



    1. 不同域名:当页面的域名与请求的资源的域名不一致时,会触发跨域问题,如从 example.com 页面请求资源来自 api.example.net

    2. 不同协议:如果页面使用了 https 协议加载,但试图请求非 https 资源,也会触发跨域问题。

    3. 不同端口:如果页面加载的是 example.com:3000,但试图请求资源来自 example.com:4000,同样会触发跨域问题。

    4. 不同子域名:即使是不同的子域名也会被认为是不同的源。例如,subdomain1.example.comsubdomain2.example.com 是不同的源。


    image.png


    跨域问题会影响到浏览器执行以下操作:



    • JavaScript的XMLHttpRequest或Fetch API请求其他源的资源。

    • 通过<img><link><script>等标签加载其他源的资源。

    • 使用CORS(跨源资源共享)机制实现跨域数据传输。


    解决跨域的方法


    解决跨域问题的方法有多种,具体的选择取决于你的应用场景。以下是一些常见的跨域解决方法:



    1. 跨域资源共享(CORS) :CORS是一种标准机制,通过在服务器端设置响应头来允许或拒绝跨域请求。这是解决跨域问题的最常见方法。

      • 在服务器端设置响应头中的Access-Control-Allow-Origin字段来指定允许访问的域名或使用通配符*表示允许所有域名访问。

      • 其他相关的CORS头,如Access-Control-Allow-MethodsAccess-Control-Allow-Headers,用于控制允许的HTTP方法和请求头。



    2. JSONP(JSON with Padding): 通过动态创建 <script> 标签来实现跨域请求的技术。服务器端返回的数据被包装在一个函数调用中,该函数名由客户端指定。虽然 JSONP 简单易用,但只支持GET请求,由于安全性较差(容易受到跨站脚本攻击),存在安全风险。
      // 客户端代码
      function handleResponse(data) {
      console.log('Received data:', data);
      }

      const script = document.createElement('script');
      script.src = 'https://example.com/api/data?callback=handleResponse';
      document.head.appendChild(script);


    3. 代理服务器:设置一个位于同一域的代理服务器,将跨域请求代理到目标服务器,并将响应返回给客户端。这个方法需要服务器端的额外配置。

    4. 跨文档消息传递: 使用window.postMessage()方法,可以在不同窗口或iframe之间进行跨域通信。

    5. WebSocket: WebSocket是一种双向通信协议,不受同源策略的限制。通过WebSocket,客户端和服务器可以建立持久连接进行跨域通信。

    6. Nginx反向代理: 使用 Nginx 或其他反向代理服务器可以将跨域请求转发到目标服务器,同时解决跨域问题。这种方法适用于前端无法直接控制目标服务器的情况。


    每种方法都有其适用的场景和安全考虑,具体的选择取决于项目的需求和架构。


    背景与跨域设置


    image.png


    项目背景介绍


    最近我负责了一个前端迁移第三方云(阿里云)的工作,由于这是一个多项目组合成的微前端项目,我考虑在前端迁移中,尽可能统一各个应用项目流程、规范和技术。一是形成统一的规范和方式,二是团队项目各负责人按照方案迁移各自项目时避免因各自不一致导致出现问题。


    而在这其中就存在着资源存储和加载不一致的情况,我遇到了三种不同的方法:




    1. 直接使用云存储提供的资源地址


      这是一种常见方式,但也伴随着一些潜在问题。首先,访问云资源可能会有一定的延迟,尤其对于大型文件或数据集。其次,公共云资源地址可能存在安全性和隐私风险,特别是涉及敏感信息时。此外,直接使用OSS资源地址可能导致资源管理分散,难以跟踪和监控资源的使用情况,也可能限制了一些高级功能的实现,如CDN缓存控制、分布式访问控制以及资源日志记录。


      https://company.oss-cn-beijing-internal.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



    2. 使用配置了云存储的CDN域名地址


      这种方式是我比较推荐的方式,然而团队在配置这块链路上存在一些潜在问题。首先 CDN 请求打到了团队内部的一个老服务器,目前老服务器的稳定性不如预期,稳定性方面较差,出现过故障,影响用户体验。前端迁移到第三方云的主要目的之一就是解耦该服务,提供更稳定的前端资源环境。此外,该服务器与其他服务器存在依赖关系,增加了项目的复杂性和不稳定性,解耦是必要的。并且使用这个 CDN 的项目很多,随着时间推移,项目的增加可能会使得该资源地址的维护变得相当复杂。


      https://static.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



    3. 直接加载服务器内部的前端资源


      直接加载服务器内部的前端资源是通过网站域名的代理访问服务器内部资源的一种方法。有几个项目使用这个种方式,这种方式具备简单、快捷等优势。然而,这种方式可能引入网络延迟和性能影响,因为资源请求需要经过团队内部的服务器。同时,存在单点故障的风险,如果内部服务器发生故障或不可用,将导致网站前端资源无法加载,可能对用户造成不便。它也依赖于团队内部的网络环境,需要保持网络的稳定性和可靠性以确保资源能够顺利加载。




    为了统一这三种方式并规避潜在问题,我想到了一个综合性的前端资源请求链路方案。通过将 OSS 存储桶、CDN 和网关服务器相互结合,以提升资源分发速度和安全性,同时减轻 OSS 服务器的负载。此外,我还将所有资源引用集中到一个配置文件中,位于网关服务器,以便轻松进行维护和跟踪。(这里只是简要介绍,我将在后续文章分享详细细节


    然而,在初步方案制定后,也需要考虑如何处理在同源策略下可能出现的跨域问题。


    image.png


    前端静态资源跨域设置


    我在OSS存储桶的跨域设置中配置了允许跨域头,使得网页可以通过浏览器访问OSS资源,免受同源策略的限制。


    image.png


    为什么我会选择在 OSS 存储桶配置呢?


    主要因为这个存储桶非常整洁,只有两个项目在使用,已经提前简单配置了跨域处理。而且这两个项目后续会按照前端迁移方案进行统一迁移处理,因此我认为直接在 OSS 存储桶配置跨域会更为简洁和可维护,我还和 SRE 老师调整了一下配置(然而,没想到恰恰因为我的这个行为,导致后面出现了跨域问题)。


    此外,为了确保安全性,我采取了以下措施:



    • 将项目单独、分批迁移到阿里云 OSS。

    • 在网关服务器中使用nginx进行项目正则匹配,每次迁移就开放一个项目。
      location ~ ^/(gateway|message)/prod/

    • 项目在提测、测试环境都各自运行一段时间(有的甚至在1~2个月)。

    • 在未迁移到正式环境前,各项目按照各自排期计划进行过多次发版。


    这些措施是为了确保有问题,可以在提测环境、测试环境中暴露出来。然而,在迁移第3个项目到正式服环境时,出现了问题。。。


    奇怪的CSS资源跨域问题


    为什么只有某个CSS文件受影响?


    跨域问题通常由浏览器的同源策略引起,该策略限制了来自不同源的资源之间的交互。


    如果资源有跨域问题,不应该只有某个CSS文件出现跨域问题呀?


    3p55k2cus.png


    分析后,我发现浏览器中 CSS 资源的返回头中缺少 CORS 头信息,截图如下:


    image.png


    正常情况下,应该是下图这样:


    image.png


    这时候我在想不应该呀,我已经在源站 OSS 存储桶配置了允许跨域头,这里的返回头中应用是要携带的,而且别的文件(如html、js)返回头中都是携带了允许跨域,但是为什么只有这个 CSS 资源的就没有呢?





    需要注意的是,通常情况下,HTML 文件本身不受同源策略的限制,因此可以从不同源加载 CSS 文件。但如果 CSS 文件中包含引用其他跨域资源(如字体、图片等),那么同源策略仍然会生效,需要特别注意处理这些跨域资源的加载。


    问题的深层原因分析


    image.png


    排除了自身导致的问题


    面对这样一个看似简单的跨域问题,我做了一系列的排查和解决过程。首先,我排除了浏览器缓存、资源代码方面以及浏览器本身的问题,并同 SRE 老师否定了前端资源链路(如OSS、CDN)配置错误的可能性。随后,我们还仔细查看了网关日志,但未能发现问题。


    一直没找到导致跨域问题出现的原因,我们也想到了直接在网关服务器或 CDN 中强制加入允许跨域头。然而我们一讨论,发现不行,因为 OSS 中已经配置了跨域,强制加入允许跨域头,会出现双重跨域问题;如果移除 OSS 中跨域头也不行,因为已经有两个项目已经直接引用阿里云地址,移除后那两个项目也会出现跨域问题。





    寻求阿里云 CDN 运维工程师的帮助


    结合我们自己的分析,我们认为是前端资源请求链路的哪个环节出现了问题,但是迟迟找不到原因,于是我们咨询了阿里云 CDN 运维工程师,因为阿里云 CDN 的日志隔天才出来,所以借此希望通过阿里云 CDN 运维老师能够查看下当天的 CDN 日志,从而找到问题。查看日志后,阿里云 CDN 运维老师也只是给出了日志显示一切正常,但随后我们继续沟通。


    随后,给到了我们一个关键点:“OSS要能响应跨域头,请求头必须携带 Origin 请求头”。阿里云 CDN 运维老师也说传任何值都可以,但是我多次查看到浏览器请求已经携带了 Origin 请求头。如下图:


    image.png


    这就奇怪了!此时测试环境提测环境又无法复现 CORS 跨域问题,我们又不能直接在生产环境调试这个问题。


    借助工具复现问题


    于是我在思考是否能够在提测环境模拟出加载有问题资源的场景。我想到了可以通过拦截浏览器对提测环境的资源请求地址,并将其代理到具有问题的资源地址上来实现这个目的。为了实现这一方案,我使用了一个名为 GoRes 的谷歌浏览器插件。


    image(2).png


    成功复现,见下图:


    3p55k2cus.png


    随后,在多次代理调试中,我发现只有在正式服这个项目的资源地址中出现了这个问题。我和 SRE 老师一起再次确认了提测环境、测试环境和正式环境中各自网关服务器和 CDN 域名等的差异性,当然还是没发现问题!





    问题逐渐浮现出水面


    经过综合分析,我们怀疑 CDN 缓存可能是导致问题的原因。然而,我们无法直接查看缓存的资源,只能再次联系阿里云 CDN 的运维老师。经过多次沟通,我们得知如果客户端在第一次请求 CDN 时没有携带 Origin 请求头,CDN 就不会将 Origin 请求头传递到 OSS,OSS 因此不会响应跨域头,而后续 CDN 便会将没有跨域头的资源内容缓存下来。


    这时我才意识到,OSS 内部存在着对 Origin 辨别的跨域处理机制。而在此之前,上传代码资源到 OSS 后,由于是正式环境,为了安全起见测试资源是否上传成功,我直接在浏览器中访问了一个 CSS 文件地址(当时请求到了资源,我还信心满满,丝毫没有注意到还有这么一个坑),但这一步的操作却间接成为了导致跨域问题出现的导火索


    通常情况下,当网页加载跨域资源时,由于违反了同源策略,浏览器会自动添加源 Origin 到资源的请求头中。然而,由于我直接请求了 CSS 资源地址,未触发同源策略,浏览器也就没有自动添加 Origin 请求头,导致请求到的 OSS 资源中没有跨域头配置。这也就是为什么 CDN 缓存了没有跨域头的资源。


    在网页加载资源时,由于 CDN 缓存了没有跨域头的资源,无论你如何请求,只要 CDN 缓存一直存在,浏览器加载的就是没有跨域头的资源。 因此,这也导致了资源跨域问题的出现。



    本来是为了谨慎一点,提前验证资源是否已上传成功的操作,没想到却成为了跨域问题出现的导火索!!!



    image.png


    这个问题的教训很深刻,让我们意识到必须在向 OSS 请求资源时强制添加 Origin 请求头,以确保今后前端资源的稳定性。否则,这个问题将成为一个定时炸弹。我们决定在网关服务器上分工合作解决这个问题,以杜绝类似情况再次发生。这个经验教训也提醒了我和SRE老师要更加谨慎地处理类似操作,以避免潜在的问题。


    如何稳定解决跨域问题


    尽管我们已经找到了问题的根源,但是不排除是不是还有其他类似问题,为了保险起见,我决定还是缩小影响范围。在确保测试无问题后,逐步放开所有项目。


    SRE 老师负责处理向 OSS 传递Origin请求头的部分,而我负责处理 Nginx location 的正则匹配项目的部分。以下是我们的网关服务器配置:


    location ~ ^/(message|dygateway|logcenter)/tice/ { 
    set $cors_origin "";
    if ($http_origin = "" ) {
    set $cors_origin 'static.example.com';
    }
    if ($http_origin != "") {
    set $cors_origin $http_origin;
    }
    proxy_set_header Origin $cors_origin;
    }



    • location ~ ^/(message|dygateway)/tice/:这是一个正则匹配,能更容易地添加或移除项目。




    • proxy_set_header Origin $cors_origin;:如果请求中包含 Origin 头部,它会被直接传递给 OSS;如果没有,它会被设置为一个值后再传递给 OSS。




    配置完成后,直接在浏览器中请求下面这个资源地址,你会发现请求头并没有添加上去。这并不是配置出错,而是因为上面我们提到的CDN不仅缓存了资源,还缓存了请求头。


    https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css

    所以我们在这个资源的地址后面拼接了参数,相当于是请求新的 CDN 资源地址,此时可以发现跨域头已经添加上了。


    https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css?abc=111

    image.png


    接下来就是在真实项目中测试下,首先在 CDN 后台刷新了有问题的项目资源文件目录,清除掉有跨域问题 CDN 资源缓存后。然后重新刷新浏览器,此时这个文件就成功加上了跨域的请求头,页面访问也正常了。





    image.png


    image.png


    后面我又测试了多次,跨域问题彻底解决。为了避免以后出现类型的问题,所以我又整理了跨域资源共享(CORS)方案,希望对大家有用,请大家接着往下看。


    跨域资源共享方案


    image.png


    跨域资源共享方案是解决前端资源跨域问题的最常见方法,可维护性强,配置简单,可以说这是业界普遍处理前端资源跨域的方式。下面我们将深入探讨三种不同的 CORS 配置方案,并分析各自的优缺点。


    OSS存储桶配置跨域


    我们都知道 OSS(对象存储服务)是阿里云提供的海量、安全、低成本、高可靠的云存储服务。但其实 OSS 也能设置跨域处理,可以让客户端前端应用从其他域名请求资源。


    实施步骤:



    1. 登录阿里云控制台,找到对应的OSS存储桶。

    2. 进入存储桶的管理界面,选择“跨域设置”。

    3. 添加CORS规则,指定允许的来源、方法、头信息等。


    image.png


    优点:



    • 简单易用:配置简单,通过图形界面即可完成。

    • 安全性高:可以灵活控制允许访问的来源,减少安全风险。


    缺点:



    • 依赖云服务商:此方法只适用于使用阿里云OSS的情况,不适用于其他云服务商或自建服务器。


    注意:OSS存储桶配置完成跨域后,需要在请求 OSS 存储桶资源时,在请求头中配置 Origin。因为 OSS 内部的机制是 OSS 响应跨域头的前提是必须要携带源站Origin请求头。 建议大家强制配置必传 Origin 请求头,否则容易出现我这次的问题。使用OSS存储桶配置跨域制定方案时,可以参考我在上面的处理:“如何稳定解决跨域问题”。


    网关服务器配置跨域


    在网关服务器配置跨域,网关服务器通常配置了 Nginx 反向代理服务器。通过配置 Nginx location,可以实现对特定域名的允许跨域支持。


    实施步骤:



    1. 修改nginx配置文件(通常位于/etc/nginx/nginx.conf),添加CORS相关配置。

    2. 配置Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers等头信息。


    location / {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE';
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }

    优点:



    • 灵活性高:可以自由配置适应特定需求。

    • 适用性广:适用于各种服务器环境,不依赖特定云服务商。


    缺点:



    • 配置复杂:需要熟悉nginx配置。


    CDN配置跨域


    CDN(内容分发网络)是一种通过将内容缓存到全球各地节点,加速用户访问速度的网络服务。但也能通过 CDN 配置 CORS,可以在边缘节点处实现跨域。


    实施步骤:



    1. 登录CDN服务提供商的控制台,找到相应CDN加速域名。

    2. 进入域名配置界面,找到CORS配置选项。

    3. 添加CORS规则,指定允许的来源、方法、头信息等。


    image.png


    优点:



    • 高性能:CDN 服务通常提供全球分发,可以加速跨域请求,提供更好的性能。

    • 规模化:适用于大规模的Web应用,可支持高并发的跨域请求。


    缺点:



    • 成本:使用 CDN 服务可能会产生额外的费用,特别是对于大量的数据传输。

    • 配置复杂性:相对于 OSS 或 Nginx,CDN 的配置可能会更为复杂,需要在控制台进行详细的设置。


    注意:腾讯云 CDN 中有专门针对跨域设置的勾选项,只需要选中保存就行。


    三种跨域处理方案各有优缺点,选择合适的方案取决于具体的业务需求和技术栈。我上面所说的也只供大家参考,毕竟 CDN、存储桶这种很大程度受限于云平台,这也是我把允许跨域配置在网关服务器的原因之一。可以综合考虑选择合适的方案或者结合多种方案来实现跨域资源共享。


    u=2094032080,194978745&fm=30&app=106&f=JPEG.jpeg


    结语


    前端资源加载问题往往受多种因素的影响,包括 CDN 配置、资源请求链路、云存储配置等。因此,需要全面分析并综合考虑可能出现问题的任何风险点。也要合理使用浏览器插件工具、网络抓包工具和服务器日志分析等工具,可以帮助我们更快速地诊断和解决问题。如果问题复杂或涉及云服务配置,与云厂商的支持团队联系可以提供专业的帮助。


    这是我关于资源跨域的一篇文章,里面关于定位问题和跨域方案希望对您有所帮助和参考。如果您需要进一步的协助或有任何问题,请随时提问!


    作者:Sailing
    来源:juejin.cn/post/7279429009796546623
    收起阅读 »

    别再用 float 布局了,flex 才是未来!

    web
    大家好,我是树哥! 前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。 什么是 Flex 布局? 在经过了长...
    继续阅读 »

    大家好,我是树哥!


    前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。


    什么是 Flex 布局?


    在经过了长达 10 年的发展之后,CSS3 才终于迎来了一个简单好用的布局属性 —— flex。Flex 布局又称弹性布局,它使用 flexbox 属性使得容器有了弹性,可以自动适配各种设备的不同宽度,而不必依赖于传统的块状布局和浮动定位。


    举个很简单地例子,如果我们想要实现一个很简单左侧定宽,右侧自适应的导航布局,如下图所示。


    -w1239


    在没有 flex 之前,我们的代码是这么写的。


    <div>
    <h1>4.1 两栏布局 - 左侧定宽、右侧自适应 - float</h1>
    <div class="container">
    <div class="left41"></div>
    <div class="right41"></div>
    </div>
    </div>

    /** 4.1 两栏布局 - 左侧定宽、右侧自适应 - float **/
    .left41 {
    float: left;
    width: 300px;
    height: 500px;
    background-color: pink;
    }
    .right41 {
    width: 100%;
    height: 500px;
    background-color: aquamarine;
    }

    这种方式不好的地方在于,我们还需要去理解 float 这个概念。一旦需要理解 float 这个概念,我们就会拖出一大堆概念,例如文档流、盒子模型、display 等属性(虽然这些东西确实应该学)。但对于 flex 来说,它就很简单,只需要设置一个伸缩系数即可,如下代码所示。


    <div>
    <h1>4.2 两栏布局 - 左侧定宽、右侧自适应 - flex</h1>
    <div class="container42">
    <div class="left42"></div>
    <div class="right42"></div>
    </div>
    </div>

    .container42 {
    display: flex;
    }
    .left42 {
    width: 300px;
    height: 500px;
    background-color: pink;
    }
    .right42 {
    flex: 1;
    width: 100%;
    height: 500px;
    background-color: aquamarine;
    }

    上面的代码里,我们只需要将父级容器设置为 flex 展示形式(display: flex),随后在需要自动伸缩的容器里设置属性即可。上面代码中的 flex: 1 表示其占据所有其他当行所剩的空间。通过这样的方式,我们非常方便地实现了弹性布局。


    当然,上面只是一个最简单的例子,甚至还不是很能体现出 flex 的价值。flex 除了在响应式布局方面非常方便之外,它在对齐等方面更加方便,能够极大地降低学习成本、提高工作效率。


    Flex 核心概念


    对于 Flex 布局来说,其有几个核心概念,分别是:主轴与交叉轴、起始线和终止线、Flex 容器与 Flex 容器项。


    主轴和交叉轴


    在 Flex 布局中有一个名为 flex-direction 的属性,可以取 4 个值,分别是:



    • row

    • row-reverse

    • column

    • column-reverse


    如果你选择了 row 或者 row-reverse,那么主轴(Main Axis)就是横向的 X 轴,交叉轴(Cross Axis)就是竖向的 Y 轴,如下图所示。


    主轴是横向的X轴,交叉轴是竖向的Y轴


    如果你选择了 column 或者 column-reverse,那么主轴(Main Axis)就变成是竖向的 Y 轴,交叉轴(Cross Axis)就是横向的 X 轴,如下图所示。


    主轴是竖向的Y轴,交叉轴是横向的X轴


    起始线和终止线


    过去,CSS 的书写模式主要被认为是水平的,从左到右的。但现代的布局方式涵盖了书写模式的范围,所以我们不再假设一行文字是从文档的左上角开始向右书写的。


    对于不同的语言来说,其书写方向不同,例如英文是从左到右,但阿拉伯文则是从右到左。那么对于这两种语言来说,其xx会有所不同 TODO。举个简单的例子,如果 flex-direction 是 row ,并且我是在书写英文。由于英文是从左到右书写的,那么主轴的起始线是左边,终止线是右边,如下图所示。


    -w557


    但如果我在书写阿拉伯文,由于阿拉伯文是从右到左的,那么主轴的起始线是右边,终止线是左边,如下图所示。


    -w541


    在 Flex 布局中,起始线和终止线决定了 Flex 容器中的 Flex 元素从哪个方向开始排列。 举个简单例子,如果我们通过 direction: ltr 设置了文字书写方向是从左到右,那么起始线就是左边,终止线就是右边。此时,如果我们设置的 flex-direction 值是 row,那么 Flex 元素将会从左到右开始排列。但如果我们设置的 flex-direction 值是 row-reverse,那么 Flex 元素将会从右到左开始排列。


    在上面的例子中,交叉轴的起始线是 flex 容器的顶部,终止线是底部,因为两种语言都是水平书写模式。但如果有一种语言,它的书写形式是从底部到顶部,那么当设置 flex-direction 为 column 或 column-reverse 时,也会有类似的变化。


    Flex 容器与 Flex 元素


    我们把一个容器的 display 属性值改为 flex 或者 inline-flex 之后,该容器就变成了 Flex 容器,而容器中的直系子元素就会变为 flex 元素。如下代码所示,parent 元素就是 Flex 容器,son 元素就是 Flex 元素。


    <style>
    #parent {
    display: flex;
    }
    </style>
    <div id="parent">
    <div id="son"></div>
    </div>

    Flex 核心属性


    对于 Flex 来说,它有非常多的用法,但核心属性却相对较少。这里我只简单介绍几个核心属性,如果你想了解更多 Flex 的属性,可以去 Mozilla 官网查询,这里给个传送门:flex 布局的基本概念 - CSS:层叠样式表 | MDN


    对于 Flex 布局来说,其核心属性有如下几个:



    1. flex-direction 主轴方向

    2. flex 伸缩系数及初始值

    3. justify-content 主轴方向对齐

    4. align-items 交叉轴方向对齐


    flex-direction 主轴方向


    如上文所介绍过的,flex-direction 定义了主轴的方向,可以取 4 个值,分别是:



    • row 默认值

    • row-reverse

    • column

    • column-reverse


    一旦主轴确定了,交叉轴也确定了。主轴和交叉轴与后续的对齐属性有关,因此弄懂它们非常重要!举个很简单的例子,如下的代码将展示下图的展示效果。


    .box {
    display: flex;
    flex-direction: row-reverse;
    }

    <div class="box">
    <div>One</div>
    <div>Two</div>
    <div>Three</div>
    </div>

    -w538


    如果你将 flex-direction 改成 column-reverse,那么将会变成如下的效果,如下图所示。


    -w541


    flex 伸缩系数及初始值


    前面说到 Flex 布局可以很方便地进行响应式布局,其实就是通过 flex 属性来实现的。flex 属性其实是 flex-grow、flex-shrink、flex-basis 这三个参数的缩写形式,如下代码所示。


    flex-grow: 1;
    flex-shrink: 1;
    flex-basis: 200px;
    /* 上面的设置等价于下面 flex 属性的设置 */
    flex: 1 1 200px;

    在考虑这几个属性的作用之前,需要先了解一下 可用空间 available space 这个概念。这几个 flex 属性的作用其实就是改变了 flex 容器中的可用空间的行为。


    假设在 1 个 500px 的容器中,我们有 3 个 100px 宽的元素,那么这 3 个元素需要占 300px 的宽,剩下 200px 的可用空间。在默认情况下,flexbox 的行为会把这 200px 的空间留在最后一个元素的后面。


    -w537


    如果期望这些元素能自动地扩展去填充满剩下的空间,那么我们需要去控制可用空间在这几个元素间如何分配,这就是元素上的那些 flex 属性要做的事。


    flex-basis


    flex-basis 属性用于设置 Flex 元素的大小,其默认值是 auto。此时浏览器会检查元素是否有确定的尺寸,如果有确定的尺寸则用该尺寸作为 Flex 元素的尺寸,否则就采用元素内容的尺寸。


    flex-grow


    flex-grow 若被赋值为一个正整数,flex 元素会以 flex-basis 为基础,沿主轴方向增长尺寸。这会使该元素延展,并占据此方向轴上的可用空间(available space)。如果有其他元素也被允许延展,那么他们会各自占据可用空间的一部分。


    举个例子,上面的例子中有 a、b、c 个 Flex 元素。如果我们给上例中的所有元素设定 flex-grow 值为 1,容器中的可用空间会被这些元素平分。它们会延展以填满容器主轴方向上的空间。


    但很多时候,我们可能都需要按照比例来划分剩余的空间。此时如果第一个元素 flex-grow 值为 2,其他元素值为 1,则第一个元素将占有 2/4(上例中,即为 200px 中的 100px), 另外两个元素各占有 1/4(各 50px)。


    flex-shrink


    flex-grow 属性是处理 flex 元素在主轴上增加空间的问题,相反 flex-shrink 属性是处理 flex 元素收缩的问题。如果我们的容器中没有足够排列 flex 元素的空间,那么可以把 flex 元素 flex-shrink 属性设置为正整数,以此来缩小它所占空间到 flex-basis 以下。


    与flex-grow属性一样,可以赋予不同的值来控制 flex 元素收缩的程度 —— 给flex-shrink属性赋予更大的数值可以比赋予小数值的同级元素收缩程度更大。


    justify-content 主轴方向对齐


    justify-content 属性用来使元素在主轴方向上对齐,它的初始值是 flex-start,即元素从容器的起始线排列。justify-content 属性有如下 5 个不同的值:



    • flex-start:从起始线开始排列,默认值。

    • flex-end::从终止线开始排列。

    • center:在中间排列。

    • space-around:每个元素左右空间相等。

    • space-between:把元素排列好之后,剩余的空间平均分配到元素之间。


    各个不同的对齐方式的效果如下图所示。


    flex-start:


    -w454


    flex-end:


    -w444


    center:


    -w449


    space-around:


    -w442


    space-between:


    -w453


    align-items 交叉轴方向对齐


    align-items 属性可以使元素在交叉轴方向对齐,它的初始值是 stretch,即拉伸到最高元素的高度。align-items 属性有如下 5 个不同的值:



    • stretch:拉伸到最高元素的高度,默认值。

    • flex-start:按 flex 容器起始位置对齐。

    • flex-end:按 flex 容器结束为止对齐。

    • center:居中对齐。

    • baseline:始终按文字基线对齐。


    各个不同的对齐方式的效果如下图所示。


    stretch:


    -w448


    flex-start:


    -w439


    flex-end:


    -w438


    center:


    -w444


    要注意的事,无论 align-items 还是 justify-content,它们都是以主轴或者交叉轴为参考的,而不是横向和竖向为参考的,明白这点很重要。


    Flex 默认属性


    由于所有 CSS 属性都会有一个初始值,所以当没有设置任何默认值时,flex 容器中的所有 flex 元素都会有下列行为:



    • 元素排列为一行 (flex-direction 属性的初始值是 row)。

    • 元素从主轴的起始线开始。

    • 元素不会在主维度方向拉伸,但是可以缩小。

    • 元素被拉伸来填充交叉轴大小。

    • flex-basis 属性为 auto。

    • flex-wrap 属性为 nowrap。


    弄清楚 Flex 元素的默认值有利于我们更好地进行布局排版。


    实战项目拆解


    看了那么多的 Flex 布局知识点,总感觉干巴巴的,是时候来看看别人在项目中是怎么用的了。


    -w1290


    上面是我在 CodePen 找到的一个案例,这样的一个布局就是用 Flex 布局来实现的。通过简单的分析,其实我们可以拆解出其 Flex 布局方法,大致如下图所示。


    -w1297


    首先整体分为两大部分,即导航栏和内容区域,这部分的主轴纵向排列的(flex-direction: column),如上图红框部分。随后在内容区域,又将其分成了左边的导航栏和右边的内容区域,此时这块内容是横向排列的(flex-direction: row),如下上图蓝框部分。


    剩下的内容布局也大致类似,其实就是无限套娃下去。由于偏于原因,这里就不继续深入拆解了,大致的布局思路已经说得很清楚了。


    有了 Flex 布局之后,貌似布局也变得非常简单了。但纸上得来终觉浅,还是得自己实际动手练练才知道容易不容易,不然就变成纸上谈兵了!


    总结


    看到这里,关于 Flex 布局的核心点就介绍得差不多了。掌握好这几个核心的知识点,开始去实践练习基本上没有什么太大的问题了。剩下的一些比较小众的属性,等用到的时候再去查查看就足够了。


    接下来更多的时间,就是找多几个实战案例实践,唯有实践才能巩固所学知识点。后面有机会,我将分享我在 Flex 布局方面的项目实践。


    如果这篇文章对你有帮助,记得一键三连支持我!


    参考资料



    作者:树哥聊编程
    来源:juejin.cn/post/7280054182996033548
    收起阅读 »

    看完这位小哥的GitHub,我沉默了

    web
    就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
    继续阅读 »

    就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



    而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



    出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


    即:在网页端实现了Windows 12的UI界面和交互效果。


    这里也放几张图片感受一下。



    • 登录页面




    • 开始菜单




    • 资源管理器




    • 设置




    • 终端命令行




    • AI Copilot




    • 其他应用



    这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


    可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



    项目包含:



    • 精美的UI设计

    • 流畅丰富的动画

    • 各种高级的功能(相较于网页版)


    不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。



    • 项目规划




    • 项目畅想



    刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


    作者出生于2009年,在成都上的小学和初中,目前刚上初三。


    这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


    从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



    作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



    文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


    聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


    而14岁的我,当年在干嘛呢?


    我想了又想。。


    额,我好像在网吧里玩红警。。(手动doge)


    作者:CodeSheep
    来源:juejin.cn/post/7275978708644151354
    收起阅读 »

    蒙提霍尔问题

    最近看韩国电视剧【D.P:逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题 意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余...
    继续阅读 »



    最近看韩国电视剧【D.P逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题


    意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余两个门中的一个并且是山羊(去掉一个错误答案),这时,让你重新选择。那么你是会坚持原来的选择,还是换选另外一个未被打开过的门呢?


    大家可以想一想如果是自己,我们是会换还是不会换?


    好了,我当时看到后感觉很有意思,所以我简单写了一套代码,源码贴在下面,大家可以验证一下,先告诉大家,换赢得汽车的概率是2/3,不换赢得汽车的概率是1/3

    <header>
    <h1>请选择换不换?</h1><button class="refresh">刷新</button>
    </header>
    <section>
    <div class="box">
    <h2>1</h2>
    <canvas width="300" height="100"></canvas>
    <div class="prize">奖品</div>
    </div>
    <div class="box">
    <h2>2</h2>
    <canvas width="300" height="100"></canvas>
    <div class="prize">奖品</div>
    </div>
    <div class="box">
    <h2>3</h2>
    <canvas width="300" height="100"></canvas>
    <div class="prize">奖品</div>
    </div>
    </section>
    <span>请选择号码牌</span>
    <select name="" id="">
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    </select>
    <button class="confirm">确认</button>
    <span class="confirm-text"></span>
    <span class="opater">
    <button class="change">换</button>
    <button class="no-change">不换</button>
    </span>
    <p>
    <strong>游戏规则:</strong>
    <span>
    上面有三个号码牌,其中一个号码牌的里面有汽车,选中里面是汽车的号码牌,
    你就可以赢得该辆汽车,另外两个号码牌里面则都是一只山羊,
    你任意选择其中一个,然后打开其余两个号码牌中的一个并且是山羊(去掉一个错误答案),
    这时,你有一个重新选择的机会,你选择换还是不换?
    </span>
    </p>
    .prize {
    width: 300px;
    height: 100px;
    background-color: pink;
    font-size: 36px;
    line-height: 100px;
    text-align: center;
    position: absolute;
    }

    canvas {
    position: absolute;
    z-index: 2;
    }

    section {
    display: flex;
    }

    .box {
    width: 300px;
    height: 200px;
    cursor: pointer;
    }

    .box+.box {
    margin-left: 8px;
    }

    header {
    display: flex;
    align-items: center;
    }

    header button {
    margin-left: 8px;
    height: 24px;
    }
    p {
    width: 400px;
    background-color: pink;
    }
    function shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
    }
    function getRandomNumber() {
    return Math.random() > 0.5 ? 1 : 2;
    }
    let a1 = [0, 1, 2]
    let i1 = undefined
    let i2 = undefined
    let isChange = false
    const opater = document.querySelector('.opater')
    opater.style.display = 'none'
    // 随机一个奖品
    const prizes = document.querySelectorAll('.prize')
    let a0 = [0, 1, 2]
    a0 = shuffleArray(a0)
    a0.forEach((v,i) => {
    const innerText = !!v ? '山羊' : '汽车'
    prizes[i].innerText = innerText
    })

    const canvas = document.querySelectorAll('canvas')
    const confirmText = document.querySelector('.confirm-text')
    canvas.forEach(c => {
    // 使用canvas实现功能
    // 1. 使用canvas绘制一个灰色的矩形
    const ctx = c.getContext('2d')
    ctx.fillStyle = '#ccc'
    ctx.fillRect(0, 0, c.width, c.height)
    // 2. 刮奖逻辑
    // 鼠标按下且移动的时候,需要擦除canvas画布
    let done = false
    c.addEventListener('mousedown', function () {
    if (i1 === undefined) return alert('请先选择号码牌,并确认!')
    if (!isChange) return alert('请选择换不换!')
    done = true
    })
    c.addEventListener('mousemove', function (e) {
    if (done) {
    // offsetX 和 offsetY 可以获取到鼠标在元素中的偏移位置
    const x = e.offsetX - 5
    const y = e.offsetY - 5
    ctx.clearRect(x, y, 10, 10)
    }
    })
    c.addEventListener('mouseup', function () {
    done = false
    })
    })
    const confirm = document.querySelector('.confirm')
    const refresh = document.querySelector('.refresh')
    confirm.onclick = function () {
    let select = document.querySelector('select')
    const options = Array.from(select.children)
    confirmText.innerText = `您选择的号码牌是${select.value},请问现在换不换?`
    // 选择后,去掉一个错误答案
    // i1是下标
    i1 = select.value - 1
    // delValue是值
    let delValue = undefined
    // 通过下标找值
    if (a0[i1] === 0) {
    delValue = getRandomNumber()
    } else {
    delValue = a0[i1] === 1 ? 2 : 1
    }
    // 通过值找下标
    i2 = a0.indexOf(delValue)
    // 选择的是i1, 去掉的是
    const ctx = canvas[i2].getContext('2d')
    ctx.clearRect(0, 0, 300, 100)
    options.map(v => v.disabled = true)
    confirm.style.display = 'none'
    opater.style.display = 'inline-block'
    }
    const change = document.querySelector('.change')
    const noChange = document.querySelector('.no-change')
    change.onclick = function () {
    isChange = true
    const x = a1.filter(v => v !== i1 && v !== i2)
    confirmText.innerText = `您确认选择的号码牌是${x[0] + 1},请刮卡!`
    opater.style.display = 'none'
    }
    noChange.onclick = function () {
    isChange = true
    confirmText.innerText = `您确认选择的号码牌是${i1 + 1},请刮卡!`
    opater.style.display = 'none'
    }
    refresh.onclick = function () {
    window.location.reload()
    }

    作者:JoyZ
    链接:https://juejin.cn/post/7278684023757553727
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    改了 3 个字符,10 倍的沙箱性能提升?!!

    确实会慢,但不多 🤪 qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因...
    继续阅读 »

    确实会慢,但不多 🤪


    qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因为你的 JS 太大(一个不分片的超大的 bundle),接口响应太长,UI 不够有「弹性」导致的。
    但在面临一些 CPU 密集型的 UI 操作时,如图表、超量 DOM 变更(1000以上)等场景,确实存在明显的卡顿现象。所以我们也不好反驳什么,通常的解决方案就是推荐用户关闭沙箱来提升性能。


    去年底我们曾尝试过一波优化,虽然略有成效,但整体优化幅度不大,因为有一些必要访问耗时省不掉,最终以失败告终。


    重启优化之路 😤


    近期有社区用户又提到了这个问题,加之年初的时候「获取」到了一些灵感,中秋假期在家决定对这个问题重新做一次尝试。
    我们知道 qiankun 的沙箱核心思路其实是这样的:

    const windowProxy = new Proxy(window, traps);

    with(windowProxy) {
    // 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
    ${appCode}
    }

    此前主要的性能问题出在应用的代码会频繁的访问沙箱,比如 Symbol.unscopables 在图表场景很容易就达到千万级次的访问。
    优化的思路也很简单,就是要减少全局变量在 proxy 里的 lookup 次数。比如可以先缓存起来,后续访问直接走作用域里的缓存即可:

    const windowProxy = new Proxy(window, traps);

    with(windowProxy) {
    + // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
    + var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
    // 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
    ${appCode}
    }

    看上去很完美,不过手上没有 windows 设备没法验证(M1性能太强测不出来),于是先提了个 pr


    验证 👻


    假期结束来公司,借了台 windows 设备,验证了一下。
    糟了,没效果。优化前跟优化后的速度几乎没有变化。🥲


    想了下觉得不应该啊,理论上来讲多少得有点作用才是,百思不得其解。


    苦恼之际,突然好像想到了什么,于是做出了下面的修改:

    const windowProxy = new Proxy(window, traps);

    with(windowProxy) {
    + // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
    - var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
    + const undefined = windowProxy.undefined; const Array = windowProxy.Array; const Promise = windowProxy.Promise;
    // 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
    ${appCode}
    }

    改动更简单,就是将 var 声明换成了 const,立马保存验证一把。


    直接起飞!


    场景 1:vue 技术栈下大 checkbox 列表变更





    在有沙箱的情况下,耗时基本与原生框架的一致了。


    场景 2:10000 个 dom 插入/变更


    在 vue 的 event handler 中做原生的 10000 次 的 for 循环,然后插入/更新 10000 个 dom,记录中间的耗时:

    <template>
    <div>
    <ul>
    <li v-for="item in aaa" :key="item">{{ item }}</li>
    </ul>
    <button @click="test">test</button>
    </div>
    </template>

    <script>
    import logo from "@/assets/logo.png";
    export default {
    data() {
    return {
    aaa: 1
    };
    },
    methods: {
    test() {
    console.time("run loop", 10000);

    for (let index = 2; index < 1 * 10000; index++) {
    this.aaa = index;
    }

    console.timeLog("run loop", 10000);

    this.$nextTick(() => {
    // 10000 个 dom 更新完毕后触发
    console.timeEnd("run loop", 10000);
    });
    }
    }
    };
    </script>
     

     

    可以看到,这个优化后的提升已经不止 10 倍了,都超过 50 倍了,跟原生的表现基本无异。


    如何做到的 🧙


    完成最后的性能飞跃,实际上我只改了 3 个字符,就是把 with 里的 var 换成了 const,这是为什么呢?
    其实我之前的这篇文章早就告诉了我答案:
    ES 拾遗之 with 声明与 var 变量赋值
    里面有一个重要的结论:
    image.png
    因为 windowProxy 里有所有的全局变量,那么我们之前使用 var 去尝试做作用域缓存的方案其实是无效的,声明的变量实际还是在全局的词法环境中的,也就避免不了作用域链的查找。而换成 const,就可以顺利的将变量写到 with 下的词法环境了。


    one more thing 😂


    至此,如果以后你的应用在微前端场景下表现的不尽如人意,请先考虑:


    1. 是否是应用的打包策略不合理,导致 bundle 过大 js 执行耗时过长
    2. 是否是前置依赖逻辑过多执行过慢(如接口响应),阻塞了页面渲染
    3. 是否是微应用的加载策略不合理,导致过晚的加载
    4. 没有加载过渡动画,只有硬生生的白屏

    别再试图甩锅给微前端了,瑞思拜🫡。


    作者:支付宝体验科技
    链接:https://juejin.cn/post/7153509630155423752
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    比 React 快 30%?Gyron 是怎么做到的。

    距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。 响应式...
    继续阅读 »

    距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。


    响应式


    Gyron.js是一款零依赖的响应式框架,和社区中大多数响应式方案一样,Gyron.js也是利用Proxy提供的能力进行的依赖收集和更新,理论上来说只要支持Proxy能力的环境都支持这套响应式的逻辑,只是需要修改(接入)对应的渲染部分。这里也要感谢 Vue 实现的响应式依赖更新方案,我们基本上参考着这套方案实现了自己的 API。


    响应式核心部分就是 effect 结构,基本上所有提供出来的 api 都是在 effect 之上实现的,所以我们先来介绍 effect 这个数据结构。


    我们先来看一眼 effect 长什么样子

    export type Dep = Set<Effect>;

    export type EffectScheduler = (...args: any[]) => any;

    export type Noop = () => void;

    export interface Effect {
    // A 等于 self effect
    // B 等于 other effect
    // deps 是一个 Set 结构的 Effect ,用于存放 A 依赖的 B
    // 当A不再使用时需要清除所有依赖 A 的 B
    deps: Dep[];
    // 在边界情况下强制执行依赖的B
    allowEffect: boolean;
    // 在首次执行时收集依赖的函数
    scheduler: EffectScheduler | null;
    // 存放所有手动设置的依赖
    wrapper: Noop;
    // 当需要更新时执行的函数
    run: Noop;
    // 当不再使用时清除依赖的B
    stop: Noop;
    }

    effect 中有很多属性,我也在其中注释了每个属性的作用。每个属性都有自己的应用场景,比如 deps 就是用于卸载组件后清除所有组件依赖的数据,避免数据更新后组件被卸载,导致组件更新异常。其实响应式核心分为两个部分,第一个部分就是依赖收集,第二个部分就是响应更新,我们先来看看下面这张图。




    上面这张图就是说明两个不同的数据之间的依赖关系,第一块(左上角)表明 variable proxy 2 依赖 variable proxy 1,在 variable proxy 2 中访问 variable proxy 1 时,会触发 variable proxy 1 的自动收集任务,当 variable proxy 1 的值更新后会触发依赖 variable proxy 2 的任务,也就是 run 或者 scheduler。那么我们是通过什么将这两个变量关联在一起的呢?我们引入了一个WeakMap数据effectTracks,用变量作为一个 key 值,然后在变更 variable proxy 1 时从这个模块变量中找到依赖再做更新,也就是图中右边部分。至此,依赖收集和依赖更新都已经完成,接下来,如何对应到组件上面呢?


    上面我们介绍了两个响应式变量是如何完成这一整套响应式方案,那么我们把上述的变量变更为组件可不可以呢?答案是可以的。组件是什么?在 Gyron.js 中组件就是一个函数,一个被内装函数包装的函数。那么,组件在初次渲染时如何进行依赖收集呢?在讲解组件的依赖收集之前,我们先讲一讲另外一个模块变量activeEffect,这个变量主要用于组件初次渲染时保存组件的 effect 对象,然后在响应式数据 track 时,获取到组件的 effect 对象保存在上面讲的effectTracks模块变量中,在响应式数据发生变更后触发组件 effect 的 update 方法(也就是 run)来更新组件。这里值得一提的是,所有的更新我们全部都是异步,并且是可选支持中断继续模式的,这部分内容我们接下来再进行介绍。


    好了,响应式的核心内容其实并不多,其实如何实现只是冰山一角,最主要的是其中的想法可以应用在你的业务之中,尽量少写一些恶心同事的代码。


    任务调度


    上面我们讲解了Gyron.js是如何做到响应式更新的,接下来我们说一说多个组件同时更新应该怎么处理呢?如果组件更新阻止了用户操作应该怎么办呢?如何在组件中拿到更新后的DOM呢?这些问题在平时开发中相信大多数开发中都遇到过,其实这些问题可以在编写业务代码时去进行优化,但是不怎么优雅,也可能会导致代码不可读,比如


    获取更新后的 DOM

    // 获取更新后的 D<DOM>
    任务: [update component A, update component B, [update component C, [update component D]]]
    等待任务更新完成: A、B、C、D
    获取组件D更新后的DOM

    为了提高开发效率和用户体验,开发者可以合理选择使用哪种模式更新组件。具体有哪些模式可以选择呢?答案是两种,第一种就是默认模式,组件更新全部在异步队列中完成。第二种模式就是在异步队列中的任务会受到外部状态控制。接下来我们分开讲一讲这两种模式。


    第一种模式,我们可以使用Gyron.js暴露的FC方法定义组件,然后正常使用 JSX 去描述 UI,在组件更新时通过暴露的nextRender获取到更新后的 DOM。这是一个很常见的实现方式,这也和 Vue 的nextTick一样。我们重点讲讲另外一种更新模式。


    延迟更新:组件更新的前面几步都是一样,有一个异步队列,但是延迟更新模式中有一个priority属性,当组件 effect 拥有这种属性的时候会自动根据这批组件的更新时间,或者用户操作来中断队列中后续任务的更新,当浏览器告诉我们,现在有空闲了可以继续任务时再继续未更新的任务。其实这种模式还可以更进一步,设定一个冷却时间,在冷却时间内再次发现相同的任务直接抛弃上一次相同的任务(根据任务 ID 来区分),这样做可以减少浏览器开销,因为这些任务在下一个周期中肯定会被覆盖。我们有计划的去实现这个内容,但不是现在。


    第二种模式的实现完全得益于浏览器提供的 API,让这种模式实现变为可能,也让用户体验得到提升。


    其实第二种模式后面的理念可以用在一些大型的编辑场景和文档协作中以此来提升用户体验,这也是在研究 React 的调度任务之后得出的结论。


    所以,有人在反驳说看这些源码时没用时可以硬气的告诉他们,看完之后学到了什么。(不过不要盲目的去看,针对具体的问题去研究和借鉴)


    复合直观的


    如果你是一个 React 的用户,你会发现函数式组件的状态心智负担太高,不符合直觉,直接劝退新手。那么,什么是符合直观的代码?当我的组件依赖更新后组件的内容发生响应的更新即可,这也是响应式的核心。在Gyron.js中编写一个简单的响应式组件会如此简单。

    import { FC, useValue } from "gyron";

    interface HelloProps {
    initialCount: number;
    }

    const Hello = FC<HelloProps>(({ initialCount = 0 }) => {
    const count = useValue(initialCount);
    const onClick = () => count.value++;

    // Render JSX
    return <div onClick={onClick}>{count.value}</div>;
    });

    上面定义了一个 Hello 的组件,这个组件接受一个参数 initialCount,类型为数字,组件的功能也很简单,当用户点击这个数字然后自增一。而如果要用 React 去实现或者 Vue 去实现这样一个功能,我们应该怎么做呢?


    我们用 Vue 去实现一个一样的组件,代码如下(使用了 setup 语法)

    <script lang="ts" setup>
    import { ref } from "vue";

    const props = withDefaults(
    defineProps<{
    initialCount: number;
    }>(),
    {
    initialCount: 0,
    }
    );

    const count = ref(props.initialCount);
    function onClick() {
    count.value++;
    }
    </script>

    <template>
    <div @click="onClick">{{ count }}</div>
    </template>

    那么我们用 React 也去实现一个一样的组件,代码如下

    import { useState, useCallback } from "react";

    export const Hello = ({ initialCount = 0 }) => {
    const [count, setCount] = useState(initialCount);

    const onClick = useCallback(() => {
    setCount(count + 1);
    }, [count, setCount]);

    console.log("refresh"); // 每点击一次都会打印一次

    return <div onClick={onClick}>{count}</div>;
    };

    好了,上面是不同框架实现的 Hello 组件。这里并不是说其它框架不好,只是我认为在表达上有一些欠缺。Vue2 中需要理解 this,并且没办法让 this 稳定下来,因为它可以在任何地方修改然后还无法被追踪,在 Vue3 中需要理解 setup 和 template 之间的关系,然后实现类型推断需要了解 defineXXX 这种 API。在 React 中想要更新组件需要注意 React 更新机制,比如内部状态何时才是预期的值,在遇到复杂的组件时这往往比较考验开发者的编码水平。


    以上,Gyron.js是如何解决这些问题的呢?其实,这完全得益于 babel 的强大能力,让开发者不需要知道编译构建优化的知识也能介入其中,改变源码并能重新构建。如果想了解其中的用法可以去 babel 官网plugin 页面


    然后,Gyron.js是如何解决上面提到的问题?我们以上面编写的一个简单组件 Hello 为例,介绍其中到底发生了什么。
    首先,我们的组件用 FC 函数进行了一个包装,这里 FC 就好比一个标识符,在 AST 中属于 BinaryExpression 节点,然后函数体的返回值就是JSX.Element。我们有了这个规则,然后在 babel 中就可以根据这个规则定位到组件本身,再做修改。为了解决重复渲染的问题,我们需要把返回值做一些修改,把JSX.Element用一个函数进行包裹再进行返回。具体转换如下:

    const Hello = FC(({ numbers }) => {
    return <div>{numbers}</div>;
    });
    // ↓ ↓ ↓ ↓ ↓
    const Hello = FC(({ numbers }) => {
    return ({ numbers }) => <div>{numbers}</div>;
    });


    名词解释

    组件函数:我们熟知的 JSX 组件

    渲染函数:转换后的 JSX 函数,用于标记哪些部分是渲染部分,哪些是逻辑部分。类似于 Vue3 的 setup 和 render 的区别。



    这是一个最简单的转换,但是这又引入了另外几个问题。第一,在JSX.Element中的元素内容是组件的参数,但是在下次渲染时取到的是顶层函数中的numbers,为了解决这个问题,我们将顶层函数中的第一个参数作为渲染函数中的第一个参数,然后在渲染函数中访问到的状态就是最新状态。


    这其中还有一个问题,我在组件函数中访问 props 状态也无法保证是最新的,这时候就需要使用Gyron.js提供的onBeforeUpdate方法,这个方法会在组件更新之前调用,然后我们需要把组件函数中定义的 props 全部放进这个函数中,然后根据函数的 new props 去更新用户定义的 props。但是真实的使用场景比较复杂,比如可以这样定义({ a, ...b }) => {},将 props 的 a 单独拎出来,然后其余部分全部归纳到 b 中。


    举一个简单的例子:

    const Hello = FC(({ numbers }) => {
    function transform() {
    return numbers;
    }
    return <div>{transform()}</div>;
    });
    // ↓ ↓ ↓ ↓ ↓
    import { onBeforeUpdate as _onBeforeUpdate } from "gyron";
    const Hello = FC(({ numbers }) => {
    _onBeforeUpdate((_, props) => {
    var _props = props;
    numbers = _props.numbers;
    });
    function transform() {
    return numbers;
    }
    return <div>{transform()}</div>;
    });

    可以看到转换后的组件中多出了一个_onBeforeUpdate方法调用,其作用就是更新组件函数作用域中的 props。



    小结:为了让用户在开发中编写符合直观的代码,Gyron.js在背后做了很多事情。这其实也是 babel 在实际项目中的一种使用方法。



    极快的 hmr


    hmr(hot module replacement )就是模块的热更新,其实这部分功能都是编译工具提供,我们只需要按照他们提供的 API 然后更新我们的组件。

    if (module.hot) {
    module.hot.accept("./hello.jsx", function (Comp1) {
    rerender("HashId", Comp1);
    });
    }

    以上代码我们的插件会自动插入,无需手动引入。(目前还只接入 vite,后续有计划的支持 webpack 等有 hot 模块的工具)


    我们大致了解一下这其中发生了什么?首先,我们还是借助 babel 把每一个组件的名字和内容生成的 hash 值作为注释节点存放在模块中,然后通过编译工具获取到所有本模块的组件,然后通过注册 hot 事件重新渲染更新后的组件。


    好了,讲解了编译工具提供的功能,这里着重讲解一下Gyron.js是如何做到重新渲染的。首先,我们通过编译工具获取到了组件 Hash 和组件函数,然后通过rerender函数执行重新渲染。那么rerender所需要的数据又是从哪里来的呢?其实,在实例第一次初始化的时候这个数据全部都收集到一个Map<string, Set<Component>>数据结构中,然后再通过Component上的 update 方法执行组件的更新。


    SEO 友好


    其实这段内容和Gyron.js本身关系不太大,但是没有Gyron.js提供的能力也很难办到。Gyron.js提供了 SSR(Server Side Render)的渲染模式,也就是我们熟知的服务端渲染。其中大致的原理就是服务端将实例渲染成字符串之后返回给浏览器,然后再通过客户端的hydrate功能让“静态”文本变的可响应。


    以上是简单的用法,然后大致流程图如下所示:




    为了让组件变得更通用,我们在所有组件的 props 上注入了一个变量告诉开发者当前处于何种模式的渲染当中,在服务端渲染当中时不能使用客户端提供的 API,在客户端渲染的过程中不能使用服务端的 API。

    const App = ({ isSSR }) => {
    // ...
    if (!isSSR) {
    document.title = "欢迎";
    }
    };
    import { strict as assert } from "node:assert";
    const App = ({ isSSR }) => {
    // ...
    if (isSSR) {
    assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, "3"]], 4, 5]);
    }
    };

    这是服务端渲染的方式,还有一种介于服务端渲染和客户端渲染之间,就是完全输出静态资源然后就可以部署到任何机器或者在线平台服务商中,比如app.netlify.comgithub.com等。这里不再介绍 SSR 模式的使用方法,可以去gyron.cc/docs/ssr这里有更详细的介绍。


    所见即所得



    这里介绍的是官方文档中的在线编辑器,相比于接入其它平台,我们占用的资源更少,功能齐全。



    经过一段时间的折腾,终于弄出一个简单版的在线编辑器,支持实时预览、语法纠错、语法高亮、智能跳转等功能。


    语言的话目前支持 jsx、tsx、less,并且还支持加载在线资源,比如import { h } from 'https://cdn.jsdelivr.net/npm/gyron'。因为所有数据都不保存在远端,只保存在本地,所以没有使用 standalone 沙盒技术隔离运行环境,也没有防范 xss 攻击。在线编辑器的目标就是让用户可以在线使用,支持用户编辑源代码,支持本地模块导入,支持实时预览,支持多个编辑器运行互不干扰。


    目前这个编辑器支持本地编译和服务端编译,本地编译会让首屏加载变慢所以在线编辑器使用的服务端编译。现在,可以访问gyron.cc/explorer这个地址在线体验。


    如果使用本地编译,这里面最终的就是需要实现一套本地的虚拟文件系统,让打包工具能够正常访问到对应的本地资源。而在 esbuild 中实现一套虚拟文件系统其实很简单,只需要编写一个插件,然后用 resolve 和 load 两种勾子就可以将本地文件输出到 esbuild 中。

    const buildModuleRuntime = {
    name: "buildModuleRuntime",
    setup(build) {
    build.onResolve({ filter: /\.\// }, (args) => {
    return {
    path: args.path,
    namespace: "localModule",
    };
    });
    build.onLoad({ filter: /\.\//, namespace: "localModule" }, async (args) => {
    // 具体实现可以去github https://github.com/gyronorg/core/blob/main/packages/babel-plugin-jsx/src/browser.ts
    const source = findSourceCode(config.sources, args.path);

    if (source) {
    const filename = getFileName(args, source.loader);
    const result = await transformWithBabel(
    source.code,
    filename,
    main,
    true
    );
    return {
    contents: result.code,
    };
    }
    return {
    contents: "",
    loader: "text",
    warnings: [
    {
    pluginName: "buildModuleRuntime",
    text: `Module "${args.path}" is not defined in the local editor`,
    },
    ],
    };
    });
    },
    };

    然后会输出一个 module 文件,最终只需要将文件塞到 script 中让其运行。


    在页面中引用多个编辑器,需要注意的是在不用这个 module 文件后及时删除。可以使用命名空间给 module 加上一个标签,新增和删除都使用这个命名空间作为变量控制当前运行时的资源。


    作者:gyron
    链接:https://juejin.cn/post/7212550699321917501
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    停止编写 API 函数

    如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。 RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的...
    继续阅读 »

    如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。


    RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的相似,只是为了服务于不用的实体。举个例子,假设我们有这些函数。

    // api/users.js

    // 创建
    export function createUser(userFormValues) {
    return fetch('users', { method: 'POST', body: userFormValues });
    }

    // 查询
    export function getListOfUsers(keyword) {
    return fetch(`/users?keyword=${keyword}`);
    }

    export function getUser(id) {
    return fetch(`/users/${id}`);
    }

    // 更新
    export updateUser(id, userFormValues) {
    return fetch(`/users/${is}`, { method: 'PUT', body: userFormValues });
    }

    // 删除
    export function removeUser(id) {
    return fetch(`/users/${id}`, { method: 'DELETE' });
    }

    类似的功能可能存在于其他实体,例如:城市、产品、类别...但是我们可以用一个简单的函数调用来代替这些函数:

    // apis/users.js
    export const users = crudBuilder('/users');

    // apis/cities.js
    export const cities = crudBuilder('/regions/cities');


    然后像这样去使用:

    users.create(values);
    users.show(1);
    users.list('john');
    users.update(values);
    users.remove(1);

    你可能会问为什么?有一些很好的理由:


    • 减少了代码行数:你编写的代码,和当你离开公司时其他人维护的代码
    • 强制执行 API 函数的命名约定,这可以增加代码的可读性和可维护性。例如你已经见过的函数名称: getListOfUsersgetCitiesgetAllProductsproductIndexfetchCategories等, 他们都在做相同的事情,那就是“获取实体列表”。使用这种方法,你将始终拥有entityName.list()函数,并且团队中的每个人都知道这一点。

    所以,让我们创建crudBuilder()函数,然后再添加一些糖。


    一个非常简单的 CRUD 构造器


    对于上边的简单示例,crudBuilder()函数将非常简单:

    export function crudBuilder(baseRoute) {
    function list(keyword) {
    return fetch(`${baseRoute}?keyword=${keyword}`);
    }
    function show(id) {
    return fetch(`${baseRoute}/${id}`);
    }
    function create(formValues) {
    return fetch(baseRoute, { method: 'POST', body: formValues });
    }
    function update(id, formValues) {
    return fetch(`${baseRoute}/${id}`, { method: 'PUT', body: formValues });
    }
    function remove(id) {
    return fetch(`${baseRoute}/${id}`, { method: 'DELETE' });
    }

    return {
    list,
    show,
    create,
    update,
    remove
    };
    }

    假设约定 API 路径并且给相应实体提供一个路径前缀,他将返回该实体上调用 CRUD 操作所需的所有方法。


    但老实说,我们知道现实世界的应用程序并不会那么简单。在将这种方法应用于我们的项目时,有很多事情需要考虑:


    • 过滤:列表 API 通常会提供许多过滤器参数
    • 分页:列表 API 总是分页的
    • 转换:API 返回的值在实际使用之前可能需要进行一些转换
    • 准备:formValues对象在发送给 API 之前需要做一些准备工作
    • 自定义接口:更新特定项的接口不总是${baseRoute}/${id}

    因此,我们需要可以处理更多复杂场景的 CRUD 构造器。


    高级 CRUD 构造器


    让我们通过上述方法来构建一些日常中我们真正使用的东西。


    过滤


    首先,我们应该在 list输出函数中处理更加复杂的过滤。每个实体列表可能有不同的过滤器并且用户可能应用了其中的一些过滤器。因此,我们不能对应用过滤器的形状和值有任何假设,但是我们可以假设任何列表过滤都可以产生一个对象,该对象为不同的过滤器名称指定了一些值。例如,我们可以过滤一些用户:

    const filters = {
    keyword: 'john',
    createdAt: new Date('2020-02-10')
    };

    另一方面,我们不知道这些过滤器应该如何传递给 API,但是我们可以假设(跟 API 提供方进行约定)每一个过滤器在列表 API 中都有一个相应的参数,可以以'key=value'URL 查询参数的形式被传递。


    因此我们需要知道如何将应用的过滤器转换成相对应的 API 参数来创建我们的 list 函数。这可以通过将 transformFilters 参数传递给 crudBuilder() 来完成。举一个用户的例子:

    function transformUserFilters(filters) {
    const params = [];
    if (filters.keyword) {
    params.push(`keyword=${filters.keyword}`);
    }
    if (filters.createdAt) {
    params.push(`create_at=${dateUtility.format(filters.createdAt)}`);
    }

    return params;
    }

    现在我们可以使用这个参数来创建 list 函数了。

    export function crudBuilder(baseRoute, transformFilters) {
    function list(filters) {
    let params = transformFilters(filters)?.join('&');
    if (params) {
    params += '?';
    }

    return fetch(`${baseRoute}${params}`);
    }
    }

    转换和分页


    从 API 接收的数据可能需要进行一些转换才能在我们的应用程序中使用。例如,我们可能需要将 snake_case 转换成驼峰命名或将一些日期字符串转换成用户时区。


    此外,我们还需要处理分页。


    我们假设来自 API 的分页数据都按照如下格式(与 API 提供者约定):

    {
    data: [], // 实体对象列表
    pagination: {...} // 分页信息
    }

    因此,我们需要知道如何转换单个实体对象。然后我们可以遍历列表对象来转换他们。为此,我们需要一个 transformEntity 函数作为 crudBuilder 的参数。

    export function crudBuilder(baseRoute, transformFilters, transformEntity, ) {
    function list(filters) {
    const params = transformFilters(filters)?.join('&');
    return fetch(`${baseRoute}?${params}`)
    .then((res) => res.json())
    .then((res) => ({
    data: res.data.map((entity) => transformEntity(entity)),
    pagination: res.pagination
    }));
    }
    }

    list() 函数我们就完成了。


    准备


    对于 createupdate 函数,我们需要将 formValues 转换成 API 需要的格式。例如,假设我们在表单中有一个 City 的城市选择对象。但是 create API 只需要 city_id。因此,我们需要一个执行以下操作的函数:

    const prepareValue = formValue => ({city_id: formValues.city.id});

    这个函数会根据用例返回普通对象或者 FormData,并且可以将数据传递给 API:

    export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues) {
    function create(formValues) {
    return fetch(baseRoute, {
    method: 'POST',
    body: prepareFormValues(formValues)
    });
    }
    }

    自定义接口


    在一些少数情况下,对实体执行某些操作的 API 接口不遵循相同的约定。例如,我们不能使用 /users/${id} 来编辑用户,而是使用 /edit-user/${id}。对于这些情况,我们应该指定一个自定义路径。


    在这里我们允许覆盖 crud builder 中使用的任何路径。注意,展示、更新、移除操作的路径可能取决于具体实体对象的信息,因此我们必须使用函数并传递实体对象来获取路径。


    我们需要在对象中获取这些自定义路径,如果没有指定,就退回到默认路径。像这样:

    const paths = {
    list: 'list-of-users',
    show: (userId) => `users/with/id/${userId}`,
    create: 'users/new',
    update: (user) => `users/update/${user.id}`,
    remove: (user) => `delete-user/${user.id}`
    };

    最终的 BRUD 构造器


    这是创建 CRUD 函数的最终代码。

    export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues, paths) {
    function list (filters) {
    const path = paths.list || baseRoute;
    let params = transformFilters(filters)?.join('&');
    if (params) {
    params += '?';
    }

    return fetch(`${path}${params}`)
    .then((res) => res.json())
    .then(() => ({
    data: res.data.map(entity => transformEntity(entity)),
    pagination: res.pagination
    }));
    }
    function show(id) {
    const path = paths.show?.(id) || `${baseRoute}/${id}`;

    return fetch(path)
    .then((res) => res.json())
    .then((res => transformEntity(res)));
    }
    function create(formValues) {
    const path = paths.create || baseRoute;

    return fetch(path, { method: 'POST', body: prepareFormValues(formValues) });
    }
    function update(id, formValues) {
    const path = paths.update?.(id) || `${baseRoute}/${id}`;

    return fetch(path, { method: 'PUT', body: formValues });
    }
    function remove(id) {
    const path = paths.remove?.(id) || `${baseRoute}/${id}`;

    return fetch(path, { method: 'DELETE' });
    }
    return {
    list,
    show,
    create,
    update,
    remove
    }
    }


    Saeed Mosavat: Stop writing API functions


    作者:七七喝可乐
    链接:https://juejin.cn/post/7147993043923107870
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    实现滚动点赞墙

    web
    需要实现的效果如下: 需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~ 纯css实现 scss如下:(如果要将scss改为less,将$改为@就可以了) 当移动到第8行结束的时候,同屏出现的两行(第9行和第1...
    继续阅读 »

    需要实现的效果如下:



    需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~


    纯css实现



    scss如下:(如果要将scss改为less,将$改为@就可以了)


    当移动到第8行结束的时候,同屏出现的两行(第9行和第10行),就需要结束循环,重头开始了


    这是一个上移的动画,动画执行的时间就是8s


    itemShowTime+(itemShowTime + (oneCycleItemNum - oneScreenItemNum)(oneScreenItemNum) * (itemShowTime / $oneScreenItemNum)


    $itemHeight: 60px; // 单个item的高度

    $itemShowTime: 2s; // 单个item从完整出现到消失的时长
    $oneCycleItemNum: 8; // 单个循环上移的item条数
    $oneScreenItemNum: 2; // 同屏出现的item条数(不能大于 oneCycleItemNum)

    $oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

    @keyframes dynamics-rolling {
    from {
    transform: translateY(0);
    }
    to {
    transform: translateY(-$itemHeight * $oneCycleItemNum);
    }
    }
    .container {
    height: 600px;

    animation: dynamics-rolling $oneCycleItemTime linear infinite;
    .div {
    line-height: 60px;
    }
    }

    .visibleView {
    width: 100%;
    height: 120px;
    overflow: hidden;
    background-color: skyblue;

    }
    .box {
    width: 100%;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    }

    简单的demo:



    import React, { useEffect, useRef, useState } from 'react'
    import styles from './index.module.scss'

    const dataSource = new Array(50).fill(0).map((_, index) => index + 1)

    export default function CycleScrollList() {
    const [data, setData] = useState(dataSource.slice(0, 10))

    return (
    <div className={styles.box}>
    <div className={styles.visibleView}>
    <div className={styles.container}>
    {
    data.map((item, index) => (
    <div key={ index } className={styles.div}>{ item }</div>
    ))
    }
    </div>
    </div>
    </div>

    )
    }

    setInterval监听


    css动画是定时的,所以可以定时更新列表内容,但是会有很明显的抖动,效果不太友好,应该是定时器的时间还不能太准




    import React, { useEffect, useRef, useState } from 'react'
    import styles from './index.module.scss'

    const dataSource = new Array(50).fill(0).map((_, index) => index + 1)


    export default function CycleScrollList() {
    const [data, setData] = useState(dataSource.slice(0, 10))
    const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index

    useEffect(() => {
    const timer = setInterval(() => {
    replaceData()
    },4900)

    return () => {
    clearInterval(timer)
    }
    }, [])


    const replaceData = () => {
    let newData = []
    if (nextIndex.current-5 < 0) {
    newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
    } else {
    newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
    }
    // 使用当前的后半份数据,再从 dataSource 中拿新数据
    console.log(newData)
    const nextIndexTemp = nextIndex.current + 5
    const diff = nextIndexTemp - dataSource.length
    if (diff < 0) {
    nextIndex.current = nextIndexTemp
    } else {
    // 一轮数据用完,从头继续
    nextIndex.current = diff
    }
    setData(newData)
    }

    return (
    <div className={styles.box}>
    <div className={styles.visibleView}>
    <div className={styles.container}>
    {
    data.map((item, index) => (
    <div key={ index } className={styles.div}>{ item }</div>
    ))
    }
    </div>
    </div>
    </div>

    )
    }

    IntersectionObserver监听


    监听第5个元素


    如果第五个元素可见了,意味着不可见时,需要更换数据了


    如果第五个元素不可见了,立刻替换数据


    替换的数据如下:



    使用IntersectionObserver监听元素,注意页面卸载时,需要去除绑定


    tsx如下:



    import React, { useEffect, useRef, useState } from 'react'
    import styles from './index.module.scss'

    const dataSource = new Array(50).fill(0).map((_, index) => index + 1)
    const ITEM_5_ID = 'item-5'

    export default function CycleScrollList() {
    const [data, setData] = useState(dataSource.slice(0, 10))

    const intersectionObserverRef = useRef<IntersectionObserver | null>()
    const item5Ref = useRef<HTMLDivElement | null>(null)

    const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index
    const justVisible5 = useRef<boolean>(false) // 原来是否为可视

    useEffect(() => {
    intersectionObserverRef.current = new IntersectionObserver((entries) => {
    entries.forEach((item) => {
    if (item.target.id === ITEM_5_ID) {
    // 与视图相交(开始出现)
    if (item.isIntersecting) {
    justVisible5.current = true
    }
    // 从可视变为不可视
    else if (justVisible5.current) {
    replaceData()
    justVisible5.current = false
    }
    }
    })
    })
    startObserver()

    return () => {
    intersectionObserverRef.current?.disconnect()
    intersectionObserverRef.current = null
    }
    }, [])

    const startObserver = () => {
    if (item5Ref.current) {
    // 对第五个 item 进行监测
    intersectionObserverRef.current?.observe(item5Ref.current)
    }
    }

    const replaceData = () => {
    let newData = []
    if (nextIndex.current-5 < 0) {
    newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
    } else {
    newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
    }
    // 使用当前的后半份数据,再从 dataSource 中拿新数据
    console.log(newData)
    const nextIndexTemp = nextIndex.current + 5
    const diff = nextIndexTemp - dataSource.length
    if (diff < 0) {
    nextIndex.current = nextIndexTemp
    } else {
    // 一轮数据用完,从头继续
    nextIndex.current = diff
    }
    setData(newData)
    }

    return (
    <div className={styles.box}>
    <div className={styles.visibleView}>
    <div className={styles.container}>
    {
    data.map((item, index) => (
    index === 4 ?
    <div id={ ITEM_5_ID } ref={ item5Ref } key={ index } className={styles.div}>{ item }</div>
    :
    <div key={ index } className={styles.div}>{ item }</div>
    ))
    }
    </div>
    </div>
    </div>

    )
    }

    scss样式


    $itemHeight: 60px; // 单个item的高度

    $itemShowTime: 3s; // 单个item从完整出现到消失的时长
    $oneCycleItemNum: 5; // 单个循环上移的item条数
    $oneScreenItemNum: 3; // 同屏出现的item条数(不能大于 oneCycleItemNum)

    $oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

    @keyframes dynamics-rolling {
    from {
    transform: translateY(0);
    }
    to {
    transform: translateY(-$itemHeight * $oneCycleItemNum);
    }
    }
    .container {
    height: 600px;

    animation: dynamics-rolling $oneCycleItemTime linear infinite;
    .div {
    line-height: 60px;
    }
    }

    .visibleView {
    width: 100%;
    height: 120px;
    overflow: hidden;
    background-color: skyblue;

    }
    .box {
    width: 100%;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    }

    作者:0522Skylar
    来源:juejin.cn/post/7278244755825442853
    收起阅读 »

    用了策略模式之后,再也不用写那么多 if else 了,真香!

    web
    前言 从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式。 策略模式的定义 先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起...
    继续阅读 »

    前言


    从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式


    策略模式的定义


    先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。


    简单来说就是有多种选择,然后一般只会选择一种。从代码的角度来说就是,定义一系列的ifelseif,然后只会命中其中一个。


    举个例子


    话不多说,直接来看例子,比如我们需要计算员工工资,员工工资计算规则如下:



    • 高级工:时薪为25块/小时

    • 中级工:时薪为20块/小时

    • 初级工:时薪为15块/小时


    按每天10小时的工作时长来算。


    一、第一版实现:


    const calculateSalary = function (workerLevel, workHours = 10) {
    if (workerLevel === 'high') {
    return workHours * 25
    }
    if (workerLevel === 'middle') {
    return workHours * 20
    }
    if (workerLevel === 'low') {
    return workHours * 15
    }
    }
    console.log(calculateSalary('high')) // 250
    console.log(calculateSalary('middle')) // 200

    这段代码具有明显的缺点:



    • calculateSalary函数庞大,有许多的if else语句,这些语言需要覆盖所有的逻辑分支

    • calculateSalary函数缺乏弹性,如果新增一种员工等级higher,需要修改calculateSalary函数的内部实现,违反开放——封闭原则

    • 算法的复用性差


    二、第二版实现(函数组合):


    当然,我们可以使用函数组合的方式重构代码,把每一个if中的逻辑单独抽离成一个函数。


    const workerLevelHigh = function (workHours) {
    return workHours * 25
    }

    const workerLevelMiddle = function (workHours) {
    return workHours * 20
    }
    const workerLevelLow = function (workHours) {
    return workHours * 15
    }

    const calculateSalary = function (workerLevel, workHours = 10) {
    if (workerLevel === 'high') {
    return workerLevelHigh(workHours)
    }
    if (workerLevel === 'middle') {
    return workerLevelMiddle(workHours)
    }
    if (workerLevel === 'low') {
    return workerLevelLow(workHours)
    }
    }
    console.log(calculateSalary('high', 10)) // 250
    console.log(calculateSalary('middle', 10)) // 200

    这样会提高算法的复用性,但这种改善十分有限,calculateSalary函数依旧庞大和缺乏弹性。


    三、第三版实现(策略模式):


    我们可以把不变的部分和变化的部分拆分开来。



    • 不变的部分:算法的使用方式不变,都是根据某个算法取得计算后的工资数额;

    • 变化的部分:算法的实现。


    我们js的对象是key value的形式,这可以帮助我们天然的替换掉if else


    因此,我们可以定义对象的两部分:



    • 针对变化的部分,我们可以定义一个策略对象,它封装了具体的算法,负责具体的计算过程

    • 针对不变的部分,我们提供一个Context函数,它接受客户的请求,随后把请求委托给策略对象。


    const strategies = {
    "high": function (workHours) {
    return workHours * 25
    },
    "middle": function (workHours) {
    return workHours * 20
    },
    "low": function (workHours) {
    return workHours * 15
    },
    }

    const calculateSalary = function (workerLevel, workHours) {
    return strategies[workerLevel](workHours)
    }
    console.log(calculateSalary('high', 10)) // 250
    console.log(calculateSalary('middle', 10)) // 200

    策略模式的优缺点


    从我个人在实际项目中的使用来看,策略模式的优缺点如下:


    优点:



    • 代码复杂度降低:再也不用写那么多if else了。eslint其中有一项规则配置叫圈复杂度,其中一条分支也就是一个if会让圈复杂增加1,圈复杂度高的代码不易阅读和维护,用策略模式就能很好的解决这个问题;

    • 易于切换、理解和扩展:它将算法封装在独立的strategy中,比如你要在上面代码中加一个等级higherlower,直接更改策略对象strategies就行,十分方便。

    • 复用性高:策略模式中的算法可以复用在系统的其它地方,你只需要用将策略类strategies用export或者module.exports导出,就能在其他地方很方便的复用。


    缺点:



    • 增加使用者使用负担:因为大量运用策略模式会在实际项目中堆砌很多策略类或者策略对象,这样项目的新人如果不熟悉这些策略类和策略对象,会增加他们的使用成本和学习成本,前期来说会比看if else更加难懂。


    小结


    以上就是我个人对策略模式的解读和了解啦,实际上项目中用策略模式的场景还是挺多的,因为在写业务代码中,很容易写出大量的if else,这时候就可以封装为策略模式,方便项目维护和扩展,从我个人的使用体验来看,还是相当香的。


    大家喜欢在实际项目使用策略模式么,欢迎留言和讨论~


    作者:han_
    来源:juejin.cn/post/7279041076273610764
    收起阅读 »

    八百年不面试,一面试就面得一塌糊涂

    web
    前言 好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。 最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈 目前是三面,但是估计止步于三面了,然后我稍微整理...
    继续阅读 »

    前言


    好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。


    最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈


    目前是三面,但是估计止步于三面了,然后我稍微整理了一下面试题,但是这里只说出我的思考,而不说出答案,至于为啥不写答案,我只能说,我自己不会。。。有些内容我需要进行学习,然后系统地、简单易懂地分享给大家


    我说一下我自身的情况:主要技术栈是vue全家桶,算是能深挖的那种,其他的,react、webpack、vite、less、sass、tailwindcss、unocss、Nuxt、node等系列都会,但是说实话我是没法手写出来的,只停留在会用的程度,webgl、canvas等可视化方向还可以,毕竟我之前就是做这个的,算法还算可以,不说精通,但是一般题是可以做出来的,然后在基础方面,也就是js、css这块,我只能说了解吧,因为见过css大神coco这种的,就感觉自己css从熟悉变成了听说。


    反正大概的就那样吧,会的比较多,比较杂,但是很多都不精通(不去看源码这种),关键的来了,我对于浏览器这块比较薄弱,计网、操作系统对我来说像噩梦一样,我觉得是我经验少吧,没能在工作中接触到这几个层面,所以我就是真的能答出来,也就是硬背的,并且不能举一反三,这也导致了这次面试的惨败



    下面我说一下面试吧


    注意,公司的技术栈主要是React(umi)这块,vue很少很少,然后会用node写一些中间件,大部分都是大前端



    然后算法问题的话,也不在这里说,主要说一些口述的问题


    一面


    一面是对我来说最友好的一面了,基本上都是简单的一些基础问题


    面试官主要是react技术栈,然后我和他说了,我主要是vue的,vue的原理可以,但是问我react的太深的问题我是不太会的,首先是自我介绍,然后开始问问题



    • pinia和vuex的区别,其实他想问我Redux和Mobx和其他React状态管理的区别,但是奈何我就会这几个,所以他索性问了问了我pinia

    • css实现DOM节点的水平居中有几种方式:我记得我说了四种,flex,text-align,margin,position,应该还有,但是一瞬间的话,脑袋瓦特了

    • 实现一个左右布局,左侧200px,右侧自适应,css写有几种方式:我说了浮动、定位、弹性盒、网格这四种

    • 检测js数据类型,typeof和instanceof区别,instanceof原理:这里我直接手写了instanceof,这个很简单

    • 浏览器输入url,到看到页面会发生什么:我当时懵了,我看过n个面经都说过这个问题,经典八股,但是我就是没背,只能磕磕巴巴说了一些(我八股真的不行,而且我不背这玩意)

    • 用Java的时候,对登录请求进行拦截,怎么处理的:这个很简单哈,为啥问Java,这是因为我简历上有,我之前从事过全栈,然后他就问了一下

    • 函数式编程的副作用是什么

    • 工作的经历,项目问题(这个占据了大部分的时间),其中有个问题可以分享一下,因为我用了wangeditor,他问我wangeditor的内核是什么


    一面总体来说是很友好的,而且都答出来了,面试官很礼貌,面试感受非常好,第二天下午的时候通知二面


    二面


    噩梦的开始



    • 自我介绍

    • 公司项目问题(绝大部分时间)

    • vue、react数据绑定的区别

    • 我想存储一个客户端的数据,前端有哪些存储方式:后来就存储、内存的问题开始展开

    • pinia会进行数据的存储,它最终存在了哪里

    • js的内存是怎么进行管理的

    • 垃圾回收、内存泄漏,什么情况会导致内存泄漏

    • 闭包是什么,应用场景,怎么操作会产生内存泄漏

    • 你在工作时用的哪种协议

    • 除了http还有哪些通信协议(跟前端有关的)

    • websocket通信过程是怎么样的

    • 前端跨域相关问题

    • 代理相关问题

    • 服务和服务之间有没有跨域

    • 前端安全方面有哪些攻击方式

    • 该怎么处理呢

    • node有哪些框架可以处理脚本攻击(或者是库)


    有些问题记不清了,后面有一些网络的问题,但是忘了,前面其实还好,而且问题是一步一步衍生出来的,这感觉很好,但是到网络安全这里,我就有点不会了,当时就感觉完犊子了,再见


    然后过了三天,hr电话告诉我过了,约了三面,其实是比较吃惊的,我以为已经止步了


    三面


    最难受的一面



    • 自我介绍

    • 说说最近自己认为最好的项目,然后我说了一些,然后对方:就这?我一时语塞,开始紧张(项目占据了大多数时间)

    • 说说tcp三次挥手,为什么不能两次

    • tcp粘包,讲讲

    • 还有一些计网和操作系统的问题,这里是因为,我根本不会,所以压根没记住问题。。

    • 进程、线程区别,举个生动的例子

    • 讲讲多线程

    • 浏览器的核心线程和核心进程有哪些

    • MySQL的引擎

    • 现在有一个100tb的文件,让你一分钟之内把这个文件遍历出来,怎么做


    计网和操作系统一塌糊涂,现在面试还没有反馈,凉凉了,而且看面试官的态度也能看出来是很不满意的


    总结


    平均时长在45min左右


    几乎没问vue的任何问题,这是我最难受的,而且js、ts、css也几乎不问的,反正就是我上面的技术栈几乎一个没问,面试官主要就问你两处:你的工作经验(也就是你曾经的公司项目),以及计网和操作系统


    因为我有做一些开源的项目和个人的项目,但是他们更在乎你之前公司的项目是什么样的


    我自己的项目比较多,简历就有5.6页,但是没啥用,他们都没问


    我也发现自己计网、操作系统这里太薄弱了,有时间还是得系统学习一下的,自己的确在开发中没遇到过这些,欠加思考


    希望大家也能重视一下这里吧


    作者:Shaka
    来源:juejin.cn/post/7273682292538933306
    收起阅读 »

    帮你省时间,看看 bun v1.0 怎么用!

    web
    本文基于 Window Ubuntu WSL 环境测试,本文只选取重点,细节需查看文档 一、bun v1.0 做了什么? all in JavaScript/TypeScript app。看起真的很了不起! 作为JS/TS运行时 作为包管理工具和包运行...
    继续阅读 »

    本文基于 Window Ubuntu WSL 环境测试,本文只选取重点,细节需查看文档



    一、bun v1.0 做了什么?



    all in JavaScript/TypeScript app。看起真的很了不起!




    • 作为JS/TS运行时

    • 作为包管理工具和包运行器

    • 作为构建工具

    • 作为测试运行器

    • 对外提供 API


    资源



    二、安装 bun v1.0



    bun 目前不支持 window 环境,但是可以在 wsl 中使用。



    2.1) 各种安装方法



    • curl


    curl -fsSL https://bun.sh/install | bash # 检查:which bun


    • 使用 npm 安装


    npm install -g bun # 检查:which bun


    • 其他平台的安装方法



    brew tap oven-sh/bun # for macOS and Linux
    brew install bun # brew
    docker pull oven/bun # docker

    2.2) bun 提供的命令


    命令描述
    init初始化一个 bun 项目
    run运行一个文件或者脚本
    test运行测试
    xbun x 的别名,类似于 npx
    repl进入交互式环境
    create使用模板创建项目
    install安装依赖
    add添加依赖
    remove移除依赖
    update更新依赖
    link全局链接一个 npm 包
    unlink移除全局链接的 npm 包
    pm更多的包管理命令
    build打包 TypeScript/JavaScript 文件到单个文件
    update获取最新的 bun 版本

    三、作为 JS/TS 运行时


    bun index.js // 运行 js 文件
    bun run index.ts // 运行 ts 文件
    // 其他相关的 tsx/jsx/...

    如果直接运行 index.tsx 没有任何依赖会报错:


    const Ad = <div>ad</div>

    console.log(Ad)

    // bun index.tsx
    // 错误:Cannot find module "react/jsx-dev-runtime" from "/xxx/index.tsx"

    四、作为包管理工具和包运行器


    4.1)初始化一个项目


    bun init # 与 npm init -y 类似

    4.2)使用脚手架


    # 与 npx 类似, 以下可能常用的初始化项目的脚手架
    bun create react-app
    bun create remix
    bun create next-app
    bun create nuxt-app

    五、作为构建工具



    • 初始化一个简单的项目


    cd your_dir
    bun init # 默认
    bun add react react-dom # 添加依赖包
    touch index.tsx


    • 添加 TSX 文件内容


    import React from 'react'

    const App = () => {
    return <div>This is App</div>
    }


    • 打包 bun build


    bun build ./index.tsx --outfile=bundle.js


    提示:bundle.js 中打包了 react 相关的包。



    六、作为测试运行器


    测试与 Jest 非常相似, 以下是官方示例:


    import { expect, test } from "bun:test";

    test("2 + 2", () => {
    expect(2 + 2).toBe(4);
    });

    运行时测试:


    bun test

    速度很快,输出结果:


    bun test v1.0.0 (822a00c4)

    t.test.ts:
    ✓ 2 + 2 [1.03ms]

    1 pass
    0 fail
    1 expect() calls
    Ran 1 tests across 1 files. [92.00ms]

    七、对外提供 API


    项目描述
    HTTP 服务处理 HTTP 请求和响应
    WebSocket 套接字支持 WebSocket 连接
    Workers 工具在后台运行多线程任务
    Binary data处理二进制数据
    Streams流处理
    File I/O文件输入/输出操作
    import.meta访问模块元信息
    SQLite使用 SQLite 数据库
    FileSystemRouter文件系统路由器
    TCP socketsTCP 套接字通信
    Globals全局变量和对象
    Child processes创建子进程
    Transpiler转译器
    Hashing哈希函数和算法
    Console控制台输出
    FFI外部函数接口
    HTMLRewriterHTML 重写和转换
    Testing测试工具和框架
    Utils实用工具函数
    Node-APINode.js API 访问

    八、展望



    • windows 支持


    小结


    本文主要讲解了 bun v1.0 中所做的事情,包含极速的运行时、一体化的包管理工具、内置测试运行器、构建应用(打包)和对象提供各种类型的 API(兼容 Node API(非完全)),如此功能能完整的 bun 你想尝试一下吗?


    作者:进二开物
    来源:juejin.cn/post/7277399972916428835
    收起阅读 »

    为什么react中的hooks都要放在顶部?

    1. 使用场景: 公司开会的时候有师兄问到为什么hooks定义一般都写在顶部,对于这个问题我以前总结过,这次看了 react新文档后我给出更加详细的解释并给出具体代码来解释为什么要放在顶部。 2.官网解释: 1.官网截图镇楼: 2.那我写在条件语句中会怎样 ...
    继续阅读 »

    1. 使用场景:


    公司开会的时候有师兄问到为什么hooks定义一般都写在顶部,对于这个问题我以前总结过,这次看了
    react新文档后我给出更加详细的解释并给出具体代码来解释为什么要放在顶部。


    2.官网解释:


    1.官网截图镇楼:




    2.那我写在条件语句中会怎样


    我给出一段代码:其中const [message, setMessage] = useState('');写在了条件语句里面

    import { useState } from 'react';

    export default function FeedbackForm() {
    const [isSent, setIsSent] = useState(false);
    if (isSent) {
    return <h1>Thank you!</h1>;
    } else {
    // eslint-disable-next-line
    const [message, setMessage] = useState('');
    return (
    <form onSubmit={e => {
    e.preventDefault();
    alert(`Sending: "${message}"`);
    setIsSent(true);
    }}>
    <textarea
    placeholder="Message"
    value={message}
    onChange={e => setMessage(e.target.value)}
    />
    <br />
    <button type="submit">Send</button>
    </form>
    );
    }
    }

    效果图:这是一个收集用户反馈的小表单。当反馈被提交时



     它应该显示一条感谢信息,当我点击确定时出现一条错误。




    “渲染的 hooks 比预期的少”


    3.那我不写在顶部可能会怎样


    下方的const [message, setMessage] = useState('');并没有写在顶部

       import { useState } from 'react';

    export default function FeedbackForm() {
    const [isSent, setIsSent] = useState(false);
    if (isSent) {
    return <h1>Thank you!</h1>;
    }
    const [message, setMessage] = useState('');
    return (
    <form onSubmit={e => {
    e.preventDefault();
    alert(`Sending: "${message}"`);
    setIsSent(true);
    }}>
    <textarea
    placeholder="Message"
    value={message}
    onChange={e => setMessage(e.target.value)}
    />
    <br />
    <button type="submit">Send</button>
    </form>
    );
    }
    }

    效果图:



     点击确认后:
    同样出现这个错误:提前return导致后面一个hooks没有渲染。




    4.原因分析


    从源码的角度来说的话,React会在内部创建一个名为“Hooks”(中文为钩子)的数据结构来追踪每个组件的状态。


    在函数组件中调用Hook时,React会根据Hook的类型将其添加到当前组件的Hooks链表中。然后,React会将这些Hooks存储在Fiber节点的“memoizedState”字段中,以便在下一次渲染时使用。


    如果你在代码中多次调用同一个Hook,React会根据Hooks的顺序将其添加到当前组件的Hooks链表中。这样,React就可以确定哪个状态应该与哪个组件关联,并且能够正确地更新UI。


    以下是一个示例代码片段:

    import { useState, useEffect } from 'react';

    function useCustomHook() {

    const [count, setCount] = useState(0);

    useEffect(() => {
    document.title = `Count: ${count}`;
    }, [count]);
    return [count, setCount];
    }

    export default function MyComponent() {

    const [count, setCount] = useCustomHook();
    return (
    <div>
    <p>Count: {count}</p>
    <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
    );
    }

    在上面的代码中,useCustomHook是一个自定义Hook,它使用useStateuseEffectHook来管理状态。在MyComponent中,我们调用自定义Hook并使用返回值来渲染UI。


    由于useCustomHook只能在函数组件或其他自定义Hooks的最顶层调用,我们不能将它嵌套在条件语句、循环或其他函数内部。如果这样做,React将无法正确追踪状态并更新UI,可能导致不可预测的结果。如果我们条件渲染中使用可能导致没有引入useCustomHook(),从而导致错误。


    总结描述就是创建了一个链表,当在条件语句中使用hooks时可能会导致前后两次链表不同,从而导致错误,所以我们必须尽可能避免这种错误从而写在顶部。


    作者:北海天空
    链接:https://juejin.cn/post/7223586612927971388
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    你可能并不需要useEffect

    相信大家在写react时都有这样的经历:在项目中使用了大量的useEffect,以至于让我们的代码变得混乱和难以维护。 难道说useEffect这个hook不好吗?并不是这样的,只是我们一直在滥用而已。 在这篇文章中,我将展示怎样使用其他方法来代替useEff...
    继续阅读 »

    相信大家在写react时都有这样的经历:在项目中使用了大量的useEffect,以至于让我们的代码变得混乱和难以维护。


    难道说useEffect这个hook不好吗?并不是这样的,只是我们一直在滥用而已。


    在这篇文章中,我将展示怎样使用其他方法来代替useEffect。


    什么是useEffect


    useEffect允许我们在函数组件中执行副作用。它可以模拟 componentDidMount、componentDidUpdate 和componentWillUnmount。我们可以用它来做很多事情。但是它也是一个非常危险的钩子,可能会导致很多bug。


    为什么useEffect是容易出现bug的


    来看一个定时器的例子:

    import React, { useEffect } from 'react'

    const Counter = () => {
    const [count, setCount] = useState(0)

    useEffect(() => {
    const timer = setInterval(() => {
    setCount((c) => c + 1)
    }, 1000)
    })

    return <div>{count}</div>
    }

    这是一个非常常见的例子,但是它是非常不好。因为如果组件由于某种原因重新渲染,就会重新设置定时器。该定时器将每秒调用两次,很容易导致内存泄漏。


    怎样修复它?


    useRef

    import React, { useEffect, useRef } from 'react'

    const Counter = () => {
    const [count, setCount] = useState(0)
    const timerRef = useRef()

    useEffect(() => {
    timerRef.current = setInterval(() => {
    setCount((c) => c + 1)
    }, 1000)
    return () => clearInterval(timerRef.current)
    }, [])

    return <div>{count}</div>
    }

    它不会在每次组件重新渲染时设置定时器。但是我们在项目中并不是这么简单的代码。而是各种状态,做各种事情。


    你以为你写的useEffect

    useEffect(() => {
    doSomething()

    return () => cleanup()
    }, [whenThisChanges])

    实际上是这样的

    useEffect(() => {
    if (foo && bar && (baz || quo)) {
    doSomething()
    } else {
    doSomethingElse()
    }
    // 遗忘清理函数。
    }, [foo, bar, baz, quo, ...])

    写了一堆的逻辑,这种代码非常混乱难以维护。


    useEffect 到底是用来干啥的


    useEffect是一种将React与一些外部系统(网络、订阅、DOM)同步的方法。如果你没有任何外部系统,只是试图用useEffect管理数据流,你就会遇到问题。



    有时我们并不需要useEffect


    1.我们不需要useEffect转化数据

    const Cart = () => {
    const [items, setItems] = useState([])
    const [total, setTotal] = useState(0)

    useEffect(() => {
    setTotal(items.reduce((total, item) => total + item.price, 0))
    }, [items])

    // ...
    }

    上面代码使用useEffect来进行数据的转化,效率很低。其实并不需要使用useEffect。当某些值可以从现有的props或state中计算出来时,不要把它放在状态中,在渲染期间计算它。

    const Cart = () => {
    const [items, setItems] = useState([])
    const [total, setTotal] = useState(0)

    const totalNum = items.reduce((total, item) => total + item.price, 0)

    // ...
    }

    如果计算逻辑比较复杂,可以使用useMemo:

    const Cart = () => {
    const [items, setItems] = useState([])
    const total = useMemo(() => {
    return items.reduce((total, item) => total + item.price, 0)
    }, [items])

    // ...
    }

    2.使用useSyncExternalStore代替useEffect


    useSyncExternalStore


    常见方式:

    const Store = () => {
    const [isConnected, setIsConnected] = useState(true)

    useEffect(() => {
    const sub = storeApi.subscribe(({ status }) => {
    setIsConnected(status === 'connected')
    })

    return () => {
    sub.unsubscribe()
    }
    }, [])

    // ...
    }

    更好的方式:

    const Store = () => {
    const isConnected = useSyncExternalStore(
    storeApi.subscribe,
    () => storeApi.getStatus() === 'connected',
    true
    )

    // ...
    }

    3.没必要使用useEffect与父组件通信

    const ChildProduct = ({ onOpen, onClose }) => {
    const [isOpen, setIsOpen] = useState(false)

    useEffect(() => {
    if (isOpen) {
    onOpen()
    } else {
    onClose()
    }
    }, [isOpen])

    return (
    <div>
    <button
    onClick={() => {
    setIsOpen(!isOpen)
    }}
    >
    Toggle quick view
    </button>
    </div>
    )
    }

    更好的方式,可以使用事件处理函数代替:

    const ChildProduct = ({ onOpen, onClose }) => {
    const [isOpen, setIsOpen] = useState(false)

    const handleToggle = () => {
    const nextIsOpen = !isOpen;
    setIsOpen(nextIsOpen)

    if (nextIsOpen) {
    onOpen()
    } else {
    onClose()
    }
    }

    return (
    <div>
    <button
    onClick={}
    >
    Toggle quick view
    </button>
    </div>
    )
    }

    4.没必要使用useEffect初始化应用程序

    const Store = () => {
    useEffect(() => {
    storeApi.authenticate()
    }, [])

    // ...
    }

    更好的方式:


    方式一:

    const Store = () => {
    const didAuthenticateRef = useRef()

    useEffect(() => {
    if (didAuthenticateRef.current) return

    storeApi.authenticate()

    didAuthenticateRef.current = true
    }, [])

    // ...
    }

    方式二:

    let didAuthenticate = false

    const Store = () => {
    useEffect(() => {
    if (didAuthenticate) return

    storeApi.authenticate()

    didAuthenticate = true
    }, [])

    // ...
    }

    方式三:

    if (typeof window !== 'undefined') {
    storeApi.authenticate()
    }

    const Store = () => {
    // ...
    }

    5.没必要在useEffect请求数据


    常见写法

    const Store = () => {
    const [items, setItems] = useState([])

    useEffect(() => {
    let isCanceled = false

    getItems().then((data) => {
    if (isCanceled) return

    setItems(data)
    })

    return () => {
    isCanceled = true
    }
    })

    // ...
    }

    更好的方式:


    没有必要使用useEffect,可以使用swr:

    import useSWR from 'swr'

    export default function Page() {
    const { data, error } = useSWR('/api/data', fetcher)

    if (error) return <div>failed to load</div>
    if (!data) return <div>loading...</div>

    return <div>hello {data}!</div>
    }

    使用react-query:

    import { getItems } from './storeApi'
    import { useQuery, useQueryClient } from 'react-query'

    const Store = () => {
    const queryClient = useQueryClient()

    return (
    <button
    onClick={() => {
    queryClient.prefetchQuery('items', getItems)
    }}
    >
    See items
    </button>
    )
    }

    const Items = () => {
    const { data, isLoading, isError } = useQuery('items', getItems)

    // ...
    }

    没有正式发布的react的 use函数

    function Note({ id }) {
    const note = use(fetchNote(id))

    return (
    <div>
    <h1>{note.title}</h1>
    <section>{note.body}</section>
    </div>
    )
    }

    reference


    http://www.youtube.com/watch?v=bGz…


    作者:今天学到了
    链接:https://juejin.cn/post/7182110095554117692
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    我的天!多个知名组件库都出现了类似的bug!

    前言 首先声明,没有标题党哈! 以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库: 阿里系:ant design, fusion design,字节系:arco design...
    继续阅读 »

    前言


    首先声明,没有标题党哈!


    以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库:



    本来字节还有一个semi design,结果我发现它没有Affix组件,也就是固钉组件,让他躲过一劫,他有这个组件我也觉得肯定会复现相同的bug。


    Affix组件是什么,以及bug复现


    Affix组件(固钉组件)能将页面元素钉在可视范围。如下图:




    这个button组件,会在距离顶部80px的时候会固定在屏幕上(position: fixed),如下图:




    如何复现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,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

    • transform 或 perspective 的值不是 none
    • will-change 的值是 transform 或 perspective
    • filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。
    • contain 的值是 paint(例如:contain: paint;
    • 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组件库感兴趣的小伙伴,欢迎加群一起交流哦!


    作者:孟祥_成都
    链接:https://juejin.cn/post/7265121637497733155
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    彻底搞懂小程序登录流程-附小程序和服务端代码

    web
    编者按:本文作者奇舞团高级前端开发工程师冯通 用户登录是大部分完整 App 必备的流程 一个简单的用户系统需要关注至少这些层面 安全性(加密) 持久化登录态(类似cookie) 登录过期处理 确保用户唯一性, 避免出现多账号 授权 绑定用户昵称头像等信息 ...
    继续阅读 »

    编者按:本文作者奇舞团高级前端开发工程师冯通



    用户登录是大部分完整 App 必备的流程


    一个简单的用户系统需要关注至少这些层面



    • 安全性(加密)

    • 持久化登录态(类似cookie)

    • 登录过期处理

    • 确保用户唯一性, 避免出现多账号

    • 授权

    • 绑定用户昵称头像等信息

    • 绑定手机号(实名和密保方式)


    很多的业务需求都可以抽象成 Restful 接口配合 CRUD 操作


    但登录流程却是错综复杂, 各个平台有各自的流程, 反倒成了项目中费时间的部分, 比如小程序的登录流程



    对于一个从零开始的项目来说, 搞定登录流程, 就是一个好的开始, 一个好的开始, 就是成功的一半


    本文就以微信小程序这个平台, 讲述一个完整的自定义用户登录流程, 一起来啃这块难啃的骨头


    名词解释


    先给登录流程时序图中出现的名词简单做一个解释



    • code 临时登录凭证, 有效期五分钟, 通过 wx.login() 获取

    • session_key 会话密钥, 服务端通过 code2Session 获取

    • openId 用户在该小程序下的用户唯一标识, 永远不变, 服务端通过 code 获取

    • unionId 用户在同一个微信开放平台帐号(公众号, 小程序, 网站, 移动应用)下的唯一标识, 永远不变

    • appId 小程序唯一标识

    • appSecret 小程序的 app secret, 可以和 code, appId 一起换取 session_key


    其他名词



    • rawData 不包括敏感信息的原始数据字符串,用于计算签名

    • encryptedData 包含敏感信息的用户信息, 是加密的

    • signature 用于校验用户信息是否无篡改

    • iv 加密算法的初始向量



    哪些信息是敏感信息呢? 手机号, openId, unionId, 可以看出这些值都可以唯一定位一个用户, 而昵称, 头像这些不能定位用户的都不是敏感信息



    小程序登录相关函数



    • wx.login

    • wx.getUserInfo

    • wx.checkSession


    小程序的 promise


    我们发现小程序的异步接口都是 success 和 fail 的回调, 很容易写出回调地狱


    因此可以先简单实现一个 wx 异步函数转成 promise 的工具函数


    const promisify = original => {
    return function(opt) {
    return new Promise((resolve, reject) => {
    opt = Object.assign({
    success: resolve,
    fail: reject
    }, opt)
    original(opt)
    })
    }
    }

    这样我们就可以这样调用函数了


    promisify(wx.getStorage)({key: 'key'}).then(value => {
    // success
    }).catch(reason => {
    // fail
    })

    服务端实现


    本 demo 的服务端实现基于 express.js



    注意, 为了 demo 的简洁性, 服务端使用 js 变量来保存用户数据, 也就是说如果重启服务端, 用户数据就清空了




    如需持久化存储用户数据, 可以自行实现数据库相关逻辑



    // 存储所有用户信息
    const users = {
    // openId 作为索引
    openId: {
    // 数据结构如下
    openId: '', // 理论上不应该返回给前端
    sessionKey: '',
    nickName: '',
    avatarUrl: '',
    unionId: '',
    phoneNumber: ''
    }
    }

    app
    .use(bodyParser.json())
    .use(session({
    secret: 'alittlegirl',
    resave: false,
    saveUninitialized: true
    }))

    小程序登录


    我们先实现一个基本的 oauth 授权登录



    oauth 授权登录主要是 code 换取 openId 和 sessionKey 的过程



    前端小程序登录


    写在 app.js 中


    login () {
    console.log('登录')
    return util.promisify(wx.login)().then(({code}) => {
    console.log(`code: ${code}`)
    return http.post('/oauth/login', {
    code,
    type: 'wxapp'
    })
    })
    }

    服务端实现 oauth 授权


    服务端实现上述 /oauth/login 这个接口


    app
    .post('/oauth/login', (req, res) => {
    var params = req.body
    var {code, type} = params
    if (type === 'wxapp') {
    // code 换取 openId 和 sessionKey 的主要逻辑
    axios.get('https://api.weixin.qq.com/sns/jscode2session', {
    params: {
    appid: config.appId,
    secret: config.appSecret,
    js_code: code,
    grant_type: 'authorization_code'
    }
    }).then(({data}) => {
    var openId = data.openid
    var user = users[openId]
    if (!user) {
    user = {
    openId,
    sessionKey: data.session_key
    }
    users[openId] = user
    console.log('新用户', user)
    } else {
    console.log('老用户', user)
    }
    req.session.openId = user.openId
    req.user = user
    }).then(() => {
    res.send({
    code: 0
    })
    })
    } else {
    throw new Error('未知的授权类型')
    }
    })

    获取用户信息


    登录系统中都会有一个重要的功能: 获取用户信息, 我们称之为 getUserInfo


    如果已登录用户调用 getUserInfo 则返回用户信息, 比如昵称, 头像等, 如果未登录则返回"用户未登录"



    也就是说此接口还有判断用户是否登录的功效...



    小程序的用户信息一般存储在 app.globalData.userInfo 中(模板如此)


    我们在服务端加上前置中间件, 通过 session 来获取对应的用户信息, 并放在 req 对象中


    app
    .use((req, res, next) => {
    req.user = users[req.session.openId]
    next()
    })

    然后实现 /user/info 接口, 用来返回用户信息


    app
    .get('/user/info', (req, res) => {
    if (req.user) {
    return res.send({
    code: 0,
    data: req.user
    })
    }
    throw new Error('用户未登录')
    })

    小程序调用用户信息接口


    getUserInfo () {
    return http.get('/user/info').then(response => {
    let data = response.data
    if (data && typeof data === 'object') {
    // 获取用户信息成功则保存到全局
    this.globalData.userInfo = data
    return data
    }
    return Promise.reject(response)
    })
    }

    专为小程序发请求设计的库


    小程序代码通过 http.get, http.post 这样的 api 来发请求, 背后使用了一个请求库


    @chunpu/http 是一个专门为小程序设计的 http 请求库, 可以在小程序上像 axios 一样发请求, 支持拦截器等强大功能, 甚至比 axios 更顺手


    初始化方法如下


    import http from '@chunpu/http'

    http.init({
    baseURL: 'http://localhost:9999', // 定义 baseURL, 用于本地测试
    wx // 标记是微信小程序用
    })

    具体使用方法可参照文档 github.com/chunpu/http…


    自定义登录态持久化


    浏览器有 cookie, 然而小程序没有 cookie, 那怎么模仿出像网页这样的登录态呢?


    这里要用到小程序自己的持久化接口, 也就是 setStorage 和 getStorage


    为了方便各端共用接口, 或者直接复用 web 接口, 我们自行实现一个简单的读 cookie 和种 cookie 的逻辑


    先是要根依据返回的 http response headers 来种上 cookie, 此处我们用到了 @chunpu/http 中的 response 拦截器, 和 axios 用法一样


    http.interceptors.response.use(response => {
    // 种 cookie
    var {headers} = response
    var cookies = headers['set-cookie'] || ''
    cookies = cookies.split(/, */).reduce((prev, item) => {
    item = item.split(/; */)[0]
    var obj = http.qs.parse(item)
    return Object.assign(prev, obj)
    }, {})
    if (cookies) {
    return util.promisify(wx.getStorage)({
    key: 'cookie'
    }).catch(() => {}).then(res => {
    res = res || {}
    var allCookies = res.data || {}
    Object.assign(allCookies, cookies)
    return util.promisify(wx.setStorage)({
    key: 'cookie',
    data: allCookies
    })
    }).then(() => {
    return response
    })
    }
    return response
    })

    当然我们还需要在发请求的时候带上所有 cookie, 此处用的是 request 拦截器


    http.interceptors.request.use(config => {
    // 给请求带上 cookie
    return util.promisify(wx.getStorage)({
    key: 'cookie'
    }).catch(() => {}).then(res => {
    if (res && res.data) {
    Object.assign(config.headers, {
    Cookie: http.qs.stringify(res.data, ';', '=')
    })
    }
    return config
    })
    })

    登录态的有效期


    我们知道, 浏览器里面的登录态 cookie 是有失效时间的, 比如一天, 七天, 或者一个月


    也许有朋友会提出疑问, 直接用 storage 的话, 小程序的登录态有效期怎么办?


    问到点上了! 小程序已经帮我们实现好了 session 有效期的判断 wx.checkSession


    它比 cookie 更智能, 官方文档描述如下



    通过 wx.login 接口获得的用户登录态拥有一定的时效性。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效



    也就是说小程序还会帮我们自动 renew 咱们的登录态, 简直是人工智能 cookie, 点个赞👍


    那具体在前端怎么操作呢? 代码写在 app.js 中


    onLaunch: function () {
    util.promisify(wx.checkSession)().then(() => {
    console.log('session 生效')
    return this.getUserInfo()
    }).then(userInfo => {
    console.log('登录成功', userInfo)
    }).catch(err => {
    console.log('自动登录失败, 重新登录', err)
    return this.login()
    }).catch(err => {
    console.log('手动登录失败', err)
    })
    }

    要注意, 这里的 session 不仅是前端的登录态, 也是后端 session_key 的有效期, 前端登录态失效了, 那后端也失效了需要更新 session_key



    理论上小程序也可以自定义登录失效时间策略, 但这样的话我们需要考虑开发者自己的失效时间和小程序接口服务的失效时间, 还不如保持统一来的简单



    确保每个 Page 都能获取到 userInfo


    如果在新建小程序项目中选择 建立普通快速启动模板


    我们会得到一个可以直接运行的模板


    点开代码一看, 大部分代码都在处理 userInfo....



    注释里写着



    由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回




    所以此处加入 callback 以防止这种情况



    但这样的模板并不科学, 这样仅仅是考虑了首页需要用户信息的情况, 如果扫码进入的页面也需要用户信息呢? 还有直接进入跳转的未支付页活动页等...


    如果每个页面都这样判断一遍是否加载完用户信息, 代码显得过于冗余


    此时我们想到了 jQuery 的 ready 函数 $(function), 只要 document ready 了, 就可以直接执行函数里面的代码, 如果 document 还没 ready, 就等到 ready 后执行代码


    就这个思路了! 我们把小程序的 App 当成网页的 document


    我们的目标是可以这样在 Page 中不会出错的获取 userInfo


    Page({
    data: {
    userInfo: null
    },
    onLoad: function () {
    app.ready(() => {
    this.setData({
    userInfo: app.globalData.userInfo
    })
    })
    }
    })

    此处我们使用 min-ready 来实现此功能


    代码实现依然写在 app.js 中


    import Ready from 'min-ready'

    const ready = Ready()

    App({
    getUserInfo () {
    // 获取用户信息作为全局方法
    return http.get('/user/info').then(response => {
    let data = response.data
    if (data && typeof data === 'object') {
    this.globalData.userInfo = data
    // 获取 userInfo 成功的时机就是 app ready 的时机
    ready.open()
    return data
    }
    return Promise.reject(response)
    })
    },
    ready (func) {
    // 把函数放入队列中
    ready.queue(func)
    }
    })

    绑定用户信息和手机号


    仅仅获取用户的 openId 是远远不够的, openId 只能标记用户, 连用户的昵称和头像都拿不到


    如何获取这些用户信息然后存到后端数据库中呢?


    我们在服务端实现这两个接口, 绑定用户信息, 绑定用户手机号


    app
    .post('/user/bindinfo', (req, res) => {
    var user = req.user
    if (user) {
    var {encryptedData, iv} = req.body
    var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
    var data = pc.decryptData(encryptedData, iv)
    Object.assign(user, data)
    return res.send({
    code: 0
    })
    }
    throw new Error('用户未登录')
    })

    .post('/user/bindphone', (req, res) => {
    var user = req.user
    if (user) {
    var {encryptedData, iv} = req.body
    var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
    var data = pc.decryptData(encryptedData, iv)
    Object.assign(user, data)
    return res.send({
    code: 0
    })
    }
    throw new Error('用户未登录')
    })

    小程序个人中心 wxml 实现如下


    <view wx:if="userInfo" class="userinfo">
    <button
    wx:if="{{!userInfo.nickName}}"
    type="primary"
    open-type="getUserInfo"
    bindgetuserinfo="bindUserInfo">
    获取头像昵称 </button>
    <block wx:else>
    <image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
    <text class="userinfo-nickname">{{userInfo.nickName}}</text>
    </block>

    <button
    wx:if="{{!userInfo.phoneNumber}}"
    type="primary"
    style="margin-top: 20px;"
    open-type="getPhoneNumber"
    bindgetphonenumber="bindPhoneNumber">
    绑定手机号 </button>
    <text wx:else>{{userInfo.phoneNumber}}</text>
    </view>

    小程序中的 bindUserInfo 和 bindPhoneNumber 函数, 根据微信最新的策略, 这俩操作都需要用户点击按钮统一授权才能触发


    bindUserInfo (e) {
    var detail = e.detail
    if (detail.iv) {
    http.post('/user/bindinfo', {
    encryptedData: detail.encryptedData,
    iv: detail.iv,
    signature: detail.signature
    }).then(() => {
    return app.getUserInfo().then(userInfo => {
    this.setData({
    userInfo: userInfo
    })
    })
    })
    }
    },
    bindPhoneNumber (e) {
    var detail = e.detail
    if (detail.iv) {
    http.post('/user/bindphone', {
    encryptedData: detail.encryptedData,
    iv: detail.iv
    }).then(() => {
    return app.getUserInfo().then(userInfo => {
    this.setData({
    userInfo: userInfo
    })
    })
    })
    }
    }

    代码


    本文所提到的代码都可以在我的 github 上找到


    小程序代码在 wxapp-login-demo


    服务端 Node.js 代码在 wxapp-login-server


    关于奇舞周刊


    《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。



    作者:奇舞精选
    来源:juejin.cn/post/6844903702726180871
    收起阅读 »

    Bun 1.0 正式发布,爆火的前端运行时,速度遥遥领先!

    web
    9 月 8 日,前端运行时 Bun 1.0 正式发布,如今,Bun 已经稳定并且适用于生产环境。Bun 不仅是一个专注性能与开发者体验的全新 JavaScript 运行时,还是一个快速的、全能的工具包,可用于运行、构建、测试和调试JavaScript和Type...
    继续阅读 »

    9 月 8 日,前端运行时 Bun 1.0 正式发布,如今,Bun 已经稳定并且适用于生产环境。Bun 不仅是一个专注性能与开发者体验的全新 JavaScript 运行时,还是一个快速的、全能的工具包,可用于运行、构建、测试和调试JavaScript和TypeScript代码,无论是从单个文件还是完整的全栈应用。
    image.png
    2022年,Bun 发布,随即爆火,成为年度最火的前端项目:
    image.png
    Bun 的流行程度伴随着在去年夏天发布的第一个 Beta 版而爆炸性增长:仅一个月内,就在 GitHub 上获得了超过两万颗 Star。
    star-history-202397.png



    Bun 不仅仅是一个运行时。它也是:



    • 一个包管理器 (类似 Yarn、 NPM、 PNPM)

    • 一个构建工具 (类似 Webpack、 ESBuild、 Parcel)

    • 一个测试运行器

    • ...


    所以 Bun 可以通过读取 package.json 来安装依赖项。Bun 还可以运行脚本。不管它做什么都比其他工具更快。Bun 在 JavaScript 生态系统的许多方面都有新的尝试,其中的重点是性能。它优先支持标准的 Web API,如 Fetch。它也支持许多 Node.js APIs,使其能与大多数 NPM 包兼容。



    安装 Bun:


    // npm
    npm install -g bun

    // brew
    brew tap oven-sh/bun
    brew install bun

    // curl
    curl -fsSL https://bun.sh/install | bash

    // docker
    docker pull oven/bun
    docker run --rm --init --ulimit memlock=-1:-1 oven/bun

    更新 Bun:


    bun upgrade

    下面就来看看 Bun 是什么,1.0 版本带来了哪些更新!


    Bun:全能的工具包


    JavaScript 成熟、发展迅速,并且有着充满活力和激情的开发者社区。然而,自14年前Node.js发布以来,JavaScript 的工具链变得越来越庞大和复杂。这是因为在发展过程中,各种工具被逐渐添加进来,但没有一个统一的集中规划,导致工具链缺乏整体性和效率,变得运行缓慢和复杂。


    Bun 为什么会出现?


    Bun的目标很简单,就是要消除JavaScript工具链的缓慢和复杂性,但同时保留JavaScript本身的优点。Bun希望让开发者继续使用喜欢的库和框架,并且无需放弃已经熟悉的规范和约定。


    为了实现这个目标,可能需要放弃一些在使用Bun之后变得不再必要的工具:



    • Node.js:Bun 的一个可以直接替代的工具,因此不再需要以下工具:

      • node

      • npx:Bun 的 bunx 命令比 npx 快5倍。

      • nodemon:Bun 内置了监听模式,无需使用 nodemon

      • dotenvcross-env:Bun 默认支持读取.env文件的配置。



    • 转译器:Bun 可以运行.js.ts、``.cjs.mjs.jsx.tsx文件,因此不再需要以下工具:

      • tsc:仍然可以保留它用于类型检查!

      • babel.babelrc@babel/preset-*:不再需要使用 Babel 进行转译。

      • ts-nodets-node-esm:Bun 可以直接运行 TypeScript 文件。

      • tsx:Bun可以直接运行 TypeScript 的 JSX 文件。



    • 构建工具:Bun 具有一流的性能和与esbuild兼容的插件API,因此不再需要以下工具:

      • esbuild

      • webpack

      • parcel, .parcelrc

      • rollup, rollup.config.js



    • 包管理器:Bun 是一个与 npm 兼容的包管理器,可以使用熟悉的命令。它可以读取 package.json文件并将依赖写入node_modules目录,与其他包管理器的行为类似,因此可以替换以下工具:

      • npm, .npmrc, package-lock.json

      • yarn,yarn.lock

      • pnpm, pnpm.lock, pnpm-workspace.yaml

      • lern



    • 测试库:Bun是一个支持Jest的测试运行器,具有快照测试、模拟和代码覆盖率等功能,因此不再需要以下测试相关的工具:

      • jest, jest.config.js

      • ts-jest, @swc/jest, babel-jest

      • jest-extended

      • vitest, vitest.config.ts




    尽管这些工具都有自己的优点,但使用它们时往往需要将它们全部集成在一起,这会导致开发过程变得缓慢和复杂。而Bun通过成为一个单一的工具包,提供了最佳的开发者体验,从性能到API设计都力求做到最好。


    Bun:JavaScript 运行时


    Bun是一个快速的JavaScript运行时。旨在提供出色的性能和开发体验。它的设计旨在解决开发过程中的各种痛点,使开发者的工作更加轻松和愉快。


    与Node.js兼容


    Bun 是可以直接替代 Node.js 的。这意味着现有的 Node.js 应用和 npm 包可以在 Bun 中正常工作。Bun 内置了对 Node.js API 的支持,包括:



    • 内置模块,如fspathnet

    • 全局对象,如__dirnameprocess

    • Node.js 模块解析算法(例如node_modules


    尽管与 Node.js 完全兼容是不可能的,特别是一些依赖于v8版本的特性,但 Bun 几乎可以运行任何现有的 Node.js 应用。


    Bun经过了与最受欢迎的Node.js包的兼容性测试,支持与Express、Koa、Hapi等服务端框架以及其他流行的全栈框架的无缝集成。开发者可以放心地在Bun中使用这些库和框架,并享受到更好的开发体验。
    image.png
    使用Next.js、Remix、Nuxt、Astro、SvelteKit、Nest、SolidStart和Vite构建的全栈应用可以在Bun中运行。


    速度


    Bun的速度非常快,启动速度比 Node.js 快 4 倍。当运行TypeScript文件时,这种差异会更加明显,因为在Node.js中运行TypeScript文件需要先进行转译才能运行。
    image.png
    Bun在运行一个简单的"Hello World" TypeScript文件时,比在Node.js中使用esbuild运行速度快5倍。


    Bun使用的是Apple的WebKit引擎,而不是像Node.js和其他运行时一样使用Google的V8引擎。WebKit引擎是Safari浏览器的核心引擎,每天被数十亿的设备使用。它经过了长时间的实际应用和测试,具备快速和高效的特性。


    TypeScript 和 JSX 支持


    Bun内置了JavaScript转译器,因此可以运行JavaScript、TypeScript甚至JSX/TSX文件,无需任何依赖。


    // 运行 TS 文件
    bun index.ts

    // 运行 JSX/TSX 文件
    bun index.tsx

    ESM 和 CommonJS 兼容


    从CommonJS到ES模块的过渡一直是缓慢而充满挑战的。在引入ESM之后,Node.js花了5年时间才在没有--experimental-modules标志的情况下支持它。尽管如此,生态系统仍然充斥着CommonJS。


    Bun 同时支持这两种模块系统。无论是使用CommonJS的.js扩展名、.cjs扩展名,还是使用ES模块的.mjs扩展名,Bun都会进行正确的解析和执行,而无需额外的配置。


    甚至可以在同一个文件中同时使用importrequire()


    import lodash from "lodash";
    const _ = require("underscore");

    Web API


    Bun 内置支持浏览器中可用的Web标准API,如fetchRequestResponseWebSocketReadableStream等。


    const response = await fetch("https://example.com/");
    const text = await response.text();

    开发者不再需要安装像node-fetchws这样的包。Bun内置的 Web API 是使用原生代码实现的,比第三方替代方案更快速和可靠。


    热重载


    Bun提供了热重载功能,可以在开发过程中实现文件的自动重新加载。只需在运行Bun时加上--hot参数,当文件发生变化时,Bun 就会自动重新加载你的应用,从而提高开发效率。


    bun --hot server.ts

    与像nodemon这样完全重新启动整个进程的工具不同,Bun 在重新加载代码时不会终止旧进程。这意味着HTTP和WebSocket连接不会断开,并且状态不会丢失。
    hot (1).gif


    插件


    Bun 被设计为高度可定制的。
    可以定义插件来拦截导入操作并执行自定义的加载逻辑。插件可以添加对其他文件类型的支持,比如.yaml.png。插件API的设计灵感来自于esbuild,这意味着大多数esbuild插件在 sBun 中也可以正常工作。


    import { plugin } from "bun";

    plugin({
    name: "YAML",
    async setup(build) {
    const { load } = await import("js-yaml");
    const { readFileSync } = await import("fs");
    build.onLoad({ filter: /.(yaml|yml)$/ }, (args) => {
    const text = readFileSync(args.path, "utf8");
    const exports = load(text) as Record<string, any>;
    return { exports, loader: "object" };
    });
    },
    });

    Bun API


    Bun内部提供了针对开发者最常用需求的标准库API,并对其进行了高度优化。与Node.js的API不同,Node.js的API存在着向后兼容的考虑,而Bun的原生API则专注于提供更快速和更易于使用的功能。


    Bun.file()


    使用Bun.file()可以懒加载位于特定路径的文件。


    const file = Bun.file("package.json");
    const contents = await file.text();

    它返回一个扩展了 Web 标准FileBunFile对象。文件内容可以以多种格式进行懒加载。


    Bun.serve({
    port: 3000,
    fetch(request) {
    return new Response("Hello from Bun!");
    },
    });

    Bun每秒可以处理的请求比 Node.js 多 4 倍。


    也可以使用tls选项来配置TLS(传输层安全协议)。


    Bun.serve({
    port: 3000,
    fetch(request) {
    return new Response("Hello from Bun!");
    },
    tls: {
    key: Bun.file("/path/to/key.pem"),
    cert: Bun.file("/path/to/cert.pem"),
    }
    });

    Bun内置了对WebSocket的支持,只需要在websocket中定义一个事件处理程序来实现同时支持HTTP和WebSocket。而Node.js没有提供内置的WebSocket API,所以需要使用第三方依赖库(例如ws)来实现WebSocket的支持。因此,使用Bun可以更加方便和简单地实现WebSocket功能。


    Bun.serve({
    fetch() { ... },
    websocket: {
    open(ws) { ... },
    message(ws, data) { ... },
    close(ws, code, reason) { ... },
    },
    });

    Bun 每秒可以处理的消息比在 Node.js 上使用 ws 库多 5 倍。


    bun:sqlite


    Bun内置了对 SQLite 的支持。它提供了一个受到better-sqlite3启发的API,但是使用本地代码编写,以达到更快的执行速度。


    import { Database } from "bun:sqlite";

    const db = new Database(":memory:");
    const query = db.query("select 'Bun' as runtime;");
    query.get(); // => { runtime: "Bun" }

    在 Node.js 上,Bun 执行 SQLite 查询操作的速度比better-sqlite3快 4 倍。


    Bun.password


    Bun 还支持一些常见但复杂的API,不用自己去实现它们。


    例如,可以使用Bun.password来使用bcryptargon2算法进行密码哈希和验证,无需外部依赖。


    const password = "super-secure-pa$$word";
    const hash = await Bun.password.hash(password);
    // => $argon2id$v=19$m=65536,t=2,p=1$tFq+9AVr1bfPxQdh...

    const isMatch = await Bun.password.verify(password, hash);
    // => true

    Bun:包管理器


    Bun是一个包管理器。即使不使用Bun作为运行时环境,它内置的包管理器也可以加速开发流程。以前在安装依赖项时需要盯着npm的加载动画,现在可以通过Bun的包管理器更高效地进行依赖项的安装。


    Bun可能看起来像你熟悉的包管理器:


    bun install
    bun add <package> [--dev|--production|--peer]
    bun remove <package>
    bun update <package>

    安装速度


    Bun的安装速度比 npm、yarn 和 pnpm 快好几个数量级。它利用全局模块缓存来避免从npm注册表中重复下载,并使用每个操作系统上最快速的系统调用。
    image.png


    运行脚本


    很可能你已经有一段时间没有直接使用 Node 来运行脚本了。相反,通常使用包管理器(如npm、yarn等)与框架和命令行界面(CLI)进行交互,以构建应用。


    npm run dev

    你可以用bun run来替换npm run,每次运行命令都能节省 150 毫秒的时间。


    这些数字可能看起来很小,但在运行命令行界面(CLI)时,感知上的差异是巨大的。使用"npm run"会明显感到延迟:
    265893417-fbfb4172-5a91-4158-904f-55f2dbb0acde.gif而使用bun run则感觉几乎瞬间完成:
    image.png
    并不只是针对npm进行比较。实际上,bun run <command>的速度比yarn和pnpm中相应的命令更快。


    脚本运行平均时间
    npm run176ms
    yarn run131ms
    pnpm run259ms
    bun run7ms 🚀

    Bun:测试运行器


    如果你以前在 JavaScript 中写过测试,可能了解 Jest,它开创了“expect”风格的API。


    Bun有一个内置的测试模块bun:test,它与Jest完全兼容。


    import { test, expect } from "bun:test";

    test("2 + 2", () => {
    expect(2 + 2).toBe(4);
    });

    可以使用bun test命令来运行测试:


    bun test

    还将获得 Bun 运行时的所有优势,包括TypeScript和JSX支持。


    从Jest或Vite迁移很简单。@jest/globalsvitest的任何导入将在内部重新映射到bun:test,因此即使不进行任何代码更改,一切也将正常运行。


    import { test } from "@jest/globals";

    describe("test suite", () => {
    // ...
    });

    在与 zod 的测试套件进行基准测试中,Bun比Jest快13倍,比Vite快8倍。
    image.png
    Bun的匹配器由快速的原生代码实现,Bun中的expect().toEqual()比Jest快100倍,比Vite快10倍。


    可以使用bun test命令来加快 CI 构建速度,如果在Github Actions中,可以使用官方的oven-sh/setup-bun操作来设置Bun


    name: CI
    on: [push, pull_request]

    jobs:
    test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: oven-sh/setup-bun@v1
    - run: bun test

    Bun会自动为测试失败的部分添加注释,以便在持续集成(CI)日志中更容易理解。这样,当出现测试失败时,可以直接从日志中读取Bun提供的注释,而不需要深入分析代码和测试结果,从而更方便地检查问题所在。
    image.png


    Bun:构建工具


    Bun是一个JavaScript和TypeScript的构建工具和代码压缩工具,可用于将代码打包成适用于浏览器、Node.js和其他平台的形式。


    bun build ./index.tsx --outdir ./build

    Bun 受到了 esbuild 的启发,并提供了兼容的插件API。


    import mdx from "@mdx-js/esbuild";

    Bun.build({
    entrypoints: ["index.tsx"],
    outdir: "build",
    plugins: [mdx()],
    });

    Bun 的插件 API 是通用的,这意味着它适用于打包工具和运行时。所以前面提到的.yaml插件可以在这里使用,以支持在打包过程中导入.yaml文件。


    根据esbuild的基准测试,Bun比esbuild快1.75倍,比Parcel 2快150倍,比Rollup + Terser快180倍,比Webpack快220倍。
    image.png
    由于Bun的运行时和打包工具是集成在一起的,这意味着Bun可以做其他打包工具无法做到的事情。


    Bun引入了JavaScript宏机制,可以在打包时运行JavaScript函数。这些函数返回的值会直接内联到打包文件中。


    // release.ts
    export async function getRelease(): Promise<string> {
    const response = await fetch(
    "https://api.github.com/repos/oven-sh/bun/releases/latest"
    );
    const { tag_name } = await response.json();
    return tag_name;
    }

    // index.ts
    import { getRelease } from "./release.ts" with { type: "macro" };

    // release的值是在打包时进行评估的,并且内联到打包文件中,而不是在运行时执行。
    const release = await getRelease();

    bun build index.ts
    // index.ts
    var release = await "bun-v1.0.0";

    Bun:可以做更多事


    Bun 在 macOS 和 Linux 上提供了原生构建支持,但 Windows 一直是一个明显的缺失。以前,在 Windows 上运行 Bun 需要安装 Windows 子系统来运行Linux系统,但现在不再需要。


    Bun 首次发布了一个实验性的、专为Windows平台的本地版本的 Bun。这意味着Windows用户现在可以直接在其操作系统上使用 Bun,而无需额外的配置。
    image.png
    尽管Bun的macOS和Linux版本已经可以用于生产环境,但Windows版本目前仍然处于高度实验阶段。目前只支持JavaScript运行时,而包管理器、测试运行器和打包工具在稳定性更高之前都将被禁用。性能方面也还未进行优化。


    Bun:面向未来


    Bun 1.0 只是一个开始。Bun 团队正在开发一种全新的部署JavaScript和TypeScript到生产环境的方式,期待 Bun 未来更好的表现!


    作者:CUGGZ
    来源:juejin.cn/post/7277387014046335010
    收起阅读 »

    Htmx 意外走红,我们从 React“退回去”后:代码行数减少 67%,JS 依赖项从 255 下降到 9

    htmx 的走红 过去 Web 非常简单。URL 指向服务器,服务器将数据混合成 html,然后在浏览器上呈现该响应。围绕这种简单范式,诞生了各种 Javascript 框架,以前可能需要数月时间完成的一个应用程序基本功能,现在借助这些框架创建相对复杂的项目却...
    继续阅读 »

    htmx 的走红


    过去 Web 非常简单。URL 指向服务器,服务器将数据混合成 html,然后在浏览器上呈现该响应。围绕这种简单范式,诞生了各种 Javascript 框架,以前可能需要数月时间完成的一个应用程序基本功能,现在借助这些框架创建相对复杂的项目却只需要数小时,我们节省了很多时间,从而可以将更多精力花在业务逻辑和应用程序设计上。


    但随着 Web 不断地发展,Javascript 失控了。不知何故,我们决定向用户抛出大量 App,并在使用时发出不断增加的网络请求;不知何故,为了生成 html,我们必须使用 JSON,发出数十个网络请求,丢弃我们在这些请求中获得的大部分数据,用一个越来越不透明的 JavaScript 框架黑匣子将 JSON 转换为 html,然后将新的 html 修补到 DOM 中......


    难道大家快忘记了我们可以在服务器上渲染 html 吗?更快、更一致、更接近应用程序的实际状态,并且不会向用户设备发送任何不必要的数据?但是如果没有 Javascript,我们必须在每次操作时重新加载页面。



    现在,有一个新的库出现了,摒弃了定制化的方法,这就是 htmx。作为 Web 开发未来理念的一种实现,它的原理很简单:

    • 从任何用户事件发出 AJAX 请求。

    • 让服务器生成代表该请求的新应用程序状态的 html。

    • 在响应中发送该 html。

    • 将该元素推到它应该去的 DOM 中。


    htmx 出现在 2020 年,创建者Carson Gross 说 htmx 来源自他于 2013 年研究的一个项目intercooler.js。2020 年,他重写了不依赖 jQuery 的 intercooler.js,并将其重命名为 htmx。然后他惊讶的发现 Django 社区迅速并戏剧性地接受了它!



    图片来源:lp.jetbrains.com/django-deve…2021-486/


    Carson Gross认为 htmx 设法抓住了开发者对现有 Javascript 框架不满的浪潮,“这些框架非常复杂,并且经常将 Django 变成一个愚蠢的 JSON 生产者”,而 htmx 与开箱即用的 Django 配合得更好,因为它通过 html 与服务器交互,而 Django 非常擅长生成 html。


    对于 htmx 的迅速走红,Carson Gross 发出了一声感叹:这真是“十年窗下无人问,一举成名天下知(this is another example of a decade-long overnight success)”。


    htmx 的实际效果


    可以肯定的一点是 htmx 绝对能用,单从理论上讲,这个方法确实值得称道。但软件问题终究要归结于实践效果:效果好吗,能不能给前端开发带来改善?


    在 DjangoCon 2022 上,Contexte 的 David Guillot 演示了他们在真实 SaaS 产品上实现了从 React 到 htmx 的迁移,而且效果非常好,堪称“一切 htmx 演示之母”(视频地址:http://www.youtube.com/watch?v=3GO…)。


    Contexte 的项目开始于 2017 年,其后端相当复杂,前端 UI 也非常丰富,但团队非常小。所以他们在一开始的时候跟随潮流选择了 React 来“构建API绑定 SPA、实现客户端状态管理、前后端状态分离”等。但实际应用中,因为 API 设计不当,DOM 树太深,又需要加载很多信息,导致 UI“非常非常缓慢”。在敏捷开发的要求下,团队里唯一的 Javascript 专家对项目的复杂性表现得一无所措,因此他们决定试试 htmx。


    九大数据提升



    于是我们决定大胆尝试,花几个月时间用简单的 Django 模板和 htmx 替换掉了 SaaS 产品中已经使用两年的 React UI。这里我们分享了一些相关经验,公布各项具体指标,希望能帮同样关注 htmx 的朋友们找到说服 CTO 的理由!

    • 这项工作共耗费了约 2 个月时间(使用 21K 行代码库,主要是 JavaScript)

    • 不会降低应用程序的用户体验(UX)

    • 将代码库体积减小了 67%(由 21500 行削减至 7200 行)

    • 将 Python 代码量增加了 140%(由 500 行增加至 1200 行);这对更喜欢 Python 的开发者们应该是好事

    • 将 JS 总体依赖项减少了 96%(由 255 个减少至 9 个)


    • 将 Web 构建时间缩短了 88%(由 40 秒缩短至 5 秒)

    • 首次加载交互时间缩短了 50%至 60%(由 2 到 6 秒,缩短至 1 到 2 秒)

    • 使用 htmx 时可以配合更大的数据集,超越 React 的处理极限

    • Web 应用程序的内存使用量减少了 46%(由 75 MB 降低至 40 MB)



    这些数字令人颇为意外,也反映出 Contexte 应用程序高度契合超媒体的这一客观结果:这是一款以内容为中心的应用程序,用于显示大量文本和图像。很明显,其他 Web 应用程序在迁移之后恐怕很难有同样夸张的提升幅度。


    但一些开发者仍然相信,大部分应用程序在采用超媒体/htmx 方法之后,肯定也迎来显著的改善,至少在部分系统中大受裨益。


    开发团队组成


    可能很多朋友没有注意,移植本身对团队结构也有直接影响。在 Contexte 使用 React 的时候,后端与前端之间存在硬性割裂,其中两位开发者全职管理后端,一位开发者单纯管理前端,另有一名开发者负责“全栈”。(这里的「全栈」,代表这位开发者能够轻松接手前端和后端工作,因此能够在整个「栈」上独立开发功能。)



    而在移植至 htmx 之后,整个团队全都成了“全栈”开发人员。于是每位团队成员都更高效,能够贡献出更多价值。这也让开发变得更有乐趣,因为开发人员自己就能掌握完整功能。最后,转向 htmx 也让软件优化度上了一个台阶,现在开发人员可以在栈内的任意位置进行优化,无需与其他开发者提前协调。


    htmx 是传统思路的回归


    如今,单页应用(SPA)可谓风靡一时:配合 React、Redux 或 Angular 等库的 JS 或 TS 密集型前端,已经成为创建 Web 应用程序的主流方式。以一个需要转译成 JS 的 SPA 应用为例:



    但 htmx 风潮已经袭来,人们开始强调一种“傻瓜客户端”方法,即由服务器生成 html 本体并发送至客户端,意味着 UI 事件会被发送至服务器进行处理。



    用这个例子进行前后对比,我们就会看到前者涉及的活动部件更多。从客户端角度出发,后者其实回避了定制化客户端技术,采取更简单的方法将原本只作为数据引擎的服务器变成了视图引擎。


    后一种方法被称为 AJAX(异步 JavaScript 与 XML)。这种简单思路能够让 Web 应用程序获得更高的响应性体验,同时消除了糟糕的“回发”(postback,即网页完全刷新),由此回避了极其低效的“viewstate”等.NET 技术。


    htmx 在很多方面都体现出对 AJAX 思路的回归,最大的区别就是它仅仅作为新的声明性 html 属性出现,负责指示触发条件是什么、要发布到哪个端点等。


    另一个得到简化的元素是物理应用程序的结构与构建管道。因为不再涉及手工编写 JS,而且整个应用程序都基于服务器,因此不再对 JS 压缩器、捆绑器和转译器做(即时)要求。就连客户端项目也能解放出来,一切都由 Web 服务器项目负责完成,所有应用程序代码都在.NET 之上运行。从这个角度来看,这与高度依赖服务器的Blazor Server编程模型倒是颇有异曲同工之妙。


    技术和软件开发领域存在一种有趣的现象,就是同样的模式迭起兴衰、周而复始。随着 SPA 的兴起,人们一度以为 AJAX 已经过气了,但其基本思路如今正卷土重来。这其中当然会有不同的权衡,例如更高的服务器负载和网络流量(毕竟现在我们发送的是数据视图,而不只是数据),但能让开发者多个选择肯定不是坏事。


    虽然不敢确定这种趋势是否适用于包含丰富用户体验的高复杂度应用程序,但毫无疑问,相当一部分 Web 应用程序并不需要完整的 SPA 结构。对于这类用例,简单的 htmx 应用程序可能就是最好的解决方案。


    参考链接:


    news.ycombinator.com/item?id=332…


    htmx.org/essays/a-re…


    http://www.reddit.com/r/django/co…


    mekhami.github.io/2021/03/26/…


    http://www.compositional-it.com/news-blog/m…


    作者:MobTech开发者
    链接:https://juejin.cn/post/7156507227669413895
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Taro开发小程序记录-海报生成

    Taro开发小程序记录-海报生成在公司开发的第一个项目是一个简单的Taro小程序项目,主要应用的则是微信小程序,由于是第一次使用Taro,所以在开发过程中遇到的一些问题和一些开发技巧,记录一下,方便以后进行查看,也与大家进行分享。 自定义海报 说到自定义海报...
    继续阅读 »

    Taro开发小程序记录-海报生成


    在公司开发的第一个项目是一个简单的Taro小程序项目,主要应用的则是微信小程序,由于是第一次使用Taro,所以在开发过程中遇到的一些问题和一些开发技巧,记录一下,方便以后进行查看,也与大家进行分享。



    自定义海报


    说到自定义海报可以说是很多小程序中都会进行开发的内容,比如需要进行二维码的保存,然后再对二维码进行一点文字的修饰,涉及到这方面的时候我们就需要使用canvas了。


    在实际开发的过程中,遇到了一些很坑的问题,当我们需要使用离屏canvas来进行绘制时,我们可能就会遇到问题(我自己就遇到了)。


    对于安卓端,我们可以正常的使用OffscreenCanvas来创建离屏canvas,然后绘制相关内容,最后在使用Taro.canvasToTempFilePath方法保存到临时文件下,Taro.canvasToTempFilePath方法会返回文件路径,我们就可以通过获取到的文件路径来进行下载。


    下面是安卓端的一个🌰,大家有需要也可以直接拿去使用


    • 需要使用到的方法
     /**
      * @description 获取二维码图像
      */
     export const qrCodeImage = async (qrCodeValue: string, size: number = 128) => {
         /* NOTE: 通过创建离屏canvas承载code */
         const context = createOffscreenCanvas('2d', size, size);
         QRCode.toCanvas(context, qrCodeValue, { width: size, height: size, margin: 1 });
         return (context as unknown as HTMLCanvasElement).toDataURL();
     };
     /**
      * @description 创建离屏canvas对象,width与height单位为px
      */
     export const createOffscreenCanvas = (type: '2d' | 'webgl', width: number = 100, height: number = 100) => {
         return Taro.createOffscreenCanvas({ type, width, height });
     };
     /**
      * @description 将传入的图片url转换成一个ImageElement对象
      */
     export const loadImageByUrlToCanvasImageData = async (url: string, width: number = 100, height: number = 100) => {
         const context = createOffscreenCanvas('2d', width, height);
         const imageElement = context.createImage();
         await new Promise(resolve => {
             imageElement.onload = resolve;
             imageElement.src = url;
        });
         return imageElement;
     };
     /**
      * @description 将canvas转成图片文件并保存在临时路径下
      */
     export const changeCanvasToImageFileAndSaveToTempFilePath = async (options: Taro.canvasToTempFilePath.Option) => {
         const successCallback = await Taro.canvasToTempFilePath(options);
         return successCallback.tempFilePath;
     };
     interface SettingOptions {
         title: string;
         titleInfo: {
             dx: number;
             dy: number;
             color?: string;
             font?: string;
        };
         imageUrl: string;
         imagePos: {
             dx: number;
             dy: number;
        };
         width: number;
         height: number;
     }
     /**
      * @description 获取二维码图像并设置标题
      */
     export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
         const {
             title,
             titleInfo,
             imageUrl,
             imagePos,
             width,
             height,
        } = option;
         const context = await createOffscreenCanvas('2d', width, height);
         const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
         const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
         ctx.fillStyle = 'white';
         ctx.fillRect(0, 0, width, height);
         ctx.fillStyle = titleInfo.color || 'black';
         ctx.font = titleInfo.font || '';
         ctx.textAlign = 'center';
         ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
         ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
     
         const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
             canvas: (context as Canvas),
             width,
             height,
             fileType: 'png',
             destWidth: width,
             destHeight: height,
        });
         return filePath;
     };
     /**
      * @description 保存图片
      */
     export const saveImage = async (urls: string[], isLocal: boolean = true) => {
         let filePath = urls;
         if (!isLocal) {
             filePath = await netImageToLocal(urls);
        }
         await Promise.all(filePath.map(path => {
             return Taro.saveImageToPhotosAlbum({ filePath: path });
        }));
     
         return true;
     };
     /**
      * @description 加载在线图片,并返回临时图片文件地址
      */
     export const netImageToLocal = async (urls: string[]) => {
         const res = await Promise.all(urls.map((url:string) => {
             return Taro.downloadFile({ url });
        }));
     
         const result = res.map(data => {
             if (data.statusCode === 200) {
                 return data.tempFilePath;
            }
             throw new Error(data.errMsg);
        });
     
         return result;
     };
     /**
      * @description 判断用户是否授权保存图片
      */
     export const checkHasAuthorizedSaveImagePermissions = async () => {
         const setting = await Taro.getSetting();
         const { authSetting } = setting;
         return authSetting['scope.writePhotosAlbum'];
     };
     /**
      * @description 下载图片,需要区分是本地图片还是在线图片
      */
     export const downloadImage = async (urls: string[], isLocal: boolean = true) => {
         const hasSaveImagePermissions = await checkHasAuthorizedSaveImagePermissions();
         if (hasSaveImagePermissions === undefined) {
             // NOTE: 用户未授权情况下,进行用户授权,允许保存图片
             await Taro.authorize({ scope: 'scope.writePhotosAlbum' });
             return await saveImage(urls, isLocal);
        } else if (typeof hasSaveImagePermissions === 'boolean' && !hasSaveImagePermissions) {
             return new Promise((resolve, reject) => {
                 Taro.showModal({
                     title: '是否授权保存到相册',
                     content: '需要获取您的保存图片权限,请确认授权,否则图片将无法保存到相册',
                     success: (result) => {
                         if (result.confirm) {
                             Taro.openSetting({
                                 success: async (data) => {
                                     if (data.authSetting['scope.writePhotosAlbum']) {
                                         showLoadingModal('正在保存...');
                                         resolve(await saveImage(urls, isLocal));
                                    }
                                },
                            });
                        } else {
                             reject(new Error('未授予保存权限'));
                        }
                    },
                });
            });
        }
         await saveImage(urls, isLocal);
         return true;
     };

    • 生成海报(二维码+标题头)

     /**
      * @description 获取二维码图像并设置标题
      */
     export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
         const {
             title,
             titleInfo,
             imageUrl,
             imagePos,
             width,
             height,
        } = option;
         const context = await createOffscreenCanvas('2d', width, height);
         const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
         const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
         ctx.fillStyle = 'white';
         ctx.fillRect(0, 0, width, height);
         ctx.fillStyle = titleInfo.color || 'black';
         ctx.font = titleInfo.font || '';
         ctx.textAlign = 'center';
         ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
         ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
     
         const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
             canvas: (context as Canvas),
             width,
             height,
             fileType: 'png',
             destWidth: width,
             destHeight: height,
        });
         return filePath;
     };

    • 具体使用

     export const saveQrCodeImageWithTitle = async () => {
         const url = await qrCodeImage(enterAiyongShopUrl(), 160);
         const imgUrl: string = await generateQrCodeWithTitle({
             title: 'adsionli菜鸡前端',
             titleInfo: {
                 dx: 95,
                 dy: 20,
                 font: '600 14px PingFang SC',
                 color: 'black',
            },
             imageUrl: url,
             imagePos: {
                 dx: 15,
                 dy: 34,
            },
             width: 190,
             height: 204,
        });
         await downloadImage([imgUrl]);
     }


    上面三块内容就可以组成我们的海报生成了,这里面的主要步骤不是很难,包括了几个方面:


    1. 用户授权鉴定,主要是是否允许保存,这里做了一点处理,就是可以在用户第一次授权不允许时,进行二次授权调起,这个可以看一下上面的downloadImage这个函数,以及用于判断用户是否授权的checkHasAuthorizedSaveImagePermissions这个函数
    2. 创建OffscreenCanvas并进行绘制,这里其实没有太多的难点,主要就是需要知道,如果我们使用image的内容的话,或者是一个图片的url时,我们需要先将其绘制到一个canvas上(这里可以获取imageElement对象,也可以直接使用canvas),这样方便我们后面进行drawImage时进行使用
    3. 图片保存,这里也有一个需要注意的点,如果图片(或二维码)是网络图片的话,我们需要处理以下,先将其转成本地图片,也就是通过netImageToLocal这个方法,然后再还给对应的将图片画在canvas上的方法。最后的保存很简单,我们可以直接使用Taro.canvasToTempFilePath这个方法转到临时地址,再通过downloadImage就可以搞定了。

    感觉好像很麻烦,其实就四步:图片加载转化—>canvas绘制—>用户鉴权—>图片保存。


    安卓端实现起来还是很简单的,但是这些方法对于ios端就出现了问题,如果按照上面的路线进行海报绘制保存的话,在ios端就会报一个错误(在本地开发的时候并不会抛出): canvasToTempFilePath:fail invalid viewId


    这一步错误就是发生在Taro.canvasToTempFilePath这里,保存到临时文件时会触发,然后这一切的原因就是使用了OffscreenCanvas离屏canvas造成的。


    所以为了能够兼容ios端的这个问题,有了以下的修改:


    首先需要在我们要下载海报的pages中,添加一个Canvas,帮助我们可以获取CanvasElement

     <Canvas
         type='2d'
         id='qrCodeOut'
         className='aiyong-shop__qrCode'
     />

    这里需要注意一下,我们需要添加一个type='2d'的属性,这是为了能够使用官方提供的获取Canvas2dContext的属性,这样就可以不使用createCanvasContext这个方法来获取了(毕竟已经被官方停止维护了)。


    然后我们就可以获取一下CanvasElement对象了

     /**
      * @description 获取canvas标签对象
      */
     export const getCanvasElement = (canvasId: string): Promise<Taro.NodesRef> => {
         return new Promise(resolve => {
             const canvasSelect: Taro.NodesRef = selectQuery().select(`#${canvasId}`);
             canvasSelect.node().exec((res: Taro.NodesRef) => {
                 resolve(res);
            });
        });
     };

    注:这里又有一个小坑,我们在获取CanvasElement之后,如果直接进行绘制的话,这里存在一个问题,就是这个CanvasElementwidth:300、height:150被限制死了,所以我们需要自己在拿到CanvasElement之后,在设置一下width、height

     const canvasNodeRef = await getCanvasElement(canvas);
     let context;
     if (canvasNodeRef && canvasNodeRef[0].node !== null) {
         context = canvasNodeRef[0].node;
        (context as Taro.Canvas).width = width;
        (context as Taro.Canvas).height = height;
     }

    好了,改造完成,这样就可以兼容ios端的内容了,实际我们只需要修改generateQrCodeWithTitle这个方法和page新增Canvas用于获取CanvasElement就可以了,其他可以不要动。修改后的generateQrCodeWithTitle方法如下:

     /**
      * @description 获取二维码图像并设置标题
      */
     export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
         const {
             title,
             titleInfo,
             imageUrl,
             imagePos,
             width,
             height,
             qrCodeSize,
             canvas,
        } = option;
         const canvasNodeRef = await getCanvasElement(canvas);
         let context;
         if (canvasNodeRef && canvasNodeRef[0].node !== null) {
             context = canvasNodeRef[0].node;
            (context as Taro.Canvas).width = width;
            (context as Taro.Canvas).height = height;
        }
         const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
         const imgElement: Taro.Image = await loadImageByUrlToCanvasImageData(imageUrl, qrCodeSize.width, qrCodeSize.height);
         ctx.fillStyle = 'white';
         ctx.fillRect(0, 0, width, height);
         ctx.fillStyle = titleInfo.color || 'black';
         ctx.font = titleInfo.font || '';
         ctx.textAlign = 'center';
         ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
         ctx.drawImage((imgElement as HTMLImageElement), imagePos.dx, imagePos.dy, imgElement.width, qrCodeSize.height);
     
         const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
             canvas: (context as Canvas),
             width,
             height,
             fileType: 'png',
             destWidth: width,
             destHeight: height,
        });
         return filePath;
     };


    如果大家不想让海报被人看到,那可以设置一下css

     .qrCode {
         position: fixed;
         left: 100%;
     }

    这样就可以啦



    突然发现内容可能有点多了,所以打算分成两篇进行Taro使用过程中的总结,开发完之后进行总结,总是可以让自己回顾在开发过程中遇到的问题的进一步进行思考,这是一个很好的进步过程,加油加油!!!


    作者:Adsionli
    链接:https://juejin.cn/post/7244452419458498616
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    chrome 搞事,下个月全面删除 Event.path

    背景 前两天下午,测试同学反馈生产环境有个功能出现异常,然后给我发了张报错截图。  随后在测试同学的电脑复现并排查问题,通过异常断点,捕捉到异常信息如下:  可以看到异常的原因是 n.path 的值为 undefined,因此 n.path...
    继续阅读 »

    背景


    前两天下午,测试同学反馈生产环境有个功能出现异常,然后给我发了张报错截图。 



    随后在测试同学的电脑复现并排查问题,通过异常断点,捕捉到异常信息如下: 



    可以看到异常的原因是 n.path 的值为 undefined,因此 n.path.find 等价于 undefined.find,因此程序报错。其中 n 是一个 Event 实例;path 是事件冒泡经过的节点组成的数组。n.path 有值的情况如下: 


    Event.path 不是标准属性,常见于 chrome 浏览器,取不到值出现异常不足为奇,只要做个兼容就搞定了,当 Event.path 取不到值时就取 Event.composedPath()。那这是兼容性问题吗,事情好像没有这么简单。仔细对比上述两张截图可以发现,异常时 Event 实例甚至不存在 path 属性,这跟属性存在但值为空是两码事。


    进一步排查


    好好的 path 属性怎么就不翼而飞了,这是个神奇的问题。当我用自己电脑尝试复现问题时,发现功能正常且无法复现,事情变得更加神奇。两边浏览器都升到了最新版 chrome 108,区别是系统不同,一个是 windows 一个是 macOS。也就是 说同样的代码在同样的软件上跑出了不同结果,这说明可能不是代码或兼容性问题。


    为找出真正的原因,我做了几组对照实验,以排除代码、硬件、操作系统和浏览器的影响。情况如下 :


    分析这些结果,出现了更有意思的事:只有一种情况会出现异常,使用测试同学的电脑且浏览器是 chrome 108;当改变电脑、系统、浏览器、浏览器版本等因素时结果都是正常。 也就是说导致异常的因素居然不是单一的,而是多个因素组合(测试同学电脑+chrome+108 版本)产生的结果。



    chromium issue 的助攻


    从上面的结果看好像没办法再继续排查下去,不过从经验判断,多半是 chrome 又在搞事,这时候可以去 chromium issue 里找找蛛丝马迹,经过一番搜索找到了这条 issue: Issue 1277431: Remove event.path。 



    issue 标题很直白,Event.path 将被删除。 从 issue 内容可以看到,这次搞事是从 2021 年 12 月 7 日开始,起因是 chromium 开发团队认为 Event.path 属于非标准 API,会导致 Firefox 等其他浏览器的兼容性问题,于是他们决定将其删除。目前这个变更在 chrome 108 属于灰度阶段,在即将发布的 chrome 109 上会全面应用,webview 则是从 109 版本开始逐步禁用。


    变更详情和计划


    另外 issue 中提到这个变更会在 console 中进行告警。 



    console 中确实有这个告警,不过藏在 console 面板的右上角,不太容易发现,而且需要调用 Event.path 后才会显示。点进去之后会跳转到 Issues 面板并显示详细信息。
     



    从图中可以看到这个变更属于 Breaking Change,即破坏性变更。另外可以看到变更详情链接版本计划链接。打开变更详情链接可以看到详细的说明、目的、状态、开发阶段等信息。 



    打开版本计划链接可以看到,chrome 108 已经在 2022-11-29 正式发布(Stable Release Tue, Nov 29, 2022),chrome 109 将在 2023-01-10 正式发布(Stable Release Tue, Jan 10, 2023)。 



    验证


    由于英文水平有限,为了避免个人理解存在歧义,使用 chrome 的前瞻版本进行测试,以验证 chrome 108 之后的版本是否真的会应用这个变更。


    • 测试使用的系统为 macOS,浏览器版本包括:chrome-stable(108.0.5359.124)、chrome-beta(109.0.5414.36)、chrome-dev(110.0.5464.2)、chrome-canary(110.0.5477.0)。



    • 测试代码如下
    <!DOCTYPE html>
    <html lang="en">
    <head></head>
    <body>
    <script>
    function test() {
    console.log("event.path is:", window.event.path);
    }
    </script>
    <h1 onclick="test()">click me</h1>
    </body>
    </html>

    • 测试结果如下

    chrome-stable(108.0.5359.124)在 macOS 下 Event.path 有值,结合上文的对照实验中 windows10 下一个有值一个为空。说明 chrome 108 中该变更属于灰度阶段。
    image.png


    chrome-beta(109.0.5414.36)、chrome-dev(110.0.5464.2)、chrome-canary(110.0.5477.0)在 macOS 下 Event.path 都为空,说明 chrome 109 之后全面删除了 Event.path 属性。
    image.png


    解决方案


    先看影响范围,从项目维度来看,所有前端项目都可能受到影响;从代码维度来看,项目源码和第三方依赖都可能受影响。在 github 中搜索发现 swipperopenlayers 等第三方库中都有相关 issue。因此解决方案需要全面考虑:最好对所有项目都进行排查修复,另外不仅要排查源码,还要考虑第三方库。


    根据官方建议及综合考虑,推荐在前端项目中统一添加如下 polyfill 代码:

      Object.defineProperty(Event.prototype, "path", {
    get() {
    return this.composedPath();
    },
    });

    最后


    chrome 109 预计在 2023-01-10 正式发布,届时会全面禁用 Event.path,所有源码中使用该属性或第三方库使用该属性的前端项目都可能会出现异常,还有 20 几天时间,建议尽快排查修复。


    一些经验

  • 关注 devtools 中的 console、issue 等各种告警信息,有助于调试和排查问题、以及发现潜在的问题
    • 关注 chorme 迭代计划,有条件可以做前瞻性测试,预防未来可能发生的异常



    作者:Whilconn
    链接:https://juejin.cn/post/7177645078146449466
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    如何在上班时间利用终端控制台摸鱼🧐🧐🧐

    web
    作为一个资深的摸鱼小能手,班我们每天要上,终端也是我们也要每天要用到的,那么有什么办法可以在控制台终端中去摸鱼呢,那么在接下来的文章中我们就来看看它可以做到怎么样摸鱼。 简介 在我们开发的项目中,几乎有很多项目要都是使用 webpack 作为构建工具来进行开发...
    继续阅读 »

    作为一个资深的摸鱼小能手,班我们每天要上,终端也是我们也要每天要用到的,那么有什么办法可以在控制台终端中去摸鱼呢,那么在接下来的文章中我们就来看看它可以做到怎么样摸鱼。


    简介


    在我们开发的项目中,几乎有很多项目要都是使用 webpack 作为构建工具来进行开发的,在它进行构建的时候,会有一些信息会输出在控制台上面,如下图所示:


    20230910150719


    爱瞎折腾的朋友们可能就会想了,为什么 create-react-pp 也是用的 webpack 作为构建工具,为什么我的输出和它的输出是不一样的呢?


    20230910150945


    compiler


    通过查阅文档,我发现了问题所在,原来在 webpack 中它提供了一个 compiler 钩子,它用来监控文件系统的 监听(watching) 机制,并且在文件修改时重新编译。 当处于监听模式(watch mode)时, compiler 会触发诸如 watchRun, watchClose 和 invalid 等额外的事件。


    done 钩子就是当我们的代码被编译完成的时候被调用的。


    如何调用 done 钩子


    要想调用我们的 done 钩子,首先我们要引入 webpack 包,并把 webpack 配置传递给 webpack 函数,如下图所示:


    20230910151624


    接下来我们看看终端输出:


    20230910151749


    这些就是我们的一些 webpack 配置,在这个 compiler 对象上,它存在一个 hooks 对象,如下代码所示:


    compiler.hooks.done.tap("done", async (stats) => {
    console.log(11111111111111);
    });

    它会在代码编译完成阶段调用该回调函数:


    20230910152621


    咦,你会发现了,代码编译执行完成,我的终端上的输出会这么干净,是因为在输出控制台之前, 已经被我调用了一个函数清空了。


    通过这个函数,你可以情况控制台上的一些输出信息,如下代码所示:


    function clearConsole() {
    process.stdout.write(
    process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H"
    );
    }

    再调用以下,你会发现控制台上面很干净的,图下图所示:


    20230910153357


    要想这一些个性化的输出,我们直接在这个回调函数中打印输出就可以了,如果你要你输出的信息和项目中的信息有关,你可以利用 stats 这个参数:


    20230910160905


    大概就这样子,如果你想更好玩的话,你可以使用一些网络请求库,去获取一些网络资源:


    20230910161247


    去获取这些资源都是可以的呀。


    总结


    如果你的项目是使用的 webpack,并且要想在项目的开发中自定义,你可以通过 compiler.hooks 的方式去监听不同的钩子,然后通过不同的方式来实现不同的信息输出。


    源代码地址


    作者:Moment
    来源:juejin.cn/post/7277065056575848448
    收起阅读 »