注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

什么!一个项目给了8个字体包???

web
🙋 遇到的问题 在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。 首先,字体包的使用分为了以下几种情况: 无特殊要求的语言使用字体A,阿拉伯语言使用字体B; 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用...
继续阅读 »

🙋 遇到的问题


在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。


首先,字体包的使用分为了以下几种情况:



  1. 无特殊要求的语言使用字体A,阿拉伯语言使用字体B;

  2. 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用 BoldMediumRegularThin 四种字体包;


所以,我现在桌面上摆着 8 个字体包:



  • A-Bold.tff

  • A-Medium.tff

  • A-Regular.tff

  • A-Thin.tff

  • B-Bold.tff

  • B-Medium.tff

  • B-Regular.tff

  • B-Thin.tff


image.png
不同语言要使用不同的字体包,不同粗细也要使用不同的字体包!


还有一个前提是,设计给的设计图都是以字体A为准,所以在 Figma 中复制出来的 CSS 代码中字体名称都是A。


刚接到这个需求时还是比较懵的,一时想不出来怎么样才能以最少的逻辑判断最少的文件下载最少的代码改动去实现在不同情况下自动的去选择对应的字体包。


因为要涉及到语言的判断,最先想到的还是通过 JS,然后去添加相应的类名。但这样也只能判断语言使用A或B,粗细还是解决不了。


image.png


看来还是要用 CSS 解决。


首先我将所有的8个字体先定义好:


@font-face {
font-family: A-Bold;
src: url('./fonts/A-Bold.ttf');
}

/* ... */

@font-face {
font-family: B-Thin;
src: url('./fonts/B-Thin.ttf');
}

image.png


🤲🏼 如何根据粗细程度自动选择对应字体包


有同学可能会问,为什么不直接使用 font-weight 来控制粗细而是用不同的字体包呢?


我们来看下面这个例子,我们使用同一个字体, font-weight 分别设置为900、500、100,结果我们看到的字体粗细是一样的。


对的,很多字体不支持 font-weight 所以我们需要用不同粗细的字体包。


image.png


所以,我们可以通过 @font-face 中的 font-weight 属性来设置字体的宽度:


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}
@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

注意,这里我们把字体名字都设为相同的,如下图所示,这样我们就成功的解决了第一个问题:不同粗细也要使用不同的字体包;


image.png


并且,如果我们只是定义而未真正使用时,不会去下载未使用的字体包,再加上字体包的缓存策略,就可以最大程度节省带宽:


image.png


🔤 如何根据不同语言自动选择字体包?


通过张鑫旭的博客找到了解决办法,使用 unicode-range 设置字符 unicode 范围,从而自定义字体包。


unicode-range 是一个 CSS 属性,用于指定字体文件所支持的 Unicode 字符范围,以便在显示文本时选择适合的字体。


它的语法如下:


@font-face {
font-family: "Font Name";
src: url("font.woff2") format("woff2");
unicode-range: U+0020-007E, U+4E00-9FFF;
}

在上述例子中,unicode-range 属性指定了字体文件支持的字符范围。使用逗号分隔不同的范围,并使用 U+XXXX-XXXX 的形式表示 Unicode 字符代码的范围。


通过设置 unicode-range 属性,可以优化字体加载和页面渲染性能,只加载所需的字符范围,减少不必要的网络请求和资源占用。


通过查表得知阿拉伯语的 unicode 的范围为:U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F 这么几个区间。所以我们设置字体如下,因为设计以 A 字体为准,所以在 Figma 中给出的样式代码字体名均为 A,所以我们把 B 字体的字体名也设置为 A:


image.png


当使用字体的字符中命中 unicode-rang 的范围时,自动下载相应的字体包。


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}

@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}

@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}

@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

:root {
--ARABIC_UNICODE_RANGE: U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F;
}
@font-face {
font-family: A;
src: url('./fonts/B-Bold.ttf');
font-weight: 600;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Medium.ttf');
font-weight: 500;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Regular.ttf');
font-weight: 400;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Thin.ttf');
font-weight: 300;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
p {
font-family: A;
}

总结


遇到的问题:



  1. 两种字体,B 字体为阿拉伯语使用,A 字体其他语言使用。根据语言自动选择。

  2. 根据字宽自动选择相应的字体包。

  3. 可以直接使用 Figma 中生成的样式而不必每次手动改动。

  4. 尽可能节省带宽。


我们通过 font-weight 解决了问题2,并通过 unicode-range 解决了问题1。


并且实现了按需下载相应字体包,不使用时不下载。


Figma 中的代码可以直接复制粘贴,无需任何修改即可根据语言和自宽自动使用相应字体包。




参考资料:http://www.zhangxinxu.com/wordpress/2…


作者:Mengke
来源:juejin.cn/post/7251884086536781880
收起阅读 »

前端地位Up!

web
背景 大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C... 然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种...
继续阅读 »

背景


大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C...


然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种小白进阶培训班、是业务形态的致命一击。最终是前端在技术圈子里不如狗的地位。


何至于此?


框架-砒霜or蜜糖?


说说框架


前段跟一个google的算法大佬聊天,他说你们前端很奇怪,现在浏览器已经非常快了,但你们非要搞这样那样的框架,然后去算这个算那个,搞些hooks虚拟dom,完全看不懂在干嘛。


也许我想他说的是对于web的性能层面是对的。


首先虚拟dom也就是中间层如果纯论性能在我看来其实是并不适合现在的时代的,它在这个时代的作用就是作为多端统一以及在真实dom操作前的数据缓冲/计算层...这可能也是这个时代出现了SvelteSolid为代表的此类框架的原因。我倒是希望现在web前端的方向能走向SolidSvelte这种框架的周边社区完善开发。


可是时代的潮流不会随着个人的意志改变,果然时代是分分合合吗,现在ssr为代表的next越来越火(可能有一部分vecel的商业原因),但更重要的一个原因是去改善开发人员的体验(在多端和最佳实践方面),也就是卷颗粒度


以next举例子:我对于next其实是这么理解的,粗颗粒度reactcsr已经进无可进了,改也不好改,那么转化一下方向把,在ssr的领域去降低粒度,就像流式渲染等。


说说人


啊真糟糕,怎么情不自禁就在说框架了,是不是发现我们前端情不自禁在开发or说某个技术的时候就会与框架挂钩。就像是面试我懂某个框架的原理、我学了几个框架诸如此类的,于是我们就从一个框架到另外个框架反反复复的在路上走、打包工具也是一样的(我真的需要这么快的打包工具吗?)。


于是我们就在框架中沉溺了,也许后面会出现一些5年vue工程师10年react工程师,我们整日沉浸在框架之中,日复一日,用着固定的写法(其实我在说Vue,React在这方面会好一些),做着相似的事情,技术的成长变为了我某个框架用得怎么样。


前端工程师or框架工程师


我想啊,前端的潮流很快(娱乐圈),但其实我们要明白一个道理,我用这个东西学这个东西对我有没有收益,对用户体验是不是有很大的提升,对团队开发有没有效率的进步。如果没有的话,不如搞浏览器,当然也可以学学当个PPT天才(纯褒义)或者业务、算法、其他语言等(好堕落啊现在不是PPT就是搞业务),着实没必要把绝大多数时间留着框架上,看多了就会觉得自己很牛逼,然后开摆。


毕竟我们不是框架开发工程师(也就是资源型工具人)、我们是前端开发工程师,我们是面向屏幕开发,我们是人机交互工程师,也就是现在的一个词终端工程师,如果你不能把你的应用在所有屏幕(安卓、Ios、桌面、PC、平板)跑那应该是不合格的。


业务形态


害,说这个之前闲聊一下,我们可以看到一些产品诸如语雀云凤蝶Antd等。蚂蚁体验技术部真的把前端的地位上拉了一截,他们真的很好,可能是未来5年在国内都不会再有这么好的标志性的前端产品,可惜没有一个闭环的商业业务形态,就类似next这种,我在这里不讨论具体的一些原因和后面发生的一些事情。


进入正文,产品和业务形态决定了前端的地位,后端开发通常被认为是应用程序的基础和核心。但其实怎么说那,有的时候其实是因为国内产品思维的局限于和上文提到的沉溺于框架和搜索工具


因为我做过不少国外的产品,有一些很有创造力和创新思维的产品会提出很多天马行空,极具艺术的产品交互效果和体验,在这类产品中其实前端的地位并不算低。


但是在国内就会有这种情况:



  1. 产品不会有这种想法,他的脑子里也是一些国内的那些很普通的竞品,和数据流转逻辑

  2. 前端自己拒绝,一般来说心路历程是这样的,我先看看能不能做 => 去百度掘金搜索 => 搜不到或者框架里没有,好感觉不好做,太复杂了 => 我们换一个普通一点的效果(理由五花八门)。

  3. 大家都是这么做的,那我们这个也这么做吧。


但其实我们自己作为科技触达用户的桥梁,是有能力去推动这个事情的,一个炫酷的配色、合理的交互效果、好看的页面,是可以去给产品去给设计说的,比如我自己有时候会figma或者lottie去自己画一些图和动画效果,去主动纠正设计的颜色和间距。难道产品会拒绝让产品变得更好?设计会拒绝更好看?


说白了,自己不想去做不想去推不会也不想学,觉得很复杂,当然如果实在没有这个土壤果断跑路。


Javascript本身的问题


Javascript吊打RustC估计我是这辈子都看不到了。


Javascript是解释性语言肯定没法跟一些编译语言竞天生就不行,再加上单线程即使有解决方案也就那样。这意味着前端掌握更多语言几乎是一个必要的事情,JavaSwift,Oc这些本来就会用到的不必多说,RustPython选一门掌握也很好。


会得越多你越强,当然我还是建议大家去当PPT天才


前端自信


以后,大伙自信一点,别觉得前端就不如其他技术岗位,地位都是自己争取的,前端优势很大,语言统一、前端立马可见的效果、前端基建相对较小、前端宿主环境统一Docker和容器配置相对统一等。主要是时间,有更多的时间意味着可以做更多的事学更多的东西更多的钱~。


总结


So,改变前端环境从你我做起,你卷一波我卷一波,前端的门槛就提起来了,以后面试的基本要求就是:前端要会Js、Ts、Java、Swift、混合框架、PWA,然后薪资30k起步。


作者:溪饱鱼
来源:juejin.cn/post/7283642910301192244
收起阅读 »

一次性弄清前端上线和生产环境地址

web
💡Tips:不知道小伙伴在前端开发的时候有没有这样的困惑 前端项目打包后,我打包过的静态资源是如何访问后端服务的? 访问后端服务的时候难道不存在跨域的问题吗?如何解决的? 假如我想自己简单修改下部署后的目录该如何去做? 🥲其实不仅仅是你会有这样的疑惑,包...
继续阅读 »

💡Tips:不知道小伙伴在前端开发的时候有没有这样的困惑




  1. 前端项目打包后,我打包过的静态资源是如何访问后端服务的?

  2. 访问后端服务的时候难道不存在跨域的问题吗?如何解决的?

  3. 假如我想自己简单修改下部署后的目录该如何去做?


🥲其实不仅仅是你会有这样的疑惑,包括我在内刚接触前端的时候,由于没有后端的开发经验也会被这些问题所困扰,但是今天我们将一次性弄清楚这个问题,让我们前端的知识体系由点成线,由线成面~


一.明确问题




我们知道,我们平时在开发的时候一般都是使用proxy进行代理的,它的原理是:浏览器会先去访问本地的node服务器,然后node服务器再去代理到你要访问的后端api接口,但是我们可能平时没有node服务器的概念,因为node服务器在webpack中,我们一般是通过下面这种方式来设置



但是我们的项目上线后这种方式就不能用了,(因为Node是我们本地开发的环境,并没有办法在线上使用。)其实,我们一般会通过后端的Nginx代理来解决跨域的问题,但是你知道前端的生产地址配置是什么吗?如何通过Nginx访问后端接口呢?是直接配置的类似于http://www.xxxx.com/api/aaa这样的路径呢?还是直接是一个相对路径/prod?要想搞清楚这些,首先就要了解什么是Nginx


二.什么是Nginx




🐻是一个开源的高性能、轻量级的Web服务器和反向代理服务器,它具有事件驱动,异步非阻塞的架构,被广泛应用于构建高性能的网站,应用程序和服务。


🤡在平时开发中我们经常听到正向代理反向代理这两个名词,那么什么是反向代理,什么是正向代理哪?



  1. 反向代理:服务器的IP是被屏蔽的,也就是说客户端不知道服务器真实的地址是哪个,客户端访问的地址或者域名是访问的Nginx服务器的地址。




  1. 正向代理:和反向代理刚好相反,这个时候服务器不知道真正的客户端是哪个,也就是相当于直接访问服务器的是nginx代理服务器。



三.前端使用Nginx解决跨域




🤗什么是跨域,跨域是指在浏览器的环境下,当一个网页的JavaScript代码通过Ajax Websocket或其他技术发送HTTP请求的目标资源位于不同的域名,端口或者协议下,就会发生跨域。


🐻Nginx如何解决跨域,因为服务器和服务器之间互相请求不发生跨域,所以解决跨域的方法之一就是使用这种方案



  1. 浏览器或者客户端发送请求:http:www.xxx.com:80Nginx服务器对80端口进行监听,Nginx服务器将请求转发到后端真实的服务器地址,这样就实现了代理。




  1. Nginx基本配置项解析


server {
listen 80;
server_name yourdomain.com;

location / { // 无论访问所有路径都返回前端静态资源dist内容
root /path/to/your/frontend;
index index.html;
try_files $uri $uri/ /index.html;
}

location /api/ {
proxy_pass http://backend-server-address/api/; // 真实后端api地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

当在前端代码中使用相对路径/api/users发起请求时候Nginx会将这个请求转发到真实的后端地址,不过你发现了没很多前端项目种生产环境地址都仅仅是一个/api类似这样的相对路径,很少直接是一个绝对路径。


当你请求 yourdomain.com 时,Nginx 会将前端静态资源返回给前端。前端代码中使用的相对路径 /api会基于当前域名 yourdomain.com构建完整的请求 URL。因此,前端代码请求后端地址的完整 URL 将是 yourdomain.com/api/xxx,其中 /xxx表示具体的后端接口路径。


Nginx 的反向代理配置中的 location /api/ 指令将匹配以 /api/ 开头的请求,并将这些请求代理到后端服务器上。因此,当前端代码发起相对路径请求 /api/xxx 时,Nginx 会将请求转发到 yourdomain.com/api/xxx,实现与后端接口的通信。


总结来说,前端代码中的相对路径 /api会根据当前域名构建完整的请求 URL,而 Nginx 的反向代理配置将这些请求转发到后端服务器上的相应路径。这样,前端代码就能够与后端接口进行通信。


四.前端生产环境配置




🥲既然Nginx如何代理的,以及前端打包的路径一般是什么样的我们知道了,那么我们就来唠唠作为一个前端小白该如何快速的完整的构建一个基础项目吧,其实如果基础开发的话,我们往往会使用脚手架,就以Vue开发的话,我们可以使用vuecli来快速构建项目,其实构建完之后你就可以直接npm run build打出的包就可以部署在后端服务器的,这个打出的包的根路径是默认的/,通过上面的Nginx的知识我们应该不难理解。


🤡如果我们要自己想去修改一个路径哪?我们可以在vue.config.js中进行配置,配置如下


module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? '/prod' : '/'
};

👹这样打出的包的静态资源的路径就是下边这样的



🥰如果是直接使用的默认的打包路径就是如下这种



五.总结




🤡最后总结一下,前端上线打包完就是一个静态文件,是一个相对路径,后端会通过Nginx来监听这个资源的请求,当匹配到/就返回静态资源,当匹配到某个/prod就将请求反向代理到后端真实服务器的地址,前端打包的是一个相对路径,Nginx会在前面拼上去具体的域名或者ip,这样就打通了线上前端访问的基本内容。


作者:一溪风月
来源:juejin.cn/post/7291952951048060940
收起阅读 »

同事看到我填写bug原因的表单, 惊呆了, 那么多字段怎么自动填充了?

web
在敏捷开发协作工具中, 公司会要求在关闭bug的时候必须填写一些字段. 其实每次填写的内容都一样的, 要选择bug原因, 是否选择, 填写根因等. 同事在旁边看我关闭了个bug, 看到我的表单是自动填充的, 就问我咋那么方便呢? 实现难度非常简单, 而在日常工...
继续阅读 »

在敏捷开发协作工具中, 公司会要求在关闭bug的时候必须填写一些字段.
其实每次填写的内容都一样的, 要选择bug原因, 是否选择, 填写根因等.


同事在旁边看我关闭了个bug, 看到我的表单是自动填充的, 就问我咋那么方便呢?


实现难度非常简单, 而在日常工作中非常有用, 并且有点小帅. 所以分享给大家.


用什么工具来修改你的网页


我选择的是arc浏览器的boost功能. 在网页上新建一个boost, 点击code, 选到js的tab就可以把编写的js插入到当前host的网页里运行了. 还有辅助功能zag可以帮你抓dom.


对于没有用arc浏览器的大家, 可以写一个chrome extension, 只需要使用content_scripts功能, 可以实现和arc的boost类似的功能: match网址url并且加载一段js. (其实功能是比arc多且灵活的)


修改jira页面


jira是个必须用, 且很多重复操作的网站. 我做了这些修改:


站会看板过滤器顺序调整


每天站会轮到的人的顺序和jira看板上不一致, 导致站会轮下一个人的时候得去找下一个人的位置. 只要获取一下看板过滤器, 调整一下子元素就行了.


const container = document.getElementsByClassName('aui-expander-content ghx-quick-content')[0]
container.children[6].remove()
container.children[10].remove()
container.children[6].after(container.children[2])
container.children[9].after(container.children[1])

看板过滤器多选改单选


jira看板的过滤器是多选的, 所以切换下一个人的时候必须把前一个人取消了, 这样每次都多一次操作.


我们只要给每个过滤器加一个点击事件, 把其他active的过滤器都点击一下就行了.


let child = null
container.onclick = function (e) {
if (child) return
child = e.target
for (let i = 0;i < container.children.length; i++) {
if (container.children[i].children[0] && container.children[i].children[0].classList.contains('ghx-active') && container.children[i].children[0].innerHTML !== child.innerHTML) {
container.children[i].children[0].click()
}
}
child = null
}

关闭bug的时候必须填写原因


公司有个规定, 关闭jira必须填写一些字段. 其实每次填写的内容都一样的, 自动填写可以节省非常多时间.


实现也非常简单, 定时器来寻找指定dom, 然后为这些dom附上指定的值.


const setInputValue = (id, value) => {
if (document.getElementById(id)) {
document.getElementById(id).value = value
}
}

setInterval(() => {
setInputValue('resolution', 10000)
setInputValue('customfield_10903', 12502)
setInputValue('customfield_12301', `故障原因:代码错误
解决方式:修复
影响范围:界面
故障处理人:yo-cwj.com`
)
}, 1000)

获取vue应用的实例来修改界面


老婆画了几套微信表情, 于是我经常登录上去看数据.


但dashboard上信息很少, 需要点到每个表情的详情中才能查看.


通过网络请求, 我看到其实在dashboard的界面, 数据已经请求到了, 于是开始我们的修改.


从dom中寻找vue实例


通过基础的vue知识, 我们知道vue实例是会挂在dom上的.


(vue作者说可以认为他是可用的, 因为vue的devtool也是依赖这个特性的, 那我们一个小脚本是更可用了)


那么哪些dom上有vue实例, 有点像个面试题, 写个简单的脚本就可以找到:


let traverse = (dom) => {
if (dom.__vue__) {
console.log(dom.__vue__._data)
}
for (let i = 0; i < dom.children.length; i++) {
traverse(dom.children[i])
}
}
traverse()

找到目标数据所在的dom, 正式的脚本就这样获取vue实例就可以了.


编写脚本


首先通过vue实例的_data属性获取到数据:


const list = document.querySelector('.page_mod_page_simple.page_home').__vue__.$parent.currentList;

然后把数据贴到对应的dom上:


const emotion_dom = document.querySelector('.table_wrp_emotion_list').querySelector('.table_body');
for (let i = 0; i < emotion_dom.children.length; i++) {
emotion_dom.children[i].children[2].innerHTML += `(${list[i].SendNum} - ${list[i].DataTime})`
}

到这里脚本就写完了, 其他的vue应用其实还可以调用vue实例中的方法获取数据, 或自己获取数据放进vue实例.


解决执行环境的问题


但把这段代码放到boost中会出现拿不到dom的\_\_vue\_\_实例的问题, 因为boost和chrome extension的执行环境并不是浏览器执行环境. 可以通过创建script并执行的方式.


let script = document.createElement('script');
script.textContent = "const list = document.querySelector('.page_mod_page_simple.page_home').__vue__.$parent.currentList;" +
"const emotion_dom = document.querySelector('.table_wrp_emotion_list').querySelector('.table_body');" +
"for (let i = 0; i < emotion_dom.children.length; i++) {" +
" emotion_dom.children[i].children[2].innerHTML += `(${list[i].SendNum} - ${list[i].DataTime})`" +
"}";
setTimeout(() => {
document.documentElement.appendChild(script);
}, 1000)

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

关于我调部门感觉又重新面试一次这件事,做出知识总结

web
前言 这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识...
继续阅读 »

前言



这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识掌握的差不多了,路还远着,还得学啊!本来那天我还准备一下班就回去玩战地2042,免费周啊!啪的一下兴趣全无,总结一下知识吧,指不定什么时候用上(手动狗头)



节流


节流是指一定时间内只触发一次函数调用,如果在指定时间内多次触发,执行第一次,其他的触发将会被忽略,直到过了设定的时间间隔才触发。


function throttle (fn,delay) {
let timer;
retrun function (...args) {
if(!timer) {
fn(this,args)
timer = settimeout(()=>{
timer=null
},delay)
}
}
}

防抖


防抖是在函数调用后,在指定时间间隔后才触发一次。如果在这个时间间隔内再次触发函数,将重新计时,直到过了设定的时间间隔才会触发最后一次函数调用。


function debounce (fn,delay) {
let timer;
retrun function (...args) {
if(timer) {
clearTimetout(timer)
}
timer = settimeout(()=>{
fn(this,args)
timer=null
},delay)
}
}

数据扁平化


数组


function flatter(arr) {
let result = []
for(let i =0;i<arr.length;i++) {
if(Array.isArray(arr[i]) {
result = result.concat(flatter(arr[i]))
} esle {
result.push(arr[i])
}
}
return result
}

去重


const arr1 = [...new Set(arr)]

const arr1 = arr.map((item,index)=>{
return arr.indexof(item)==index
})

查找字符串中出现最多的字符


当时手写了一半,str.split(item).length应该还要-1才是当前字符出现的次数


  const str = ref<string>('sdfgsgdd');
const fn = (str: string) => {
const arr = Array.from(str);
let maxCount = 0;
let mostFrequentChar = '';
const Nsome = [...new Set(arr)];
Nsome.forEach((item) => {
const count = str.split(item).length - 1;
if (count > maxCount) {
maxCount = count;
mostFrequentChar = item;
}
});
console.log('出现最多的次数,字符', maxCount, mostFrequentChar);
};

闭包及其应用场景


我的回答是:
函数里面嵌套函数,并且内部函数引用了外部函数的变量,就是函数能访问其作用域外的变量


应用场景:
我的回答其中之一是:vueX中状态共享是使用了闭包,节流,防抖
但在 Vuex 中,闭包主要用于封装和共享状态,而不是用于访问和操作外部函数的变量。它使用了闭包的概念,但不是严格意义上的闭包。


1.模块化开发 2.回调函数 3.延迟执行(节流,防抖)


原型&原型链及其应用场景



  1. 原型(Prototype):



  • 每个 JavaScript 对象都有一个原型(prototype),它是一个对象。

  • 对象的原型用于共享属性和方法,当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型可以通过 proto 属性访问,也可以通过 Object.getPrototypeOf() 方法获取。



  1. 原型链(Prototype Chain):



  • 原型链是由对象的原型组成的链式结构,它用于实现对象之间的继承。

  • 当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型链的顶端是 Object.prototype,它是所有对象的原型。


应用场景:



  • 继承:通过原型链,我们可以实现对象之间的继承,一个对象可以继承另一个对象的属性和方法。这样可以避免重复定义和维护相似的代码,提高代码的重用性和可维护性。

  • 共享属性和方法:通过原型链,我们可以将属性和方法定义在原型上,从而实现对象之间的共享。这样可以节省内存空间,避免重复创建相同的属性和方法。

  • 扩展原生对象:通过修改原型链,我们可以扩展 JavaScript 的原生对象,为其添加新的方法和属性。这样可以为原生对象添加自定义的功能,满足特定的需求。


在没有class之前,js是怎么做面向对象的


没答出来,只知道js可以通过class实现面向对象,然后又被问在没有class之前,js是怎么做面向对象的。这也是原型链的应用场景之一,可能是前面原型链的应用场景没说这个,想给我一个提示。


在没有class关键字之前,JavaScript使用原型继承来实现面向对象编程。
javaScript 中的每个对象都有一个原型(prototype),原型是一个对象,它包含了共享的属性和方法。当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。


通过原型链,我们可以实现对象之间的继承和共享属性和方法。下面是一个使用原型继承的示例:


// 创建一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在构造函数的原型上定义方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// 创建一个 Person 对象
const person1 = new Person('Alice', 25);
// 调用对象的方法
person1.sayHello(); // 输出 "Hello, my name is Alice and I am 25 years old."

node是什么,express是什么,node服务中的中间件是用来干什么的


Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,可以用于构建高性能的网络应用程序。它允许使用 JavaScript 在服务器端运行代码,而不仅仅局限于在浏览器中运行。


Express 是一个基于 Node.js 的 Web 应用程序框架,它提供了一组简洁而灵活的功能,用于构建 Web 应用程序和 API。Express 提供了路由、中间件、模板引擎等功能,使得构建 Web 应用程序变得更加简单和高效。


中间件的作用是增强和扩展 Node.js 服务的功能,使得处理请求和响应的过程更加灵活和可定制。通过使用中间件,可以将常见的功能模块化,提高代码的可维护性和可重用性。


Express 提供了一些内置的中间件,同时也支持自定义中间件。您可以使用内置的中间件,如 express.json()、express.urlencoded() 来处理请求体的解析,或者编写自己的中间件来满足特定的需求。


你h5怎么处理兼容性


因为是vite+v3项目,vite官方有推荐的插件库,在插件库中有一个关于浏览器兼容支持的插件:@vitejs/plugin-legacy


插件@vitejs/plugin-legacy的作用是为打包后的文件提供传统浏览器兼容性支持



  1. 首先安装插件:npm i @vitejs/plugin-legacy -D

  2. 然后在vite.config.js中配置


import legacyPlugin from '@vitejs/plugin-legacy'
export default defineConfig( {
plugins: [
legacyPlugin({
targets:['chrome 52'], // 需要兼容的目标列表,可以设置多个
additionalLegacyPolyfills:['regenerator-runtime/runtime'] // 面向IE11时需要此插件
})
]
})

rem,px,em这些有什么区别




  1. px(像素):px 是绝对单位,表示屏幕上的一个物理像素点。它是最常用的单位,具有固定的大小,不会根据其他因素而改变。例如,font-size: 16px; 表示字体大小为 16 像素。




  2. rem(根元素字体大小的倍数):rem 是相对单位,相对于根元素(即 元素)的字体大小。如果根元素的字体大小为 16 像素,那么 1rem 就等于 16 像素。如果根元素的字体大小为 20 像素,那么 1rem 就等于 20 像素。通过设置根元素的字体大小,可以方便地调整整个页面的大小。例如,font-size: 1.5rem; 表示字体大小为根元素字体大小的 1.5 倍。




  3. em(相对于父元素字体大小的倍数):em 也是相对单位,相对于父元素的字体大小。如果父元素的字体大小为 16 像素,那么 1em 就等于 16 像素。如果父元素的字体大小为 20 像素,那么 1em 就等于 20 像素。通过设置父元素的字体大小,可以影响其子元素的大小。例如,font-size: 1.2em; 表示字体大小为父元素字体大小的 1.2 倍。




总结来说,px 是绝对单位,不会随其他因素改变;rem 是相对于根元素字体大小的倍数,可以方便地调整整个页面的大小;em 是相对于父元素字体大小的倍数,可以影响子元素的大小。


在实际使用中,可以根据需求选择合适的单位。对于响应式设计,使用 rem 可以方便地调整整个页面的大小;对于局部样式,可以使用 px 或 em 来控制具体的大小。


你工作中遇到了什么坑或者解决什么让自己印象深刻的问题



  • element-plus的el-table表格的二次封装(可以使用tsx)

  • el-table表格的动态合并

  • h5 ios时调起键盘会把整个布局往上推

  • h5调用封装的app JSbrige完成返回

  • 登录的拼图验证

  • h5嵌套在微信小程序中时,由我们h5跳到三方提供的安全验证h5页面,返回时,本地存储的东西没了

  • 利用git hooks+husky+eslint完成前端代码规范和提交规范

  • 银行卡拖拽排序,把排完的顺序返回服务端


上面这些都是我解决了,也不仅仅只有这些,回头想了了下明明自己有很多可以说的,在当时就说了2,3个,然后负责人问我还有吗时,我卡壳了,居然不知道还要说什么。后面我感觉也是根据这个展开来问的


V2混入和V3的hooks,为什么V3要改成hooks的方式


感觉应该是问hooks的好处吧?反正我是答的不太对的,以下是总结:


Vue 3 引入了 Composition API(包括 setup 函数和 hooks),这是一个新的方式来组织和复用代码,与 Vue 2 的混入(mixins)有所不同。

混入在 Vue 2 中被广泛使用,它们允许你在多个组件之间共享行为。然而,混入有一些问题:



  1. 命名冲突:如果混入和组件有相同的方法或数据属性,可能会导致冲突。

  2. 来源不明:当一个组件使用了多个混入时,可能很难确定一个特定的方法或数据属性来自哪个混入。

  3. 复杂性:混入可以包含生命周期钩子、方法、数据等,这可能会增加理解和维护组件的复杂性。

    相比之下,Vue 3 的 Composition API(包括 hooks)提供了一种更灵活、更可控的方式来组织和复用代码:

  4. 更好的逻辑复用和代码组织:你可以将相关的代码(如数据、方法和生命周期钩子)组织在一起,而不是强制按照 Vue 的选项(data、methods、created 等)来组织代码。

  5. 更好的类型推断:对于使用 TypeScript 的项目,Composition API 提供了更好的类型推断。

  6. 更清晰的来源:每个函数和响应式数据的来源都非常明确,因为它们都是从特定的 hook 或 setup 函数返回的。
    因此,虽然 Vue 3 仍然支持混入,但推荐使用 Composition API 来组织和复用代码。


vue3中怎么封装一个自定义指令



  • 通过app.directive()方法注册指令,该方法接受两个参数,第一个参数是指令的名称,第二个参数是一个对象,包含指令的各个生命周期的钩子函数

  • 然后我们就可以在生命周期的钩子函数中定义指令的行为,根据指令的需求,在相应的生命周期钩子函数中编写逻辑代码


什么情况下会使用自定义指令


我的回答是:想要操作dom元素时并且这种类似情况经常出现,如节流和防抖指令,就是给dom加上disabled。按钮权限指令,给当前按钮dom一个显示和隐藏


拖拽排序


拖拽排序的实现原理主要涉及一下几个步骤:



  • 1.监听拖拽事件: 浏览器提供了一系列的拖拽事件,设置draggable="true"



    1. 开始拖拽:当用户开始拖拽一个元素时,会触发 dragstart 事件。在这个事件的处理函数中,我们可以通过 传入的dragstart(e,index) ,中的index来设置当前被拖拽元素的下标。





    1. 拖拽过程:当用户拖拽元素时,会不断触发 dragover 事件。在这个事件的处理函数中,我们需要调用 event.preventDefault 方法来阻止浏览器的默认行为,否则无法触发 拖拽 事件。





    1. 拖拽到另一个元素区域时:当用户拖拽到另一个元素时,会触发 dragenter 事件。在这个事件的处理函数中,我们可以通过 dragente(e,index)方法来获取拖拽到的元素的下标,然后根据获取的两下标来更新列表的排序。




表格动态合并


element-plus表格合并(例如前两列合并) | 耀耀切克闹 (yaoyaoqiekenao.com)


模拟new实例创建的过程



  • 1.创建了新对象并将._proto_指向构造函数.prototype

  • 2.将this指向新创建的对象

  • 3.返回新对象


function newSimulator() {
//1.创建新对象
const obj = new Object()
//2.设置_proto_为构造函数prototype
const constructor = [].shift.call(arguments)
obj._proto_ = constructor.prototype
//3.this指向新对象,也就是改变this的指向
const ret = constructor.apply(obj,arguments)
//4.返回对象或this
return typeof ret = 'object' ? ret : obj
}

冒泡排序


const arr = [1,7,9,2,3,5]
for(let i=0;i<arr.length;i++){
for(let j=0;j<arr.length-i-1;j++){
let a = []
if(arr[j]<arr[j+1]){
a =arr[j]
arr[j]=arr[j+1]
arr[j+1]=a
}
}
}


深拷贝


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


const obj={
arr:[1,2]
}
const clone = JSON.parse(JSON.stringify(obj))

2.使用递归完成深拷贝


这种方式通过递归地遍历原始对象,并对该对象的的属性进行逐一的深拷贝,以创建一个原对象的独立副本。


function deepCloneObject(obj) {
if(obj ===null||typeof object !='object') {
return obj
}
const clone = Array.isArray(obj)?[]:{}
for(let key in obj) {
if(object.prototype.hasOwnProperty.call(obj,key))
clone[key] = deepClone(obj[key])
}
retrun clone
}

函数柯里化


函数柯里化是一种将具有多个参数的函数转换为逐个应用参数的函数序列的技术。通过柯里化,我们可以将一个函数的多个参数转化为一系列嵌套的函数调用。


柯里化的优点是可以创建可复用的函数模板。通过部分应用来生成新的函数。这样可以更灵活地使用函数,并且可以方便的创建更专注于特定功能的函数。
简单的函数柯里化例子:


function add(x) {
return function(y) {
return x + y;
}
}

// 使用柯里化的add函数
var add5 = add(5);
console.log(add5(3)); // 输出 8
console.log(add5(7)); // 输出 12

封装一下


function curry(fn) {
return function curried(...args) {
if(args.length>=fn.length) {
return fn.apply(this,args)
} else {
return function(...moreArgs) {
return curried.apply(this,args.concat(moreArgs))
}
}
}
}

数组API的实现


forEach


Array.portotype.my_forEach = function(callback) {
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)
}
}

map


Array.portotype.my_map = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)&&res.push( callback(this[i],i,this))
}
return res
}

filter


Array.portotype.my_filter = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i], i, this) && res.push(this[i])
}
return res
}

前端模块化


问:你讲讲前端模块化吧
答:模块化的开发方式可以提高代码复用率,方便进行代码的管理,通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。


问:模块化有哪几种标准?
答:目前流行的js模块化规范有CommonJS、AMD、CMD以及Es6的模块系统


问:ES Modules 和CommonJS的一些区别
答:
1.使用语法层面,CommonJs是通过modules.exports,exports导出,require导入;ES Modules则是export导出,import导入
2.CommonJs是运行时加载模块,EsModules是在静态编译期间就确定模块的依赖
3.EsModulse在编译期间会将所有import提升到顶部,CommonJs不会提升require
4.CommonJs导出是一个值拷贝,会对加载结果进行缓存,一但内部再修改这个值,则不会同步到外部。ESModule是导出的一个引用,内部修改可以同步到外部
5. CommonJs中顶层的this指向这个模块本身,而ESModule中顶层this指向undefined
6. CommonJS加载的是整个模块,将所有的接口全部加载进来,ESModule可以单独加载其中的某个接口


vue的数据双向绑定的原理


vue的响应式原理是采用‘发布-订阅’的设计模式结合object.defineProperty()劫持各个属性的getter和setter,在数据发生变动时通过调用Deo.notity函数发布订阅给观察者watcher,让其更新响应的视图。


虚拟dom


虚拟dom是用来表现真实dom结果的javaScript对象树,是构建在浏览器真实dom上的抽象层,虚拟dom是可以直接在内存中操作的,可以通过diff算法来新旧dom的差异,将最终变化应用到真实dom上


diff算法


diff算法又称虚拟Dom的周界算法,vue的diff算法是通过深度优先、先序遍历的方式进行的,它将前后两个虚拟Dom树进行逐层比较,当找到某一层不一样的节点时,停止下降,然后比较这些节点的子节点,当所有的子节点都完成比较之后,算法会由下至上进行回溯,此过程被称为执行patch操作。在执行patch操作时,Vue对于不同类型的节点的更新方式也不同,对于元素节点可以更新他的属性和子节点;对于文本节点,只能更新它的文本内容;对于每个子节点,如果key值相同,可以进行复用或者重新排序,或者将其他的节点移动到这个位置。


vue中nextTick的理解及作用


使用场景描述:更改一个数据,导致dom元素的width发生了更改,但又要获取这个更新后的dom元素的width,可以用nextTick
vue2 中的nextTick是在下次Dom更新循环之后执行回调函数,并且是作为vue实例的方法调用的


this.$nextTick(() => { // 组件的DOM已经更新完毕,可以进行相应操作 // ... });

Vue 3的nextTick作为一个独立的函数导入,返回一个Promise,并且可以直接传递回调函数作为参数。这些变化使得Vue 3中的nextTick更加灵活和易于使用。


// Vue 3 
import { nextTick } from 'vue';
nextTick(() => { // 在下次DOM更新循环之后执行 });

vue在实例挂载的过程中发生了什么?




  1. 实例化:首先,Vue.js会创建一个新的Vue实例。在这个过程中,Vue.js会设置实例的各种属性和方法,包括数据对象、计算属性、方法、指令等。




  2. 编译模板:Vue.js会将模板编译成渲染函数。模板就是包含Vue特定语法的HTML代码。编译过程中,Vue.js会解析模板中的指令(如v-if、v-for等)和插值表达式(如{{ message }}),并将它们转换为JavaScript代码。




  3. 创建虚拟DOM:渲染函数会被调用,生成一个虚拟DOM树。虚拟DOM是对真实DOM的轻量级表示,它可以更高效地处理DOM的更新。




  4. 挂载:最后,Vue.js会将虚拟DOM渲染为真实DOM,并将其挂载到指定的元素上。这个过程通常在调用vm.$mount()方法或者在实例化Vue时传入el选项后发生。




  5. 更新:当数据变化时,Vue.js会重新执行渲染函数,生成新的虚拟DOM,并与旧的虚拟DOM进行对比(这个过程称为diff)。然后,Vue.js会根据diff结果,以最小的代价更新真实DOM。




这个过程中还会触发一系列的生命周期钩子,如created、mounted等,开发者可以在这些钩子中执行自己的代码。


vue2中data是一个函数而不是对象的原因


data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染


vue2中给对象添加新属性界面页面不刷新


vue2是用Object.defineProperty实现数据响应式,而后面新增的属性,并没有通过Object.defineProperty设置成响应式数据,所以页面没变化,常用解决方式:



  • Vue.set()

  • Object.assign()

  • $forcecUpdated()


Vue SSR的实现原理


vue.js的ssR是一种在服务器上预渲染Vue.js应用程序的技术。



  1. 服务器接收请求:当服务器接收一个请求时,它会创建一个新的Vue实例。

  2. 创建渲染器:使用vue-server-renderer包创建一个渲染器。

  3. 渲染页面:服务器使用渲染器将Vue实例渲染为Html字符串。

  4. 发送响应:服务器将渲染后的Html字符串作为响应发送给客户端。

  5. 客户端接收响应:客户端接收到服务器的响应后,将HTML字符串解析为DOM并显示给用户。

  6. 激活(Hydration): Vue在客户端创建一个新的Vue实例,将其挂载到服务器收到的Dom上


keep-alive的使用


keep-alive的主要作用是缓存路由组件,以提高性能


<router-view v-slot="{ Component }">  
<keep-alive :include="permissionStore.keepAliveName">
<component :is="Component" :key="$route.path" />
</keep-alive>

</router-view>



  1. router-view是 Vue Router 的一个组件,用于渲染当前路由对应的组件。




  2. v-slot="{ Component }" 是一个插槽,用于获取当前路由对应的组件。




  3. keep-alive 是 Vue 的一个内置组件,用于缓存组件,避免重复渲染。




  4. :include="permissionStore.keepAliveName" 是 的一个属性,表示只有名称在 permissionStore.keepAliveName 中的组件会被缓存。




  5. 是一个动态组件,:is="Component" 表示组件的类型由 Component 决定,:key="$route.path" 表示每个路由路径对应一个唯一的组件实例。




Vue项目中有封装axios吗?主要是封装哪方面的?



  • 1.封装前需要和后端协商好一些约定,请求头,状态码,请求时间....

  • 2.设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分

  • 3.移除重复的请求,如果请求在pending中,提示'操作太频繁,请稍后再试'

  • 4.用map结构根据相应状态码处理错误信息

  • 5.请求拦截,若headers中没有token的,移除请求

  • 6.响应拦截器,例如服务端返回的message中有'message',提示'请求超时,请刷新网页重试'

  • 7.请求方法的封装,封装get、post请求方法,使用起来更为方便


css预处理器


css预处理器扩充了css语言,增加了诸如变量、混合(mixin)、函数等功能,让css更易维护、方便。本质上。预处理是css的超集。包含一套自定义的语法及一个解析器,根据这些语法定义自己的样式规则,这些规则最终会通过解析器编译生成对应的css文件。


如何实现上拉加载


image.png
触底公式:


scrollTop + clientHeight >= scrollHeight

简单实现:


    let clientHeight = document.documentElement.clientHeight;//浏览器高度
let scrollHigiht = documnet.body.scrollHeight;//元素内容高度的度量,包括由于溢出导致的视图中不可见内容
let scrollTop = documnet.body.scrollTop; //滚动视窗的高度距离`window`顶部的距离
let distance = 50; //距离视窗还用50的时候,开始触发;

if ((scrollTop + clientHeight) >= (scrollHeight - distance)) {
console.log("开始加载数据");
}

如何实现下拉刷新


关于下拉刷新的原生实现,主要分成三步:



  1. 监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY;

  2. 监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的3. translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值

  3. 监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置。


封装和使用JSBrige




  1. 定义协议:首先,需要定义一种协议,用于约定H5页面与App之间的通信规则。这可以是一组自定义的URL Scheme或JavaScript函数。




  2. 注册事件监听:在H5页面中,通过JavaScript代码注册事件监听器,用于接收来自App的消息或回调。可以使用window.addEventListener或其他类似的方法来监听特定的事件。




  3. 发送消息给App:在H5页面中,通过调用JSBridge提供的方法,将消息发送给App。这可以是通过修改URL Scheme的方式,或者调用App提供的JavaScript接口。




  4. 处理App的消息或回调:在App原生代码中,通过监听URL Scheme或执行JavaScript代码的方式,接收来自H5页面的消息或回调。根据协议约定,处理相应的逻辑或调用相应的功能。




  5. 回调结果给H5页面:在App原生代码中,根据协议约定,将处理结果或回调信息发送回H5页面。可以通过修改URL Scheme的方式,或者调用H5页面中注册的JavaScript回调函数。




个人博客


耀耀切克闹 (yaoyaoqiekenao.com)


gitHub


DarknessZY (zhangyao) (github.com)


作者:耀耀切克闹灬
来源:juejin.cn/post/7291834381315719220
收起阅读 »

分享一个Java小项目:Java实现超级马里奥的冒险之旅

引言超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利...
继续阅读 »

引言

超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利之地。

游戏目标

在这款游戏中,我们的目标是通过控制马里奥完成三个关卡的挑战。每个关卡都有不同的难度和障碍物,玩家需要灵活运用跳跃技巧和反应能力,才能成功通关。同时,消灭普通砖块还可以赚取积分,增加游戏的趣味性和挑战性。

Java实现

为了实现这个经典的游戏,我们将使用Java编程语言进行开发。Java是一种功能强大且广泛使用的编程语言,它具有丰富的图形界面库和游戏开发工具,非常适合用于制作平台跳跃类游戏。

在实现过程中,我们可以使用Java的Swing库来创建游戏的图形界面,包括游戏窗口、角色、背景等元素。同时,我们还需要处理用户的输入操作,例如键盘按键的监听和处理,以便玩家能够控制马里奥的移动和跳跃。

此外,我们还需要考虑游戏的物理引擎和碰撞检测机制,以确保马里奥能够与障碍物和敌人进行正确的交互。这可以通过使用Java的物理引擎库或自己编写相应的算法来实现。

总结

通过使用Java编程语言和相关库,我们可以成功地实现一个经典的超级马里奥小游戏。这将是一个非常有趣和有挑战性的项目,不仅可以锻炼我们的编程技能,还能够让我们体验到游戏开发的乐趣。让我们一起踏上这段冒险之旅吧!

收起阅读 »

Vue 实现 PDF 导出功能

web
笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。 安装依赖 yarn add html2canvas yarn add jspdf 思路 通过网上的一...
继续阅读 »

笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。


安装依赖


yarn add html2canvas
yarn add jspdf

思路


通过网上的一些教程,初步实现了 html 转 pdf 的功能,将一整个 DOM 元素放进去,虽然可以粗糙实现,但是出现了很多地方被分页截断的情况,这个时候就需要在某一张图片被截断时,将其自动切换到下一页中。


1.拆解父节点


所以第一步:拆解父节点,一行一行细分为很多子节点,循环遍历这些子节点,累加这些子节点的高度,如果超出了 a4 纸(210*297)的高度,则分页。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function oneNodeMultipleChildren(title, node) {
html2Canvas(node, {
scale: 2, // 清晰度
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4"); // 以mm为单位
let position = 0; // 页面偏移
let contentWidth = canvas.width; // 转换成canvas后的宽度
let contentHeight = canvas.height; // 转换成canvas后的高度
let proportion = 210 / node.offsetWidth; // html缩小至a4纸大小时的比例
let currentHeight = 0; // 当前高度
let imgWidth = 210; // canvas缩小至a4纸大小时的宽度
let imgHeight = (210 / contentWidth) * contentHeight; // canvas缩小至a4纸大小时的高度
let pageData = canvas.toDataURL("image/jpeg", 1.0); // 将canvas转成图片

for (let j = 0; j < node.children.length; j++) {
let childHeight = (node.children[j].offsetHeight + 8) * proportion; // 页面中每行的间距 margin-bottom: 8px

if (currentHeight + childHeight > 297) {
// 如果加上这个子节点后内容超出a4纸高度,就从当前位置开始分页
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight; // 这一页放了多少高度的内容,下一页就从这个高度开始偏移
if (position >= -contentHeight) {
PDF.addPage(); // 添加新pdf页
}
currentHeight = childHeight; // 下一页第一个元素的高度
} else {
currentHeight += childHeight;
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight); // 最后一页
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
// PDF.rect参数分别为:起始横坐标、起始纵坐标、绘制宽度、绘制高度、填充色
}

2.合并父节点


经过上述步骤,一个父节点多个子节点,并且每个子节点独占一行的布局可以实现分页,那要是有很多父节点呢?就需要遍历每个父节点,合并所有子节点,进行分页截断。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);

for (let i = 0; i < nodes.length; i++) {
// 根据传入的父节点数量进行循环,遍历父节点,合并所有子节点
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

3.每行多个元素


这个时候新的问题出现了,由于页面布局为 flex 布局,不同缩放下,每行的元素数量会出现变化。所以我们获取第一个子元素与 a4 纸宽度关系,如果为 n 倍,那后面 n-1 个子元素的高度不进行累加。


warning 注意
这里只解决了一行 n 个子元素宽度相等,且近似等于 a4 纸宽度的 1/n 的情况。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;
let childWidth = nodes[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth);
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

4.添加左右间距和页眉页脚


为了美化 pdf 布局,上下左右留白,就需要添加左右间距和页眉页脚:减少 html 缩小至 a4 纸大小时的比例和 canvas 缩小至 a4 纸大小时宽高,增加偏移量,并对页眉页脚进行空白遮挡。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, fNode, sNode) {
html2Canvas(fNode, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / fNode.offsetWidth; // 减少10mm
let currentHeight = 0;
let imgWidth = 200; // 减少10mm
let imgHeight = (200 / contentWidth) * contentHeight; // 减少10mm
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < sNode.length; i++) {
for (let j = 0; j < sNode[i].children.length; j++) {
let childHeight = (sNode[i].children[j].offsetHeight + 8) * proportion;
let childWidth = sNode[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth); // 减少10mm
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
// 减小10mm
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 5, position + 5, imgWidth, imgHeight); // 增加偏移量
PDF.setFillColor(255, 255, 255);
PDF.rect(0, 0, 210, 4, "F"); // 添加页眉遮挡
PDF.rect(0, currentHeight + 5, 210, Math.ceil(292 - currentHeight), "F"); // 添加页脚遮挡
}

成果展示


不同缩放下导出 PDF 对比:


每行一个子元素


每行多个子元素


作者:虚惊一场
来源:juejin.cn/post/7291142504123875364
收起阅读 »

如何创建五彩纸屑效果

web
前言 很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。 本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果 简单使用 我们可以通过 npm 安装或...
继续阅读 »

前言


很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。


本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果


简单使用


我们可以通过 npm 安装或从 cdn 导入两种方式来使用这个库。


这里我采用的是导入的方式。


在你导入完成这个库之后,我们需要一个按钮


<button onclick="myClick()">button</button>

最简单的特效只需要在点击函数当中调用 confetti 函数即可


function myClick () {
confetti()
}

动画4.gif


细节配置参数


通过传入 options 属性,我们可以在特效上自定义很多我们需要的部分,下面是部分配置属性的作用,后面我们会挑出部分属性来展示一下效果:


属性名作用
particleCount飞出的纸屑的数量,默认 50
angle飞出的纸屑的角度 90 是向上,默认 90
spread飞出的纸屑偏离中心的角度,默认 45
startVelocity飞出的纸屑的初始速度,默认 45
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
gravity重力,默认 1
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
flat是否关闭纸屑的翻转,默认 false
ticks纸屑消失速度,默认 200
origin对象,设置发射纸屑的原点,有 x y 两个参数,取值都是 0 - 1 对应上下边缘,默认 0.5 0.5
colors数组:十六进制格式的颜色字符串数组
shapes数组:纸屑的形状
scalar每个五彩纸屑粒子的比例,默认 1
zIndex纸屑的z-index,默认 100
disableForReducedMotion禁用五彩纸屑,默认 false

根据上面的参数,你可以很容易的定义自己想要的效果,下面我们随意定义部分例子:


<body>
  <button onclick="myClick1()">左倾斜</button>
  <button onclick="myClick2()">全是黑色</button>
  <button onclick="myClick3()">数量很多多多</button>
 <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.0/dist/confetti.browser.min.js"></script>
  <script>
    function myClick1 () {
      confetti({
        angle: 135,
      });
    }
    function myClick2 () {
      confetti({
        colors: ['#000000']
      });
    }
    function myClick3 () {
      confetti({
        particleCount: 500,
      });
    }
 
</script>

</body>

动画4 1.gif


详细定义纸屑形状


在上面的配置当中我们已经可以定义纸屑的大部分配置,其中 shapes 可以用于定义纸屑的形状,官方为我们预留了三个形状 'square', 'circle', 'star' 分别对应 方形,圆形,星星,这个字段传入的是一个数组,这个数组中元素的数量,决定了这个形状在所有纸屑中的比例,比如说你要是传入了 ['square', 'circle', 'star'] 那么三种形状就都是占比三分之一。


除了通过官方预留的形状,我们还可以通过两个函数来进行形状自定义,分别是 confetti.shapeFromPathconfetti.shapeFromText


confetti.shapeFromPath


这个函数可以传入一个对象,对象中存在一个 pathkey,这个就决定了创建出来的形状长什么样子


下面的代码可以创建一个三角形的纸屑


var triangle = confetti.shapeFromPath({ path: 'M0 10 L5 0 L10 10z' });

confetti({
shapes: [triangle],
});

confetti.shapeFromText


这个函数同样传入一个对象,对象存在 textscalartext 可以传入任何文字,甚至是一些文字表情也可以使用, scalar 配合 optionsscalar 使用,两个相差过大会导致字体变得很模糊。


下面的代码你就可以创建一个字符串纸屑


var scalar = 2;
var pineapple = confetti.shapeFromText({ text: '🍍🍍🍍', scalar });
confetti({
shapes : [pineapple],
scalar,
});

逻辑事件


纸屑在生成的时候,我们可以会需要一些事件,比如说在我们想要的时候清除掉屏幕上还未消失的纸屑,又或者在一次纸屑彻底消失的时候执行某些逻辑。


消除纸屑


我们可以通过调用 confetti.reset(); 来消除屏幕上的纸屑


监听纸屑消失事件


在我们调用 confetti() 的时候会返回一个 promise 对象,这个对象将会在纸屑完全消失的时候回调。


自定义纸屑产生的位置


在上面的例子当中,我们可能会发现,纸屑都只能在屏幕的正中心产生,这是因为 options 里的 origin 属性的默认值,我们可以通过一些方式来自定义这个产生的位置


动态设置 origin


我们可以通过动态的来设置这个值来做到自定义产生的位置。


在点击事件当中,我们能够拿到当前的点击对象,通过这个 event 对象以及 window 上获取到可视区域的宽高,我们不难算出当前按钮相对于左右的位置。


const windowHeight =
  window.innerHeight ||
  document.documentElement.clientHeight ||
  document.body.clientHeight;
// 获取浏览器高度
const windowWidth =
  window.innerWidth ||
  document.documentElement.clientWidth ||
  document.body.clientWidth;
// 获取浏览器宽度
const origin = {
x: event.pageX/windowWidth,
y: event.pageY/windowHeight,
}
// 获取比例

自定义 canvas


官方支持我们创建自定义画布的  confetti 实例,这样创建的  confetti 不会超出定义的这个画布的范围,这在某些时候可能会起到很重要的作用。


并且官方强调了,一个画布最好只和一个  confetti 实例做关联,所以说当我们创建多个  confetti 实例的时候,就也需要创建多个画布。


因为是自定义添加的画布,所以我们需要在不需要的时候手动去删除这个画布,避免产生多余的元素。


var myCanvas = document.createElement('canvas');
var container = document.querySelector('.container')
container.appendChild(myCanvas)
var myConfetti = confetti.create(myCanvas).then(()=>{
container.removeChild(myCanvas)
});

然后设定位置的操作我这里就不多做了,只需要定义这个 canvas 的生成位置就好了。


示例代码



作者:14332223
来源:juejin.cn/post/7290769553572397056
收起阅读 »

使用a标签下载文件

web
引言 HTML中  <a>  元素(或称锚元素)可以通过它的 href 属性创建通向其他网页、文件、电子邮件地址、同一页面内的位置或任何其他 URL 的超链接。 <a> 中的内容应该指明链接的目标。如果存在 href 属性,当 <...
继续阅读 »

引言


HTML中  <a>  元素(或称元素)可以通过它的 href 属性创建通向其他网页、文件、电子邮件地址、同一页面内的位置或任何其他 URL 的超链接。


<a> 中的内容应该指明链接的目标。如果存在 href 属性,当 <a> 元素聚焦时按下回车键就会激活它。


本文主要讲解如何通过a标签来下载文件。


download属性


浏览器将链接的 URL 视为下载资源。可以使用或不使用 filename 值:




  • 如果没有指定值,浏览器会从多个来源决定文件名和扩展名:



    • Content-DispositionHTTP 标头。

    • URL的最后一段。

    • 媒体类型。来自 Content-Type 标头,data: URL的开头,或 blob: URL 的 Blob.type




  • filename:决定文件名的值。/ 和 \ 被转化为下划线(_)。文件系统可能会阻止文件名中其他的字符,因此浏览器会在必要时适当调整文件名。





备注:



  • download 只在同源 URL或 blob:data: 协议起作用。

  • 浏览器对待下载的方式因浏览器、用户设置和其他因素而异。在下载开始之前,可能会提示用户,或者自动保存文件,或者自动打开。自动打开要么在外部应用程序中,要么在浏览器本身中。

  • 如果 Content-Disposition 标头的信息与 download 属性不同,产生的行为可能不同:

  • 如果文件头指定了一个 filename,它将优先于 download 属性中指定的文件名。

  • 如果标头指定了 inline 的处置方式,Chrome 和 Firefox 会优先考虑该属性并将其视为下载资源。旧的 Firefox 浏览器(版本 82 之前)优先考虑该标头,并将内联显示内容。



下载方式


1. 直接使用a标签的href属性指定文件的URL


可以在a标签中使用href属性指定文件的URL,点击链接时会直接下载文件。


<a href="file_url">Download</a>

优点:简单易用,只需在a标签中指定文件的URL即可。


缺点:无法控制下载文件的名称和保存位置。


2. 使用download属性指定下载文件的名称


可以在a标签中使用download属性指定下载文件的名称,点击链接时会将文件以该名称保存到本地。


<a href="file_url" download="file_name">Download</a>

优点:可以控制下载文件的名称。


缺点:无法控制下载文件的保存位置。


3. 将文件数据转为Blob进行下载


当需要将文件数据转为Blob或Base64进行下载时,可以使用以下方法:


1. 将文件数据转为Blob进行下载


function downloadFile(data, filename, type) {
const blob = new Blob([data], { type: type });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = filename;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
URL.revokeObjectURL(url);
}

function fileToBlob(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onloadend = () => {
resolve(new Blob([reader.result], { type: file.type }));
};

reader.onerror = reject;

reader.readAsArrayBuffer(file);
});
}

// 使用示例
const fileData = ...; // 文件数据
const fileName = 'example.txt';
const fileType = 'text/plain';

fileToBlob(fileData).then(blob => {
downloadFile(blob, fileName, fileType);
});

首先,我们定义了一个名为downloadFile的函数,它接受三个参数:文件数据(data),文件名(filename)和文件类型(type)。 在函数内部,我们使用Blob构造函数将文件数据和类型传递给它,从而创建一个Blob对象。然后,我们使用URL.createObjectURL()方法创建一个URL,该URL指向Blob对象。 接下来,我们创建一个<a>元素,并设置其href属性为之前创建的URL,并将下载属性设置为指定的文件名。然后将该元素添加到文档的body中。 最后,我们模拟用户点击该链接进行下载,并在完成后清理相关资源。


在使用时,我们首先调用fileToBlob函数将文件数据转换为Blob对象。该函数返回一个Promise对象,在Promise的resolve回调中返回了转换后的Blob对象。 然后,在Promise的回调中调用了downloadFile函数来进行下载。


2. 将文件数据转为Base64进行下载


function downloadBase64File(base64Data, filename, type) {
const byteCharacters = atob(base64Data);
const byteArrays = [];

for (let i = 0; i < byteCharacters.length; i++) {
byteArrays.push(byteCharacters.charCodeAt(i));
}

const blob = new Blob([new Uint8Array(byteArrays)], { type: type });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = filename;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
}

function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onloadend = () => {
resolve(reader.result.split(',')[1]);
};

reader.onerror = reject;

reader.readAsDataURL(file);
});
}

// 使用示例
const fileData = ...; // 文件数据
const fileName = 'example.txt';
const fileType = 'text/plain';

fileToBase64(fileData).then(base64Data => {
downloadBase64File(base64Data, fileName, fileType);
});

首先,我们定义了一个名为downloadBase64File的函数,它接受三个参数:Base64字符串(base64Data),文件名(filename)和文件类型(type)。 在函数内部,我们首先将Base64字符串解码为字节数组,并将其存储在byteArrays数组中。然后,我们使用这些字节数组创建一个Blob对象,并使用URL.createObjectURL()方法创建一个URL。 接下来,我们创建一个<a>元素,并设置其href属性为之前创建的URL,并将下载属性设置为指定的文件名。然后将该元素添加到文档的body中。 最后,我们模拟用户点击该链接进行下载,并在完成后清理相关资源。


在使用时,我们首先调用fileToBase64函数将文件数据转换为Base64字符串。该函数返回一个Promise对象,在Promise的resolve回调中返回了转换后的Base64字符串。 然后,在Promise的回调中调用了downloadBase64File函数来进行下载。


总结


您可以根据需要选择将文件数据转为Blob或Base64进行下载。如果您已经有文件数据,可以使用fileToBlob函数将其转为Blob对象并进行下载。如果您希望将文件数据转为Base64进行下载,可以使用fileToBase64函数将其转为Base64字符串,并使用downloadBase64File函数进行下载。


作者:前端俊刚
来源:juejin.cn/post/7291200719683502120
收起阅读 »

前端实现蜂巢布局思路

web
效果图如下 上图的组成可以分为两部分,一个为底图六边形的形成,一个为内容六边形的形成。 要生成对应的六边形,首先要获得需要绘制的六边形的中心坐标。 观察不难得出结论,以中心的六边形为基点,第二圈很明显能排6个,第三圈能排12个,以此进行类推。 这里可以以中...
继续阅读 »

效果图如下


image.png
上图的组成可以分为两部分,一个为底图六边形的形成,一个为内容六边形的形成。


要生成对应的六边形,首先要获得需要绘制的六边形的中心坐标。


观察不难得出结论,以中心的六边形为基点,第二圈很明显能排6个,第三圈能排12个,以此进行类推。


image.png


这里可以以中心点为坐标原点[0,0],以[1,0], [1,1],[-1,1],[-1,0],[-1,-1],[1,-1]这种形式来表示第二圈在轴线上的六个六边形的中心点关系(这是一种形式,并非真实的坐标系坐标)。


// 第二圈时的对应圆上的坐标值
const pixiArr = [
[1, 0],
[1, 1],
[-1, 1],
[-1, 0],
[-1, -1],
[1, -1]
]

// 根据圈数生成对应轴线上的坐标
function generatePixiArrByWeight(weight) {
if (weight === 2) {
return pixiArr
}
const multiple = weight - 1
const tempPixiArr = pixiArr.map((x) => {
return [x[0] * multiple, x[1] * multiple]
})
return tempPixiArr
}

进一步观察,可知,第三圈时两条发散的轴中间夹了一个六边形,第四圈时两条发散的轴中间夹了两个六边形,依次类推。


六条发散轴上的六边形中心点坐标是最容易计算的,不过要计算三圈及其开外的,就得有那么一点点的数学基础,知道sin60度cos60度的意思。


const sin60 = Math.sin(Math.PI / 3)
const cos60 = Math.cos(Math.PI / 3)

有了上面的铺垫后就可以开始了,定义一个函数,传入的参数为六边形总个数和六边形的边长


// 生成六边形中心坐标
function getHexagonCoordinateArrayByTotal(total = 0, radius = 0){
// 1、获取圈数weight
if (total === 0) return []
let tierList = [] // 用于存放每圈的个数
let tierIndex = 0
while (total > 0) {
if (tierIndex === 0) {
tierList.push(1)
total = total - 1
} else {
let n = 6 * tierIndex
total = total - n
if (total < 0) {
tierList.push(total + n)
} else {
tierList.push(n)
}
}
tierIndex++
}
const weight = tierList.length
// 2、根据圈数去获取coordinateArray坐标列表
// getHexagonCoordinateArrayByWeight:根据圈数和边长返回对应的坐标点
const weight = tierList.length
let coordinateArray = []
for (let i = 0; i < weight; i++) {
if (i + 1 === weight) {
coordinateArray = [
...coordinateArray,
...getHexagonCoordinateArrayByWeight(i + 1, radius).slice(
0,
tierList[weight - 1]
)
]
} else {
coordinateArray = [
...coordinateArray,
...getHexagonCoordinateArrayByWeight(i + 1, radius)
]
}
}

return coordinateArray
}

有个getHexagonCoordinateArrayByWeight需要实现其,方式为


function _abs(val = 0) {
return Math.abs(val)
}

function getHexagonCoordinateArrayByWeight(weight = 1, radius = 0) {
if (weight === 0) return []
if (weight === 1) return [[0, 0]]
const addNum = weight - 2
const addArr = generatePixiArrByWeight(weight)
const hypotenuse = radius * sin60 * 2 // 两倍的边心距长度
let offsetArr = []
let offsetX
let offsetY
for (let i = 0; i < addArr.length; i++) {
const t = addArr[i]
if (t[1] !== 0) {
offsetX = t[0] * hypotenuse * cos60
offsetY = t[1] * hypotenuse * sin60
} else {
offsetX = t[0] * hypotenuse
offsetY = 0
}
offsetArr.push([offsetX, offsetY])
}
const tempOffsetArr = JSON.parse(JSON.stringify(offsetArr))
let resArr = new Array(6 * (weight - 1))
let lineArr = []
for (let i = 0; i < 6; i++) {
let lindex = i * (weight - 1)
resArr[lindex] = tempOffsetArr[i]
lineArr.push(lindex)
}
// 利用已知的六个发散轴上的中心坐标点推出剩余的中心坐标点
if (addNum > 0) {
for (let i = 0; i < 6; i++) {
let s = tempOffsetArr[i]
let e = i + 1 === 6 ? tempOffsetArr[0] : tempOffsetArr[i + 1]
let si = lineArr[i]
let sp = addNum + 1
let fx
let fy
if (i === 0) {
fx = (s[0] - e[0]) / sp
fy = (e[1] - s[1]) / sp
}
if (i === 1) {
fx = (_abs(s[0]) + _abs(e[0])) / sp
fy = 0

}
if (i === 2) {
fx = (_abs(e[0]) - _abs(s[0])) / sp
fy = (_abs(s[1]) - _abs(e[1])) / sp
}
if (i === 3) {
fx = (_abs(s[0]) - _abs(e[0])) / sp
fy = (_abs(e[1]) - _abs(s[1])) / sp
}
if (i === 4) {
fx = (_abs(s[0]) + _abs(e[0])) / sp
fy = 0
}
if (i === 5) {
fx = _abs(s[0]) / sp
fy = (_abs(e[1]) - _abs(s[1])) / sp
}
let mr = []
for (let j = 0; j < addNum; j++) {
if (i === 0 || i === 1) {
mr.push([s[0] - fx * (j + 1), s[1] + fy * (j + 1)])
}
if (i === 2) {
mr.push([s[0] - fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 3) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 4) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 5) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
}
mr.forEach((x, index) => {
resArr[si + index + 1] = x
})
}
}
return resArr
}

至此,生成六边形中心坐标点的方法完成。
有了中心坐标生成方式之后,就可以使用Konva这种辅助绘图的库来进行效果绘制了。


作者:前端_六一
来源:juejin.cn/post/7291125785796018236
收起阅读 »

前端调取摄像头并实现拍照功能

web
前言: 最近接到的一个需求十分有意思,设计整体实现了前端仿微信扫一扫的功能。整理了一下思路,做一个分享。 tips: 如果想要实现完整扫一扫的功能,你需要掌握一些前置知识,这次我们先讲如何实现拍照并且保存的功能。 一. window.navigator 你...
继续阅读 »

前言: 最近接到的一个需求十分有意思,设计整体实现了前端仿微信扫一扫的功能。整理了一下思路,做一个分享。


tips: 如果想要实现完整扫一扫的功能,你需要掌握一些前置知识,这次我们先讲如何实现拍照并且保存的功能。


一. window.navigator




  1. 你想调取手机的摄像头,首先你得先检验当前设备是否有摄像设备,window 身上自带了一个 navigator 属性,这个对象有一个叫做 mediaDevices 的属性是我们即将用到的。




  2. 于是我们就可以先设计一个叫做 checkCamera 的函数,用来在页面刚开始加载的时候执行。。

    image.png




  3. 我们先看一下这个对象有哪些方法,你也许会看到下面的场景,会发现这个属性身上只有一个值为 nullondevicechange 属性,不要怕,真正要用的方法其实在它的原型身上。
    image.png




  4. 让我们点开它的原型属性,注意下面这两个方法,这是我们本章节的主角。

    image.png




  5. 我们到这一步只是需要判断当前设备是否有摄像头,我们先调取 enumerateDevices 函数来查看当前媒体设备是否存在。它的返回值是一个 promise 类型,我们直接用 asyncawait 来简化一下代码。
    image.png
    image.png
    从上图可以看出,我的电脑有两个音频设备和一个视频设备,那么我们就可以放下进行下一步了。




二. 获取摄像头




  1. 接下来就需要用到上面提到的第二个函数,navigator.getUserMedia。这个函数接收一个对象作为参数,这个对象可以预设一些值,来作为我们请求摄像头的一些参数。




  2. 这里我们的重点是 facingMode 这个属性,因为我们扫一扫一般都是后置摄像头
    image.png
    当你执行了这个函数以后,你会看到浏览器有如下提示:

    image.png




  3. 于是你高兴的点击了允许,却发现页面没有任何变化。

    image.png




  4. 这里你需要知道,这个函数只是返回了一个媒体流信息给你,你可以这样简单理解刚刚我们干了什么,首先浏览器向手机申请我想用一下摄像头可以吗?在得到了你本人的确认以后,手机将摄像头的数据线递给了浏览器,:“诺,给你。”




  5. 浏览器现在仅仅拿到了一根数据线,然而浏览器不知道需要将这个摄像头渲染到哪里,它不可能自动帮你接上这根线,你需要自己找地方接上这根数据线。




  6. 这里不卖关子,我们需要请到我们的 Video 标签。我没听错吧?那个播放视频的 video 标签?没错,就是原生的 video 标签。




  7. 这里创建一个 video 标签,然后打上 ref 来获取这个元素。

    image.png




  8. 这里的关键点在于将流数据赋值给 video 标签的 srcObject 属性。就好像你拿到了数据线,插到了显示器上。

    (tips: 这里需要特别注意,不是 video.src 而是 video.srcObject 请务必注意)

    image.png




  9. 现在你应该会看到摄像头已经在屏幕上展示了,这里是我用电脑前置摄像头录制的一段视频做成了gif。(脉动请给我打钱,哼)
    camera.gif




三. 截取当前画面




  1. 这里我随手写了一个按钮当作拍摄键,接下来我们将实现点击这个按钮截取当前画面。

    image.png




  2. 这里你需要知道一个前提,虽然我们现在看到的视频是连贯的,但其实在浏览器渲染的时候,它其实是一帧一帧渲染的。就像宫崎骏有些动漫一样,是一张一张手写画。




  3. 让我们打开 Performance 标签卡,记录一下打开掘金首页的过程,可以看到浏览器的整个渲染过程其实也是一帧一帧拼接到一起,才完成了整个页面的渲染。

    11.gif




  4. 知道了这个前提,那么举一反三,我们就可以明白,虽然我们现在已经打开了摄像头,看到的视频好像是在连贯展示,但其实它也是一帧一帧拼到一起的。那现在我们要做的事情就非常明了,当我按下按钮的时候,想办法将 video 标签当前的画面保存下来。




  5. 这里不是特别容易想到,我就直接说答案了,在这个场景,我们需要用到 canvas 的一些能力。不要害怕,我目前对 canvas 的使用也不是特别熟练,今天也不会用到特别复杂的功能。




  6. 首先创建一个空白的 canvas 元素,元素的宽高设置为和 video 标签一致。

    image.png




  7. 接下来是重点: 我们需要用到 canvasgetContext 方法,先别着急头晕,这里你只需要知道,它接受一个字符串 "2d" 作为参数就行了,它会把这个画布的上下文返回给你。
    tips 如果这里还不清楚上下文的概念,也不用担心,这里你就简单理解为把这个 canvas 这个元素加工了一下,帮你在它身上添加了一些新的方法而已。)
    image.png




  8. 在这个 ctx 对象身上,我们只需要用到一个 drawImage 方法即可,不需要关心其它属性。

    image.png




  9. 感觉参数有点多?没关系,我们再精简一下,我们只需要考虑第二个用法,也就是5参数的写法。(sx,sy 是做裁切用到的,本文用不到,感兴趣可以自行了解。)

    image.png




  10. 这里先简单解释一下 dxdy 是什么意思。在 canvas 里也存在一个看不见的坐标系,起点也是左上角。设想你想在一个 HTMLbody 元素里写一个距离左边距离 100px 距离顶部 100px的画面,是不是得写 margin-left:100px margin-top:100px 这样的代码?没错,这里的 dydx 也是同样的道理。

    image.png




  11. 我们再看 dwidth,和 dheight,从这个名字你就能才出来,肯定和我们将要在画笔里画画的元素的宽度和高度有关,是的,你猜的没错,它就好像你设置一个 div 元素的高度和宽度一样,代表着你将在画布上画的截图的宽高属性。




  12. 现在只剩下第一个参数还没解释,这里直接说答案,我们可以直接将 video 标签填进去,ctx 会自动将当前 video 标签的这一帧画面填写进去。现在按钮的代码应该是这个样子。


    function shoot() {
    if (!videoEl.value || !wrapper.value) return;
    const canvas = document.createElement("canvas");
    canvas.width = videoEl.value.videoWidth;
    canvas.height = videoEl.value.videoHeight;
    //拿到 canvas 上下文对象
    const ctx = canvas.getContext("2d");
    ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
    wrapper.value.appendChild(canvas);//将 canvas 投到页面上
    }



  13. 测试一下效果。

    112.gif




四. 源码


<script lang="ts" setup>
import { ref, onMounted } from "vue";

const wrapper = ref<HTMLDivElement>();
const videoEl = ref<HTMLVideoElement>();

async function checkCamera() {
const navigator = window.navigator.mediaDevices;
const devices = await navigator.enumerateDevices();
if (devices) {
const stream = await navigator.getUserMedia({
audio: false,
video: {
width: 300,
height: 300,
// facingMode: { exact: "environment" }, //强制后置摄像头
facingMode: "user", //前置摄像头
},
});
if (!videoEl.value) return;

videoEl.value.srcObject = stream;
videoEl.value.play();
}
}

function shoot() {
if (!videoEl.value || !wrapper.value) return;
const canvas = document.createElement("canvas");
canvas.width = videoEl.value.videoWidth;
canvas.height = videoEl.value.videoHeight;
//拿到 canvas 上下文对象
const ctx = canvas.getContext("2d");
ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
wrapper.value.appendChild(canvas);
}

onMounted(() => {
checkCamera();
});
</script>
<template>
<div ref="wrapper" class="w-full h-full bg-red flex flex-col items-center">
<video ref="videoEl" />
<div
@click="shoot"
class="w-100px leading-100px text-center bg-black text-30px"
>
拍摄
</div>
</div>
</template>



五. 总结


实现拍照的整体思路其实很简单,仅仅需要了解到视频其实也是一帧一帧画面构成的,而 canvas 恰好有捕捉当前帧的能力。


预告:在下一篇会讲解如何实现扫一扫的功能,需要用到插件,感兴趣的同学可以先预习一下功课。🎁二维码扫码插件


趁热打铁🧭:前端实现微信扫一扫的思路


作者:韩振方
来源:juejin.cn/post/7289662055183597603
收起阅读 »

前端实现微信扫一扫的思路

web
前言: 在有了获取手机摄像头权限并且记录当前画面的前置知识以后,我们现在已经可以进行下一步实现一个仿微信扫一扫的功能了。 tips: 如果你是直接看的本文,对如何打开摄像头拍照这个功能还不太熟悉,请移步 🎁前端如何打开摄像头拍照。这是你阅读本篇的必需...
继续阅读 »

前言: 在有了获取手机摄像头权限并且记录当前画面的前置知识以后,我们现在已经可以进行下一步实现一个仿微信扫一扫的功能了。


tips: 如果你是直接看的本文,对如何打开摄像头拍照这个功能还不太熟悉,请移步 🎁前端如何打开摄像头拍照。这是你阅读本篇的必需前置知识。


一. 效果预览


这里先简单放一下整体界面效果,接下来带大家一步一步分析其中的功能如何实现。


2.gif


本篇将重点讲解多张二维码识别的处理场景。


二. 简单了解二维码




  1. 现在流行使用的二维码是 qrcode,其中 qr 两个字母其实就是 quick response 的缩写,简单来说就是快速响应的意思。三个角用来定位,黑点表示二进制的1,白色点代表0。(这里感兴趣可以自行了解) 它的本质其实就是将一个地址链接利用某种约定好的规则隐藏到一个图片当中,

    image.png




  2. 我们可以利用 chrome 自带的创建当前网站二维码的功能快速体验一下。
    qr.gif




  3. 你可以用手机自带的二维码扫码软件扫一下这个二维码,它将会将你引导到我掘金的个人主页。

    qrcode_juejin.cn.png




  4. 细心的你可能会发现二维码下面已经给你提示了你准备保存的链接地址,现在你观察一下浏览器地址栏是否正对应下面这个地址呢?
    image.png




三. 实现扫码本地图片功能




  1. 我们不需要深入了解这个二维码的转换规则,我们可以直接选用现有的插件即可完成识别功能。 这里我们选用 antfu 大佬的轮子。这里我们不过多介绍,你只需要它可以识别出图片中的二维码即可。如果感兴趣,这是具体仓库地址 qr-sanner-wechat




  2. 首先安装 npm i qr-scanner-wechat




  3. 它的使用方法也十分简单,这个依赖导出了一个方法,我们直接引入这个方法即 import { scan } from 'qr-scanner-wechat




  4. 这个函数可以接收一个 image 元素或者 canvas 元素作为参数,并且返回一个 promise 类型的值。




  5. 我们先来测试最简单的,传入一个 image 元素,利用 input 标签的 type=file 属性,就可以从本地选择图片,比较基础的知识,不过多赘述,代码如下。


    function getImageFromLocal(e: Event) {
    const inputEl = e.target as HTMLInputElement;
    if (!inputEl) return;
    console.log("inputEl.files", inputEl.files);
    }



    然后我们可以通过 input 元素绑定的 onChange 回调中拿到 input 元素身上的 files 属性,就可以获取到刚刚我们选择的文件信息。

    ee.gif




  6. 但是目前这个数据对象我们还无法使用,需要借助 URL.createObjectUrl 方法来创建一个普通的 url 地址。

    image.png




  7. 当拿到这个 url 地址以后该如何使用呢?🤔
    image.png




  8. 一个熟悉的老朋友,有请 img 标签出场,👏,我们只需要将 img 标签的 src 替换成刚刚保存的 url 地址即可。

    image.png

    现在整体效果应该是这样的:

    code.gif




  9. 有了 img 元素,我们直接将这个元素赋值给 qr-scanner-wechat 插件提供的 scan 函数即可。

    image.png




  10. 我们来测试一下整体流程。

    qw.gif




  11. 可以看到,scan 函数返回了一个对象,这个对象身上有两个十分重要的属性。一个叫做 rect (rectangle 长方形的单词缩写),这个属性描述了这个插件在什么位置扫描到了二维码,另外一个属性就是 text,也就是这个图片上隐藏的字符串地址

    image.png




  12. 这里我们再讲解一下 rect 属性,因为后面的功能需要你对这个属性有比较清晰的理解。我们对比一个现实世界的例子。当你掏出手机扫描二维码的时候,往往并不会正好对准一个二维码的图片,或者会遇到一个图片中存在两个二维码的情况,如下图:

    image.png




  13. 这个 qr-scanner 插件会帮你把二维码所在整张图片的相对位置告诉你,因为这个插件每次调用 scan 函数只会返回一次结果。并不是说图片上有两个二维码,它的识别结果就会有两个,所以说这个 qr-scanner 插件的识别效果也并不是百分之一百准确的。




四. 理清思路




  1. 说了这么多,那么这个 rect 我们该如何利用起来呢?别着急,我们先理清思路再动手写代码,到了目前这一步会出现两种情况。

    image.png




  2. 第一种是图片压根就没有二维码,这个简单,提示用户重新放置图片即可。




  3. 关键点就在于第二张情况,当图片上扫码到存在一个二维码后,我们该如何判断是否存在第二个或多个维码呢?

    image.png




  4. 我们看一下微信的实现效果,当只有一张二维码的时候,它会直接跳转,当有多个二维码的时候,它会将整个页面暂停,并且提示用户有两张二维码,请点击选择一个进行跳转。

    image.png




  5. 但是我们上面提到了,scan 函数每次只会扫描一次图片,返回一个识别结果,它并不能准确知道图片上到底有几个二维码。那放到现实生活我们会怎么做呢?




  6. 举个例子,假如我们现在掏出手机扫一扫的功能,现在给你的图片上有两个二维码,但是我明确的知道我就想扫第二个,你会怎么做?

    image.png




  7. 这不是很简单的道理吗?我拿手挡住第一个二维码不就可以了吗?

    image.png




  8. 那么利用同样的思路,我们可以再扫描到一张二维码的时候,想办法把当前识别到的这个二维码位置给遮挡住,然后将被遮挡后的照片传递给 scan 函数再次扫描。




  9. 那么整个过程就是,我们首先将完整的照片传给 scan,然后 scan 觉得第一张二维码比较帅,就先识别了它。(tips: 这里需要提醒一下,scan 有时候会觉得第二张二维码比较帅,那我就识别第二张二维码,要注意的它的顺序性是随机的)

    image.png




  10. 然后我们想办法盖上遮挡物,然后将这个图片传给 scan,让它再次确认是否有第二个二维码。

    image.png




  11. 在哪覆盖?还记不记 rect 属性保留有这个二维码的位置信息?现在的问题就转变为如何覆盖了?




  12. 这里需要用到 canvas 元素的一丢丢基础知识,这是 mdn canvas 基础知识的介绍,十分简单的就画出了一个绿色长方体。

    image.png

    ctx.filleRect可以接收四个参数,分别是相对于画布起始轴的 xy 的距离。

    简单来讲就可以理解为每一个 canvas 就相当于一个独立的 HTML 文件,也有自己的独立坐标系系统,x,y 就相当于 margin,至于后面两个参数,其实就代表着你要画的长方形宽度高度
    image.png




13.那这不巧了吗,scan 的返回值 rect 恰好就有这几个值。

image.png



  1. 话不多说,马上开始实践。⛽️


五. 处理存在多张二维码的图片




  1. 注意: 以下内容我统一选用从本地照片上传作为演示,从摄像头获取图片是同样的道理,详细介绍请移步 🎁前端如何打开摄像头拍照。在下面的讲解过程,我会默认你已经阅读了前置知识。




  2. 这里我就继续沿用之前提到的图片,我将他们拼接到了一张图片上。

    二.png




  3. 下面应该是你目前从本地选择二维码图片识别的代码。


    async function getImageFromLocal(e: Event) {
    const inputEl = e.target as HTMLInputElement;
    if (!inputEl) return;
    if (!inputEl.files?.length) return;
    const image = inputEl.files[0];
    const url = URL.createObjectURL(image);
    src.value = url;
    const result = await scan(imgEl.value!);
    console.log("result", result);
    }



  4. 接下来我们需要先创建一个 canvas 来将当前的照片拷贝到画布上,然后准备利用得到的 rect 信息在这个 canvas 元素上绘画。

    image.png




  5. 为了方便展示,我调用 appendChildren 方法将这个 canvas 元素打印到了界面上。

    1.gif




  6. 然后用 resultrect坐标宽度信息,去调用我们提到的 canvasfillStyle fillRect 方法。

    image.png

    下面是目前实现的效果:

    1.gif




  7. 注意scan 函数不仅仅可以接受 imgElment 作为扫描的参数,它还可以接受 canvas 元素作为扫描的参数。聪明的你看到这里,或许已经猜到我们下一步准备做什么了。




  8. 那么此时我们就可以将这个已经被黑色涂鸦覆盖过的 canvas 进行二次扫描。(暂时不要考虑代码的优雅性,这里只是更清晰的说明我们在干什么,之后我们会封装几个方法,然后整理一下代码)

    image.png

    让我们再看一下效果:

    2.gif




  9. 通过多次重复上面的操作,就可以将图片上所有的二维码都尽量识别出来。

    image.png

    现在实现的效果:

    11.gif

    同时图片上相对应的识别内容也全都被正确的被获取到了。

    image.png




  10. 此时我们创建一个 Map 来保存这些数据。Mapkey 就是 text ,对应的 value 就是 rect 坐标信息。

    image.png

    image.png




六. 弹出可以点击的小蓝块




  1. 有了坐标信息和位置信息,并且我们的 canvasimg 元素的坐标轴系统是一一对应的,那么我们就可以写一个函数来遍历这个 resultMap,然后根据位置信息在 img 元素所在的 div 上打印出我们想要的样式。




  2. 首先在 img 元素外面包一层 div,打上 ref 叫做 imgWrapper 。因为之后我们要用它当作小蓝块的定位元素,所以先设置 position:relative

    image.png




  3. 绘画代码如下,都是基础的方法,不再过多赘述。


    //多个二维码时添加动态小蓝点
    function draw() {
    resultMap.forEach((rect, link) => {
    if (!imgWrapper.value) return;
    const dom = document.createElement("div");
    const { x, y, width, height } = rect;
    const _x = (x || 0) + width / 2 - 20;
    const _y = (y || 0) + height / 2 - 20;
    dom.className = "blue-chunk";
    dom.style.width = "40px";
    dom.style.height = "40px";
    dom.style.background = "#2ec1cc";
    dom.style.position = "absolute";
    dom.style.zIndex = "9999999";
    dom.style.top = _y + "px";
    dom.style.left = _x + "px";
    dom.style.color = "#fff";
    dom.style.textAlign = "center";
    dom.style.borderRadius = "100px";
    dom.style.borderBlockColor = "#fff";
    dom.style.borderColor = "unset";
    dom.style.borderRightStyle = "solid";
    dom.style.borderWidth = "3px";
    dom.style.animation = "scale-animation 2s infinite";
    dom.addEventListener("click", () => {
    console.log(link);
    });
    imgWrapper.value.appendChild(dom);
    });
    }



  4. 然后再 for 循环以后开始绘画小蓝块。

    image.png




  5. 让我们预览一下现在的效果:

    112.gif




  6. 让我们测试一下相对应的点击事件

    3.gif




七. 源码





八.总结


本篇文章的关键点就是讲解了我在实现处理多张二维码的场景时的思路,利用 canvas 遮挡识别过的二维码这个思路是pbk-bin大佬最先想到的,在实现这个需求以后还是很感叹这个思路的巧妙。👏


再次特别感谢pbk-bin🎁~


如果文章对你有帮助,不妨赠人玫瑰,手有余香,预计将会在下篇更新较为完整的微信扫一扫界面和功能。


作者:韩振方
来源:juejin.cn/post/7290813210276724771
收起阅读 »

🤔公司实习生居然还在手动切换node版本?

web
前段时间看了实习生的新项目,发现他启动不了项目,因为node版本太低,我让他去用nvm来管理node的版本,然后看到他切换版本的时候是这样的,先用nvm下载安装目标的node版本,然后在把安装好的node版本替换掉原先的node路径下的node_modules...
继续阅读 »

前段时间看了实习生的新项目,发现他启动不了项目,因为node版本太低,我让他去用nvm来管理node的版本,然后看到他切换版本的时候是这样的,先用nvm下载安装目标的node版本,然后在把安装好的node版本替换掉原先的node路径下的node_modules,而不是用命令行进行版本切换,才发现原来他使用nvm来切换node版本虽然显示切换成功,但全局的node版本一直是不变的,所以才用文件覆盖的方式强制进行解决,对于这个问题进行解决并且梳理



可以直接跳到第四步查看解决方案


1️⃣ 安装nvm


where nvm

找不到nvm路径的朋友可以用这个命令来找到nvm的安装路径,默认的安装路径都是差不多的


image.png


2️⃣ 查看目前node版本


可以看到目前的版本是node V16.14.2


image.png


3️⃣ nvm安装目标node版本



nvm的主要作用就是用来切换node的版本,为什么要切换node的版本,就是因为你的当前node版本和你要启动的项目不兼容,有两种可能,要么是你的项目太旧,你的node版本相对来说比较高,需要用到向下兼容;另外一种可能就是你项目用到的node比较新,你需要进行升级



先安装需要安装的目标版本,用isntall来安装你需要的对应node版本


image.png


回到你的nvm安装路径,就可以看到你已经安装的各种版本的node文件夹
image.png


当然也可以用命令行


nvm list

image.png


4️⃣ nvm切换到目标node版本


切换到目标node版本使用nvm use


nvm use

查看目前nvm安装了哪些版本 然后use来进行切换


image.png


到切换的时候发现了问题,这里无论怎么切换,node的版本依然不会变


image.png


可以看到我用的use来切换到15的版本,但是再次查看nvm的node历史版本,可以看到还是位于16.14.2的node版本,明明就是这么顺利的问题,出了一个让人摸不到头脑的事情


5️⃣寻找问题


既然nvm切换版本已经成功,那么为什么node版本不会变,有没有可能根本改的不是同一个node,或者是存在两个node,直到我打开环境变量一看,为啥会存在两个node的路径,可能的原因就是之前的node版本没有删除,node -v一直输出的是安装前的node


image.png


原来已经安装了一个node的,全局的node指向的node路径和nvm切换node的路径是不一样的


nvm切换的node是基于他文件夹中的nodejs


image.png


image.png



点进去看你会发现他也是有一个node.exe的程序的,所以问题是已经找到的了,目前系统上出现了两个node,并且nvm切换的node版本并不是全局的node,因为环境变量已经指向了旧的node,他的版本不会改变,那么nvm去怎么切换都是没有用的



6️⃣解决方案


image.png


看了网上的一些解决方案都是要在nvm中新建两个文件夹的方式来解决,但是其实直接把nodejs删除也是一个很直接的办法,先通过where node找到当前的node的安装目录,直接进行删除


image.png


最后是通过把另外一个目录的node进行删除,重新看一下node的安装路径,也就是重新执行一下 where node


image.png


可以看到在nvm配置正确的情况下是能直接指向nvm下的node的


最后重新切换一下node的版本,也就是上文的操作


image.png


PS


我指的手动切换是nvm下载node版本之后手动去替换node_modules,原来大家觉得用nvm use也是手动替换(确实是我的问题),经过评论区广大jym指正,可以尝试一下volta这个工具来进行切换版本,真正做到不用手动替换,后续我会亲自去体验一下并且发文,感谢评论区小伙伴


🙏 感谢您花时间阅读这篇文章!如果觉得有趣或有收获,请关注我的更新,给个喜欢和分享。您的支持是我写作的最大动力!


作者:一只大加号
来源:juejin.cn/post/7291096702021304354
收起阅读 »

推荐一款“自学编程”的宝藏网站!详解版~(在线编程练习,项目实战,免费Gpt等)

🌟云端源想学习平台,一站式编程服务网站🌟 云端源想官网传送门 ⭐ 📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯 📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学...
继续阅读 »

🌟云端源想学习平台,一站式编程服务网站🌟


云端源想官网传送门


📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯


📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学习。🌈


🎯章节实战:每一章课程都配有完整的项目实战,帮助初学者巩固所学的理论知识,在实战中运用知识点,不断积累项目经验。🔥


💼项目实战:企业级项目实战和小型项目实战结合,帮助初学者积累实战经验,为就业打下坚实的基础,成为实战型人才。🏆




🧩配套练习:根据课程小节设置配套选择练习或编程练习,帮助学习者边学边练,让学习事半功倍。💪


💻在线编程:支持多种编程语言,创建Web前端、Java后端、PHP等多种项目,开发项目、编程练习、数据存储、虚拟机等都可在线使用,并可免费扩展内存,为你创造更加简洁的编程环境。🖥


🤝协作编程:可邀请站内的好友、大佬快速进入你的项目,协助完成当前项目。与他人一起讨论交流,快速解决问题。👥


📂项目导入导出:可导入自己在在线编辑过的项目再次编辑,完成项目后也可以一键导出项目,降低试错成本。🔗


🤖AI协助编程:AI智能代码协助完成编程项目,随时提问,一键复制代码即可使用。💡



🔧插件工具:在使用在线编程时,可在插件工具广场使用常用的插件,安装后即可使用,帮助你提高编程效率,带来更多便捷。🛠


📞一对一咨询:编程过程中,遇到问题随时提问,网站1V1服务(在职程序员接线,不是客服),实时解决你在项目中遇到的问题。📬


🛠工具广场:提供一些好用的在线智能工具,让你能够快速找到各种实用工具和应用。覆盖了多个领域,包括智能AI问答等。🔍



收起阅读 »

面试官:如何判断两个数组的内容是否相等

web
题目 给定两个数组,判断两数组内容是否相等。 不使用排序 不考虑元素位置 例: [1, 2, 3] 和 [1, 3, 2] // true [1, 2, 3] 和 [1, 2, 4] // false 思考几秒:有了😀😀 1. 直接遍历✍ 直接遍历第...
继续阅读 »

题目


给定两个数组,判断两数组内容是否相等。



  • 不使用排序

  • 不考虑元素位置


例:


[1, 2, 3] 和 [1, 3, 2] // true
[1, 2, 3] 和 [1, 2, 4] // false


思考几秒:有了😀😀


1. 直接遍历✍



  • 直接遍历第一个数组,并判断是否存在于在第二个数组中

  • 求差集, 如果差集数组有长度,也说明两数组不等(个人感觉比上面的麻烦就不举例了)


const arr1 =  ["apple", "banana", 1]
const arr2 = ["apple", 1, "banana"]

function fn(arr1, arr2) {
// Arrary.some: 有一项不满足 返回false
// Arrary.indexOf: 查到返回下标,查不到返回 -1
if (arr1.length !== arr2.length) {
return false;
}
return !arr1.some(item => arr2.indexOf(item)===-1)
}

fn(arr1,arr2) // true


  • 细心的小伙伴就会发现:NaN 会有问题


const arr1 =  ["apple", "banana", NaN]
const arr2 = ["apple", NaN, "banana"]

function fn(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}
return !arr1.some(item => arr2.indexOf(item)===-1)
}

fn(arr1,arr2) // false


Arrary.prototype.indexOf() 是使用的严格相等算法 => NaN值永远不相等


Array.prototype.includes() 是使用的零值相等算法 => NaN值视作相等




  • 严格相等算法: 与 === 运算符使用的算法相同

  • 零值相等不作为 JavaScript API 公开, -0和0 视作相等,NaN值视作相等,具体参考mdn文档:


image.png



  • 使用includes


const arr1 =  ["apple", "banana", NaN]
const arr2 = ["apple", NaN, "banana"]

function fn(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}
return !arr1.some(item => !arr2.includes(item))
}

fn(arr1,arr2) // true

使用includes 确实可以判断NaN了,如果数组元素有重复呢?


// 重复的元素都是banana
const array1 = ["apple", "banana", "cherry", "banana"];
const array2 = ["banana", "apple", "banana", "cherry"];
// 或者
// 一个重复的元素是banana, 一个是apple
const array1 = ["apple", "banana", "cherry", "banana"];
const array2 = ["banana", "apple", "apple", "cherry"];


由上可知:这种行不通,接下来看看是否能从给数组元素添加标识入手


2. 把重复元素标识编号✍


这个简单:数组 元素重复 转换成val1, val2


function areArraysContentEqual(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}

// 重复数组元素 加1、2、3
const countArr1 = updateArray(arr1)
const countArr2 = updateArray(arr2)

/**
*
* @param {*} arr 数组 元素重复 转换成val1, val2
* @returns
*/

function updateArray(arr) {
const countMap = new Map();
const updatedArr = [];

for (const item of arr) {
if (!countMap.has(item)) {
// 如果元素是第一次出现,直接添加到结果数组
countMap.set(item, 0);
updatedArr.push(item);
} else {
// 如果元素已经出现过,添加带有编号的新元素到结果数组
const count = countMap.get(item) + 1;
countMap.set(item, count);
updatedArr.push(`${item}${count}`);
}
}
return updatedArr;
}
const flag = countArr1.some(item => !countArr2.includes(item))
return !flag
}

const array1 = ["apple", "banana", "cherry", "banana"];
const array2 = ["banana", "apple", "banana", "cherry"];

areArraysContentEqual(array1, array2) // true

// 其实这种存在漏洞的
const array3 = ["apple", "banana", "cherry", "banana", 1, '1', '1' ];
const array4 = ["banana", "apple", "banana", "cherry", '1', 1, 1];
// 应该是false
areArraysContentEqual(array3, array4) // true

因为把判断的 转为了字符串 updatedArr.push(${item}${count}) 所以出问题了


3. 统计元素次数(最终方案)✍


function areArraysContentEqual(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}

// 创建计数对象,用于记录每个元素在数组中的出现次数
const countMap1 = count(arr1)
const countMap2 = count(arr2)

// 统计数组中的元素出现次数
function count(arr = []) {
const resMap = new Map();
for (const item of arr) {
resMap.set(item, (resMap.get(item) || 0) + 1);
}
return resMap
}
// 检查计数对象是否相等
for (const [key, count] of countMap1) {
if (countMap2.get(key) !== count) {
return false;
}
}

return true;
}

const array1 = ["apple", "banana", "cherry", "banana", 1, '1', '11', 11];
const array2 = ["banana", "apple", "banana", "cherry", '1', 1, '11', 11];

areArraysContentEqual(array1, array2) // true


注意事项


这个题需要注意:



  • 先判断长度,长度不等 必然不等

  • 元素可重复

  • 边界情况考虑

    • '1' 和 1 (Object的key是字符串, Map的key没有限制)

    • NaN

    • null undefined




结语:


如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!👻👻👻


因为收藏===会了


如果有不对、更好的方式实现、可以优化的地方欢迎在评论区指出,谢谢👾👾👾


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

JavaScript 基础(一)

web
变量 变量概述 数据类型 运算符 算数运算符 递增递减运算符 比较运算符 逻辑运算符 赋值运算符 运算符优先级 选择结构 流程控制(顺序,分支,循环) 小结 数组 函数 函数基本概念 arguments 函数声明方式 Java...
继续阅读 »

变量


变量概述


image.png


数据类型


image.png
image.png


运算符


算数运算符


image.png


递增递减运算符


image.png


比较运算符


image.png


逻辑运算符


image.png


赋值运算符


image.png


运算符优先级


image.png


选择结构


流程控制(顺序,分支,循环)


image.png


小结


image.png


数组


image.png


函数


函数基本概念


image.png
image.png
image.png


arguments


image.png


函数声明方式


image.png


JavaScript作用域


image.png


预解析


image.png
image.png


sum(1)(2,3)(4,5,6) js的链式调用


function sum(){
let arr = Array.prototype.slice.call(arguments);
fn = function(){
let arr1 = Array.prototype.slice.call(arguements)
return sum.apply(null,arr.concat(arr1))
}
//`reduce()` 方法将数组缩减为单个值。
//`reduce()` 方法为数组的每个值(从左到右)执行提供的函数。
//函数的返回值存储在累加器中(结果/总计)。
//注释:对没有值的数组元素,不执行 `reduce()` 方法。
//注释:`reduce()` 方法不会改变原始数组。
fn.toString = function(){
return arr.reduce((value,n)=>{
return value+n
})
}
return fn
}

//柯里化的高级实现
function curry(func){
return curried(...args){
if(args.length >= func.length){
return func.apply(this,args);
}else{
return function(...args2){
return curried.apply(this,args.concat(args2))
}
}
}
}

面试点


image.png


sum(1)(2)(3)怎么实现


面试的时候,面试官让我讲一个这个怎么实现
当时想到的是嵌套闭包,调用返回值,进行累加


//这个是我在其他文章上看到的代码
function sum(a){
function add(b){
a =a+b
return add;
}
add.toString = function(){
return a;
}
return sum;


查询了一下,涉及到了一个知识点,函数柯里化


//正常的函数
function sum(a,b){
var sum = 0;
sum = a+ b;
console.log(sum);
}

//柯里化函数
function curry(a){
return function(b){
console.log(a+b)
}
}
const sum = curry(1);
// 这个过程分为三步
// step1:
// addCurry(1)
// 返回下面的函数
// ƒ (arg) {
// return judge(1, arg);
// }
// step2:
// addCurry(1)(2)
// 返回下面的函数
// ƒ (arg) {
// return judge(1,2, arg);
// }
// step3:
// addCurry(1)(2)(3)
// 返回并执行下面的函数
// return fn(1,2,3);
// 最终得到结果6

函数柯里化


把接收多个参数的函数变换成接受一个单一参数的函数,并且但会接收余下的采纳数并且返回结果的新函数的技术。eg:它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)


参数复用


用于正则表达式的检验


//记录思路
柯里化检验函数curry
const check = curry();
生成工具化函数,进行验证
const checkphone = check(/'正则表达式'/)
检验电话号码
checkphone('122333232');

延迟计算


返回的函数都不会立即执行,而是等待调用(开发者调用)

动态生成函数


比如说,在dom结点中每每绑定一次事件,都需要对环境进行判断,再去绑定这个事件;将这个过程进行柯里化,在使用前进行一次判断

const addEvent = (function() {
if (window.addEventListener) {
return function(ele) {
return function(type) {
return function(fn) {
return function(capture) {
ele.addEventListener(type, (e) => fn.call(ele, e), capture);
}
}
}
}
} else if (window.attachEvent) {
return function(ele) {
return function(type) {
return function(fn) {
return function(capture) {
ele.addEventListener(type, (e) => fn.call(ele, e), capture);
}
}
}
}
}
})();

// 调用
addEvent(document.getElementById('app'))('click')((e) => {console.log('click function has been call:', e);})(false);

// 分步骤调用会更加清晰
const ele = document.getElementById('app');
// get environment
const environment = addEvent(ele)
// bind event
environment('click')((e) => {console.log(e)})(false);
//来自于[一文搞懂Javascript中的函数柯里化(currying) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/120735088)

柯里化函数后虽然代码比较冗长,但是它的适用性增加了

柯里化封装


eg : sum(1,2,3...)


    //函数柯里化封装(这个封装可以直接复制走使用)
function curry(fn, args) {
var length = fn.length;
var args = args || [];
return function () {
newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {
return curry.call(this, fn, newArgs);
} else {
return fn.apply(this, newArgs);
}
}
}

//需要被柯里化的函数
function multiFn(a, b, c) {
return a * b * c;
}

//multi是柯里化之后的函数
var multi = curry(multiFn);
console.log(multi(2)(3)(4));
console.log(multi(2, 3, 4));
console.log(multi(2)(3, 4));
console.log(multi(2, 3)(4));
//转载自[Javascript高级篇之函数柯里化 - 掘金 (juejin.cn)](https://juejin.cn/post/7111902909796712455)


Function.prototype


四个方法:apply,bind,call,toString


Function.prototype.bind()


定义


bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。(MDN)


const module = {
x: 42,
getX: function () {
return this.x;
},
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// Expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// Expected output: 42


Function.prototype.apply()


function 的实例的apply()方法会以给定的this值和作为数组(或类数组对象)提供arguments调用该函数


const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);
console.log(max);
// Expected output: 7
const min = Math.min.apply(null, numbers);
console.log(min);
// Expected output: 2

语法


apply(thisArg) apply(thisArg, argsArray)


参数


//argsArray --- 用于指定调用func时的参数,或者如果不需要向函数提供参数,则为null或undefined

//thisArg --- 调用func 时提供的this值,如果函数不处于严格对象,则null 和undefined 会被替换成全局对象,原始值会被转换成对象

返回值


使用指定的this值和参数调用函数的结果


Function.prototype.call()


Function 实例的 call() 方法会以给定的 this 值和逐个提供的参数调用该函数


//示例
function Product(name, price) {
this.name = name;
this.price = price;
}

function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}

console.log(new Food('cheese', 5).name);
// Expected output: "cheese"

//语法
call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
call(thisArg, arg1, arg2, /* …, */ argN)


Function.prototype.toString()


Function 实例的 toString() 方法返回一个表示该函数源码的字符串。


function sum(a, b) {
return a + b;
}

console.log(sum.toString());
// Expected output: "function sum(a, b) {
// return a + b;
// }"

console.log(Math.abs.toString());
// Expected output: "function abs() { [native code] }"

放一个JavaScript的链接


转载自:http://www.cnblogs.com/wangdan0915…


作者:有玺鹤呖
来源:juejin.cn/post/7288962917399035956
收起阅读 »

你写的 CSS 太过冗余,以至于我对它下手了!

web
:is() 你是否曾经写过下方这样冗余的CSS选择器: .active a, .active button, .active label { color: steelblue; } 其实上面这段代码可以这样写: .active :is(a, button...
继续阅读 »

:is()


你是否曾经写过下方这样冗余的CSS选择器:


.active a,
.active button,
.active label {
color: steelblue;
}

其实上面这段代码可以这样写:


.active :is(a, button, label) {
color: steelblue;
}

看~是不是简洁了很多!


是的,你可以使用 :is() 对选择器的任何部分进行分组,例如,你可以对如下代码:


.section h2,
.aside h2,
.nav h2 {
color: steelblue;
}

进行转换:


:is(.section, .aside, .nav) h2 {
color: steelblue;
}

但是 :is() 不仅对父选择器和子选择器有用,它也可以选择多个相邻的选择器,比如:


button:is(:focus, :hover, :active) {
color: steelblue;
}

button:is(.active, .pressed) {
color: lightsteelblue;
}

上述代码等价于:


button:focus, button:hover, button:active {
color: steelblue;
}

button.active, button.pressed {
color: lightsteelblue;
}

:where()


:where() 是一个与 :is() 非常相似的伪类,也值得注意。它们看起来非常相似:


:where(.section, .aside, .nav) h2 {
color: steelblue;
}

但区别在于 :where 的权重为 0,而:is() 总是会采用列表中最特高的选择器的权重。例如,你知道下面的 CSS 代码中的按钮是什么颜色吗?


:is(html) button {
color: red;
}

:where(html) button {
color: blue;
}

在上面的例子中,虽然以 :where() 开头的块在以 :is() 开头的块下面,但 :is() 块具有更高的权重


:has()


一个相关但非常不同的伪类是:has():has() 允许选择包含匹配选择器(或选择器集)的子元素的父元素


:has() 的一个示例是不显示下划线的情况下包含图像或视频的链接:


a { text-decoration: underline }

/* 链接有下划线,除非它们包含图像或视频 */
a:has(img, video) {
text-decoration: none;
}

现在,如果默认情况下我们的 a 标记有下划线文本,但其中有图像或视频,则任何匹配的锚元素的下划线将被删除。


你也可以结合 :is() 使用:



:is(a, button):has(img, video) {
text-decoration: none;
}

我们还需要预处理器吗?


现在你可能会说“SCSS可以做到这一点!,你甚至可能更喜欢它的语法:


.active {
button, label, a {
color: steelblue;
}
}

说的没错,这很优雅。但是,CSS 似乎现在已经都能获取到我们曾经需要SCSS(或其他预处理器)才能获得的特性。


CSS 变量也是 CSS 本身的另一个不可思议的补充,它回避了一个问题:就是什么时候或者多久你真的需要预处理程序:


.active :is(a, button, label) {
--color: steelblue;
color: var(--steelblue);
}

这并不是说预处理器没有它们的用例和优点。


但我认为在某个时间点上,它们确实是处理任何重要CSS的强制要求,而现在情况不再如此了。


惊喜


我想说的是,CSS的未来仍然是光明的。CSS 工作组正积极致力于直接向CSS中添加嵌套选择器。他们正在积极地在3种可能的语法之间进行选择:


/* 1 */
article {
font-family: avenir;
& aside {
font-size: 1rem;
}
}

/* 2 */
article {
font-family: avenir;
} {
aside {
font-size: 1rem;
}
}

/* 3 */
@nest article {
& {
font-family: avenir;
}
aside {
font-size: 1rem;
}
}

你最喜欢哪一个?


庆幸的是,第 1 种已经被官方采纳!所以我们可能很快就会看到一个非常像 scss 的嵌套语法。


浏览器支持


目前所有主流浏览器都支持 :is():where() 伪类:


image.png
但是,需要注意,我们在这里提到的 :has() 伪类没有相同级别的支持,所以使用 :has() 时要小心:


image.png


作者:编程轨迹
来源:juejin.cn/post/7212079828480016442
收起阅读 »

Token无感知刷新,说说我对解决方案的理解~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。 大家设想一下,如果有一个超级大的表单页面,用户好不容易填完了,然后点击提交,这个时候请求接口居然返回401,然后跳转到登录页。。。那用户心里肯定是一万个草泥马...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。



大家设想一下,如果有一个超级大的表单页面,用户好不容易填完了,然后点击提交,这个时候请求接口居然返回401,然后跳转到登录页。。。那用户心里肯定是一万个草泥马~~~


所以项目里实现token无感知刷新是很有必要的~



这几天在项目中实践了一套token无感知刷新的方案,其实也有看了一下网上那些解决方案,也知道这类的方案已经烂大街了,但是感觉不太符合我想要的效果,主要体现在以下几个方面:



  • 逻辑都写拦截器里,耦合性高,不太好

  • 接口重试的机制做的不太好

  • 接口并发时的逻辑处理做的不太好


我为什么不想要让这套逻辑耦合在拦截器里呢?一方面是因为,我想要写一套代码,在很多的项目里面可以用,把代码侵入性降到最低


另一方面,因为我觉得token无感知刷新涉及到了接口重发,我理解是接口维度的,不应该把这套逻辑放在响应拦截器里去做。。我理解重发之后就是一个独立的新接口请求了,不想让两个独立的接口请求相互有交集~


所以我还是决定自己写一套方案,并分享给大家,希望大家可以提提意见啥的,共同进步~



温馨提示:需要有一些Promise基础



思路


其实大体思路是一样的,只不过实现可能有差别~就是需要有两个 token



  • accessToken:普通 token,时效短

  • refreshToken:刷新 token,时效长


accessToken用来充当接口请求的令牌,当accessToken过期时效的时候,会使用refreshToken去请求后端,重新获取一个有效的accessToken,然后让接口重新发起请求,从而达到用户无感知 token 刷新的效果


具体分为几步:



  • 1、登录时,拿到accessTokenrefreshToken,并存起来

  • 2、请求接口时,带着accessToken去请求

  • 3、如果accessToken过期失效了,后端会返回401

  • 4、401时,前端会使用refreshToken去请求后端再给一个有效的accessToken

  • 5、重新拿到有效的accessToken后,将刚刚的请求重新发起

  • 6、重复1/2/3/4/5



有人会问:那如果refreshToken也过期了呢?


好问题,如果refreshToken也过期了,那就真的过期了,就只能乖乖跳转到登录页了~


Nodejs 模拟 token


为了方便给大家演示,我用 express 模拟了后端的 token 缓存与获取,代码如下图(文末有完整代码)由于这里只是演示作用,所以我设置了



  • accessToken:10秒失效

  • refreshToken:30秒失效



前端模拟请求


先创建一个constants.ts来储存一些常量(文末有完整源码)



接着我们需要对axios进行简单封装,并且模拟:



  • 模拟登录之后获取双 token 并存储

  • 模拟10s后accessToken失效了

  • 模拟30s后refreshToken失效了



理想状态下,用户无感知的话,那么控制台应该会打印


test-1
test-2
test-3
test-4

打印test-1、test-2比较好理解


打印test-3、test-4是因为虽然accessToken失效了,但我用refreshToken去重新获取有效的accessToken,然后重新发起3、4的请求,所以会照常打印test-3、test-4


不会打印test-5、test-6是因为此时refreshToken已经过期了,所以这个时候双token都过期了,任何请求都不会成功了~


但是我们看到现状是,只打印了test-1、test-2




不急,我们接下来就实现token无感知刷新这个功能~


实现


我的期望是封装一个class,这个类提供了以下几个功能:



  • 1、能带着refreshToken去获取新accessToken

  • 2、不跟axios拦截器耦合

  • 3、当获取到新accessToken时,可以重新发起刚刚失败了的请求,无缝衔接,达到无感知的效果

  • 4、当有多个请求并发时,要做好拦截,不要让多次去获取accessToken


针对这几点我做了以下这些事情:



  • 1、类提供一个方法,可以发起请求,带着refreshToken去获取新accessToken

  • 2、提供一个wrapper高阶函数,对每一个请求进行额外处理

  • 3/4、维护一个promise,这个promise只有在请求到新accessToken时才会fulfilled


并且这个类还需要支持配置化,能传入以下参数:



  • baseUrl:基础url

  • url:请求新accessToken的url

  • getRefreshToken:获取refreshToken的函数

  • unauthorizedCode:无权限的状态码,默认 401

  • onSuccess:获取新accessToken成功后的回调

  • onError:获取新accessToken失败后的回调


以下是代码(文末有完整源码)



使用示例如下



最后实现了最终效果,打印出了这四个文本




完整代码


constants.ts


// constants.ts

// localStorage 存储的 key
export const LOCAL_ACCESS_KEY = 'access_token';
export const LOCAL_REFRESH_KEY = 'refresh_token';

// 请求的baseUrl
export const BASE_URL = 'http://localhost:8888';
// 路径
export const LOGIN_URL = '/login';
export const TEST_URL = '/test';
export const FETCH_TOKEN_URL = '/token';


retry.ts


// retry.ts

import { Axios } from 'axios';

export class AxiosRetry {
// 维护一个promise
private fetchNewTokenPromise: Promise<any> | null = null;

// 一些必须的配置
private baseUrl: string;
private url: string;
private getRefreshToken: () => string | null;
private unauthorizedCode: string | number;
private onSuccess: (res: any) => any;
private onError: () => any;

constructor({
baseUrl,
url,
getRefreshToken,
unauthorizedCode = 401,
onSuccess,
onError,
}: {
baseUrl: string;
url: string;
getRefreshToken: () => string | null;
unauthorizedCode?: number | string;
onSuccess: (res: any) => any;
onError: () => any;
}
) {
this.baseUrl = baseUrl;
this.url = url;
this.getRefreshToken = getRefreshToken;
this.unauthorizedCode = unauthorizedCode;
this.onSuccess = onSuccess;
this.onError = onError;
}

requestWrapper<T>(request: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
// 先把请求函数保存下来
const requestFn = request;
return request()
.then(resolve)
.catch(err => {
if (err?.status === this.unauthorizedCode && !(err?.config?.url === this.url)) {
if (!this.fetchNewTokenPromise) {
this.fetchNewTokenPromise = this.fetchNewToken();
}
this.fetchNewTokenPromise
.then(() => {
// 获取token成功后,重新执行请求
requestFn().then(resolve).catch(reject);
})
.finally(() => {
// 置空
this.fetchNewTokenPromise = null;
});
} else {
reject(err);
}
});
});
}

// 获取token的函数
fetchNewToken() {
return new Axios({
baseURL: this.baseUrl,
})
.get(this.url, {
headers: {
Authorization: this.getRefreshToken(),
},
})
.then(this.onSuccess)
.catch(() => {
this.onError();
return Promise.reject();
});
}
}


index.ts


import { Axios } from 'axios';
import {
LOCAL_ACCESS_KEY,
LOCAL_REFRESH_KEY,
BASE_URL,
LOGIN_URL,
TEST_URL,
FETCH_TOKEN_URL,
} from './constants';
import { AxiosRetry } from './retry';

const axios = new Axios({
baseURL: 'http://localhost:8888',
});

axios.interceptors.request.use(config => {
const url = config.url;
if (url !== 'login') {
config.headers.Authorization = localStorage.getItem(LOCAL_ACCESS_KEY);
}
return config;
});

axios.interceptors.response.use(res => {
if (res.status !== 200) {
return Promise.reject(res);
}
return JSON.parse(res.data);
});

const axiosRetry = new AxiosRetry({
baseUrl: BASE_URL,
url: FETCH_TOKEN_URL,
unauthorizedCode: 401,
getRefreshToken: () => localStorage.getItem(LOCAL_REFRESH_KEY),
onSuccess: res => {
const accessToken = JSON.parse(res.data).accessToken;
localStorage.setItem(LOCAL_ACCESS_KEY, accessToken);
},
onError: () => {
console.log('refreshToken 过期了,乖乖去登录页');
},
});

const get = (url, options?) => {
return axiosRetry.requestWrapper(() => axios.get(url, options));
};

const post = (url, options?) => {
return axiosRetry.requestWrapper(() => axios.post(url, options));
};

const login = (): any => {
return post(LOGIN_URL);
};
const test = (): any => {
return get(TEST_URL);
};

// 模拟页面函数
const doing = async () => {
// 模拟登录
const loginRes = await login();
localStorage.setItem(LOCAL_ACCESS_KEY, loginRes.accessToken);
localStorage.setItem(LOCAL_REFRESH_KEY, loginRes.refreshToken);

// 模拟10s内请求
test().then(res => console.log(`${res.name}-1`));
test().then(res => console.log(`${res.name}-2`));

// 模拟10s后请求,accessToken失效
setTimeout(() => {
test().then(res => console.log(`${res.name}-3`));
test().then(res => console.log(`${res.name}-4`));
}, 10000);

// 模拟30s后请求,refreshToken失效
setTimeout(() => {
test().then(res => console.log(`${res.name}-5`));
test().then(res => console.log(`${res.name}-6`));
}, 30000);
};

// 执行函数
doing();


结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡


如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点


image.png


作者:Sunshine_Lin

链接:juejin.cn/post/728169…

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


作者:Sunshine_Lin
来源:juejin.cn/post/7289741611809587263
收起阅读 »

5分钟回顾webpack的前世今生

web
引言 模块化编程是软件设计的一个重要思想。在JavaScript中,处理模块一直是个问题,由于浏览器只能执行JavaScrip、CSS、HTML 代码,所以模块化的前端代码必须进行转换后才能运行。例如 CommonJS 或 AMD,甚至是ECMA 提出的 Ja...
继续阅读 »

引言


模块化编程是软件设计的一个重要思想。在JavaScript中,处理模块一直是个问题,由于浏览器只能执行JavaScrip、CSS、HTML 代码,所以模块化的前端代码必须进行转换后才能运行。例如 CommonJS 或 AMD,甚至是ECMA 提出的 JavaScript 模块化规范——ES6 模块,这些模块系统要么是在浏览器无法运行,要么是无法被浏览器识别和加载,所以针对不同的模块系统,就需要使用专门的工具将源代码转换成浏览器能执行的代码。


整个转化过程被称为构建,构建过程就是“模块捆绑器”或“模块加载器”发挥作用的地方。


Webpack是JavaScript模块捆绑器。在Webpack之前,已经有针对各类型的代码进行编译和构建的流程,例如使用Browserify对CommonJS模块进行编译和打包,然后将打包的资源通过HTML去加载;或者通过gulp进行任务组排来完成整个前端自动化构建。


但是这些方式的缺点是构建环节脱离,编译、打包以及各类资源的任务都分离开。


Webpack模块系统的出现,能将应用程序的所有资源(例如JavaScript、CSS、HTML、图像等)作为模块进行管理,并将它们打包成一个或多个文件并进行优化。Webpack的强大和灵活性使得其能够处理复杂的依赖关系和资源管理,已经成为了构建工具中的首选。


本文主要来扒一扒Webpack的发展进阶史,一起来看看Webpack是如何逐渐从一个简单的模块打包工具,发展成一个全面的前端构建工具和生态系统。


webpack发展历程


webpack从2012年9月发布第一个大版本至2020年10月一共诞生了5个大的版本,我们从下面一张图可以清晰具体地看到每一个版本的主要变化

Webpack发展史.png

Webpack 版本变化方向



  1. Webpack 1:在此之前多是用gulp对各个类型的编译任务进行编排,最后在Html文件中将各种资源引用进来,而Webpack的初始版本横空出世,凭借如下其功能、理念、内核等优点成为众多前端构建工具的最新选择。



  • 理念:一切皆资源,在代码中就能能对Html、Js、Css、图片、文本、JSON等各类资源进行模块化处理。

  • 内核:实现了独有的模块加载机制,引入了模块化打包和代码分割的概念。

  • 功能:集合了编译、打包、代码优化、性能改进等以前各类单一工具的功能,成为前端构建工具标准选择。

  • 特点:通过配置即可完成前端构建任务,同时支持开发者自定义LoaderPlugin对Webpack的生态进行更多的扩展。



  1. Webpack 2: Webpack 2的在第一个版本后足足过了4年,其重点在于满足更多的打包需求以及少量对打包产物的优化



  • 引入对ES6模块的本地支持。

  • 引入import语法,支持按需加载模块。

  • 支持Tree Shaking(无用代码消除)。



  1. Webpack 3:Webpack 3提供了一些优化打包速度的配置,同时对打包体积的优化再次精益求精



  • 引入Scope Hoisting(作用域提升),用于减小打包文件体积。

  • 引入module.noParse选项,用于跳过不需要解析的模块。



  1. Webpack 4:Webpack 4带来了显著的性能提升,同时侧重于用户体验,倡导开箱即用



  • 引入了mode选项,用于配置开发模式或生成模式,减少用户的配置成本,开箱即用

  • 内置Web Workers支持,以提高性能



  1. Webpack 5:Webpack 5继续在构建性能和构建输出上进行了改进,且带来跨应用运行时模块共享的方案



  • 支持WebAssembly模块,使前端能够更高效地执行计算密集型任务。

  • 引入了文件系统持久缓存,提高构建速度

  • 引入Module Federation(模块联邦),允许多个Webpack应用共享模块


webpack打包后的代码分析


为了更方便理解后续章节,我们先看一下Webpack打包后的代码长什么样(为了方便理解,这里以低版本Webpack为例,且不做过多描述)


jsx
复制代码
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};

/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ /* 省略 */
/******/ }

/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;

/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;

/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";

/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {/*省略*/})
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {/*省略*/})
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {/*省略*/})
/******/ ]);

可以看到其实入口文件就是一个IIFE(立即执行函数),在这个IIFE里核心包括两块:



  1. 模块系统:Webpack 在IIFE里实现了模块系统所需要的Module、Require、export等方法组织代码。每个模块都被包装在一个函数内,这个函数形成了一个闭包,模块的作用域在这个闭包内。

  2. 模块闭包IIFE的入参即是Modules,它是一个数组,数组的每一项则是一个模块,每个模块都有自己的作用域。模块和模块之间通过Webpack的模块系统可以进行引用。


webpack的发展长河中,笑到最后和沦为历史

笑到最后:OccurrenceOrderPlugin



有趣的是该插件在Webpack 1叫做OccurenceOrderPluginWebpack 2才更名为OccurrenceOrderPluginWebpack 3则不需要手动配置该插件了。


插件作用:用于优化模块的顺序,以减小输出文件的体积。其原理基于模块的使用频率,将最常用的模块排在前面,以便更好地利用浏览器的缓存机制。


有了前面对于Webpack打包后的代码分析,OcurrenceOrderPlugin的优化效果也就很好理解了。它的原理主要基于两个概念:模块的使用频率模块的ID



  1. 模块的使用频率:OccurrenceOrderPlugin 插件会分析在编译过程中每个模块的出现次数。这个出现次数是指模块在其他模块中被引用的次数。插件会统计模块的出现次数,通常情况下,被引用次数更多的模块将被认为更重要,因此会更早地被加载和执行。

  2. 模块的 ID:Webpack 使用数字作为模块的 ID,OccurrenceOrderPlugin 插件会根据模块的出现次数,为每个模块分配一个优化的 ID。这些 ID 的分配是按照出现次数从高到低的顺序进行的,以便出现次数较多的模块获得较短的 ID,这可以减小生成的 JavaScript 文件的大小。假设一共有100个模块,最高的频率为被引用100次,则减小文件体积200B。(确实好像作用很小,但是作为最贴近用户体验的前端er,不应该是追求精益求精嘛)


这个插件的主要目标是减小 JavaScript 文件的体积,并提高加载性能,因为浏览器通常更倾向于缓存较小的文件。通过将频繁使用的模块分配到较短的 ID,可以减小输出文件的体积,并提高缓存的效率。


笑到最后:Scope Hoisting


过去 Webpack 打包时的一个取舍是将 bundle 中各个模块单独打包成闭包。这些打包函数使你的 JavaScript 在浏览器中处理的更慢。相比之下,一些工具像 Closure Compiler 和 RollupJS 可以提升(hoist)或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。


而Scope Hoisting 就是实现以上的预编译功能,通过静态分析代码,确定哪些模块之间的依赖关系,然后将这些模块合并到一个函数作用域中。这样,多个模块之间的函数调用关系被转化为更紧凑的代码,减少了函数调用的开销。这样不仅减小了代码体积,同时也提升了运行时性能。


Scope Hoisting 的原理是在 Webpack 的编译过程中自动进行的,开发人员无需手动干预。要启用 Scope Hoisting,你可以使用 Webpack 4 版本中引入的 moduleConcatenation 插件。在 Webpack 5 及更高版本中,Scope Hoisting 是默认启用的,不需要额外的配置。


CommonsChunkPlugin的作用和不足,为何会被optimization.splitChunks所取代


CommonsChunkPlugin 插件,是一个可选的用于建立一个独立chunk的功能,这个文件包括多个入口 chunk 的公共模块。主要配置项包含


json
复制代码
{
name: string, // or
names: string[],
filename: string,
minChunks: number|Infinity|function(module, count) => boolean,
chunks: string[],
// 通过 chunk name 去选择 chunks 的来源。chunk 必须是 公共chunk 的子模块。
// 如果被忽略,所有的,所有的 入口chunk (entry chunk) 都会被选择。

children: boolean,
deepChildren: boolean,
}

通过上面的配置项可以看到虽然CommonsChunkPlugin将一些重复的模块传入到一个公共的chunk,以减少重复加载的情况,尤其是将第三方库提取到一个单独的文件中,但是其首要依赖是通过Entry Chunk进行的。在Webpack4以及更高的版本当中被optimization.splitChunks所替代,其提供了配置让webpack根据策略来自动进行拆分,被替代的原因主要有以下几点:



  1. 灵活度不足:在配置上相对固定,只能将指定 Entry Chunk的共享模块提取到一个单独的chunk中,可能无法满足复杂的代码拆分需求。

  2. 配置复杂:需要手动指定要提取的模块和插件的顺序,配置起来相对复杂,开发者需要约定好哪些chunk可以被传入,有较高的心智负担。而optimization.splitChunks只需要配置好策略就能够帮你自动拆分。


因此在Webpack 4这个配置和开箱即用的版本里,它自然也就“香消玉损”。只能遗憾地看到一句:


the CommonsChunkPlugin 已经从 Webpack v4 legato 中移除。想要了解在最新版本中如何处理 chunk,请查看 SplitChunksPlugin


被移除的DedupePlugin


这是 Webpack 1.x 版本中的插件,用于在打包过程中去除重复的模块(deduplication),其原理不知道是通过内容hash,还是依赖调用关系图。但是在Webpack 2中引入了Tree Shaking功能,则不再需要了。原因有以下几点:



  • Tree Shaking控制更精确:能通过静态分析来判断哪些代码是不需要的,实现了更细力度的优化。

  • Scope Hositing减少了重复模块:Webpack 3引入了Scope Hositing,将模块包裹在函数闭包中,进一步减少了重复模块的依赖


因此我们在Webpack的文档中看到:



DedupePlugin has been removed


不再需要 Webpack.optimize.DedupePlugin。请从配置中移除。



总结


或许有些插件你已经看不到它的身影,有些特性早已被webpack内置其中。webpack从第一个版本诞生后一直致力于以下几个方面的提升:



  1. 性能优化:通过去除重复代码、作用域提升、压缩等方式减少代码体积和提高运行时性能。

  2. 构建提效:通过增量编译、缓存机制、并行处理等提升打包速度。

  3. 配置简化:通过内置必要的特性和插件以及简化配置提升易用性。


作者:古茗前端团队
来源:juejin.cn/post/7289718324858355769
收起阅读 »

”调试小技巧,让未来更美好“

web
① 自动打断点(抛异常时自动断点) 偶然一次可能不小心打开某个设置选项,可能设置了英文又不知道是打开了什么,只知道当每次打开F11打开控制台调试看数据的时候,就是不会自动停在某个位置,又不知道怎么停掉,怀疑会不会是安装了什么谷歌插件或者是油猴哪个脚本代码写错写...
继续阅读 »

自动打断点(抛异常时自动断点)


偶然一次可能不小心打开某个设置选项,可能设置了英文又不知道是打开了什么,只知道当每次打开F11打开控制台调试看数据的时候,就是不会自动停在某个位置,又不知道怎么停掉,怀疑会不会是安装了什么谷歌插件或者是油猴哪个脚本代码写错写了什么。


不小心打钩了断点调试的遇到未捕获的异常时暂停,或者在遇到异常时暂停这两个选项其中一个。就有可能导致了谷歌的调试器暂停,取决于这个网站有没有一些异常触发到这一点,勾选上每次异常浏览器会帮我们打断点。


image.png


所以解决办法就是把谷歌浏览器中的这两个勾去掉,如果不是你本意打开想要调试网站中一些异常的报错。


image.png


一键重发请求(不用每次重新请求就刷新页面)


排查接口的时候,需要重新请求该接口,不必要每次重新刷新页面去请求试接口里面传参对不对返回来的数据对不对。重发请求很简单,右击该接口重发xhr即可。


image.png


image.png


③ 断点调试+debugger+console+try...catch


(1) console.log


找bug解决bug是很重要滴。console.log-输出某个变量值是非常非常常用的,只要做码农一定得会各种语言的输出消息和变量的语句,方便我们查看和调试。


(2) debugger(不用每次都console)


在代码某个语句后面或者前面输debugger


在我入行到在学校生涯那段时间都不知道debugger;这玩意,有一次项目有一个比较棘手不知道怎么解决的问题,甲方公司项目负责人开会重点讲了那个问题,就见他这里输一下dubugger,那里输一个debugger,当时就觉得那玩意很神(反正意识上只要我们不懂的东西刚开始接触都是这样,这里神那里神的,接触久了就觉的也就那样不过如此,很平常),最后也没看出什么来。


debugger就是在某个状态下,用这个debugger;语句在那里断一下点,然后当下,上下文的状态和值都可以在查看,哪个分支导致变量状态错误。


使用debugger可以查看:



  • 作用域变量

  • 函数参数

  • 函数调用堆栈

  • 代码整个执行过程(从哪一句到哪一句的)

  • 如果是异步promise async...await 等这种的话就需要在then和catch里面debugger去调试


(3) try...catch 捕获异常


try {
// 可能会抛出异常的代码
} catch {
// 处理所有异常的代码
}

try...catch捕获异常,包括运行时错误和自定义以及语法错误。


try...catch中还可以在某些情况下用throw在代码中主动抛出异常。


try {
// 可能会抛出异常的代码

if (某某情况下) throw '某某错误提示信息'

} catch {
// 处理所有异常的代码
} finally {
// 结束处理用于清理操作
}

image.png


④ 联调查看接口数据


image.png


如上图这个接口,如果想要复制接口preview里面的数据,


除了去Responese里面去找我们需要的某个值去选择复制之外(这个有个缺点就是要找值不直观),还可以右击某个值,然后通过点击store object as global variable(存储为全局变量) 获取。


image.png


当我们点击了之后,控制台就会出现tempXX这个变量。


image.png


我们就只需要在控制台输入copy(temp3)copy(要复制的json名),在粘贴板上就有这个json数据了。



💡
全局方法copy()在console里copy任何你能拿到的数据资源。



image.png


⑤ 后端接口数据返回json


这个有时候有的同学有可能碰到类似这种的JSON数据{\"name\":\"John\",\"address\":\"123 Main St, City\"}


解决方法


直接打开控制台console,输入 JSON.parse("{"name":"John","address":"123 Main St, City"}"),这样


image.png


如果你想复制下来用,直接跟上面我们用copy这好碰上,赋值加上一个copy就可以了。


image.png


这样这个值就在粘贴板上了。


总结


报错和bug,多多少少会贯穿我们的职业生涯中,如何定位问题、解决问题,加以调试,是我们必须也是不能不必备的技能。


当你捕获bug的时候.gif



☎️ 希望对大家有所帮助,如有错误,望不吝赐教,欢迎评论区留言互相学习。



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

谁还没个靠bug才能正常运行的程序😌

web
最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^ 这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。 下面是演示代码和动图 <!DO...
继续阅读 »

最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^


这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。


下面是演示代码和动图


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
width: 300px;
max-height: 300px;
background-color: black;
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%);
overflow-y: auto;
}
.child {
width: 260px;
height: 600px;
margin: 0px 20px;
background-color: pink;
position: relative;
}
.flag {
position: absolute;
width: 100%;
height: 25px;
background-color: blueviolet;
color: aliceblue;
text-align: center;
line-height: 25px;
font-size: 14px;
left: 0;
right: 0;
}
.top {
top: 0;
}
.bottom {
bottom: 0px;
}
</style>
</head>

<body>
<div class="container">
<div class="child">
<div class="flag top">top</div>
<div class="flag bottom">bottom</div>
</div>
</div>
</body>
</html>


20230927105849_rec_.gif




开始计算啦,公式:滚动比例 = 滚动距离 / 可滚动距离


滚动距离: $0.scrollTop


可滚动距离: $0.scrollHeight - $0.offsetHeight


即:scrollRatio = scrollTop / (scrollHeight - offsetHeight)


滚动到底部,计算结果是 300 / (600 - 300) = 1


image.png


我们需要拿scrollRatio某界定值(比如0.1)作大小的比较,计算是true还是false(用isShow = scrollRatio < 某界定值来保存)。


这里一切正常。




不正常的情况出现了


就是没有出现滚动条的情况,即.child的高度没有超过.container的高度时,把.child的高度设成.containermax-height,就没有滚动条了(下面讲的情景也都是没有滚动条的情况)。


image.png


这个时候再去计算,得到了NaN,以至于 NaN < 0.1 = false


image.png


因为isShow的预期就是false,所以一直都没有发现这个bug。




那么它是如何暴露的呢?


后来新的需求给.container加了border。演示一下加border,然后再去计算:


image.png


发现没,这时候$0.offsetHeight的高度把border的高度也算进去了,结果就成了true,这不是想要的结果 ❌。




然后就是一番查验


offsetHeight是一个元素的总高度,包括可见内容的高度、内边距(padding)、滚动条的高度(如果存在)以及边框(border)的高度。


而我们这里只需要可见的高度,就可以用到另一个属性了clientHeight


clientHeight是指元素的可见内容区域的高度,不包括滚动条的高度和边框的高度。它仅包括元素的内部空间,即内容加上内边距。


image.png


当然这也只是继续使除数为0,然后得到结果为NaN,不过bug已经暴露出来了,后面就是一些其他的优化啦~




总结 + 复习(盒模型 box-sizing)


发现没有,offsetHeightclientHeight的区别,就像盒模型中的标准盒模型怪异盒模型的区别:


box-sizing: content-box(默认,标准盒模型):宽度和高度的计算值都 不包含 内容的边框(border)和内边距(padding)。添加padding和border时, 使整个div的宽高变大。


box-sizing: border-box(怪异盒模型):宽度和高度的计算值都 包含 内容的边框(border)和内边距(padding)。添加padding和border时, 不会 使整个div的宽高变大。


这样讲是不是加深一下对这两种属性的印象


^ - ^


作者:aomyh
来源:juejin.cn/post/7283087306603823116
收起阅读 »

一年空窗期后我是如何准备面试的?

web
在此之前我是自由职业者,满打满算一年空窗期,在被动收入不稳定,心想还是需要一份收入来维持日常生活开销,再去考虑打造自己的额外收入。 前前后后从准备到上岸历时一个半月,今天从三个方面分享这个过程我做了什么? 心态 做事情之前,心态很重要,我遇事很少否定自己,在...
继续阅读 »

在此之前我是自由职业者,满打满算一年空窗期,在被动收入不稳定,心想还是需要一份收入来维持日常生活开销,再去考虑打造自己的额外收入。



前前后后从准备到上岸历时一个半月,今天从三个方面分享这个过程我做了什么?


心态


做事情之前,心态很重要,我遇事很少否定自己,在我看来,别人可以做到的,自己也可以,虽然一年空窗,基本上不接触技术,写代码量远不如以前,但又不是要上天或者造原子弹,取决于谁执行力强,谁付出的时间多,仅此而已。


换作以前,相信大部分的同学去找半个月都可以入职自己期望的岗位,看了一下网上的情绪,行情在这个环境下的确蛮消极的,很多人找了几个月都没有上岸的,当然我自己也有感受到,简历丢出去之后没有声音,并且在各大招聘网站上坑位也减少了,相比两三年前如日中天的行情,难免会有这类情绪。


但我没有那么焦虑,为什么呢?其一是我心态比较好,其二是跟我的定位有关。


定位


第一个是我要找的岗位定位为中高级开发,而这类人在市场上来看一直都是稀缺资源,其他行业也如此。


第二个是薪酬范围定位在20k-25k范围,给不到我会觉得工作没劲,累点没关系,主要还是相信自己可以胜任。


第三个是前期投几个低于期望值的试试水,了解一下目前行情顺便找找感觉。


所以,接下来我只需要把目标定位在寻找中高级开发岗位即可,完善自己达到这个能力要求,下面是行动计划,细看下来你会发现这又是个PDCA


计划


我把计划分为这几个模块:


1. 简历优化


我一开始是不会写简历的,因为中间没有跳过槽,也没定时更新,所以就随便拿了以前的模板改了改时间和项目就开始投了,简历回复少不说,即使有机会面试了也没有把简历提到的讲清楚,结果可想而知。


后面想想不行,虽然没写过,但是我会看简历啊,之前带团队有时候一天要看上百份简历,大概知道面试官青睐哪些人才,优化之后断断续续才有面试。


其次是我在面试过程结束时问面试官哪些地方还需要提升的,不少也会反馈简历问题,诸如:



  • 管理工作内容太笼统了,看不出具体做了什么

  • 没有说清楚空窗期做了什么

  • 没有体现出你在项目中做了什么

  • ......


知道自己问题之后,前后迭代了大概十几个版本,越到后面的质量越高,直至我入职之后,还有一个目标企业发来邀请。


2. 技能要求


前端领域涉及到这么多技能,需要有方向进行准备,分享一下我是如何分类:


基础:



  • 前端三大件:HTML、CSS、JS

  • 主流框架:Vue2/Vue3、React

  • 状态管理:Vuex、redux、mobox

  • 构建工具:webpack、Vue-cli、Vite、gulp

  • 前端安全:xss、csrf、cors 常见问题和防御措施


进阶:



  • JS运行机制、事件轮询机制、浏览器运行原理

  • 前端性能监控、前端异常监控如何做?

  • 前端工程化体系包含哪些

  • 前端性能优化手段:页面性能、构建性能、代码性能

  • Vue、React核心原理

  • 基础算法数据结构

  • Http协议


面对上面的技术基础类,主要是刷官方文档+常见面试题,这些更多是概念性的东西,在这里就不多说了,相信大家手上多少都有八股文资料,如果没有可以私信我。


而面对进阶类,首先总结自己项目中用到了哪些,吃透它。其次,面对不太熟悉的板块如HTTP网络,我会通过专栏学习或者一些讲得好的课程来弥补。


除了上面的方法,还有一种我常用的技巧来覆盖知识盲区,就是下面要说的模拟面试,几乎适用于任何技能面试。


3. 模拟面试


这里要说的模拟面试并不是找一些大佬一对一模拟训练,而是换位思考(当然能够模拟面试效果更好啦~)。


即把自己想象成面试官,在考察某一个知识点的时候,你会问自己什么问题呢?


举2个栗子🌰


对于用Vue的同学,我会问:



  • vue diffreact diff有什么区别?

  • 为什么v-for中建议带:key,有什么情况下是可以不带的?

  • 写过组件或者插件吗,有什么注意点?

  • vue-router原理是什么


结合一些热门的话题,我会问:



  • vue2vue3对比,你觉得主要区别是什么?

  • vue2vue3在性能优化做了什么?两者的构建产物有什么区别?

  • 如果你去学vue3,你会从哪里开始,怎么学?


除了以上我给自己虚构的问题之外,还有诸如vue生命周期啊、组件通信啊等等基础肯定是要会的,我会刷文档或虚构题目,这些比较简单,搞懂就行了。


对于设计模式,我也问了自己几个问题:



  • 你知道的设计模式有哪些,知道他们的应用场景吗(解决了什么问题)?

  • 在工作中用到的设计模式有哪些?说说它们的优劣势

  • Vue中用了哪些设计模式?

  • 观察者和发布订阅有什么不同?


基本上这个薪酬范围的设计模式,搞懂了以上问题大差不差。


再来说说这种方式有什么优势?


首先,问题是通过我们自身思考提出并主动寻求解决的,这本身已经存在闭环了,有利于我们理解一个知识点。其次,我们思考提出某个问题,意味着大脑🧠的神经元网络中有存在某些游离神经节点,它没有被连接到一起,随着提出并解决的问题越多,连接起来的网络就越大,这就形成了所谓的知识网络,相比没有目的刷题,它的持久性更强,更能抗遗忘。


总结


结束之前,再分享面试过程中的一个小插曲,当时面了一家小企业,终面的时候面试官问我期望薪酬,就报了18k,但是面试官说给不到,17k考不考虑?我当时没有回绝,就说回去考虑一下。


回去考虑一番之后,我根据当时岗位给到的薪资范围,加上当时家里事情比较多,想先稳定下来再考虑其他的,打算接了这个offer准备上班,突然闹了个乌龙,HR说老板那边重新定了价,只能给到16k,我说还能这么操作?这不明摆着欺负老实人嘛?


想了想如果接了这个offer,岂不是比之前离职时更低,更别说对比以前的同事了。心里忍不下这口气,以至于那两周,每天都撸到一点钟,功夫不负有心人,最后顺利上岸了!


分享几点个人觉得比较关键的:



  • 永远相信自己,心态很重要,不仅仅面试,它贯穿人的一生

  • 简历真实,不玩心思,例如空窗期这种,如实说明

  • 吃透简历内容,不留疑点

  • 面试过程中不着急回答问题,可以先澄清问题动机,不要为了回答而拼凑答案

  • 前面几次不通过没关系,但一次要比一次好


以至于如何备战高级开发,等我升级了再来分享~


最后,祝愿所有航海者都能够顺利靠岸!!!


注:由于最近比较多朋友私信我咨询简历优化建议或者八股文,可以加我的微信followJavaScript,丢简历过来即可,备注“掘金”。


作者:寻找奶酪的mouse
来源:juejin.cn/post/7285915718666944547
收起阅读 »

前端马农:抢不到消费券,我还不会自己做一张吗

web
前言 最近,政府为了刺激消费,发放了大量的消费券,大家应该参与了抢券大军吧。但是如果你是一个前端程序员,你有没有想过,这个消费券样式我能实现吗?今天给大家分享一下,常用的票券的样式实现。 抽象一下 对我们常用的票券进行抽象后,大概就是下面几种样式了,我们...
继续阅读 »

前言



最近,政府为了刺激消费,发放了大量的消费券,大家应该参与了抢券大军吧。但是如果你是一个前端程序员,你有没有想过,这个消费券样式我能实现吗?今天给大家分享一下,常用的票券的样式实现。



image.png


抽象一下


对我们常用的票券进行抽象后,大概就是下面几种样式了,我们来看看怎么实现吧


image.png


实现方案



对于内凹圆角或者镂空的这类样式,我们一般实现方案是使用mask(遮罩);mask语法很简单,我们就当成background来用就好了,可以是PNG图片、SVG图片、也可以是渐变绘制的图片,同时也支持多图片叠加。然后我们了解一下他的遮罩原理:最终效果只显示不透明的部分,透明部分将不可见,半透明类推。



<1>实现一个内凹圆角


image.png


class="content">

.content {    
width: 300px;    
height: 150px;    
margin: auto;    
-webkit-mask: radial-gradient(circle at left center, transparent 20px, red 0);    
background: red; }
ellipse


当前(2016.10.19)mask 处于 候选标准阶段(CR),还不是正式标准(REC),webkit/blink 内核加前缀 -webkit- 可使用



<2>实现两个内凹圆角之遮罩合成


image.png



  .content{           


    width:300px;           


    height:150px;           


    background:red ;           


   -webkit-mask:radial-gradient(circle at left center,transparent 20px,red 20px)  ,     radial-gradient(circle at right center,transparent 20px,red 20px) ;           


    }


上面的写法是没有效果的,此时使用为两个重合后,整个区域都是不透明的,导致没有效果,这个时候我们需要使用遮罩合成;我们通过ps了解一下遮罩合成




遮罩合成mask-composite


-webkit-mask-composite: destination-in; /只显示重合的地方/



image.png


<3>实现两个内凹圆角之平铺


image.png



 .content{           


         width:300px;           


         height:150px;           


         background:red ;           


          -webkit-mask:radial-gradient(circle at 20px center,transparent 20px,red 20px);           


         -webkit-mask-position: -20px;       


}



<4>实现四个内凹圆角


image.png



 .content{           


       width:300px;           


       height:150px;           


       background:red ;           


       -webkit-mask:radial-gradient(circle at 20px 20px,transparent 20px,red 20px);           


      -webkit-mask-position: -20px -20px;       


}



<5>实现六个内凹圆角


image.png



 .content{           


     width:300px;           


     height:150px;           


     background:red ;           


     -webkit-mask:radial-gradient(circle at 20px 20px,transparent 20px,red 20px);           


     -webkit-mask-position: -20px -20px;           


     -webkit-mask-size:50% 100%;       


}



<6>实现中间一排的镂空


image.png



.content{           


       width:300px;           


       height:150px;           


       background:red;           


        -webkit-mask:           


            radial-gradient(circle at 20px 20px,transparent 20px,red 20px) -20px -20px/50% 100% ,           


            radial-gradient(circle at center 5px,transparent 5px,red 5px) 0px -5px/100% 30px;           


      -webkit-mask-composite: destination-in;        }



<7>实现两边多个内凹圆角


image.png



其实很简单:只需把遮罩的高度,变小,让他们平铺就可以了




 .content{           


       width:300px;           


      height:150px;           


       background:red ;           


      -webkit-mask:radial-gradient(circle at 10px 10px,transparent 10px,red 10px);           


     -webkit-mask-position: -10px 5px;           


     -webkit-mask-size:100% 30px;       


}


作者:我们一起学前端
来源:juejin.cn/post/7155025450043965454
收起阅读 »

注意啦⚠️ 别让正则把你网站搞垮⚠️⚠️⚠️

web
引言 事情起源还得从一个需求讲起, 需求内容如下: 假设有串字符串如下: const str = `Pharmaceuticals progress events. JSON output: { "name": "moyuanjun", "a...
继续阅读 »

引言



事情起源还得从一个需求讲起, 需求内容如下:




  1. 假设有串字符串如下:


const str = `Pharmaceuticals progress events.

JSON output:
{
"name": "moyuanjun",
"age": 28
}`



  1. 现需要从字符串中, 提取到 JSON output: 后面的所有字符串, 后面还需要将其解析为对象(当然这不是本文的重点)



需求本身很简单, 实现起来也容易, 具体方案如下, 那么请问以下实现方法有啥问题呢?



const jsonStr = str.replace(/^(\s|\S|.)*?JSON output:/, '')


由于字符串是 gpt 返回的, 它是不可控的, 这里当字符串为 No, this text is not a transaction event. Therefore, the requested entities cannot be extracted. 时, 通过上文的正则进行匹配时就会导致页面卡住, 这里如果大家好奇的话, 可以尝试将下面代码复制到 浏览器控制台 并执行



'No, this text is not a transaction event. Therefore, the requested entities cannot be extracted.'.replace(/^(\s|\S|.)*?JSON output:/, '')

上面主要问题还是出在正则上, 执行上面正则匹配, 会陷入 回溯 陷阱, 我们可以看下上面正则在 regex101 的测试结果, 从测试结果来看正则的匹配次数是有点夸张的


image.png


下面我们来针对 回溯 问题进行展开....


一、正则引擎


传统正则引擎分为 NFA (非确定性有限状态自动机) 和 DFA(确定性有限状态自动机), 那么, 什么是确定型、非确定型、有限状态以及自动机呢?


确定型与非确定型: 假设有一个字符串 abc 需要匹配, 在没有编写正则表达式的前提下, 就能够确定 字符匹配顺序 的就是确定型, 不能确定字符匹配顺序的则为非确定型


有限状态: 所谓有限, 指的是在有限次数内能够得到结果


自动机: 自动机即自动完成, 在我们设置好匹配规则后由引擎自动完成, 不需要人为干预即为自动


根据上面的解释我们可得知 NFA 引擎和 DFA 引擎的主要区别就在于: 在没有编写正则表达式的前提下, 是否能确定字符执行顺序;, 下面我们来简单介绍下这两种引擎:


1.1 NFA 引擎


NFA(Nondeterministic finite automaton)又名 非确定性有限状态自动机, 主要特点如下:




  1. 表达式驱动: 由要执行的正则表达式进行驱动的算法, 正则引擎从正则表达式起始位置开始, 尝试与文本进行匹配, 如果匹配成功, 都前进一步, 否则文本一直前进到下一个字符, 直到匹配成功




  2. 会记录位置: 当正则表达式需要进行选择时, 它会 选择 一个 路径 进行匹配, 同时会 记录 当前的 位置, 如果选择的路径匹配不成功则需要回退回去, 重新选择一个路径进行尝试, 直到匹配完成, 如果所有可能情况全部匹配不成功, 则本次匹配失败




  3. 单个字符可能会检查多次: 从👆🏻可以看出, 字符串中一个字符可能会被多次匹配到, 因为当一条正则路径不通时, 会进行回退




  4. 支持零宽断言: 因为具有回退功能, 所以可以很容易实现零宽、断言、捕获、反向引用等功能





最后借用 猪哥 制作的一个小动画, 方便大家理解:



klx.pro.dbca29a199c308c6b588170ec4b2b475.gif


1.2 DFA


DFA(Deterministic finite automaton) 又名 确定性有限自动机, 主要特点如下:




  1. 文本驱动: 由要搜索的文本驱动的算法, 文本中的每个字符 DFA 引擎只会查看一次, 简单理解就是对字符串进行一次循环, 每次循环都和正则进行一次匹配, 匹配成功字符串和正则指针都相应的向下移动




  2. DFA 引擎会记得所有的匹配可能, 并且每次匹配都会返回其中 最长的匹配, 这么做的目的是为了让后面的匹配能够更加轻松, 正因为如此字符串 nfa not(nfa|nfa not) 中匹配结果为: nfa not




  3. 优点: 优点很明显, 由于只会会循环一直字符串、并且会提前记住所有可能情况, 所以相对来说匹配效率是很高的




  4. 缺点:





  • 它始终将返回最长匹配结果, 无法控制表达式来改变这个规则

  • 因为需要记住所以可能情况, 所以正则表达式预编译时间会更长, 占用更多内存

  • 没有回退, 所有重复的运算符 都是贪婪 的, 会尽可能匹配更多的内容

  • 因为不存在回退, 所以自然不支持零宽断言、捕获、反向引用等功能



最后借用 猪哥 制作的一个小动画, 方便大家理解:



klx.pro.e3c7e13fda2134e2024171f20eac6986.gif



补充说明: 上面只是对传统的两个正则引擎进行简单介绍, 在 JS 中正则引擎使用的则是 NFA 下面我们也只是对 JS 中的正则、以及 回溯 进行简单介绍, 同时在 regex101 中我们选用的语言则是 PHP, 主要是因为在 PHP 用的也是 NFA 引擎并且在 regex101 下会多一个 Regex Debugger 功能(不知道为什么 JS 没有 😭)



image.png


二、回溯


我们知道, NFA 引擎是用表达式去匹配文本, 而表达式又有若干 分支范围, 一个分支或者范围匹配失败并不意味着最终匹配失败, 正则引擎会进行回退去尝试 下一个 分支或者范围, 这种行为就被称之为 回溯


类比于迷宫, 想象一下, 面前有两条路, 我们选择了一条, 走到尽头发现是条死路, 只好原路返回尝试另一条路, 则这个原路返回的过程就被称之为 回溯, 它在正则中的含义是 吐出已经匹配过的文本, 同时 正则匹配位置也会进行回退


一般的, NFA,如果匹配失败, 会尝试进行 回溯, 因为它并不知道后面还有没有可能匹配成功, 他是蒙在鼓里的, 但是 DFA 从一开始就知道所有的可能匹配, 因为在预编译时就它就已经存储了所以可能情况, 所以正则编写的好坏对 NFA 来说是特别的重要的


引擎会真正按照正则表达式进行匹配, 让你选择达到完全匹配所需的每个步骤, 所以我们必须很谨慎地告诉它, 首先检查哪种选择才能达到您的期望, 你也有机会调整正则表达式, 以最大程度地减少回溯并尽早进行匹配


三、量词


3.1 在 JS 中量词表示要匹配的字符或表达式的数量, 常见的量词有:


字符含义
{n}n 是一个正整数, 匹配了前面一个字符刚好出现了 n
{n,}n 是一个正整数, 匹配前一个字符至少出现了 n
{n,m}n 和 m 都是整数。匹配前面的字符至少 n 次,最多 m 次, 如果 n 或者 m 的值是 0, 这个值被忽略
*匹配前一个表达式 0 次或 多次, 等价于 {0,}
+匹配前面一个表达式 1 次或者 多次, 等价于 {1,}
?匹配前面一个表达式 0 次或者 1 次, 等价于 {0,1}

3.2 贪婪 与 非贪婪


模式描述匹配规则
贪婪模式默认使用量词时就是贪婪模式尽可能多 的匹配内容
非贪婪模式量词后加 ?, 如: *?+???{n,m}?尽可能少 的匹配内容

3.3 贪婪模式下的回溯


现在我们看一个简单例子, 有如下正则 .*c 以及待匹配字符串 bbbcaaaaaaaaa, 下面我们使用 regex101 来进行测试


image.png


这里选择 Debugger 查看整个正则匹配流程(重点看 回溯)


klx.pro.91252cceab2190079775942648d23fb9.gif


从图中可以看出, .* 会优先匹配到所有内容, 然后在匹配字符串 c 时, 只要匹配失败, 字符串匹配位置就会进行回退(吐出一个字符), 然后再次进行匹配, 如此反复直到匹配到字符串 c


3.4 解决办法


针对上文回溯问题, 下面我们来简单优化下正则, 来避免 回溯



  1. 使用非贪婪模式: .*?c


klx.pro.399af59bb48824b11a2c939322d56d9f.gif



  1. 使用反向字符集: [^c]*c


klx.pro.1a8e7451e98a3b007f14ab21c8f29b66.gif


3.5 绝对不用「量词嵌套」


特别特别需要注意的是, 嵌套的量词 将会制造指数级的回溯, 下面我们就以 .*c 以及 (.+)*c 为例, 从 regex101 测试结果来看, 相同匹配字符串 .*c 需要 13 个步骤, (.+)*c 则直接飚到 61144 了, 但最终这两个表达式匹配到的结果却是一样的


image.png


image.png


四、多选分支


已知在 JS 中正则可使用 | 定义多个分支, 例如: x|y 可匹配 x 或者 y,


那么正则在匹配过程中如果遇到多选分支时, 引擎则会按照 从左到右 的顺序检查表达式中的多选分支, 如果某个分支匹配失败, 表达式和字符串都会进行回退(回溯), 然后选择另一个分支进行尝试... 这个过程会不断重复, 直到完成全局匹配,或所有的分支都尝试穷尽为止


4.1 回溯现象


假设有正则表达式 num(1234567890|1234567891) 待匹配字符串如下 num1234567891, 下面我们使用 regex101 来进行测试


image.png


这里选择 Debugger 查看整个正则匹配流程(重点看 回溯)


klx.pro.16e2d1cd7aa3d25ab46e801fb4713b05.gif


4.2 优化手段



  1. 提取多选分支中的必须元素: num123456789(0|1)


klx.pro.6ba0ee6c4bca48555392b6bbbfdf3f8e.gif



  1. 高优先级分支提前: num123456789(1|0)



由于正则引擎遇到分支是按照 从左到右 的顺序, 来选择分支进行匹配的, 所以我们可以通过调整分支的顺序来提高匹配效率



klx.pro.f6014ad5c88171023b1abe63284b4dbe.gif



  1. 使用字符组: num123456789[01]



这里我们还可以使用字符组 [], 和 | 不同的是它不存在分支选择问题, 本质上分支越多, 可能的回溯次数越多, 所以如果可以我们需要尽可能减少分支



klx.pro.825be0b2dc258ba51306126b7ec8df94.gif


五、其他正则优化手段



  • 使用非捕获型括号 (): 如果不需要引用括号内的文本, 请使用非捕获括号, 不但能节省捕获的时间, 而且会减少回溯使用的状态的数量, 从两方面提高速度

  • 不要滥用字符组 []: 不使用只包含一个字符的字符组, 需要付出处理字符组的代价

  • 分析待匹配字符串, 将最可能匹配的分支放在前面

  • 正则进行适当拆分: /最明确的规则/.test() && /更细的规则/.test(str)

  • 必要时可以考虑更换正则引擎, 比如使用 DFA

  • 使用检测工具进行测试, 比如: regex101

  • 使用有明显确定的特征的具体字符、字符组代替通配符, 说白了尽可能描述清楚你的正则


六、回到正文


回到我们最开始的那个正则, 可以优化如下


'No, this text is not a transaction event. Therefore, the requested entities cannot be extracted.'.replace(/^[\s\S]*?JSON output:/, '')

regex101 的测试结果如下, 从测试结果来看前后性能提升可不是一点两点


image.png


七、参考:



作者:墨渊君
来源:juejin.cn/post/7243413799347912760
收起阅读 »

一次移动端性能优化实践

web
背景 使用低代码接入了一个移动端老系统,新增页面(以及部分旧页面)使用低代码体系搭建。功能开发完成后才发现性能问题比较严重,所以进行了一次移动端的性能优化。本文就优化过程进行一个记录。 问题分析 为什么一接入低代码体系性能(主要是加载性能)就出现明显的下降,如...
继续阅读 »

背景


使用低代码接入了一个移动端老系统,新增页面(以及部分旧页面)使用低代码体系搭建。功能开发完成后才发现性能问题比较严重,所以进行了一次移动端的性能优化。本文就优化过程进行一个记录。


问题分析


为什么一接入低代码体系性能(主要是加载性能)就出现明显的下降,如果首屏访问的是低代码页面则更加明显



  • 最主要的原因是比之前额外加载了大量的 js 和 css,初步统计有 10 个 css 和 15 个 js

  • 老系统自身 js 资源过大,依赖包 vendor.js 有 8M 多

  • 低代码体系下,非静态资源的接口请求也成为影响页面渲染的因素。页面必须等待接口获取到 schema 后才由低代码渲染器进行渲染


低代码体系接入


有必要简单说明下低代码体系是如何接入的,这对后面的优化是有直接影响的



  • 低代码体系资源大概分为三方依赖、渲染引擎和组件库资源,都是独立的 npm 库,发布单独的 CDN

  • 三方依赖就是像 react、moment、lodash 等最基础的依赖资源

  • 渲染引擎要想渲染页面,又直接依赖于两个资源

    • 页面 schema:服务端接口返回,schema 本质上是一个 json,描述了一个组件树

    • 组件集合:由 CDN 引入的各个组件库集合,它需要先于页面 schema 加载




静态资源为何影响加载性能


静态资源加载如何影响性能,简单分析下,详细的原理可以参考 MDN



  • HTML 自上而下解析,遇到 script 标签(不带 defer 和 async 属性)就会暂停解析,等待 script 加载和执行完毕后才会继续

  • HTML 解析时如果遇到 css 资源,解析会继续进行。但是在 css 资源加载完成前,页面是不会渲染的,并且如果此时有 JavaScript 正在执行,也会被阻塞

  • 所以 js 或 css 体积越大,则在网络传输、下载、浏览器解析和执行上所花的时间就会相应的增加,而这些时间都是会阻塞页面渲染的

  • js 或者 css 的个数对于渲染的影响,很大程度上取决于项目和浏览器是否支持 http2

    • 如果使用了 http2,则静态资源个数对于加载性能影响不大,除非多到几百个资源

    • 如果还是 http1.1,静态资源个数对于加载有明显影响,因为此时浏览器存在并发限制,大概在 4-6 个左右,即一批次只能发送几个请求,等到请求完成后,再发下一批,是个同步的过程

    • 本项目已经支持 http2,所以优化加载性能的重点还是在减小总的资源体积上




优化指标


用户对于页面性能的感受是主观的,而优化工作则需要客观的数据。
更重要的是,有些优化措施是否有效果,有多少效果是需要数据说明的。举例来说,去除冗余资源几乎是可以预见性能提升。但是做 CDN 合并在移动端能够有多少优化效果,事前其实并不清楚
这里采用 2 种方式作为优化指标



  • 旧版本 chrome(69)的 perfomance

    • 使用这个版本是因为后台数据显示该引擎访问量较多

    • chrome 的 performance 不仅能获取性能数据,也有助于我们分析,找出具体问题



  • 使用 web-vitals 库获得具体的性能数据,主要关注

    • FCP,白屏时间

    • LCP,页面可视区域渲染完成时间




现状


image.png



  • 点击 performance 的刷新按钮,就会自动进行一次页面的加载

    • 建议使用无痕模式,排除其他干扰

    • network 中勾选 Disable cache,虽然最终用户会用到缓存,但在优化非缓存项时,建议先禁用缓存,获取最真实的数据



  • 静态资源的加载大概花了 3.5s

  • 而后续静态资源的解析则一直持续到页面加载完成,大概在 9 秒多

  • 使用 web-vitals 测量的平均数据

    • FCP: 5.5s

    • LCP: 9s




目标



  • performance 页面渲染完成:4s 以内

  • web-vitals 平均数据

    • FCP:3s 以内

    • LCP:4s 以内




如果从绝对性能看,这个目标只能是个中下水平。主要基于以下几点考虑



  • 策略上不会对原系统或者低代码体系进行大刀阔斧的改动

  • 老系统大概就是这么个性能情况,维持这个水平起码不会降低用户体验。作为内部系统,对性能没有极致的要求

  • 考虑到时间成本,性能优化是一项持续性的工作,而实际项目是有时间限制和上线压力的


优化措施


根据以上分析,最重要的就是要减小总的关键资源体积。
低代码体系所需要的直接资源都属于关键资源。因为用户是可能首次直接进入一个低代码页面的(也是本次主要的优化场景)


优化前包分析


CDN 三方库资源直接就能看出哪些是冗余的,或者是公共资源加载了多遍等问题,但是自己的仓库打包后就需要借助 webpack-bundle-analyzer 插件分析了
该项目中有多个 npm 仓库需要分析,这里就举老系统自己的例子,优化前的 bundle 分析图


image.png


三方依赖 vendor.min.js 8MB 左右,项目 JS 800 多 KB,下面分析下最严重的几点



  • 标 ① 部分, @ali_4ever 开头的是富文本依赖,有接近 2M 左右的大小,优化为懒加载

  • 标 ② 部分,echarts5 全量引入了,1M 左右大小,计划优化为按需加载

  • 标 ③ 部分,ali-oss,500 多 KB,ali-oss 不支持按需引入。这里因为多个低代码组件库中也用到了该依赖,所以计划提取为 CDN 作为公共依赖,但是大小还是 500 多 KB,只是去掉了重复加载部分

  • 标 ④ 部分,antd-mobile 加载了两个版本的全量仓库,按照官方推荐,考虑将 antd-mobile-v2 按需加载


一、移除冗余资源



  • 排查 CDN,是否引用了多余的 CDN,比如项目中移动端引用了 PC 端的组件库,引用了已经废弃(迁移)的工具库等等

  • 排查项目 bundle,正常情况下是不可能有冗余资源的,因为如果一点没用到这个库,webpack 也不会将其打包进去

    • 可能存在使用到了一小部分,却打包了整个库的情况,这个属于下一部分按需引入



  • 排查下线上 CDN 是否都使用生产版本或者压缩版本,这点事先没有想到,是在优化过程中意外发现存在非压缩版本


二、按需引入


按需引入即只引入三方库中项目用到的部分。现代的大部分三方库都已经支持 TreeShaking,正常打包即是按需引入。特殊情况在于 CDN、懒加载和一些老的库,这些刚好在项目中都有所实践


按需引入 和 CDN


项目中只用到了 ahooks 中的个别方法,却将整个包作为 CDN 引入,显然是不合理的



  • 需要按需引入的库,是不能使用 CDN 引入的,它们之间是互斥的

    • 因为 CDN 需要配置 external 才能在项目里使用,external 一般是将一个三方库作为整体配置的



  • CDN 自身作为一种优化手段,那是和将静态资源放置在业务服务器对比的。

    • 在该场景下,引入 ahooks CDN 导致 TreeShaking 失效,引入了全量包,同时增加了一次 http 请求,总的来看肯定是得不偿失的

    • 并且最终项目的 bundle 也会发布 CDN



  • 因此去掉了 ahooks 的 CDN,改为直接打进项目 bundle 就行了


按需引入 和 懒加载


在该项目中,echarts 也按需引入了,echarts 的按需引入总体效果就没有 ahooks 那么好了



  • echarts 无论绘制哪种类型图表,都需要引入核心库,就有 100 多 KB 的大小了

  • 所以 echarts 也可以选择懒加载,懒加载会让没有使用 echarts 的页面加载速度变快,但是最终浏览器解析的资源是全量的,可以根据实际情况选择

  • 懒加载 和 按需引入也无法并存。因为懒加载需要动态导入,动态导入 webpack 就没法做静态分析,这是 TreeShaking 的基础,所以就没法按需引入了


利用 babel-import-plugin


有一些老版本的库,可能还不支持按需引入,比方说 antd-mobile-v2,对于这种仓库,可以利用 babel-import-plugin 做按需引入
只需要做一下 babel 配置就行


{
"plugins": [
[
"import",
{
"libraryName": "antd-mobile-v2",
"style": "css"
},
"antd-mobile-v2"
]
]
}



  • 本项目最终没有那么做,因为体积几乎没有减小。对于一个完整的项目,需要使用到的组件是非常多的

  • 对于 antd-mobile 多个版本的问题,最终的优化方案还是合并为最新版,只是开发和测试的工作量大了点

  • 注意点:babel-import-plugin 插件并不能让所有仓库都支持按需。本质上还是三方库做了分包才行


三、懒加载


懒加载的资源不同,也可以分为多种类型



  • 三方库资源懒加载:比如之前说的,某个组件依赖于 echarts,那么就可以懒加载 echarts,只有页面中使用了该组件时才去请求和加载 echarts 依赖

  • 组件懒加载:将整个组件都懒加载,在本项目中没有做组件懒加载

    • 低代码体系下,组件本身不能懒加载,否则 schema 解析到这个组件时找不到会报错

    • 解决方案也可以给组件套一层,实际内容懒加载,导出的组件不懒加载

    • 更重要的原因是组件库本身不大,不是影响性能的关键因素

    • 另外低代码页面本身就是由各个组件拼凑而成,如果将组件都懒加载了,那么页面各个部分都会有 Loading 的中间态,效果不好把控



  • 路由懒加载:本质上它就是组件懒加载的一种,一个组件就是一个路由页面,项目中对于系统不太访问的页面做了路由懒加载


三方库资源懒加载


懒加载依赖也需要分析具体情况,比方说移动端使用了 antd-mobile 作为组件库,这个依赖就完全没必要等使用的时候再加载。因为几乎进入任意一个页面,都需要用到这个资源。什么情况下合适



  • 依赖资源比较大

  • 使用的频率较低,只在个别地方使用了


并且这个三方资源也是分两种情况引入,第一种是以 CDN 的形式外部引入,第二种是直接打包入库,这两种引入方式的懒加载处理是不同的,下面分别举例


CDN 引入的三方资源懒加载


比如低代码组件库中存在一个富文本组件,比较特殊,比较适合使用 CDN 的方式懒加载依赖资源



  • 富文本组件依赖于公司内部的一个富文本编辑器。鉴于富文本的复杂性,所以它的依赖很大,JS+css 将近有 3M 左右。

  • 但是其实只有极少的页面使用到了富文本,对于大多数用户来说,是不需要这个富文本的


下面介绍下具体实现,利用 ahooks 的 useExternal,动态注入 js 或 css 资源(也可以原生实现),封装一个高阶组件,方便调用


type LoadStatus = 'loading' | 'ready' | 'error';
interface LoadOptions {
url: string;
libraryName: string;
cssUrl?: string;
LoadingRender?: () => React.ReactNode;
errorRender?: () => React.ReactNode;
}
export const LazyLoad = (Component, { url, libraryName, cssUrl, LoadingRender, errorRender }: LoadOptions) => {
const LazyCom = (props) => {
const initStatus = typeof window[libraryName] === 'undefined' ? 'loading' : 'ready';
const [loadStatus, setStatus] = useState<LoadStatus>(initStatus);
const jsStatus = useExternal(url, {
keepWhenUnused: true,
});
const cssStatus = useExternal(cssUrl, {
keepWhenUnused: true,
});

useEffect(() => {
if (loadStatus === 'ready' || loadStatus === 'error') {
return;
}
if (jsStatus === 'error' || cssStatus === 'error') {
setStatus('error');
}
if (jsStatus === 'ready' && (cssStatus === 'ready' || cssStatus === 'unset')) {
setStatus('ready');
}
}, [jsStatus, cssStatus, loadStatus]);

const content = useMemo(() => {
switch (loadStatus) {
case 'loading':
return typeof LoadingRender === 'function' ? LoadingRender() : <div>加载中...</div>;
case 'ready':
return <Component {...props} />;
case 'error':
return typeof errorRender === 'function' ? errorRender() : <div>加载失败</div>;
default:
return null;
}
}, [loadStatus]);

return content;
};
return LazyCom;
};

// 使用示例,BaseEditor即需要懒加载的原组件,BaseEditor组件内部直接通过window取相应依赖
export const FormEditor = LazyLoad(BaseEditor, {
url: 'xxxx',
cssUrl: 'xxxxx',
libraryName: 'xxxxxx',
});

打包入 bundle 依赖懒加载


总体思路是一样的,只是这类资源利用 webpack 的 import 动态导入能力,import 动态导入的资源打包时会单独分包,只在使用到时才会加载
具体实现:


export const InnerLazyLoad = (Component, loadResource, LoadingRender?) => {
const LazyCom = (props) => {
const [loaded, setLoaded] = useState(false);
const [LazyResource, setResource] = useState({});

useEffect(() => {
if (loaded) {
return;
}
loadResource().then((resource) => {
setResource(resource);
setLoaded(true);
});
}, [loaded]);
const LoadingNode = typeof LoadingRender === 'function' ? LoadingRender() : <div>...加载中</div>;
return loaded ? <Component {...props} LazyResource={LazyResource} /> : LoadingNode;
};
return LazyCom;
};

// 具体使用
const loadResource = async () => {
// 动态导入的资源会单独分包,在使用到时才会加载
const echarts = await import('echarts/core');
const { PieChart } = await import('echarts/charts');
const { TitleComponent } = await import('echarts/components');
const { CanvasRenderer } = await import('echarts/renderers');
return {
echarts,
PieChart,
TitleComponent,
CanvasRenderer,
};
};

const AgentWork = InnerLazyLoad(BaseAgentWork, loadResource);

路由懒加载


路由懒加载原理和内部资源懒加载类似,分包然后首次进入该页面时才请求页面资源
本项目没有把所有页面都懒加载



  • 页面懒加载后,进入页面前会有一个短暂的加载过程,需要评估影响

  • 还是和通用懒加载一样,使用频率较低、页面 js 又比较大的比较适合懒加载


比如在该项目中



  • 应用上存在部分页面是给第三方使用的,不能通过导航点击到达,直接分享地址给第三方

  • 这些页面使用频率低,而且基本不影响本应用,因为无法通过导航点击切换到达,是通过 url 的形式直接访问,所以加载中的中间态和页面加载一起


路由懒加载的实现,不同框架都有些差异。本项目中只需在路由配置中增加配置项即可开启,就不再阐述具体代码实现


四、合并公共资源


合并公共资源,即不要重复加载相同资源
一般来说打包工具都会做依赖分析,只会打包一份相同路径的引用依赖。但是如果相同依赖分散在多个仓库中就有可能出现重复资源了
比如该项目中,老系统自身和多个组件库都使用了 ali-oss 库实现上传功能,并且还有一些条件使得将其提取为公共 CDN 是利益最大化的



  • ali-oss 打包后 500 多 KB 的大小,已经算是一个不小的包了

  • ali-oss 不支持按需引入,所以引用到它的多个仓库,无论引用了什么功能,都将全量打包入 ali-oss

  • 如果 ali-oss 支持按需引入,就需要计算是提取为公共 CDN 划算,还是将其按需打入各个仓库中划算


实现步骤比较简单



  • 在引用 ali-oss 的仓库配置 external,使仓库本身打包时不打入 ali-oss 依赖

  • 在项目 HTML 中提前引入 ali-oss CDN


五、缓存


静态资源缓存



  • 该项目静态资源使用 CDN+版本号,本身已经支持了缓存。CDN 的缓存时间是通过 Cache-Control 的 s-maxage 字段控制,这是 CDN 特有的字段

  • 如果静态资源是放置在自己的服务器上,需要考虑 http 缓存和缓存更新的事项,这个也是老生常谈的话题,这里不再赘述


如果想要详细了解 http 缓存,推荐看下这篇文章


options 请求缓存


在实际优化过程中发现,该项目的大部分 ajax 请求,都是跨域请求,所以伴随着大量的 options 请求
推动服务端做了这些预检请求的缓存,其原理就是通过 access-control-max-age 响应头设置预检请求的缓存时间


Service Worker


Service Worker 是一项很强大的技术,它能够对网络请求进行缓存和处理,它的最大应用场景是在弱网甚至离线环境下
一旦使用了 Service Worker 技术,用户在首次安装完成后,后续的访问相当于直接在本地读取静态资源,访问速度自然能够得到提升
虽然能够提升使用体验,但是使用 Service Worker 是存在一定限制和风险的



  • 必须运行在 https 协议下,调试时允许在 localhost、127.0.0.1

  • Service Worker 自身不能跨越,即主线程上注册的 Service Worker 必须在当前域名下

  • 一旦被安装成功就永远存在,除非线程被程序主动解除

  • Service Worker 的更新是比较复杂的,如果对其了解不深,建议还是只将不常更新的资源使用 Service Worker 缓存,降低风险


项目中直接使用 workbox(对 Service Worker 做了封装,并提供一些插件),以下为示例代码


主线程上注册 Service Worker


if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./sw.js')
.then((reg) => {
navigator.serviceWorker.addEventListener('message', (event) => {
// 处理Worker传递的消息逻辑
});
console.log('注册成功:', reg);
})
.catch((err) => {
console.log('注册成功:', err);
});
}

Service Worker 线程处理缓存逻辑


//首先是异常处理
self.addEventListener('error', function (e) {
self.clients.matchAll().then(function (clients) {
if (clients && clients.length) {
clients[0].postMessage({
type: 'ERROR',
msg: e.message || null,
stack: e.error ? e.error.stack : null,
});
}
});
});

self.addEventListener('unhandledrejection', function (e) {
self.clients.matchAll().then(function (clients) {
if (clients && clients.length) {
clients[0].postMessage({
type: 'REJECTION',
msg: e.reason ? e.reason.message : null,
stack: e.reason ? e.reason.stack : null,
});
}
});
});

//然后引入workbox
importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js');

// 预缓存资源示例,不更新的资源使用预缓存
const resources = ['https://g.alicdn.com/dingding/dingtalk-jsapi/2.10.3/dingtalk.open.js'];

// 预缓存功能
workbox.precaching.precacheAndRoute(resources);

// 图片缓存 使用CacheFirst策略
workbox.routing.registerRoute(
/\.(jpe?g|png)/,
new workbox.strategies.CacheFirst({
cacheName: 'image-runtime-cache',
plugins: [
new workbox.expiration.Plugin({
// 对图片资源缓存 1 天
maxAgeSeconds: 24 * 60 * 60,
// 匹配该策略的图片最多缓存 20 张
maxEntries: 20,
}),
],
})
);

// 需要更新的js和css资源使用staleWhileRevalidate策略
workbox.routing.registerRoute(
new RegExp('https://g.alicdn.com/'),
workbox.strategies.staleWhileRevalidate({
cacheName: 'static-runtime-cache',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 20,
}),
],
})
);


  • 预缓存功能:

    • 正常情况下,Service Worker 是在主程序首次请求时将资源拦截,在之后的请求中根据缓存策略处理

    • 预缓存功能是在 Service Worker 在安装阶段主动发起资源请求,并将其缓存下来

    • 当页面真正发起预缓存当中的资源请求时,资源已经被缓存了,就可以直接使用了

    • 预缓存是使用 Cache Only 策略,即在预缓存主动发起请求并获取缓存后,就只会在缓存中读取资源,不在进行缓存更新,所以适合项目中不更新的静态资源



  • 图片缓存:

    • 图片一般情况下是不更新的,所以采用 Cache First 缓存优先策略

    • 当有缓存时会优先读取缓存,读取成功直接使用本地缓存,不再发起请求

    • 读取失败时再发起网络请求,并将结果更新到缓存中



  • 对于需要更新的 JS 和 CSS

    • 使用 Stale While Revalidate 策略

    • 跟 Cache First 策略比较类似,都是优先返回本地缓存的资源

    • 区别在于 Stale While Revalidate 策略无论在缓存读取是否成功的时候都会发送网络请求更新本地缓存

    • 这是兼顾页面加载速度和缓存更新的策略,相对安全一些




六、其他


以下措施不具备通用性,但是在项目中用到了还是记录下来,仅供参考



  • 页面 schema 接口优化:低代码体系存在页面嵌套,每个页面单独请求自己的 schema,所以在嵌套层级较多的情况下,是以同步解析的顺序请求接口,页面渲染速度较慢,优化为服务端拼装完毕后直接返回

  • 部分接口的请求合并

  • 去除运行时 babel,低代码设计器中存在手写的代码,这部分代码最初在运行时由 babel 转化为 ES5(设计问题),优化为保存时转换


七、项目已经存在的措施



  • 静态资源放在 CDN

  • 启用 http2,并且浏览器支持,这一步很重要,是否使用 http2 对优化措施有直接的影响

  • js 和 css 的代码压缩,并且开启 gzip 压缩

  • 使用字体图标 iconfont 代替图片图标

  • CDN 合并:利用 CDN 的 combo 技术将多个 CDN 合并成一个发送(在 http2 中无明显效果)


最终优化效果



  • performance 表现:页面渲染完成在 3 秒以内


image.png



  • web-vitals 平均数据

    • FCP:2100

    • LCP:2400




参考文章



作者:萌鱼
来源:juejin.cn/post/7288981520946364475
收起阅读 »

我做梦都想不到😵,我被if(x)摆了一道!

web
读本文,可以收获什么? 字数:2494 花费时间:5min if(x)中的x为各种类型的数据或者值的时候会发生什么?x为各种数据类型、表达式、特殊值、位运算、函数等值的时,这些在if语句都充当了什么,实现了什么? 总结== === ≠三种情况特殊值的比较,如...
继续阅读 »


读本文,可以收获什么?


字数:2494 花费时间:5min


if(x)中的x为各种类型的数据或者值的时候会发生什么?x为各种数据类型、表达式、特殊值、位运算、函数等值的时,这些在if语句都充当了什么,实现了什么?


总结== === ≠三种情况特殊值的比较,如下图所示:



image.png


作为一个程序员的我们,相信我们写代码用的最多逻辑应该就是if语句了吧,其实我们真的了解if(x)究竟发生了什么?其实很简单,我们可能都知道中文有这样一个模板:"如果是什么,就会做什么",也就是说符合条件的某件事,才会去做某件事。同样的道理if(x)的意思就是如果符合x条件,我们就可以执行if语句块的代码了。而我们JavaScript中的哪个数据类型是涉及是否意思的?当然是Boolean类型啦,其实if内的x非布尔值都会做一次Boolean类型的转换的


1 x为一个值时


1.1 x为字符串:


x为一个空字符串时,这是一个假值,if语句会转换为false。


if ("") {
console.log("Hello World!");
}
console.log(Boolean(""));// false

x为一个非空字符串是,这是一个真值。if语句会转换为true。


if (!"") {
console.log("Hello World!");// Hello World!
}
console.log(Boolean(!""));// true

x为一个空格字符串,这是一个真值。if语句会转换为true。否则会转换为false


if (" ") {
console.log("Hello World!");// Hello World!
}
console.log(Boolean(" "));// true
if (!" ") {
console.log("Hello World!");
}
console.log(Boolean(!" "));// false

x为一个字符串,这是一个真值。if语句会转换为true。否则会转换为false


if ("JavaScript") {
console.log("Hello World!");// Hello World!
}
console.log(Boolean("JavaScript"));// true

if (!"JavaScript") {
console.log("Hello World!");
}
console.log(Boolean(!"JavaScript"));// false

1.2 x为数字


x为一个数字0时,这是一个假值,if语句会转换为false。x为一个数字!0时,这是一个真值,if语句会转换为true。


if (0) {
console.log("Hello World")
}
console.log(Boolean(0));// fasle

if (!0) {
console.log("Hello World");// Hello World
}
console.log(Boolean(!0));// true

if (1) {
console.log("Hello World") // Hello World
}
console.log(Boolean(1));// true

if (!1) {
console.log("Hello World")
}
console.log(Boolean(!1));// false

if (-0) {
console.log("Hello World")
}
console.log(Boolean(-0));// fasle
if (!-0) {
console.log("Hello World");// Hello World
}
console.log(Boolean(!-0));// true

1.3 x为数组


x为一个空数组,这是一个真值,if语句会转换为true。


if ([]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([]));// true
if (![]) {
console.log("Hello World");
}
console.log(Boolean(![]));// false

x为一个嵌套空数组时,这是一个真值,if语句会转换为true。否则是假值。if语句会转换为false


if ([[]]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([[]]));// true
if (![[]]) {
console.log("Hello World");
}
console.log(Boolean(![[]]));// false

x为一个有空字符串的数组时,这是一个真值,if语句会转换为true。否则是假值。if语句会转换为false。


if ([""]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([""]));// true
if (![""]) {
console.log("Hello World");
}
console.log(Boolean(![""]));// false

x为一个有数字0的数组时,这是一个真值,if语句会转换为true。否则是假值。if语句会转换为false。


if ([0]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([0]));// true
if (![0]) {
console.log("Hello World");
}
console.log(Boolean(![0]));// false

1.4 x为对象:


if ({}) {
console.log("Hello World") // Hello World
}
console.log(Boolean({}));// true

2 x为特殊值时


if (null) {
console.log("Hello World");
}
console.log(Boolean(null));// false

if (undefined) {
console.log("Hello World");
}
console.log(Boolean(undefined));// false

if (NaN) {
console.log("Hello World");
}
console.log(Boolean(NaN));// false

3 x为位运算时


if (true | false) {
// 按位或,只要有一个成立就为true
console.log("Hello World");
}
console.log(Boolean(true | false));// true

4 x为表达式时


比较的相方首先调用ToPrimitive(内部函数,不能自行调用)转换为原始值,如果出现非字符串,就根据ToNumber规则将双方强制转换为数字来进行比较。


const a = [42];
const b = ["43"];
console.log(a < b);// true

5 x为等式时


5.1 一个等号(=)


=: 一个等号代表的是赋值,即使x的值为a=2,也就是说变量的声明操作放在if判断位置上了,其实它还是一个变量并不是一个操作。


let a;
if (a = 2) {
console.log("条件成立!");// 条件成立!
}
console.log(a);// 2

let a;
if (a = 2) {
console.log("条件成立!");// 条件成立!
}
console.log(typeof (a = 2));// number
console.log(Boolean(a = 2));// true

let a;
if (a = 2 && (a = 3)) {
console.log("条件成立!");// 条件成立!
}

console.log(typeof (a = 2 && (a = 3)));// number;

5.2 两个等号(==)


==:宽松相等,我们可能都会这样想,==检查值是否相等,听起来蛮有道理,但不准确,真正的含义是==允许相等比较重进行强制类型转换



对于==符号尽量遵守两个原则:


如果两边的值中有true或者false,千万不要使用==


如果两边的值中有[]、""、0,尽量不要使用==





  • 两个值类型相同,则执行严格相等变量。


    🍟 都是字符串类型:


    const a = "";
    const b = "12";
    console.log(a == b);// false

    🍟 都是NaN类型:全称为not a number,理解为不是一个数值。JavaScript的规定, NaN表示的是非数字, 那么这个非数字可以是不同的数字,因此 NaN 不等于 NaN。


    const a = NaN;
    const b = NaN;
    console.log(a == b);// false

    🍟 都是Symbol类型:Symbol命名的属性都是独 一无二的,可以唯一标识变量值,不受是否相同变量值。


    const a = Symbol("1");
    const b = Symbol("1");
    console.log(a == b);// false

    🍟 都是对象类型。对象的比较是内存地址,因为对象是存储在堆中,当堆中有对象时,它会相对应内存中有一个存储的地址,在栈中其存储了其在堆中数据的地址,当调用数据时,去堆中调取对应堆中的数据的地址获取出来。也就是相同对象比较的是内存地址,变量不一样存储位置不一样。


    const a = { a: 1 };
    const b = {};
    console.log(a == b);// false

    const a = {};
    const b = {};
    console.log(a == b);// false
    console.log(Boolean(a));// true



  • 两个值类型不相同。


    🍟 一个值是null,一个是undefind。


    const a = undefined;
    const b = null;
    console.log(a == b);// true

    🍟 一个值是数字,一个值是字符串。字符串强制转换为数字在比较。


    const a = 12;
    const b = "12";
    console.log(a == b);// true

    🍟 一个值是布尔值,一个是其他类型的值。这种做法是不安全,不建议去使用,在开发中尽量不要这样使用。


    console.log("0" == false);// true
    console.log("" == false);// true
    console.log(0 == false);// true
    console.log([] == false);// true

    🍟 一个值是对象,一个值是字符串或数字。对象与非对象的比较,对象会被强制转换原始值(通过内部函数 ToPrimitive自动执行,这个是内部函数不能直接调用)再比较。


    const a = {};
    const b = "";
    console.log(a == b);// false



5.3 三个等号(===)


===: 严格相等,我们可能都会这样想,===检查值和类型是否相等,听起来蛮有道理,但不准确,真正的含义是===不允许相等比较重进行强制类型转换,也就是不做任何处理变量是什么就是什么。


const a = 0;
const b = "0";
console.log(a === b);// false

6 x为&&、||操作时


||和&&首先会对第一个操作数(a和c)执行条件判断,如果其不是布尔值(如上例)就先进行ToBoolean强制类型转换,然后再执行条件判断。


🍟 对于||来说,如果条件判断结果为true就返回第一个操作数(a和c)的值,如果为false就返回第二个操作数(b)的值。


const a = 12;
const b = "abc";
const c = null;
if (a || b) {
console.log("a||b");// a||b
}
console.log(typeof (a || b));// number
console.log(Boolean(a || b));// true
console.log(a || b);// 12

const b = "abc";
const c = null;
if (c || b) {
console.log("c||b");// c||b
}
console.log(typeof (c || b));// string
console.log(Boolean(c || b));// true
console.log(c || b);// abc

🍟 &&则相反,如果条件判断结果为true就返回第二个操作数(b)的值如果为false就返回第一个操作数(a和c)的值。


const a = 12;
const b = "abc";
if (a && b) {
console.log("a&&b");// a&&b
}
console.log(typeof (a && b));// string
console.log(Boolean(a && b));// true
console.log(a && b);// abc

const b = "abc";
const c = null;
if (c && b) {
console.log("c&&b");
}
console.log(typeof (c && b));// object
console.log(Boolean(c && b));// false
console.log(c && b);// null

7 x为函数判断时




  • typeof与instanceof的区别


    🍟 typeof:返回值是一个字符串,用来说明变量的数据类型。一般只能返回如下几个结果:number、string、function、object、undefined,对于Array、Null等特殊对象typeof一律返回object,这正是typeof的局限性。


    console.log(typeof undefined == 'undefined');// true
    console.log(typeof null);// object

    🍟instanceof:返回值为布尔值,用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。用于判断一个变量是否某个对象的实例。,注意地,instanceof只能用来判断对象和函数,不能用来判断字符串和数字等


    const arr = new Array()
    if (arr instanceof Array) {
    console.log("arr instanceof Array");// arr instanceof Array
    }
    if (arr instanceof Object) {
    // 因为Array是Object的子类
    console.log("arr instanceof Object");// arr instanceof Array
    }
    console.log(typeof (arr instanceof Array));// boolean

    🍟 typeofinstanceof都有一定的弊端,并不能满足所有场景的需求。如果需要通用检测数据类型,可以使用Object.prototype.toString.call()方法:


    Object.prototype.toString.call({});// "[object Object]"
    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call(666); // "[object Number]"
    Object.prototype.toString.call("xxx"); // "[object String]"



注意,该方法返回的是一个格式为"[object Object]"的字符串。




  • indexof与includes区别


    🍟 indexof:返回的是所含元素的下标,注意地,此函数是无法判断是否有NaN元素


    const str = "130212";
    if (str.indexOf("0")) {
    console.log("str中存在0!")
    }
    console.log(str.indexOf("0"));// 2

    🍟 includes:返回的是布尔值,代表是否存在此元素。


    const str = "130212";
    if (str.includes("0")) {
    console.log("str中存在0!")
    }
    console.log(str.includes("0"));// true



作者:路灯下的光
来源:juejin.cn/post/7154647954840616996
收起阅读 »

蒙提霍尔问题

web
最近看韩国电视剧【D.P:逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题 意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余...
继续阅读 »

f1e232d158d085038667d793dad96dc5.jpeg


最近看韩国电视剧【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
来源:juejin.cn/post/7278684023757553727
收起阅读 »

js数组方法分类

web
js数组方法分类 0.前言 我们知道,js中数组方法非常多,MDN就列举了43个方法,就连常用方法都很多,比如forEach,filter,map,push等等等,可能我们见到方法认识这个方法,但要我们列举所知道的数组方法,我们可能会遗忘漏掉某些,为了帮助大家...
继续阅读 »

js数组方法分类


0.前言


我们知道,js中数组方法非常多,MDN就列举了43个方法,就连常用方法都很多,比如forEach,filter,map,push等等等,可能我们见到方法认识这个方法,但要我们列举所知道的数组方法,我们可能会遗忘漏掉某些,为了帮助大家更好更有规律地记住更多方法,在这里我特地将数组方法分俄为七大类,每一类都有其特定共同点和功能的标签,根据这些标签去记忆,相信大家读完可以感到醍醐灌顶的感觉。


一共2+4+9+7+6+3+2=33个,放心吧,足够啦!


1.创建数组方法



  • Array.from() :将可迭代对象或类数组对象转化为新的浅拷贝数组.

  • Array.of():将可变数量的参数转化为新的浅拷贝 数组.


//Array.from()
console.log(Array.from("foo")); // ['f', 'o', 'o']
function bar() {
 console.log(arguments); //Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] 类数组
 console.log(Array.from(arguments)); // [1, 2, 3]
}
bar(1, 2, 3);
const set = new Set(["foo", "bar", "baz", "foo"]);
console.log(Array.from(set)); //从Set构建数组['foo', 'bar', 'baz'],Map也可以

//Array.of()
console.log(Array.of()); //[] 创建空数组
console.log(Array.of(1, 2, 3, 4)); //[1, 2, 3, 4]
//浅拷贝
const obj1 = { age: 18 };
const arr1 = [666, 777];
const arr = Array.of(obj1, arr1);
arr[0].age = 19;
arr[1][0] = 999;
console.log(arr); //[{age:19},[999,777]]


2.数组首端或尾端添加删除方法



  • Array.prototype.push():将指定的元素添加到数组的末尾,并返回新的数组长度.

  • Array.prototype.pop():从数组中删除最后一个元素,并返回该元素的值。此方法会更改数组的长度.

  • Array.prototype.shift():从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度.

  • Array.prototype.unshift():将指定的元素添加到数组的开头,并返回新的数组长度.


//Array.prototype.push()
const arr = [1, 2];
console.log(arr.push(3, 4, 5)); //5
console.log(arr); //[ 1, 2, 3, 4, 5 ]
//Array.prototype.pop()
console.log(arr.pop()); //数组最后一个元素:5
console.log(arr); //[ 1, 2, 3, 4 ]
//Array.prototype.shift()
console.log(arr.shift()); //1
console.log(arr); //[ 2, 3, 4 ]
//Array.prototype.unshift()
console.log(arr.unshift(66, 77, 88)); //6
console.log(arr); //[ 66, 77, 88, 2, 3, 4 ]

3.操作数组方法



  1. Array.prototype.concat():用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组.

  2. Array.prototype.copyWithin():浅复制数组的一部分到同一数组中的另一个位置,并返回该数组,不会改变原数组的长度.

  3. Array.prototype.fill():用一个固定值填充一个数组中从起始索引(默认为 0)到终止索引(默认为 array.length)内的全部元素。它返回修改后的数组。会改变原始数组.


// Array.prototype.concat()
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [7, 8, 9];
const arr4 = arr1.concat(arr2, arr3); //[1, 2, 3, 4, 5, 6, 7, 8, 9]
// Array.prototype.copyWithin()
const arr = [1, 2, 3, 4, 5, 6];
console.log(arr.copyWithin(2, 3, 5)); //[ 1, 2, 4, 5, 5, 6 ] 将 4,5替换到2索引位置
// Array.prototype.fill()
const array1 = [1, 2, 3, 4];
console.log(array1.fill(0, 2, 4)); //[ 1, 2, 0, 0 ]
console.log(array1.fill(5, 1)); //[ 1, 5, 5, 5 ]
console.log(array1.fill(6)); //[ 6, 6, 6, 6 ]
console.log(array1); //[ 6, 6, 6, 6 ]


  1. Array.prototype.flat():展开嵌套数组,默认嵌套深度为1,不改变原数组,返回新数组.

  2. Array.prototype.join():用逗号或指定分隔符将数组连接成字符串.

  3. Array.prototype.reverse():就地反转字符串,返回同一数组的引用,原数组改变.


// Array.prototype.flat()
const arr1 = [1, 2, [3, 4]];
console.log(arr1.flat()); //[ 1, 2, 3, 4 ]
console.log(arr1); // 不改变原数组 [ 1, 2, [ 3, 4 ] ]
const arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat()); //默认展开嵌套一层数组[ 1, 2, 3, 4, [ 5, 6 ] ]
console.log(arr2.flat(2)); //展开嵌套二层数组 [ 1, 2, 3, 4, 5, 6 ]
// Array.prototype.join()
const elements = ["Fire", "Air", "Water"];
console.log(elements.join()); //"Fire,Air,Water"
console.log(elements.join("+++++")); //Fire+++++Air+++++Water
console.log(elements.join("-")); //Fire-Air-Water
// Array.prototype.reverse()
const arr = [1, 2, 3];
console.log(arr.reverse()); //[3,2,1]
console.log(arr); //[3,2,1]


  1. Array.prototype.slice():截取数组,返回一个新数组,不改变原数组.

  2. Array.prototype.sort():排序数组,改变原数组,默认排序规则是将数组每一项转化为字符串,根据utf-16码升值排序.

  3. Array.prototype.splice():对数组进行增加、删除、替换元素,改变原数组.


// Array.prototype.slice();
const animals = ["ant", "bison", "camel", "duck", "elephant"];
console.log(animals.slice(2)); //["camel", "duck", "elephant"]
console.log(animals.slice(2, 4)); //["camel", "duck"]
console.log(animals.slice(-2)); //["duck", "elephant"]
console.log(animals.slice(2, -1)); //["camel", "duck"]
console.log(animals.slice()); //浅复制数组 ["ant", "bison", "camel", "duck", "elephant"]
// Array.prototype.sort();
const months = ["March", "Jan", "Feb", "Dec"];
months.sort();
console.log(months); // ["Dec", "Feb", "Jan", "March"];
const array1 = [1, 30, 4, 21, 100000];
array1.sort();
console.log(array1); //[1, 100000, 21, 30, 4]
array1.sort((a, b) => a - b); //升序
console.log(array1);
//Array.prototype.splice();
const arr = [1, 2, 3, 4, 5];
arr.splice(2, 2); //从index为2的位置开始删除两个元素[1, 2, 5];
arr.splice(2, 0, 3, 4); //从index为2的位置增加34两个元素 [1,2,3,4,5]
arr.splice(2, 2, 7, 8); //删除index为2位置的两个元素,并添加89两个元素 [ 1, 2, 7, 8, 5 ]

4.查找元素或索引方法



  1. Array.prototype.at():返回索引位置对应的元素,负索引从数组最后一个元素倒数开始.

  2. Array.prototype.find():查找符合条件的第一个元素,未找到则返回undefined,回调函数返回值为真则符合条件.

  3. Array.prototype.findIndex():查找符合条件第一个元素的索引,未找到则返回**-1**,回调函数返回值为真则符合条件.

  4. Array.prototype.findLast():从后往前查找符合条件的第一个元素,其余同理Array.prototype.find().

  5. Array.prototype.findLastIndex():从后往前查找符合条件第一个元素的索引,其余同理Array.prototype.findIndex().


// Array.prototype.at()
const arr = [1, 2, 3, 4, 5];
console.log(arr.at(0)); //1
console.log(arr.at(-1)); //5
const array = [
{ name: "jack", age: 15 },
{ name: "tom", age: 29 },
{ name: "bob", age: 23 },
];
// Array.prototype.find()
const obj = array.find((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //{ name: 'tom', age: 29 }
//Array.prototype.findIndex()
const objIndex = array.findIndex((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //1
// Array.prototype.findLast()
const lastObj = array.findLast((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //{name: 'bob', age: 23}
// Array.prototype.findLast()
const lastIndex = array.findLastIndex((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //2


  1. Array.prototype.indexOf():返回数组中给定元素第一次出现的下标,如果不存在则返回-1.

  2. Array.prototype.includes():在数组中查找指定元素,如果找到则返回true,如果找不到则返回false.


//Array.prototype.indexOf()
const arr = [1, 2, 6, 8, 9];
console.log(arr.indexOf(6)); //2
console.log(arr.indexOf(10)); //-1
//Array.prototype.includes()
console.log(arr.includes(6)); //true
console.log(arr.includes(10)); //-false

5.迭代方法


迭代方法非常常用,这里就不列举例子了.



  1. Array.prototype.forEach():对数组每一项元素执行给定的函数,没有返回值.

  2. Array.prototype.filter():过滤数组,创建符合条件的浅拷贝数组.

  3. Array.prototype.map():对数组每个元素执行给定函数映射一个新值,返回新数组.

  4. Array.prototype.every():检查数组所有元素是否符合条件,如果符合返回true,不符合返回false;

  5. Array.prototype.some():检查数组中是否有元素符合条件,如果有则返回true,不符合返回false

  6. Array.prototype.reduce():用指定函数迭代数组每一项,上一次函数返回值作为下一次函数初始值,返回最后一次函数的最终返回值.


6. 迭代器方法


这里就不赘述迭代器对象了.



  1. Array.prototype.keys():返回数组索引迭代器对象.

  2. Array.prototype.values():返回数组元素的迭代器对象.

  3. Array.prototype.entries():返回数组索引和元素构成的迭代器对象.


7.额外重要方法



  1. Array.isArray():判断是否是数组.


//都返回true 都是数组
console.log(Array.isArray([]));
console.log(Array.isArray(new Array()));
console.log(Array.isArray(Array.of(1, 2, 3)));
// 也可以用instanceof:true
console.log([] instanceof Array);
console.log(new Array() instanceof Array);
console.log(Array.of(1, 2, 3) instanceof Array);
console.log([].toString());
//惊喜:最后还可以使用Object.prototype.toString()
console.log(Object.prototype.toString.call([])); //[object Array]


  1. Array.prototype.toString():将数组去掉左右括号转化为字符串.


const array1 = [1, 2, "a", "1a"];
console.log(array1.toString()); // "1,2,a,1a"

作者:樊阳子
来源:juejin.cn/post/7288234800563961917
收起阅读 »

前端代码重复度检测

web
在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd。 jscpd简介 jscpd是一款开源的JavaScr...
继续阅读 »

在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd


jscpd简介


jscpd是一款开源的JavaScript的工具库,用于检测代码重复的情况,针对复制粘贴的代码检测很有效果。它可以通过扫描源代码文件,分析其中的代码片段,并比较它们之间的相似性来检测代码的重复度。jscpd支持各种前端框架和语言,包括HTML、CSS和JavaScript等150种的源码文件格式。无论是原生的JavaScript、CSS、HTML代码,还是使用typescriptscssvuereact等代码,都能很好的检测出项目中的重复代码。


开源仓库地址:github.com/kucherenko/jscpd/tree/master


如何使用


使用jscpd进行代码重复度检测非常简单。我们需要安装jscpd。可以通过npmyarn来安装jscpd


npm install -g jscpd

yarn global add jscpd

安装完成后,我们可以在终端运行jscpd命令,指定要检测的代码目录或文件。例如,我们可以输入以下命令来检测当前目录下的所有JavaScript文件:


jscpd .

指定目录检测:


jscpd /path/to/code

在命令行执行成功后的效果如下图所示:



简要说明一下对应图中的字段内容:




  • Clone found (javascript):
    显示找到的重复代码块,这里是javascript文件。并且会显示重复代码在文件中具体的行数,便于查找。




  • Format:文件格式,这里是 javascript,还可以是 scss、markup 等。




  • Files analyzed:已分析的文件数量,统计被检测中的文件数量。




  • Total lines:所有文件的总行数。




  • Total tokens:所有的token数量,一行代码一般包含几个到几十个不等的token数量。




  • Clones found:找到的重复块数量。




  • Duplicated lines:重复的代码行数和占比。




  • Duplicated tokens:重复的token数量和占比。




  • Detection time:检测耗时。




工程配置


以上示例是比较简单直接检测单个文件或文件夹。当下主流的前端项目大多都是基于脚手架生成或包含相关前端工程化的文件,由于很多文件是辅助工具如依赖包、构建脚本、文档、配置文件等,这类文件都不需要检测,需要排除。这种情况下的工程一般使用配置文件的方式,通过选项配置规范 jscpd 的使用。


jscpd 的配置选项可以通过以下两种方式创建,增加的内容都一致无需区分对应的前端框架。


在项目根目录下创建配置文件 .jscpd.json,然后在该文件中增加具体的配置选项:


    {
"threshold": 0,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true
}

也可直接在 package.json 文件中添加jscpd


    {
...
"jscpd": {
"threshold": 0.1,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true,
"gitignore": true
}
...
}

简要介绍一下上述配置字段含义:



  • threshold:表示重复度的阈值,超过这个值,就会输出错误报警。如阈值设为 10,当重复度为18.1%时,会提示以下错误❌,但代码的检测会正常完成。


ERROR: jscpd found too many duplicates (18.1%) over threshold (10%)


  • reporters:表示生成结果检测报告的方式,一般有以下几种:

    • console:控制台打印输出

    • consoleFull:控制台完整打印重复代码块

    • json:输出 json 格式的报告

    • xml:输出 xml 格式的报告

    • csv:输出 csv 格式的报告

    • markdown:输出带有 markdown 格式的报告

    • html:生成html报告到html文件夹

    • verbose:输出大量调试信息到控制台



  • ignore:检测忽略的文件或文件目录,过滤一些非业务代码,如依赖包、文档或静态文件等

  • format:需要进行重复度检测的源代码格式,目前支持150多种,我们常用的如 javascript、typescript、css 等

  • absolute:在检测报告中使用绝对路径


除此之外还有很多其他的配置,有兴趣的可以看源码文档中有详细的介绍。


检测报告


完成以上jscpd配置后执行以下命令即可输出对应的重复检测报告。运行完毕后,jscpd会生成一个报告,展示每个重复代码片段的信息。报告中包含了重复代码的位置、相似性百分比和代码行数等详细信息。通过这些信息,我们可以有针对性的进行代码重构。


jscpd ./src -o 'report'

项目中的业务代码通常会选择放在 ./src 目录下,所以可以直接检测该目录下的文件,如果是放在其他目录下根据实际情况调整即可。
通过命令行参数-o 'report'输出检测报告到项目根目录下的 report 文件夹中,这里的report也可以自定义其他目录名称,输出的目录结构如下所示:



生成的报告页面如下所示:


项目概览数据:



具体重复代码的位置和行数:



默认检测重复代码的行数(5行)和tokens(50)比较小,所以产生的重复代码块可能比较多,在实际使用中可以针对检测范围进行设置,如下设置参数供参考:



  • 最小tokens:--min-tokens,简写 -k

  • 最小行数:--min-lines,简写 -l

  • 最大行数:--max-lines,简写 -x


jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'

为了更便捷的使用此命令,可将这段命令集成到 package.json 中的 scripts 中,后续只需执行 npm run jscpd 即可执行检测。如下所示:


"scripts": {
...
"jscpd": "jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'",
...
}

忽略代码块


上面所提到的ignore可以忽略某个文件或文件夹,还有一种忽略方式是忽略文件中的某一块代码。由于一些重复代码在实际情况中是必要的,可以使用代码注释标识的方式忽略检测,在代码的首尾位置添加注释,jscpd:ignore-startjscpd:ignore-end 包裹代码即可。


在js代码中使用方式:


/* jscpd:ignore-start */
import lodash from 'lodash';
import React from 'react';
import {User} from './models';
import {UserService} from './services';
/* jscpd:ignore-end */

在CSS和各种预处理中与js中的用法一致:


/* jscpd:ignore-start */
.style {
padding: 40px 0;
font-size: 26px;
font-weight: 400;
color: #464646;
line-height: 26px;
}
/* jscpd:ignore-end */

在html代码中使用方式:



<meta data-react-helmet="true" name="theme-color" content="#cb3837"/>
<link data-react-helmet="true" rel="stylesheet" href="https://static.npmjs.com/103af5b8a2b3c971cba419755f3a67bc.css"/>
<link data-react-helmet="true" rel="apple-touch-icon" sizes="120x120" href="https://static.npmjs.com/58a19602036db1daee0d7863c94673a4.png"/>
<link data-react-helmet="true" rel="icon" type="image/png" href="https://static.npmjs.com/b0f1a8318363185cc2ea6a40ac23eeb2.png" sizes="32x32"/>


总结


jscpd是一款强大的前端本地代码重复度检测工具。它可以帮助开发者快速发现代码重复问题,简单的配置即可输出直观的代码重复数据,通过解决重复的代码提高代码的质量和可维护性。


使用jscpd我们可以有效地优化前端开发过程,提高代码的效率和性能。希望本文能够对你了解基于jscpd的前端本地代码重复度检测有所帮助。




看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


作者:南城FE
来源:juejin.cn/post/7288699185981095988
收起阅读 »

H5车牌输入软键盘

web
前言 公司的业务背景是个大型园区,不可避免的要接触太多与车辆收费相关的业务,于是就有了这个车牌输入软键盘。对于车牌,用户手动输入的是不可信的,而且车牌第一位的地区简称打字输入实在是太麻烦,所以界定用户的输入内容,才能让双方都更加方便。 预览: pxsgdsb...
继续阅读 »

前言


公司的业务背景是个大型园区,不可避免的要接触太多与车辆收费相关的业务,于是就有了这个车牌输入软键盘。对于车牌,用户手动输入的是不可信的,而且车牌第一位的地区简称打字输入实在是太麻烦,所以界定用户的输入内容,才能让双方都更加方便。



预览: pxsgdsb.github.io/licensePlat… (请使用移动端打开)


github:github.com/pxsgdsb/lic…


gitee:gitee.com/PxStrong/li…



screenshots.gif

实现


因为车牌内容是固定的,所以直接写死在元素内。但是,为了提高组件的复用性,需要做一些简单的封装


; (function ($) {
function LicensePlateSelector() {
// 输入框元素
this.input_dom = `<ul class="plate_input_box">
<li class="territory_key" data-type="territory_key"></li>
<li style="margin-right:.8rem;"></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li data-end="end"></li>
<li data-cls="new_energy" data-end="end" class="new_energy">
<span>新能源</span>
</li>
</ul>`

// 键盘元素
this.keyboard_dom = `...省略`
}
/**
* 初始化 车牌选择器
* @param {string} config.elem 元素
* @param {string} config.value 默认填充车牌
* @param {number} config.activeIndex 默认选中下标 (从0开始)
* @param {function} inputCallBack 输入事件回调
* @param {function} deleteCallBack 键盘删除事件回调
* @param {function} closeKeyCallBack 关闭键盘事件回调
*/

LicensePlateSelector.prototype.init = function (config) {
config = {
elem: config.elem,
value: config.value || "",
activeIndex: config.activeIndex || false,
inputCallBack: config.inputCallBack || false,
deleteCallBack: config.deleteCallBack || false,
closeKeyCallBack: config.closeKeyCallBack || false,
}
this.elemDom = $(config.elem);
this.elemDom.append(this.input_dom);
this.elemDom.append(this.keyboard_dom);
// 监听输入
this.watchKeyboardEvents(function(val){
// 键盘输入回调
if(config.inputCallBack){config.inputCallBack(val);}
},function(){
// 键盘删除事件回调
if(config.deleteCallBack){config.deleteCallBack();}
},function(){
// 关闭键盘事件回调
if(config.closeKeyCallBack){config.closeKeyCallBack();}
})
// 输入默认车牌
if (config.value) {
this.elemDom.find(".plate_input_box li").each(function (index) {
if (config.value[index]) {
$(this).text(config.value[index])
}
})
}
// 选中默认下标
if(config.activeIndex){
this.elemDom.find(".plate_input_box li").eq(config.activeIndex).click();
}
};
})(jQuery);

watchKeyboardEvents()函数用于在元素创建完成后创建事件监听


/**
* 监听键盘输入
* @param {function} inputCallBack 输入事件回调
* @param {function} deleteCallBack 键盘删除事件回调
* @param {function} closeKeyCallBack 关闭键盘事件回调
*/

LicensePlateSelector.prototype.watchKeyboardEvents = function(inputCallBack,deleteCallBack,closeKeyCallBack) {
let _this = this
// 输入框点击
_this.elemDom.find(".plate_input_box li").click(function (event) {
// 显示边框
$(".plate_input_this").removeClass("plate_input_this");
$(this).addClass("plate_input_this")
// 弹出键盘
// 关闭别的键盘
$(".territory_keyboard").css("display","none")
$(".alphabet_keyboard").css("display","none")
if ($(this).attr("data-type") && $(this).attr("data-type") == "territory_key") {
if (_this.elemDom.find(".territory_keyboard").css("display") == "none") {
_this.elemDom.find(".alphabet_keyboard").animate({ bottom: "-50rem" }).hide()
_this.elemDom.find(".territory_keyboard").show().animate({ bottom: 0 })
}
} else {
if (_this.elemDom.find(".alphabet_keyboard").css("display") == "none") {
_this.elemDom.find(".territory_keyboard").animate({ bottom: "-50rem" }).hide()
_this.elemDom.find(".alphabet_keyboard").show().animate({ bottom: 0 })
}
}
// 点击新能源
if ($(this).attr("data-cls") == "new_energy") {
$(this).empty().removeClass("new_energy").attr("data-cls", "")
}
event.stopPropagation(); // 阻止事件冒泡
})

// 地域键盘输入事件
......
}

使用时html只需要创建一个根元素,js输入配置项,自动渲染组件。


<div id="demo"></div>
<script>
let licensePlateSelector = new LicensePlateSelector();
// 初始化
licensePlateSelector.init({
elem: "#demo", // 根元素id
value: "湘A", // 默认填充车牌
activeIndex: 2, // 默认选中下标 (从0开始,不传时,默认不选中)
inputCallBack:function(val){ // 输入事件回调
console.log(val);
let plate_number = licensePlateSelector.getValue(); // 获取当前车牌
console.log(plate_number);
},
deleteCallBack:function(){ // 键盘删除事件回调
let plate_number = licensePlateSelector.getValue(); // 获取当前车牌
console.log(plate_number);
},
closeKeyCallBack:function(){ // 关闭键盘事件回调
console.log("键盘关闭");
},
})
</script>

参数


参数类型必填说明示例值
elemString指定元素选择器"#demo"
valueString默认填充车牌"湘A"
activeIndexnumber当前输入框下标,从0开始,不传时,默认不选中2
inputCallBackfunction输入事件回调函数,返回参数:当前输入的值
deleteCallBackfunction键盘删除事件回调函数
closeKeyCallBackfunction关闭键盘事件回调函数

方法


getValue 获取当前车牌


let plate_number = licensePlateSelector.getValue();

setValue 设置车牌


licensePlateSelector.setValue("粤A1E9Q3");

clearValue 清空车牌


licensePlateSelector.clearValue();

END


如果觉得对你还有些用,顺手点一下star吧。


作者:彭喜迎MAX
来源:juejin.cn/post/7288609174124576783
收起阅读 »

喂,鬼仔!你竟然还在瞒着我偷偷使用强制相等

web
我们都知道JavaScript有== (强制相等)和===(严格相等)运算符进行比较。但你可能不知道它们两个究竟有什么不同,并且更重要的是,在 js 引擎中使用它们的时候发生了什么? 前面我们提到 == 是强制比较。强制意味着 VM 试图将进行比较的双方强制...
继续阅读 »

我们都知道JavaScript有== (强制相等)和===(严格相等)运算符进行比较。但你可能不知道它们两个究竟有什么不同,并且更重要的是,在 js 引擎中使用它们的时候发生了什么?


前面我们提到 == 是强制比较。强制意味着 VM 试图将进行比较的双方强制为相同的类型然后查看它们是否相等。以下我们列举了一些自动被强制相等的例子:


"1" == 1 // true
1 == "1" // true
true == 1 // true
1 == true // true
[1] == 1 // true
1 == [1] // true


你要知道,强制是对称的,如果a == b为真,那么b == a也为真。另一方面,只有当两个操作数完全相同时===才为真(除了Number.NaN)。因此,上面的例子都真实的情况下都是假真 (即,在 === 的情况下是 false 的)。



为什么强制相等有这样的问题,这要归咎与强制相等的规则。


强制相等的规则


实际的规则很复杂(这也是不使用==的原因)。但是为了显示规则有多么复杂,我通过使用===实现了==,带大家看看强制相等的规则到底多复杂:


function doubleEqual(a, b) {
if (typeof a === typeof b) return a === b;
if (wantsCoercion(a) && isCoercable(b)) {
b = b.valueOf();
} else if (wantsCoercion(b) && isCoercable(a)) {
const temp = a.valueOf();
a = b;
b = temp;
}
if (a === b) return true;
switch (typeof a) {
case "string":
if (b === true) return a === "1" || a === 1;
if (b === false) return a === "0" || a === 0 || a == "";
if (a === "" && b === 0) return true;
return a === String(b);
case "boolean":
if (a === true) return b === 1 || String(b) === "1";
else return b === false || String(b) === "0" || String(b) === "";
case "number":
if (a === 0 && b === false) return true;
if (a === 1 && b === true) return true;
return a === Number(String(b));
case "undefined":
return b === undefined || b === null;
case "object":
if (a === null) return b === null || b === undefined;
default:
return false;
}
}

function wantsCoercion(value) {
const type = typeof value;
return type === "string" || type === "number" || type === "boolean";
}

function isCoercable(value) {
return value !== null && typeof value == "object";
}

这是不是太复杂了,我甚至不确定这是正确的! 也许有你知道更简单的算法。


但有趣的是,你会发现在上面的算法中,如果其中一个操作数是对象,VM 将调用. valueof()来允许对象将自身强制转换为基本类型。


强制转换的成本


上面的实现很复杂。那么===== 要多浪费多少性能呢? 看看下面这张图,我用基准测试做了一个对比:


image.png


其中,图表中越高表示越快(即,每秒操作次数越多)。


首先我们来讨论数字数组。当 VM 注意到数组是纯整数时,它将它们存储在一个称为PACKED_SMI_ELEMENTS的特殊数组中。在这种情况下,VM 知道将 == 处理为 === 是安全的,性能是相同的。这解释了为什么在数字的情况下,===== 之间没有区别。但是,一旦数组中包含了数字以外的内容,== 的情况就变得很糟糕了。


对于字符串,===== 的性能下降了 50%,看起来挺糟的是吧。


字符串在VM中是特殊的,但一旦我们涉及到对象,我们就慢了 4 倍。看看 mix 这栏,现在速度减慢了 4 倍!


但还有更糟的。对象可以定义 valueOf,这样在转换的时候可以将自己强制转换为原语。虽然在对象上定位属性可以通过内联缓存,内联缓存让属性读取变得快速,但在超大容量读取的情况下可能会经历 60 倍的减速,这可能会使情况更糟。如图中最坏情况(objectsMega)场景所示,===== 慢15 倍!


有其他使用 == 的理由吗


现在,=== 非常快! 因此,即使是使用 === 的15倍减速,在大多数应用程序中也不会有太大区别。尽管如此,我还是很难想出为什么要使用 == 而不是 === 的任何理由。强制规则很复杂,而且它存在一个性能瓶颈,所以在使用 == 之前请三思。


作者:编程轨迹
来源:juejin.cn/post/7216894387992477757
收起阅读 »

国庆,与山重逢

重庆多山,重庆的县城也多山。从主城回到重庆最东北,是横穿一座又一座山,是横跨一座又一座桥。桥不跨江,是用来连接两座山的。 每当向不很了解重庆的朋友分享我的回家路时,他们总不相信:“重庆不是一个直辖市么?你回家怎么可能要8小时的?” 我回家是真需要至少8小时的。...
继续阅读 »

重庆多山,重庆的县城也多山。从主城回到重庆最东北,是横穿一座又一座山,是横跨一座又一座桥。桥不跨江,是用来连接两座山的。


每当向不很了解重庆的朋友分享我的回家路时,他们总不相信:“重庆不是一个直辖市么?你回家怎么可能要8小时的?”


我回家是真需要至少8小时的。国庆第一天上午11点出发,晚上9点回到山上家中,除去路上堵车的两小时,全程整好8小时。


即便回家很远,回家的路很难走,我依然很喜欢回家。


我从没仔细想过自己为什么喜欢回家,只是每年国庆劝说阿妮回家用的说辞总一样:“爷爷奶奶外公外婆都在家。”


我的母亲今年也在家,所以今年国庆,绝大部分时间是在山上度过的。


大概是10年前,绝大部分“高山”——单纯的字面意思,山的高处——住户搬到低山,高山上的住户,只剩十几家。山上村子住的人家变少,便给了山很大的自由。


原有的山路,平日里少有人行走,路面长满各式各样我全不记得名字的草,郁郁葱葱,互相缠绕。路旁斜坡新长出许多很小的树,它们不管旁边是否有路,只向空旷处挤,挤着挤着,就没了路。如果从没走过这些路,是肯定看不出来曾经有过路的。稍远处老些的大树,掉落的枯的枝丫,触手可及,捡柴再不用去很远地方,只沿着路挨着捡便好。


原有的山田,在退耕还林时全种上了果树,核桃与板栗。不知是水土不服还是品种不佳,核桃树只剩下些印象,田中长起来的,只有板栗。十多年过去,板栗成了山田里的佼佼者,每一棵树的主干,都有大腿那么粗。


搬走的人家多了,没搬走的也大都外出打工只在过年时回家,于是还喂猪的人家更少,山中落叶不再被收集回家为猪铺床。再走远些,林间落叶铺了一层又一层,厚厚的,挡住菌子的冒头路线。


图片


母亲一大早沿路捡的菌子


菌子,是山中的特产,春天有,夏天有,秋天也有。母亲说:“秋天菌子不闹人(‘闹人’是无毒的意思),最好吃。春夏的菌子就要注意,有些吃不得,要挑一哈。”


捡菌子的最好时机,是下雨后的第二天,有些刚冒出头,有些刚长成型。长过(腐烂)生蛆?此时是不会的。


母亲是捡菌子的好手,似乎所有菌子她都认识。我没有学到捡菌子这门手艺,只在菌子回家后跟着母亲洗菌时认识几个品类。


石灰菌是白色的,山里最多,平均体型最大,吃起来脆脆的不爽口。


红菌子好吃,但需要仔细辨认,有许多其它红颜色的菌是不能吃的,能吃的要肥厚一些。


蜂窝菌伞把内部像蜂窝,伞面滑滑的,只在秋天有。它是我最喜欢吃的菌子,炒好的成品入口也滑滑的,一嗦就进了肚;如果吃的慢些,咀嚼两次,又会发现它也是脆脆的;蜂窝菌,只放油、盐、大蒜和辣椒,味道就已经很好。


我听过的名字,还有枞树菌、紫檀菌,它们并不多见,我暂且只记得名字不记得长相与口感。


我们三个帅的计划,是国庆第二天上山捡菌子。


计划依据天气预报——国庆第二天小雨,后面几天,要么是中雨要么是大雨——制定。天气预报不准确,真正的小雨,只在下午出现一小会儿。我极不愿意极不建议天黑走山路,于是宝帅的下山时间,定在下午6点。


雨真正变小的时间,是下午4点半,一个半小时时间,四个人一起,能从山中收获些什么呢?


答案是半背板栗与一碗菌子。


四个人,两个筐筐,一个装菌子一个装板栗;一把弯刀一把火钳,弯刀用来开路——砍去那些挤在路上的树枝与刺条,火钳用来捡板栗的有刺包子;再背一个背篓,万一筐筐装不下呢?


时间很紧,意犹未尽。


母亲将板栗硬塞给宝帅一行,留下的一碗菌子,是当晚桌上的一盘菜。


图片


炒熟的菌


菌的做法,是简单的。菌子去跟,摘掉树叶,洗净泥巴,煮半小时;捞出用凉水泡一泡,将大的菌撕成小的适合入口形状,再洗再煮再捞出;锅内放油放蒜放辣椒,炒香装盘。


菌的味道极好。


图片


八月瓜壳


我知道的能在山上长出果子的果树,有苹果、梨、杏、枣、桃、山楂、板栗和八月瓜。苹果、梨、杏、枣、桃和山楂,都需要人的维护——剪枝或是嫁接,不维护的果树,任它自然生长,要么过两年枯掉,要么果小不好吃。


不用维护的,是板栗和八月瓜。八月瓜纯野生,我见的不多,但板栗,是一直都存在的。


十几年前,高山上的人家很多,捡板栗需要走很远,走到悬崖边,走到“弯里”(山的最里面,很大一片山里只有一户人家),走到绝大部分人不愿去的地方。


我印象中的第一次全家捡板栗,是高中时的某个国庆,母亲和贵嬢嬢,带着各自小孩,背背篓提筐筐不带弯刀,一大群人去弯里。


弯里的板栗很小,不像新长起来的山田里的品种。


飞包土是我们家最远、最高的一块田,它是退耕还林时被最先“退”掉的,田里栽的树,是板栗。时间过去十几年,山田真的变回树林,板栗成了山里的树。


国庆离开家的那天上午不下雨,我和阿妮上飞包土,再捡半筐板栗。


图片


刚洗过的板栗


今年国庆,与山重逢。


作者:我要改名叫嘟嘟
来源:juejin.cn/post/7288163743035965440
收起阅读 »

唱衰这么多年,PHP 仍然还是你大爷!

web
PHP 是个庞然大物。 尽管有人不断宣称 PHP “即将消亡”。 但无法改变的事实是:互联网依然大量依赖 PHP。本文将通过大量的数据和事实告诉你为何 PHP 仍然在统治着互联网,你大爷仍然还是你大爷。 统计数据 PHP 仍然是首选编程语言 根据 W3 ...
继续阅读 »

PHP 是个庞然大物。


尽管有人不断宣称 PHP “即将消亡”。



但无法改变的事实是:互联网依然大量依赖 PHP。本文将通过大量的数据和事实告诉你为何 PHP 仍然在统治着互联网,你大爷仍然还是你大爷



统计数据


PHP 仍然是首选编程语言



根据 W3 Techs 对全球前 1000 万个网站使用的编程语言分析,我们可以看到:



  • PHP 占比 77.2%

  • ASP 占比 6.9%

  • Ruby 占比 5.4%


基于 PHP 的内容管理框架


绝大多数公共网站都是通过 PHP 和 CMS 来构建的。根据市场份额,12 大 CMS 软件中有 8 个是用 PHP 编写的。下面的数据来自 W3 Techs 对前 1000 万个网站的 CMS 使用情况调查,每个百分点代表前 1000 万个网站中的 10 万网站。



  • [PHP] WordPress 生态系统 (63%)

  • [Ruby] Shopify

  • Wix

  • Squarespace

  • [PHP] Joomla 生态系统 (3%)

  • [PHP] Drupal 生态系统 (2%)

  • [PHP] Adobe Magento (2%)

  • [PHP] PrestaShop (1%)

  • [Python] Google Blogger

  • [PHP] Bitrix (1%)

  • [PHP] OpenCart (1%)

  • [PHP] TYPO3 (1%)



不得不说,Wordpress 在内容管理领域依然站有绝对的统治地位。


PHP 在电商领域的应用


根据 BuiltWith 2023 年 8 月对在线商店的报告,我们可以看到 PHP 在电商领域仍然占统治地位:




趣闻轶事


Kinsta 发表了一篇文章,证明 PHP 仍然很快,仍然很活跃,仍然很流行:



早在 2011 年,人们就一直在宣称 PHP 已死。但事实是,PHP 7.3 的请求处理速度是 PHP 5.6 的 2-3 倍,而 PHP 8.1 则更快。正因为 PHP 的普及,我们可以很轻松地招聘到有经验的 PHP 开发者。



Vimeo 工程师 Matt Brown 在《这不是遗留代码,而是 PHP》一文中表示:



PHP 从未停止创新。尽管我们计划将 500,000 行的 PHP 代码划分为多个 [服务],但最终这些建议都没有被采纳。


Vimeo 自 2004 年以来规模扩大了数倍,我们的 PHP 代码库也是如此。



Ars Technica 发布了一个包含历史数据的 W3 Techs 报告,证明 PHP 仍然遥遥领先



尽管 PHP 有许多臭名昭著的怪癖,但它似乎还能活很久。从 2010 年的 72.5% 市场份额增长到今天的 78.9% 市场份额,目前还没有任何明显的竞争对手能让 PHP 感到威胁




针对 Python 创始人 Guido van Rossum 的一个采访播客中,Lex Fridman 如是说:



Lex: 目前互联网的大部分后端服务仍然是用 PHP 写的


Guido: 没错!



Daniel Stenberg 在其年度 Curl 用户调查(第 18 页)中统计了用户使用 curl 的方式。直接使用 curl 命令行的用户占比最高(78.4%),用户最熟悉的方式就是在 PHP 中使用 curl,自 2015 年调查开始以来一直都是这个结果。2023 年的调查报告显示有 19.6% 的用户在 PHP 中使用 curl。



curl (CLI) 78.4%, php-curl 19.6%, pycurl 13%, […], node-libcurl 4.1%.



Ember.js 虽然起源于 Ruby 社区,但作为一个前端框架,它可以与任何后端配合使用。Ember 的社区调查报告显示,PHP 是受访者第三喜欢的选项,仅次于 Ruby 和 Java。



Ember 的调查还询问了一些通用的行业问题。例如,有 24% 的受访者表示他们的基础设施都是“自托管”,而不是依赖于主流的云服务提供商。虽然这项调查本身不能完全代表整个行业,但结果仍可能会让人大吃一惊,特别是对那些依赖社交媒体和会议演讲来了解商业现状的人来说更是如此。对于企业来说,现在准备好云退出战略(例如 NHS)比以往任何时候都更加重要。你可以阅读 Basecamp 的文章了解云退出战略是如何为他们每年节省数百万美元的。


大规模 PHP 应用


上述统计数据衡量了不同网站和公司的数量,其中绝大多数是基于 PHP 构建的。但所有这些只告诉我们它们的规模在前 1000 万名之内。那前 500 名呢?


Jack Ellis 在《Laravel 能否扩展?》这篇文章中指出,你不应该仅根据每秒可以处理的请求数量来做选择。大部分业务都不太可能达到那个水平,而且还会面临很多其他瓶颈。但事实证明,PHP 是可以扩展到这一水平的语言之一。




当看到我们的软件(基于 Laravel 构建的 Fathom Analytics)增长迅猛时,我们从未怀疑过“这个框架是否能够扩展?”。


我与多家企业合作过,他们利用 Laravel 支撑整个业务运营。像 Twitch、Disney、New York Times、WWE 和 Warner Bros 这样的公司也在他们的多个项目中使用 Laravel。Laravel 能够轻松应对大规模的应用需求。



Vimeo 工程师 Matt Brown 在《这不是遗留代码,而是 PHP》一文中强调:




可以很明确地告诉你们,PHP 还是你大爷。Vimeo 在 PHP 方面的持续成功就是证明,在 2020 年它仍然是快速发展的公司的绝佳工具。



Vimeo 还以开发流行的 PHP 静态分析工具 Psalm 而闻名。


Slack 公司首席架构师 Keith Adams 在《认真对待 PHP》一文中提到:




Slack 服务端大部分应用逻辑都是由 PHP 来执行的。


相比于 PHP 的优势而言(通过故障隔离减少 bug 成本;安全并发;高吞吐量),PHP 存在的问题可以忽略不计。



我们再分析一下 W3 Techs 的报告,分析部分业务比较单一的公司的规模。规模最大的是 WordPress,它驱动着 Automattic 的 WordPress.com。每月有 200 亿次页面访问(Alexa 全球排名 55)。


如果我们继续往下看,来到占市场份额 0.1% 的条目,可以看到大量的网站都是靠 PHP 系统来支撑的,PHP 仍然是 10w 小网站的首选框架。



MediaWiki维基百科背后的平台,每月有 250 亿的页面浏览量(Alexa 排名 12)。同时 MediaWiki 还驱动着 Fandom(每月有 20 亿的页面浏览量,Similarweb 排名 44)和 WikiHow(每月有 1 亿访问者,Alexa 排名 215)。



除此之外还有一大批互联网公司由 PHP 驱动,例如 Facebook(Alexa 排名 7)、Etsy(Alexa 排名 66)、Vimeo(Alexa 排名 165)和 Slack(Similarweb 排名 362)。


Etsy 之所以引人关注,是因为它有高比例的活跃会话和动态内容。这与维基百科或 WordPress 不同,后者可以从静态缓存中提供大多数页面视图。这意味着尽管规模相似,但 Etsy 的 PHP 应用程序更容易受到高流量的影响。


Etsy 也是 PHP 创始人 Rasmus Lerdorf 的东家。他有时会在技术分享中展示 Etsy 的代码库片段。(极客旁注:他在 2021 年的现代 PHP 讲座中解释了 Etsy 是如何使用 rsync 进行部署的,就像 Wikipedia 在过去 10 年使用 Scap 一样)。Etsy 的官方博客偶尔会提到他们对模块化 PHP 单体的工作进展,例如 Plural 本地化。有时也会放出详细的 Etsy 站点性能报告



很高兴地告诉大家,升级到 PHP7 之后,本季度整个网站的性能都得到了提高,所有页面的性能都有了显著的提升。



我的观点


大多数人认为,PHP 社区似乎在公共舆论中占据的空间不大。无论是 PHP 核心开发者 , 还是 PHP 软件包(例如 Laravel、Symfony、WordPress、Composer 和 PHPUnit)的作者,亦或是日常工作中使用 PHP 的普通工程师,我们很少在社交媒体上的争论中看到他们的身影。


你也很少看到我们在会议上做演讲,宣称某个技术栈“绝对会”为你的公司带来裨益。如果你听了某些 JavaScript 框架粉丝的演讲,你可能会认为大多数公司今天都在使用他们的技术栈。


我不是说 JavaScript 不好,而是某些人在没有考虑技术或商业需求的前提下给出了“xxx 最好”的断言。这是一种过度营销,你怎么知道它最好?你跟别的语言比较过了吗?


我也不是说 JavaScript 没有用武之地,我们要辩证地看待世间万物。你可以分享你的经验和成果,比如哪些行得通,哪些行不通。要持续探索、持续创新、持续分享,持续推动人类前进。这就是自由软件的精神!


你可能看过《The Market for Lemons 》和《A Historical Reference of React Criticism》这两篇文章,他们都指出了 JS 的问题。但是 ... React 仅占有 3% 的市场份额。再加上其他的小框架(Vue、Angular、Svelte),这个数字才达到 5%。而基于 Node.js 的 Web 服务也仅占有 3% 的市场份额。这是否意味着超过 90% 的人都错过了 PHP?


别忘了,这 5% 代表了 50 万个主要网站,这是一个巨大的数字。Node.js 有自己的优势(实时消息流)。但是,Node.js 也有其弱点(阻塞主线程)。另外要强调一点:市场份额并不能完全反映规模。你可能驱动着排名前 1% 的几个大型组织,也可能驱动着排名后 1% 的组织。或者像 WordPress 那样同时支撑排名前 1% 和其他 4000 万个网站。


结论


无论是老公司还是小公司,无论其规模大小,可能都没有使用我们在公共场所经常听到的技术栈。如果不考虑个人项目和烧钱的初创公司,其他公司的这个现象更为明显。


对于正在成长和持续经营的企业来说,PHP 是否能够成为企业首选的前三名语言?当一个企业和其团队在扩大规模时,编程语言是否完全不重要?我们不得而知。


我只知道如今有许多企业都在使用 PHP,而 PHP 已被证明是一种可持续的选择,它经受住了时间的考验。例如,像 Fathom 这样的新公司,在短短三年内就实现了盈利。正如 Fathom 的文章所说,大部分公司的业务永远达不到那种规模。不过话又说回来,即使面对大规模的业务,PHP 仍然是一种经济可持续的选择


那么问题来了,PHP 是唯一的选择吗?当然不是。


有的语言速度更快(Rust),有的语言社区规模更大(Node.js),或者编译器更成熟(Java),但这往往会牺牲其他价值。


PHP 达到了某种柔中取刚的平衡点。它速度很快,社区规模较大语法现代化开发活跃,易于学习,易于扩展,并且拥有一个庞大的标准库。它可以在大规模场景下提供高效和安全的并发,而又没有异步复杂性或阻塞主线程的问题。由于平台稳定,加上社区重视兼容性和低依赖性,它的维护成本往往较低。


当然,每个人的需求不尽相同,但想要达到上述的这种平衡点,PHP 是少数几个能满足需求的软语言之一。除此之外还有哪个语言可以做到?


作者:米开朗基杨
来源:juejin.cn/post/7288963080855617573
收起阅读 »

离开了浪浪山,简直不要太爽

web
今年年初的时候,《中国奇谭》火了,与其说是《中国奇谭》火了,还不如说是这个动漫和普通打工人太有共鸣了,动漫里面的小猪妖是很多普通打工人的写照,毕业进入了父母亲戚以为很不错的工作,领着一份不多不少的工资,每天要处理各种工作上的事情,事情比较多的时候,还需要经常加...
继续阅读 »

今年年初的时候,《中国奇谭》火了,与其说是《中国奇谭》火了,还不如说是这个动漫和普通打工人太有共鸣了,动漫里面的小猪妖是很多普通打工人的写照,毕业进入了父母亲戚以为很不错的工作,领着一份不多不少的工资,每天要处理各种工作上的事情,事情比较多的时候,还需要经常加班。每个人都想和小猪妖一样离开浪浪山,不过最近我却离开了浪浪山。


公司裁员


准确的说是公司裁员了。人事通知我,说去下会议室,当时我就有预感到是要裁员了,因为之前公司就开始裁员了。一开始就是人事主管就说:最近工作怎么样?我就猜到了基本就是要裁员了。后面赔偿也符合我的预期。谈好了赔偿,做了工作的交接,和几个同事吃了一个饭,就和这个工作了几年的公司拜拜了。


开始的时候也是挺不适应的,自从大学毕业之后,一直都是有规律的上班生活,每个月都有一份固定的工资领,能维持日常开销,多余的钱投投资,日子过得也还行。忽然一下子没了工作,意味着就没有了收入了,要为下个月的房租担心了,不过好在赔偿金还能维持几个月。


今年行情普遍不太好,身边也有失业的朋友,有的找了几个月还没有找到工作。有的朋友还说好的公司面试基本都要二本以上和三年工作以下的面试,总体来说要求还是比较严格的,如果不出去面试,也不会意识到现在就业行情的严峻。后面索性就先玩玩吧,去周边走走、去附近的香港走走。


周边逛逛


首先就准备去盐田那边玩,经常看小红书有人分享那边的打卡地方,有海上图书馆,打定主意就出发。路过一个地铁口,看到一些可爱的动漫,灌篮高手、海贼王。



还有可爱的一个公交车,这个公交完美的贴合的墙壁上,门框刚好做成一个上下的车门,设计的比较巧妙。



之前上班的时候,走路的都是匆匆忙忙的,上班都比较辛苦,周末都基本就用来补觉休息。出门人也比较多,现在人都比较少,慢慢走,欣赏沿途的风景




坐了一个小时的地铁到了海山地铁站,映入眼帘就是清澈的海水,远离的城市的喧嚣,欣赏自然的美景。往里面走就看到了海上图书馆,环境还是挺不错的,海水比深圳湾的清澈多了。




沿着上面的海边一直散步,享受这海风吹拂的感觉,小雨淅淅沥沥的下,听着下雨的声音,一边走,一望无际的白云和天空,让人身体特别放松。






**程序员都是脑力工作为主,坐在工位上一坐就是几个小时,运动量比较少,都是固定的上下班,周末也基本是休息。**不过固定的生活模式过久了就会感觉很单调和平淡,每天都生活的都是复制,也会让人感觉很无聊,所以还是要多出去走走,体验一下不一样的生活。



雨天爬山


去玩海边之后,之后一直在下雨,之前也经常爬山,不过都是天气不错的时候爬的,这次就尝试一下雨天爬山吧。


因为开始爬山的是下午 2 点多,人不是很多,上山看到了很多下山的人。一路上也没什么人了。





快到山顶的时候,就开始下雨了,天也变暗了,雾也越来越大了。



上山的时候还能看到山下的房子,现在都看不请了。还以为误入衡山了。




在亭子上躲雨休息,随着天越来越暗,山下的灯光一点点打开,路上的车灯,路边的路灯。直到点亮所有的灯光,在马路上形成一道靓丽的风景线。



后面还去了各种公园,还去了一趟香港,再去了一趟香港大学。可以说这几周的经历比我上几年的经历都多


不上班真爽


不用每天上班,不用处理各种问题,也没有时间焦虑症(每天到哪个点就要上班,哪个点就要下班),这段时间完全不需要考虑时间的问题,想去哪里就可以立刻去哪里。不需要请假,不需要调休,晚上玩的比较晚了也不用担心第二天要早起上班起不来。不需要为工作而烦恼,只做自己想做的事情。


上面不是讲了去海边玩吗,走海边走路的时候,走着走着,竟然感觉到自己饿了,很难得有这种感觉。只有读书的时候,在外面运动了很久才会感觉的饥饿。


目的性不强的做事,也没有时间上的焦虑。没有压力的做事才是最自然的、最舒服、最享受的做事


失业焦虑吗


被通知裁员的时候,虽然心里有些准备,但是真的听到被裁的时候,心里还是有些焦虑,特别是现在就业行情也不太好,感觉找工作还是有些困难的。 习惯了每天按部就班的上班,完成各种工作上的任务。周末放假偶尔出去玩玩,休息。基本都没有太大的变化。不过心里也不是很焦虑,对自己的技术还是挺有信心,坚持写文章,写 Github,扩大自己的影响力。工作上也比较努力、认真。博客写了快两年了,每天都在积累,阅读量最高的都有十万多了,有了一些积累,心里也更有底气了。



其实给公司打工的同时也要给自己的打工,在工作中一般有问题就需要立刻去解决,解决之后及时的总结和归纳,做事的同时也要积累的自己的经验。积累的越多,自己做事也就更快,做事也更有章程了。


现在自己也是把简历改好,投投简历。没有工作也适当的放松放松,去周边城市旅旅游。有面试就去面试。


写在最后


现在就业行情不太好,打工人还是需要有被裁员的准备。现在可能很多公司给打工人更多的压力。这时候就需要放平自己的心态,尽量把自己的工作做好。同时也要多做积累,多做输出,未雨绸缪。有工作的就好好工作,尽量提高自己的能力,能力提高了,才有有更多的成长。失业的也不要气馁,多投简历,降低消费。


无论有没有离开浪浪山,都需要努力并自信的生活。


作者:小码A梦
来源:juejin.cn/post/7288602155111563264
收起阅读 »

你敢信?比 setTimeout 还快 80 倍的定时器

web
起因 很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这...
继续阅读 »

起因


很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说:



在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。



HTML Standard规范中也有提到更具体的:



Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.



简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。


用如下代码做个测试:


let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。


2021-05-13-21-04-16-067254.png


探索


假设就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,在 MDN 文档的角落里有一些线索:



如果想在浏览器中实现 0ms 延时的定时器,可以参考这里所说的window.postMessage()



这篇文章里的作者给出了这样一段代码,用postMessage来实现真正 0 延迟的定时器:


(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';

// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}

function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}

window.addEventListener('message', handleMessage, true);

// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();

由于postMessage的回调函数的执行时机和setTimeout类似,都属于宏任务,所以可以简单利用postMessageaddEventListener('message')的消息通知组合,来实现模拟定时器的功能。


这样,执行时机类似,但是延迟更小的定时器就完成了。


再利用上面的嵌套定时器的例子来跑一下测试:


2021-05-13-21-04-16-210864.png


全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。


测试


从理论上来说,由于postMessage的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。


设计一个实验方法,就是分别用postMessage版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。


实验代码:


function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}

var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}

setZeroTimeout(test1);

// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}

实验代码很简单,先通过setZeroTimeout也就是postMessage版本来递归计数到 100,然后切换成 setTimeout计数到 100。


直接放结论,这个差距不固定,在 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在硬件更好的台式机上,甚至能到 200 倍以上。


2021-05-13-21-04-16-326555.png


Performance 面板


只是看冷冰冰的数字还不够过瘾,打开 Performance 面板,看看更直观的可视化界面中,postMessage版的定时器和setTimeout版的定时器是如何分布的。


2021-05-13-21-04-16-602815.png


这张分布图非常直观的体现出了上面所说的所有现象,左边的postMessage版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。


而右边的setTimeout版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。


作用


也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。


const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};

React 把任务切分成很多片段,这样就可以通过把任务交给postMessage的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。


为什么不用执行时机更靠前的微任务呢?关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。


总结


可以了解如下几个知识点:



  1. setTimeout的 4ms 延迟历史原因,具体表现。

  2. 如何通过postMessage实现一个真正 0 延迟的定时器。

  3. postMessage定时器在 React 时间切片中的运用。

  4. 为什么时间切片需要用宏任务,而不是微任务。


作者:睡醒想钱钱
来源:juejin.cn/post/7229520942668824633
收起阅读 »

产品:能实现长列表的滚动恢复嘛?我:... 得加钱

web
前言 某一天,产品经理找到我,他希望我们能够给用户更好的体验,提供长列表的滚动记忆功能。就是说当鼠标滚轮滚动到长列表的某个位置时,单击一个具体的列表项,就切换路由到了这个列表项的详情页;当导航返回到长列表时,还能回到之前滚动到的位置去。 思路 我低头思考了一阵...
继续阅读 »

前言


某一天,产品经理找到我,他希望我们能够给用户更好的体验,提供长列表的滚动记忆功能。就是说当鼠标滚轮滚动到长列表的某个位置时,单击一个具体的列表项,就切换路由到了这个列表项的详情页;当导航返回到长列表时,还能回到之前滚动到的位置去。


思路


我低头思考了一阵儿,想到了history引入的scrollRestoration属性,也许可以一试。于是我回答,可以实现,一天工作量吧😂。产品经理听到后,满意地走了,但是我后知后觉,我为数不多的经验告诉我,这事儿可能隐隐有风险😨。但是没办法,no zuo no die。


scrollRestoration


Chrome46之后,history引入了scrollRestoration属性。该属性提供两个值,auto(默认值),以及manual。当设置为auto时,浏览器会原生地记录下window中某个元素的滚动位置。此后不管是刷新页面,还是使用pushState等方法改变页面路由,始终可以让元素恢复到之前的屏幕范围中。但是很遗憾,他只能记录下在window中滚动的元素,而我的需求是某个容器中滚动。

完犊子😡,实现不了。

其实想想也是,浏览器怎么可能知道开发者想要保存哪个DOM节点的滚动位置呢?这事只有开发者自己知道,换句话说,得自己实现。于是乎,想到了一个大致思路是:



发生滚动时将元素容器当时的位置保存起来,等到长列表再次渲染时,再对其重新赋值scrollTop和scrollLeft



真正的开发思路


其实不难想到,滚动恢复应该属于长列表场景中的通用能力,既然如此,那...,夸下的海口是一天,所以没招,只能根据上述的简单思路实现了一个,很low,位置信息保存在localStorage中,算是交了差。但作为一个有追求的程序员,这事必须完美解决,既然通用那么公共组件提上日程😎。在肝了几天之后,出炉的完美解决方案:



在路由进行切换、元素即将消失于屏幕前,记录下元素的滚动位置,当元素重新渲染或出现于屏幕时,再进行恢复。得益于React-Router的设计思路,类似于Router组件,设计滚动管理组件ScrollManager,用于管理整个应用的滚动状态。同理,类似于Route,设计对应的滚动恢复执行者ScrollElement,用以执行具体的恢复逻辑。



滚动管理者-ScrollManager


滚动管理者作为整个应用的管理员,应该具有一个管理者对象,用来设置原始滚动位置,恢复和保存原始的节点等。然后通过Context,将该对象分发给具体的滚动恢复执行者。其设计如下:


export interface ScrollManager {
/**
* 保存当前的真实DOM节点
* @param key 缓存的索引
* @param node
* @returns
*/

registerOrUpdateNode: (key: string, node: HTMLElement) => void;
/**
* 设置当前的真实DOM节点的元素位置
* @param key 缓存的索引
* @param node
* @returns
*/

setLocation: (key: string, node: HTMLElement | null) => void;
/**
* 设置标志,表明location改变时,是可以保存滚动位置的
* @param key 缓存的索引
* @param matched
* @returns
*/

setMatch: (key: string, matched: boolean) => void;
/**
* 恢复位置
* @param key 缓存的索引
* @returns
*/

restoreLocation: (key: string) => void;
/**
* 清空节点的缓存
* @param key
* @returns
*/

unRegisterNode: (key: string) => void;
}


  • 上述Manager虽然提供了各项能力,但是缺少了缓存对象,也就是保存这些位置信息的地方。使用React.useRef,其设计如下:


//缓存位置的具体内容
const locationCache = React.useRef<{
[key: string]: { x: number; y: number };
}>({});
//原生节点的缓存
const nodeCache = React.useRef<{
[key: string]: HTMLElement | null;
}>({});
//标志位的缓存
const matchCache = React.useRef<{
[key: string]: boolean;
}>({});
//清空节点方法的缓存
const cancelRestoreFnCache = React.useRef<{
[key: string]: () => void;
}>({});


  • 有了缓存对象,我们就可以实现manager,使用key作为缓存的索引,关于key会在ScrollElement中进行说明。


const manager: ScrollManager = {
registerOrUpdateNode(key, node) {
nodeCache.current[key] = node;
},
unRegisterNode(key) {
nodeCache.current[key] = null;
//及时清除
cancelRestoreFnCache.current[key] && cancelRestoreFnCache.current[key]();
},
setMatch(key, matched) {
matchCache.current[key] = matched;
if (!matched) {
//及时清除
cancelRestoreFnCache.current[key] && cancelRestoreFnCache.current[key]();
}
},
setLocation(key, node) {
if (!node) return;
locationCache.current[key] = { x: node?.scrollLeft, y: node?.scrollTop };
},
restoreLocation(key) {
if (!locationCache.current[key]) return;
const { x, y } = locationCache.current[key];
nodeCache.current[key]!.scrollLeft = x;
nodeCache.current[key]!.scrollTop = y;
},
};


  • 之后,便可以通过Context将manager对象向下传递


<ScrollManagerContext.Provider value={manager}>
{props.children}
</ScrollManagerContext.Provider>


  • 除了上述功能外,manager还有一个重要功能:获知元素在导航切换前的位置。在React-Router中一切路由状态的切换都由history.listen来发起,由于history.listen可以监听多个函数。所以可以在路由状态切换前,插入一段监听函数,来获得节点相关信息。


location改变 ---> 获得节点位置信息 ---> 路由update


  • 在实现中,使用了一个状态shouldChild,来确保监听函数一定在触发顺序上先于Router中的监听函数。实现如下:


const [shouldChild, setShouldChild] = React.useState(false);

//利用useLayoutEffect的同步,模拟componentDidMount,为了确保shouldChild在Router渲染前设置
React.useLayoutEffect(() => {
//利用history提供的listen监听能力
const unlisten = props.history.listen(() => {
const cacheNodes = Object.entries(nodeCache.current);
cacheNodes.forEach((entry) => {
const [key, node] = entry;
//如果matchCache为true,表明从当前路由渲染的页面离开,所以离开之前,保存scroll
if (matchCache.current[key]) {
manager.setLocation(key, node);
}
});
});

//确保该监听先入栈,也就是监听完上述回调函数后才实例化Router
setShouldChild(true);
//销毁时清空缓存信息
return () => {
locationCache.current = {};
nodeCache.current = {};
matchCache.current = {};
cancelRestoreFnCache.current = {};
Object.values(cancelRestoreFnCache.current).forEach((cancel) => cancel());
unlisten();
};
}, []);

//改造context传递
<ScrollManagerContext.Provider value={manager}>
{shouldChild && props.children}
</ScrollManagerContext.Provider>



  • 真正使用时,管理者组件要放在Router组件外侧,来控制Router实例化:


<ScrollRestoreManager history={history}>
<Router history={history}>
...
</Router>

</ScrollRestoreManager>

滚动恢复执行者-ScrollElement


ScrollElement的主要职责其实是控制真实的HTMLElement元素,决定缓存的key,包括决定何时触发恢复,何时保存原始HTMLElement的引用,设置是否需要保存的位置等等。ScrollElement的props设计如下:


export interface ScrollRestoreElementProps {
/**
* 必须缓存的key,用来标志缓存的具体元素,位置信息以及状态等,全局唯一
*/

scrollKey: string;
/**
* 为true时触发滚动恢复
*/

when?: boolean;
/**
* 外部传入ref
* @returns
*/

getRef?: () => HTMLElement;
children?: React.ReactElement;
}


  • ScrollElement本质上可以看作为一个代理,会拿到子元素的Ref,接管其控制权。也可以自行实现getRef传入组件中。首先要实现的就是滚动发生时,记录位置能力:


useEffect(() => {
const handler = function (event: Event) {‘
//nodeRef就是子元素的Ref
if (nodeRef.current === event.target) {
//获取scroll事件触发target,并更新位置
manager.setLocation(props.scrollKey, nodeRef.current);
}
};

//使用addEventListener的第三个参数,实现在window上监听scroll事件
window.addEventListener('scroll', handler, true);
return () => window.removeEventListener('scroll', handler, true);
}, [props.scrollKey]);


  • 接下来处理路由匹配以及DOM变更时处理的能力。注意,这块使用了对useLayoutEffectuseEffect执行时机的理解处理:


//使用useLayoutEffect主要目的是为了同步处理DOM,防止发生闪动
useLayoutEffect(() => {
if (props.getRef) {
//处理getRef获取ref
//useLayoutEffect会比useEffect先执行,所以nodeRef一定绑定的是最新的DOM
nodeRef.current = props.getRef();
}

if (currentMatch) {
//设置标志,表明当location改变时,可以保存滚动位置
manager.setMatch(props.scrollKey, true);
//更新ref,代理的DOM可能会发生变化(比如key发生了变化,remount元素)
nodeRef.current && manager.registerOrUpdateNode(props.scrollKey, nodeRef.current);
//恢复原先滑动过的位置,可通过外部props通知是否需要进行恢复
(props.when === undefined || props.when) && manager.restoreLocation(props.scrollKey);
} else {
//未命中标志设置,不要保存滚动位置
manager.setMatch(props.scrollKey, false);
}

//每次update注销,并重新注册最新的nodeRef,解决key发生变化的情况
return () => manager.unRegisterNode(props.scrollKey);
});


  • 上述代码,表示在初次加载或者每次更新时,会根据当前的Route匹配结果与否来处理。如果匹配,则表示ScrollElement组件应是渲染的,此时在effect中执行更新Ref的操作,为了解决key发生变化时DOM发生变化的情况,所以需要每次更新都处理。

  • 同时设置标识位,相当于告诉manager,node节点此刻已经渲染成功了,可以在离开页面时保存位置信息;如果路由不匹配,那么则不应该渲染,manager此刻也不用保存这个元素的位置信息。主要是为了解决存在路由缓存的场景。

  • 也可以通过when来控制恢复,主要是用来解决异步请求数据的场景。

  • 最后判断ScrollElement的子元素是否是合格的


//如果有getRef,直接返回children
if (props.getRef) {
return props.children as JSX.Element;
}

const onlyOneChild = React.Children.only(props.children);
//代理第一个child,判断必须是原生的tag
if (onlyOneChild && onlyOneChild.type && typeof onlyOneChild.type === 'string') {
//利用cloneElement,绑定nodeRef
return React.cloneElement(onlyOneChild, { ref: nodeRef });
} else {
console.warn('-----滚动恢复失败,ScrollElement的children必须为单个html标签');
}

return props.children as JSX.Element;

多次尝试机制


在某些低版本的浏览器中,可能存在一次恢复并不如预期的情况。所以实现多次尝试能力,其原理就是用一个定时器多次执行callback,同时设定时间上限,并返回一个取消函数给外部,如果最终结果理想则取消尝试,否则再次尝试直到时间上限内达到理想位置。更改恢复函数:


restoreLocation(key) {
if (!locationCache.current[key]) return;
const { x, y } = locationCache.current[key];
//多次尝试机制
let shouldNextTick = true;
cancelRestoreFnCache.current[key] = tryMutilTimes(
() => {
if (shouldNextTick && nodeCache.current[key]) {
nodeCache.current[key]!.scrollLeft = x;
nodeCache.current[key]!.scrollTop = y;
//如果恢复成功,就取消
if (nodeCache.current[key]!.scrollLeft === x && nodeCache.current[key]!.scrollTop === y) {
shouldNextTick = false;
cancelRestoreFnCache.current[key]();
}
}
},
props.restoreInterval || 50,
props.tryRestoreTimeout || 500
);
},

至此,滚动恢复的组件全部完成。具体源代码可以到github查看,欢迎star。 github.com/confuciusth…


效果


scroll-restore.gif


总结


一个滚动恢复功能,如果想要健壮,完善地实现。其实需要掌握Router,Route相关的原理、history监听路由变化原理、React Effect的相关执行时机以及一个好的设计思路。而这些都需要我们平时不断的研究,不断的追求完美。虽然这并不能“加钱”,但这种能力以及追求是我们成为技术大牛的路途中,最宝贵的财富。当然,能够加钱最好了😍。


创作不易,欢迎点赞!


作者:青春地平线
来源:juejin.cn/post/7186600603936620603
收起阅读 »

关于浏览器的一个逆天bug

web
1.问题描述: 这个bug是我在做一个二次元项目(vue+vite+mysql)的时候,最开始都没有问题,但是后来有一天我的这个项目打开控制台后出现了资源无法加载的问题,包括图片,组件等,但是我只要不打开控制台就没有问题,所以当时我觉得这个问题非常的逆天,...
继续阅读 »

1.问题描述:




这个bug是我在做一个二次元项目(vue+vite+mysql)的时候,最开始都没有问题,但是后来有一天我的这个项目打开控制台后出现了资源无法加载的问题,包括图片,组件等,但是我只要不打开控制台就没有问题,所以当时我觉得这个问题非常的逆天,


bug如图


bug效果





2.解决思路:


先说正确答案:浏览器抽风,把我默认的网络限制改成了离线,而我之前一直是无限制,因此导致了我一打开控制台就断网,最主要的惑因就是不止我常用的edg浏览器这样了,捏吗连谷歌浏览器都跟着抽风,导致我误判了




  1. 首先我遇到这种问题想的肯定先是我的代码有没有问题,因为这个bug是突然出现的。所以我检查了我的代码问题,例如图片我把原来的静态的src:“巴拉巴拉.jpg”换成了import动态引入的方法


     import src1 from "../assets/movie/miaonei/miaonei.aac";
     ​
     export default {
      name: "profile",
      components: { userTop },
      data() {
        return {
          src1,
        };
      },
      }

    但是问题依然没有得到解决。


    2.接下来我考虑到了浏览器本身的问题,但是因为我浏览器网络那里是默认,我的默认一直是无限制,接下来我就用谷歌打开了项目结果也是一样的,所以我就排除了是控制台网络的原因


    3.接下来就考虑是我nodel_modles或者vue,npm版本有问题,所以就开始检测各种的版本,但是也没有发现问题


    4.最后我就先放弃的一段时间,毕竟不用控制台也只是开发效率降低,不是不能写,后来我突然想到这种样子不就是断网吗,所以我认定了就是控制台打开导致的断网,所以一定是network那里的默认不是我之前的东西了,虽然我根本没有改过,但只有这一种可能了


    5.问题解决。


    3.解决后效果




    结语:



    山重水复疑无路,柳暗花明又一村。


    做项目遇到bug是很正常的事,对于在读生来说,遇到bug反而是一件是好事,我可以通过自己思考,结合所学的东西来解决问题,这样可以提升我们的能力,巩固我们的境界。


    就上面这个bug而言,在我成功解决这个问题之前,我都是不知道原来浏览器自己能修改我默认的东西。





作者:BittersweetYao
来源:juejin.cn/post/7189295826366103589
收起阅读 »

为什么同一表情'🧔‍♂️'.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
收起阅读 »