注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

登录页面一些有趣的css效果

web
前言 今天无意看到一个登录页,input框focus时placeholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及...
继续阅读 »

前言


今天无意看到一个登录页,inputfocusplaceholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及实现源码


919c40a2a264f683ab5e74e8a649ac5.png


title 的动画实现


首先描述一下大概的实现效果, 我们需要一个镂空的一段白底文字,在鼠标移入时给一个逐步点亮的效果。
文字镂空我们可以使用text-stroke, 逐步点亮只需要使用filter即可


text-stroke


text-stroke属性用于在文本的边缘周围添加描边效果,即文本字符的外部轮廓。这可以用于创建具有描边的文本效果。text-stroke属性通常与-webkit-text-stroke前缀一起使用,因为它目前主要在WebKit浏览器(如Chrome和Safari)中支持


text-stroke属性有两个主要值:



  1. 宽度(width) :指定描边的宽度,可以是像素值、百分比值或其他长度单位。

  2. 颜色(color) :指定描边的颜色,可以使用颜色名称、十六进制值、RGB值等。


filter


filter是CSS属性,用于将图像或元素的视觉效果进行处理,例如模糊、对比度调整、饱和度调整等。它可以应用于元素的背景图像、文本或任何具有视觉内容的元素。


filter属性的值是一个或多个滤镜函数,这些函数以空格分隔。以下是一些常见的滤镜函数和示例:



  1. 模糊(blur) : 通过blur函数可以实现模糊效果。模糊的值可以是像素值或其他长度单位。


    .blurred-image {
    filter: blur(5px);
    }


  2. 对比度(contrast) : 通过contrast函数可以调整对比度。值为百分比,1表示原始对比度。


    .high-contrast-text {
    filter: contrast(150%);
    }


  3. 饱和度(saturate) : 通过saturate函数可以调整饱和度。值为百分比,1表示原始饱和度。


    .desaturated-image {
    filter: saturate(50%);
    }


  4. 反色(invert) : 通过invert函数可以实现反色效果。值为百分比,1表示完全反色。


    .inverted-text {
    filter: invert(100%);
    }


  5. 灰度(grayscale) : 通过grayscale函数可以将图像或元素转换为灰度图像。值为百分比,1表示完全灰度。


    .gray-text {
    filter: grayscale(70%);
    }


  6. 透明度(opacity) : 通过opacity函数可以调整元素的透明度。值为0到1之间的数字,0表示完全透明,1表示完全不透明。


    .semi-transparent-box {
    filter: opacity(0.7);
    }


  7. 阴影(drop-shadow) :用于在图像、文本或其他元素周围添加阴影效果。这个属性在 CSS3 中引入,通常用于创建阴影效果,使元素看起来浮在页面上或增加深度感


    drop-shadow(<offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>?)

    各个值的含义如下:



    • <offset-x>: 阴影在 X 轴上的偏移距离。

    • <offset-y>: 阴影在 Y 轴上的偏移距离。

    • <blur-radius> (可选): 阴影的模糊半径。默认值为 0。

    • <spread-radius> (可选): 阴影的扩散半径。默认值为 0。

    • <color> (可选): 阴影的颜色。默认值为当前文本颜色。




filter属性的支持程度因浏览器而异,因此在使用时应谨慎考虑浏览器兼容性。


实现移入标题点亮的效果


想实现移入标题点亮的效果我们首先需要两个通过定位重叠的span元素,一个做镂空用于展示,另一个作为
hover时覆盖掉镂空元素,并通过filter: drop-shadow实现光影效果,需要注意的是这里需要使用inline元素实现效果。


title-animation.gif


input 的动画实现


input的效果比较简单,只需要在focusspan(placeholder)上移变成span(label)同时给inputborder-bottom做一个底色的延伸,效果确定了接着就看看实现思路。


input placeholder 作为 label


使用div作为容器包裹inputspanspan首先绝对定位到框内,伪装为placeholder, 当input状态为focus提高spantop值,即可伪装成label, 这里有两个问题是:



  1. 当用户输入了值的时候,span并不需要恢复为之前的top, 这里我们使用css或者js 去判断都可以, js就是拿到输入框的值,这里不多做赘述,css 有个比较巧妙的做法, 给input required属性值设置为required, 这样可以使用css:valid伪类去判断input是否有值。

  2. 由于span层级高于input,当点击span时无法触发input的聚焦,这个问题我们可以使用pointer-events: none; 来解决。pointer-events 是一个CSS属性,用于控制元素是否响应用户的指针事件(例如鼠标点击、悬停、触摸等)。这个属性对于控制元素的可交互性和可点击性非常有用。


    pointer-events 具有以下几个可能的值:



    1. auto(默认值):元素会按照其正常行为响应用户指针事件。这是默认行为。

    2. none:元素不会响应用户的指针事件,就好像它不存在一样。用户无法与它交互。

    3. visiblePainted:元素在绘制区域上响应指针事件,但不在其透明区域上响应。这使得元素的透明部分不会响应事件,而其他部分会。

    4. visibleFill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    5. visibleStroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。

    6. painted:元素会在其绘制区域上响应指针事件,包括填充、边框和透明区域。

    7. fill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    8. stroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。




pointer-events 属性非常有用,特别是在创建交互性复杂的用户界面时,可以通过它来控制元素的响应区域。例如,你可以使用它来创建自定义的点击区域,而不仅仅是元素的边界。它还可以与其他CSS属性和JavaScript事件处理程序结合使用,以创建特定的交互效果。


input border bottom 延伸展开效果


效果比较简单,input被聚焦的时候,一个紫色的边从中间延伸覆盖白色的底边即可。 在使用一个span作为底部的边, 初始不可见, focus时从中间向两边延伸直至充满, 唯一头痛的就是怎么从中间向两边延伸,这里可以使用transform变形,首先使用transform: scaleX(0);达到不可见的效果, 然后设置变形原点为中间transform-origin: center;,这样效果就可以实现了


input 的动画实现效果


input-animation.gif


按钮的动画实现


关于按钮的动画很多,我们这里就实现一个移入的散花效果,移入时发散出一些星星,这里需要使用到动画去实现了,首先通过伪类创建一些周边元素,这里需要用到 background-image(radial-gradient)


background-image(radial-gradient)


background-image 属性用于设置元素的背景图像,而 radial-gradient 是一种 CSS 渐变类型,可用于创建径向渐变背景。这种径向渐变背景通常以一个中心点为基础,然后颜色渐变向外扩展,形成一种放射状的效果。


radial-gradient 的语法如下:


background-image: radial-gradient([shape] [size] at [position], color-stop1, color-stop2, ...);


  • [shape]: 可选,指定渐变的形状。常用的值包括 "ellipse"(椭圆)和 "circle"(圆形)。

  • [size]: 可选,指定渐变的大小。可以是长度值或百分比值。

  • at [position]: 可选,指定渐变的中心点位置。

  • color-stopX: 渐变的颜色停止点,可以是颜色值、百分比值或长度值。


按钮移入动画效果实现


btn-animation.gif


结尾


css 能实现的效果越来越多了,遇到有趣的效果,可以自己想想实现方式以及动手实现一下,思路毕竟是思路,具体实现起来说不定会遇到什么坑,逐步解决问题带来的成就感满足感还是很强的。


作者:刘圣凯
来源:juejin.cn/post/7294908459002331171
收起阅读 »

日历表格的制作,我竟然选择了这样子来实现...

web
前言 最近有个日历表格的需求,具体效果如下所示,鼠标经过时表格还有一个十字高亮的效果,在拿到这个设计图的时候,就在想应该用什么来实现,由于我所在的项目用的是vue3 + element,所以我第一时间想到的就是饿了么里面的表格组件,但是经过一番调式之后,发现在...
继续阅读 »

前言


最近有个日历表格的需求,具体效果如下所示,鼠标经过时表格还有一个十字高亮的效果,在拿到这个设计图的时候,就在想应该用什么来实现,由于我所在的项目用的是vue3 + element,所以我第一时间想到的就是饿了么里面的表格组件,但是经过一番调式之后,发现在饿了么表格的基础上想要调整我要的样式效果太复杂太麻烦了,所以我决定用原生的div循环来实现!


soogif.gif


第一步 初步渲染表格


由于表格的表头是固定的,我们可以先渲染出来


<script setup lang="ts">

const tableFileds = Array.from({ length: 31 }, (_, i) =>
String(i + 1).padStart(2, '0')
)
</script>
<template>
<div class="table">
<div class="tabble-box">
<div class="table-node">
<div class="table-item table-header">
<div class="table-item-content diagonal-cell">
<span class="top-content"></span>
<span class="bottom-content"></span>
</div>
<div
class="table-item-content"
v-for="(item, _index) in tableFileds"
:key="item"
:style="{
background: '#EFF5FF'
}"

>

{{ item }}
</div>
</div>
</div>
</div>
</div>
</template>


<style lang="less" scoped>
.table {
flex: 1;
display: flex;
flex-direction: column;
background-color: #fff;
padding: 1.8519vh 0.83vw 2.1296vh 1.09vw;
.tabble-box {
display: flex;
flex: 1;
box-sizing: border-box;
overflow: hidden;
color: #666666;
.table-node {
display: flex;
flex-direction: column;
flex: 1;

.table-header {
.table-item-content {
background-color: #eff5ff;
}
.diagonal-cell {
position: relative; /* 使伪元素相对于此单元格定位 */
padding-bottom: 8px; /* 为对角线下方留出空间以显示内容 */
width: 4.64vw !important;
&::before {
content: ''; /* 必须有内容才能显示伪元素 */
position: absolute;
top: 0;
left: 1px;
width: 5.16vw;
right: 0;
height: 1px; /* 对角线的高度 */
background-color: #e8e8e8; /* 对角线的颜色,可自定义 */
transform-origin: top left;
transform: rotate(30.5deg); /* 斜切角度,可微调 */
}
.top-content {
position: absolute;
top: 0.2778vh;
left: 2.67vw;
font-size: 0.83vw;
}
.bottom-content {
position: absolute;
top: 2.2222vh;
left: 0.83vw;
font-size: 0.83vw;
}
}
}
.table-item {
display: flex;
.table-item-content:first-child {
width: 4.64vw;
padding-top: 1.9444vh;
padding-bottom: 1.2037vh;
}
.table-item-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
// width: calc((100% - 9.53vw) / 15);
padding: 0.1vw;
text-align: center;
font-size: 0.78vw;
border-top: 0.05vw solid #eeeeee;
border-right: 0.05vw solid #eeeeee;
width: 2.48vw;
// flex-grow: 1
}
}

.table-header {
.table-item-content {
padding-top: 1.9444vh;
padding-bottom: 1.5741vh;
}
}
}
}
}

看一下页面效果:


image.png
表格的表头初步完成!


第二步 确认接口返回的数据格式


这是接口返回的格式数据 就例如第一个对象代表着3月9号有数据


{
"3": {
"9": 1
},
"4": {
"12": 2
},
"5": {
"11": 1,
"12": 2,
"21": 1
},
"6": {
"6": 5,
"8": 1,
"9": 2,
"10": 1,
"12": 2,
"17": 1,
"20": 1
},
"7": {
"1": 8,
"4": 1,
"7": 1,
"6": 1,
"13": 1,
"22": 1,
"25": 1,
"26": 1,
"27": 1,
"29": 6,
"30": 1
},
"8": {
"1": 1,
"2": 2,
"7": 1,
"20": 1,
"24": 1,
"27": 1,
"31": 1
},
"9": {
"15": 1,
"17": 9,
"21": 2
},
"10": {
"23": 1
}
}

接着我们需要对返回的数据做处理,由于表格的表头已经渲染出来,这意味着表格的每一列都有了,接下来我们就需要渲染表格的每一行与其对应就可以了.十二个月份我们需要十二行,同时每一行的第一个单元格表示的是月份,那我们可以定义一个月份的数据,然后再根据接口数据做处理,返回一个带有对应月份数据的数组.
代码如下:


const tableDataList = [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月'
]
// 把接口数据转换为对应的月份数组对象
const parseData = (data: any) => {
const parsedData = new Array(12).fill({}) // 初始化一个包含12个空对象的数组
console.log('parsedData', parsedData)

for (let month = 1; month <= 12; month++) {
// 确保每次循环创建一个新的空对象
parsedData[month - 1] = {} // 从0开始索引
if (data[month]) {
Object.entries(data[month]).forEach(([day, value]) => {
parsedData[month - 1][parseInt(day)] = value
})
}
}
return parsedData
}

const tableData = ref<any[]>([])

onMounted(() => {
tableData.value = parseData(data)
console.log('tableData.value', tableData.value)
})

我们可以看一下控制台,此时的tableData的数据格式是怎么样的


image.png
接下来就可以开始渲染表格的内容了,给有数据的单元格做个高亮,同时固定31天,所以可以先遍历出每一行31个单元格出来


  <div class="table-list">
<div
class="table-item"
v-for="(item, rowIndex) in tableData"
:key="item"
>

<div
class="table-item-content"
:key="item"
:style="{
background: '#EFF5FF'
}"

>

{{ tableDataList[rowIndex] }}
</div>
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index] ? '#6fa7ea' : ''
}"

>

<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
</div>

</div>

image.png


到这里基本就完成了,还差一个鼠标经过表格十字高亮的需求


我们可以给每个单元格加上鼠标的移入移出事件,移入事件函数传两个参数,一个就是行一个就是列,行可以从一开始的tableData那里拿到,列就是遍历31长度的当前项;这样子就可以拿到当前单元格的坐标,再封装一个辅助函数进行判断是否为当前单元格所在的行所在的列就可以了
高亮的时候记住判断的样式需要在之前的有数据高亮的样式的后面,这样子就不会被覆盖,可以保证有数据高亮的样式会一直存在,哪怕鼠标经过也不会被覆盖!


// 表格十字高亮
const highlightedRow = ref<any>()
const highlightedColumn = ref<any>()

const isCurrentCellHighlighted = (rowIndex: number, columnIndex: number) => {
return (
(highlightedRow.value !== null && highlightedRow.value === rowIndex) ||
(highlightedColumn.value !== null &&
highlightedColumn.value === columnIndex)
)
}
//鼠标移入
const onCellMouseOver = (rowIndex: any, columnIndex: any) => {
console.log('坐标', rowIndex, columnIndex)

highlightedRow.value = rowIndex
highlightedColumn.value = columnIndex
}

// 在鼠标移出事件(onCellMouseLeave)触发时,恢复所有单元格的原始背景色
const onCellMouseLeave = () => {
highlightedRow.value = null
highlightedColumn.value = null
}
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index]
? '#6fa7ea'
: isCurrentCellHighlighted(rowIndex + 1, index)
? '#EFF5FF'
: ''
}"

@mouseover="onCellMouseOver(rowIndex + 1, index)"
@mouseout="onCellMouseLeave"
>
<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>

<span v-else>0</span>
</div>

最终的效果就是:


soogif.gif


以下就是完整的代码:


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

const tableFileds = Array.from({ length: 31 }, (_, i) =>
String(i + 1).padStart(2, '0')
)
const tableDataList = [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月'
]

const parseData = (data: any) => {
const parsedData = new Array(12).fill({}) // 初始化一个包含12个空对象的数组
console.log('parsedData', parsedData)

for (let month = 1; month <= 12; month++) {
// 确保每次循环创建一个新的空对象
parsedData[month - 1] = {} // 从0开始索引
if (data[month]) {
Object.entries(data[month]).forEach(([day, value]) => {
parsedData[month - 1][parseInt(day)] = value
})
}
}
return parsedData
}
const data = {
'3': {
'9': 1
},
'4': {
'12': 2
},
'5': {
'11': 1,
'12': 2,
'21': 1
},
'6': {
'6': 5,
'8': 1,
'9': 2,
'10': 1,
'12': 2,
'17': 1,
'20': 1
},
'7': {
'1': 8,
'4': 1,
'7': 1,
'6': 1,
'13': 1,
'22': 1,
'25': 1,
'26': 1,
'27': 1,
'29': 6,
'30': 1
},
'8': {
'1': 1,
'2': 2,
'7': 1,
'20': 1,
'24': 1,
'27': 1,
'31': 1
},
'9': {
'15': 1,
'17': 9,
'21': 2
},
'10': {
'23': 1
}
}
const tableData = ref<any[]>([])
// 表格十字高亮
const highlightedRow = ref<any>()
const highlightedColumn = ref<any>()

const isCurrentCellHighlighted = (rowIndex: number, columnIndex: number) => {
return (
(highlightedRow.value !== null && highlightedRow.value === rowIndex) ||
(highlightedColumn.value !== null &&
highlightedColumn.value === columnIndex)
)
}
const onCellMouseOver = (rowIndex: any, columnIndex: any) => {
console.log('坐标', rowIndex, columnIndex)

highlightedRow.value = rowIndex
highlightedColumn.value = columnIndex
}

// 在鼠标移出事件(onCellMouseLeave)触发时,恢复所有单元格的原始背景色
const onCellMouseLeave = () => {
highlightedRow.value = null
highlightedColumn.value = null
}
onMounted(() => {
tableData.value = parseData(data)
console.log('tableData.value', tableData.value)
})
</script>
<template>
<div class="table">
<div class="tabble-box">
<div class="table-node">
<div class="table-item table-header">
<div class="table-item-content diagonal-cell">
<span class="top-content"></span>
<span class="bottom-content"></span>
</div>
<div
class="table-item-content"
v-for="(item, _index) in tableFileds"
:key="item"
:style="{
background: '#EFF5FF'
}"

>

{{ item }}
</div>
</div>
<div class="table-list">
<div
class="table-item"
v-for="(item, rowIndex) in tableData"
:key="item"
>

<div
class="table-item-content"
:key="item"
:style="{
background: '#EFF5FF'
}"

>

{{ tableDataList[rowIndex] }}
</div>
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index]
? '#6fa7ea'
: isCurrentCellHighlighted(rowIndex + 1, index)
? '#EFF5FF'
: ''
}"

@mouseover="onCellMouseOver(rowIndex + 1, index)"
@mouseout="onCellMouseLeave"
>

<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>


<style lang="less" scoped>
.table {
flex: 1;
display: flex;
flex-direction: column;
background-color: #fff;
padding: 1.8519vh 0.83vw 2.1296vh 1.09vw;
.tabble-box {
display: flex;
flex: 1;
box-sizing: border-box;
overflow: hidden;
color: #666666;
.table-node {
display: flex;
flex-direction: column;
flex: 1;

.table-header {
.table-item-content {
background-color: #eff5ff;
}
.diagonal-cell {
position: relative; /* 使伪元素相对于此单元格定位 */
padding-bottom: 8px; /* 为对角线下方留出空间以显示内容 */
width: 4.64vw !important;
&::before {
content: ''; /* 必须有内容才能显示伪元素 */
position: absolute;
top: 0;
left: 1px;
width: 5.16vw;
right: 0;
height: 1px; /* 对角线的高度 */
background-color: #e8e8e8; /* 对角线的颜色,可自定义 */
transform-origin: top left;
transform: rotate(30.5deg); /* 斜切角度,可微调 */
}
.top-content {
position: absolute;
top: 0.2778vh;
left: 2.67vw;
font-size: 0.83vw;
}
.bottom-content {
position: absolute;
top: 2.2222vh;
left: 0.83vw;
font-size: 0.83vw;
}
}
}
.table-item {
display: flex;
.table-item-content:first-child {
width: 4.64vw;
padding-top: 1.9444vh;
padding-bottom: 1.2037vh;
}
.table-item-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
// width: calc((100% - 9.53vw) / 15);
padding: 0.1vw;
text-align: center;
font-size: 0.78vw;
border-top: 0.05vw solid #eeeeee;
border-right: 0.05vw solid #eeeeee;
width: 2.48vw;
// flex-grow: 1
}
}

.table-header {
.table-item-content {
padding-top: 1.9444vh;
padding-bottom: 1.5741vh;
}
}
}
}
}
</style>



如果对你有帮助的话,欢迎点赞留言收藏🌹


作者:coder_zsz
来源:juejin.cn/post/7413311432971141160
收起阅读 »

贼好用!五分钟搭建一个美观且易用的导航页面!

web
大家好,我是 Java陈序员。 今天,给大家介绍一个贼好用的导航网站搭建工具,只需通过几步操作,就能搭建出个性化导航网站! 关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。 项目简介 Pintree 是一...
继续阅读 »

大家好,我是 Java陈序员


今天,给大家介绍一个贼好用的导航网站搭建工具,只需通过几步操作,就能搭建出个性化导航网站!



关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。



项目简介


Pintree 是一个开源项目,旨在将浏览器书签导出成导航网站。通过简单的几步操作,就可以将书签转换成一个美观且易用的导航页面。



Pintree 支持使用 GitHub Pages 进行部署,无需购买服务器、域名等资源!


因此,只要有一个 Github 账号,就能快速搭建一个导航网站。接下来我们就来部署实现下!


项目部署


步骤一:Fork 项目


1、访问 pintree 项目地址


https://github.com/Pintree-io/pintree

2、Fork 项目到自己的仓库中


步骤二:启用 Github Pages


1、打开 GitHub 账号中 Forkpintree 项目


2、切换到仓库的 Settings 标签页,点击 Pages,在 Source 下拉菜单中,选择 gh-pages 分支,然后点击 Save



3、几分钟后,静态导航网站将会在 https://yourusername.github.io/pintree 上可用



yourusername 是你的 Github 账号,如 https://chenyl8848.github.io/pintree.




这样,一个美观且易用的导航网站就搭建好了!


这时,好奇的小明就会问,要怎么个性化修改配置网站内容呢?别急,继续看步骤三。


步骤三:替换 JSON 文件自定义导航内容


1、pintree 渲染的导航网站内容是基于 json/pintree.json 文件里面的配置信息,我们可以通过修改 pintree.json 文件来自定义导航网站内容



2、打开 pintree.json 文件,并点击修改按钮进入编辑模式



3、在修改前,我们需要先了解下具体的语法规则,一个最小化的规则配置如下:


[
{
"//": "folder 表示是一个文件夹,可以配置子模块信息",
"type": "folder",
"//": "添加的时间信息",
"addDate": 1718526477999,
"//": "标题",
"title": "Java 陈序员",
"//": "子模块",
"children": [
{
"//": "link 表示是一个网站链接,最小化的配置单元",
"type": "link",
"//": "添加的时间信息",
"addDate": 1718526687700,
"//": "网站标题",
"title": "个人博客网站",
"//": "网站图标",
"icon": "https://chencoding.top:8090/_media/logo.png",
"//": "网站地址",
"url": "https://chencoding.top/"
},
"//": "依此类推",
{
"type": "folder",
"addDate": 1718526865665,
"title": "编程网站",
"children": [
{
"type": "link",
"addDate": 1718526707006,
"title": "CSDN",
"icon": "https://img-home.csdnimg.cn/images/20201124032511.png",
"url": "https://www.csdn.net/"
},
{
"type": "link",
"addDate": 1718526707006,
"title": "掘金",
"icon": "https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg",
"url": "https://juejin.cn/"
},
{
"type": "link",
"addDate": 1718526707006,
"title": "博客园",
"icon": "https://www.cnblogs.com/images/logo.svg?v=2SMrXdIvlZwVoB1akyXm38WIKuTHVqvGD0CweV-B6cY",
"url": "https://www.cnblogs.com/"
}
]
}
]
}
]

4、文件修改完后,点击 Commit changes 保存



5、过几分钟后,再访问 https://yourusername.github.io/pintree



可以看到,网站的内容变成了个性化的配置信息了。



由于浏览器有缓存的原因,如一开始没有变化,可以使用无痕模式访问或者用其他浏览器访问。



浏览器书签导航


通过前面的内容,我们知道 pintree 只需要一个 JSON 文件,就能搭建出一个导航网站。因此我们可以将浏览器中收藏的书签导出成 JSON 文件,再生成一个静态导航网站!


步骤一:导出浏览器书签


1、安装 Pintree Bookmarks Exporter 插件


安装地址:https://chromewebstore.google.com/detail/pintree-bookmarks-exporte/mjcglnkikjidokobpfdcdmcnfdicojce


2、使用插件导出浏览器书签,并保存 JSON 文件到本地



步骤二:替换 JSON 文件


JSON 文件替换到 Fork 项目的 json/pintree.json 文件中,保存成功后过几分钟再访问。


pintree 通过简单的配置,只需要几分钟就能快速搭建出一个导航网站,而且不用提供服务器、域名等资源,是一个非常优秀的开源项目!如果你想搭建一个静态导航网站可以去试试哈。


项目地址:https://github.com/Pintree-io/pintree

最后


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


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

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/



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





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

flex 布局中更巧妙的布局方案!比 justify-content 和 align-items 好用多了!

web
在前端开发中,实现水平垂直居中一直是个热门话题。随着 CSS Flexbox 布局的普及,开发者们开始更多地使用 justify-content 和 align-items 这两个属性来解决这个问题。 然而,还有一种更加简洁、灵活的方式——使用 margi...
继续阅读 »

在前端开发中,实现水平垂直居中一直是个热门话题。随着 CSS Flexbox 布局的普及,开发者们开始更多地使用 justify-contentalign-items 这两个属性来解决这个问题。




然而,还有一种更加简洁、灵活的方式——使用 margin: auto; 来实现居中以及更多实际场景下的特定效果。让我们一起回顾一下常见方式:justify-contentalign-items,然后再来探讨一下使用:margin 的优势,以及如何在实际项目中使用它。





一、常见方式:justify-contentalign-items


1.1 justify-content (用于水平对齐)


justify-content 决定主轴(通常是水平方向)上子元素如何分配空间。常见的取值有:



  • flex-start:元素排列在容器的起始位置(默认值)。

  • flex-end:元素排列在容器的末尾。

  • center:元素在容器内水平居中。

  • space-between:第一个元素与容器起点对齐,最后一个元素与容器终点对齐,其他元素之间均匀分布空间。

  • space-around:每个元素左右两侧都分配均等的空白区域(元素两边的空隙会有一半分布在两端)。

  • space-evenly:所有元素之间、以及与容器两端的空隙都相等。


1.2 align-items(用于垂直对齐)


align-items 决定交叉轴(通常是垂直方向)上子元素如何对齐。常见的取值有:



  • stretch:子元素在交叉轴上填满整个容器高度(默认值,前提是子元素没有设置具体的高度)。

  • flex-start:子元素在交叉轴的起始位置对齐。

  • flex-end:子元素在交叉轴的末端对齐。

  • center:子元素在交叉轴上垂直居中对齐。

  • baseline:子元素以其文本基线对齐。


1.3 flexbox 的常见用法


下面给出一些常见的 flexbox 的使用案例:


示例 : 公共样式


.container {
width: 800px;
height: 200px;
margin: 50px auto;
display: flex;
border: 1px solid black;
padding: 10px;
box-sizing: border-box;
}

.box {
width: 50px;
height: 50px;
background-color: lightblue;
text-align: center;
line-height: 50px;
border: 1px solid #333;
}

示例 1: 水平居中 + 垂直居中


<div class="container example-1">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-1 {
justify-content: center;
align-items: center;
}

image.png



如上图所示,元素在水平和垂直方向都居中了。



示例 2: 水平居中 + 垂直靠顶


<div class="container example-2">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-2 {
justify-content: center;
align-items: flex-start;
}

image.png



如上图所示,justify-content: center; 使元素在水平方向居中;align-items: flex-start; 使元素垂直方向靠近顶部。



示例 3: 水平等间距 + 垂直居中


<div class="container example-3">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-3 {
justify-content: space-between;
align-items: center;
}

image.png



如上图所示,justify-content: space-between; 使元素在垂直方向居中;align-items: center; 使元素在水平方向两端对齐。



示例 4: 水平左对齐 + 垂直底部对齐


<div class="container example-4">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-4 {
justify-content: flex-start;
align-items: flex-end;
}

image.png



如上图所示,justify-content: flex-start; 使元素在水平方向居左;align-items: flex-end; 使元素在垂直方向靠底。



示例 5: 水平等间距 + 垂直拉伸


<div class="container example-5">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-5 {
height: auto;
justify-content: space-evenly;
align-items: stretch;
}

image.png



如上图所示,justify-content: space-evenly; 会使元素会在水平方向等间距;如果不设置元素的高度,使其自适应,align-items: stretch; 会使其垂直方向拉伸铺满。



1.4 思考与延伸


但你有没有想过,这些写法是否是最简洁的?能否实现我们日常开发的需求呢?有没有更优雅、更轻量的方案呢?


实际上在很多情况下这两个属性并不能够满足我们的开发需求。


比如我需要实现子元素部分集中的布局:



单纯依靠 justify-contentalign-items,很难让几个子元素集中在一起。比如我们希望某些元素靠近并且与其他元素保持一定的间距就会比较麻烦了。


此时为了实现这种布局,通常需要结合 flex-growmargin 或者 space-between,甚至需要使用嵌套的 flex 布局,增加了复杂性。



image.png


又或者是等宽子项的平均分布问题:



比如在导航菜单或展示商品卡片时,可能要求子项无论数量多少,都要从左向右均匀分布,并且保持等宽。


通过 justify-content: space-betweenspace-around 可以部分解决这个问题,但是往往会出现无法保证元素从左向右,或者是无法等分的问题。



image.png


以及一些其他的情况,如垂直排列的固定间距复杂的网格布局混合布局等,justify-contentalign-items都无法简洁、优雅的解决问题。




二、更优雅的方式:margin


2.1 下使用 margin: auto 使元素居中


其实,Flexbox 布局下还有另一种更加简洁的方法使元素居中——直接使用 margin: auto;。你可能会问,这怎么能居中呢?让我们先看一个例子:


<div class="box">
<div class="item"></div>
</div>

.box {
width: 200px;
height: 100px;
border: 2px solid #ccc;
display: flex; /* 启用 Flex 布局 */
margin: 100px auto;
}

.item {
background: red;
width: 50px;
height: 50px;
margin: auto; /* 自动分配外边距 */
}

image.png


在这个例子中,我们没有使用 justify-contentalign-items,仅通过设置 .item 元素的 margin: auto;,就实现了水平和垂直居中。



它的工作原理是:在 Flexbox 布局中,margin: auto;根据父容器的剩余空间自动调整元素的外边距,直到子元素居中。



在传统布局中,margin: auto; 主要用于水平居中对齐,不适用于垂直居中。因为普通流布局的垂直方向是由文档流控制的,不支持类似 Flexbox 中的自动调整行为。


.container {
width: 500px;
}

.element {
width: 200px;
margin: 0 auto; /* 左右外边距自动分配,实现水平居中 */
}

相比之下,在 Flexbox 布局中,margin: auto; 具有更多的灵活性,可以同时实现水平和垂直居中对齐。


它不仅可以处理水平居中,还可以在 Flexbox 布局下根据剩余空间自动调整外边距,实现完全的居中对齐。


2.2 实现更多实际开发中的布局


示例 1:实现子元素部分集中



在实际开发中,我们常遇到这样一种需求:将元素水平分布在容器内,其中某些元素需要靠近在一起,与其他元素保持一定的自适应距离。


在这种情况下使用 justify-content: space-between 是一种常见的办法,但这种方法也有一定的局限性:每个元素之间平等分配剩余空间,无法实现特定元素之间紧密靠拢。



image.png


代码实现:


<div class="container c2">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>

* {
margin: 0;
padding: 0;
}

.container {
width: 500px;
background: #eee;
margin: 50px auto;
padding: 10px;
display: flex;
}

.item {
width: 50px;
height: 50px;
border: 1px solid #333;
box-sizing: border-box;
}

.item:nth-child(odd) {
background: #046f4e;
}

.item:nth-child(even) {
background: #d53b3b;
}

.c2 .item:nth-child(2){
margin: 0 0 0 auto; /* 第二个 item 右对齐 */
}

.c2 .item:nth-child(4){
margin: 0 auto 0 0; /* 第四个 item 左对齐 */
}


在上述代码中,其实除掉一些基本样式的设置,实现了这个布局的关键代码就2行。


具体来说,.c2 .item:nth-child(2)margin: 0 0 0 auto; 使得第二个 .item 紧贴容器的右边缘,而 .c2 .item:nth-child(4)margin: 0 auto 0 0; 使得第四个 .item 紧贴容器的左边缘。这样就使第二个元素的左侧和第四个元素的右侧将会自适应边距间隔。


因此,我们可以使用 margin 巧妙地通过调整子元素的外边距,实现元素的部分集中和对齐布局。



示例 2:实现等宽子项的平均分布


在很多情况下,我们需要将商品卡片或其他内容等宽地分布在每一行中,使每个子项都具有相同的宽度并且平均分布,每一行都是从左到右。


这种布局通常用于网格展示或商品列表等场景,确保每个子项在视觉上统一且整齐。



在这种情况下直接使用 justify-contentalign-items 可能会出现以下问题:



  1. 使用 space-between 时如果最后一行的元素数量不足以填满整行,剩余的元素会分散到两侧,留出较大的空白区域,导致布局不整齐。
    image.png

  2. 使用 space-around 时如果最后一行的元素数量不满,元素会在行中均匀分布,导致它们集中在中间,而不是靠左或对齐其他行。
    image.png

    大家在遇到这些情况时是不是就在考虑换用 grid 布局了呢?先别急,我们其实直接通过 margin 就可以直接实现的!



在这里我们可以使用 margin 的动态计算来实现等宽子项的平均分布


代码实现:


<div class="container c3">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>

* {
margin: 0;
padding: 0;
}

.container {
width: 500px;
background: #eee;
margin: 50px auto;
padding: 10px;
display: flex;
flex-wrap: wrap;
}

.item {
width: 50px;
height: 50px;
border: 1px solid #333;
box-sizing: border-box;
}

.item:nth-child(odd) {
background: #046f4e;
}

.item:nth-child(even) {
background: #d53b3b;
}

.c3 .item {
--n: 5; /* 每行显示的子项数量 */
--item-width: 50px; /* 子项宽度 */
--space: calc(100% / var(--n) - var(--item-width)); /* 计算子项之间的间距 */
--m: calc(var(--space) / 2); /* 左右间距的一半 */
margin: 10px var(--m); /* 动态计算左右的间距 */
}


在在上述代码中,除掉基础的样式,实现了这个布局的关键代码仅仅5行。通过动态计算 margin,我们能够简单而有效地实现等宽子项的平均分布,使布局更加简洁明了。



image.png




三、总结


在前端开发中,实现各种页面布局一直是一个常见的需求。


传统的做法如使用 justify-contentalign-items 属性已经被广泛采用,但这种方法有时可能显得不够简洁或灵活。


在适当的情况下直接使用 margin 进行布局是一种更优雅、简洁的替代方案,可以在 Flexbox 布局中有效地实现居中对齐和一些复杂的布局需求。掌握并运用这种方法,可以提高开发效率,并使布局更加优雅。快来玩起来吧!




作者:空白诗
来源:juejin.cn/post/7413222778855964706
收起阅读 »

告别繁琐的 try-catch:JavaScript 安全赋值运算符 (?= ) 来了!

web
你是否厌倦了代码中难以阅读和维护的冗长 try-catch 代码块?全新的 ECMAScript 安全赋值运算符 (?= ) 将彻底改变游戏规则!这一突破性的特性简化了错误处理,让你的代码更简洁、更高效。让我们深入了解 ?= 运算符如何彻底改变你的编码体验! ...
继续阅读 »

你是否厌倦了代码中难以阅读和维护的冗长 try-catch 代码块?全新的 ECMAScript 安全赋值运算符 (?= ) 将彻底改变游戏规则!这一突破性的特性简化了错误处理,让你的代码更简洁、更高效。让我们深入了解 ?= 运算符如何彻底改变你的编码体验!


简化代码,轻松处理错误


告别嵌套的 try-catch 混乱


问题: 传统的 try-catch 代码块会导致代码深度嵌套,难以理解和调试。


解决方案: 使用 ?= 运算符,你可以将函数结果转换为一个元组,更优雅地处理错误。如果出现错误,你将得到 [error, null] ,如果一切正常,你将得到 [null, result] 。你的代码将会感谢你!


使用 ?= 之前:


async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
try {
const data = await response.json();
return data;
} catch (parseError) {
console.error('Failed to parse JSON:', parseError);
}
} catch (networkError) {
console.error('Network error:', networkError);
}
}

使用 ?= 之后:


async function fetchData() {
const [networkError, response] ?= await fetch("https://api.example.com/data");
if (networkError) return console.error('Network error:', networkError);
const [parseError, data] ?= await response.json();
if (parseError) return console.error('Failed to parse JSON:', parseError);
return data;
}

提升代码清晰度:保持代码线性,简洁易懂


问题: try-catch 代码块会打断代码流程,降低可读性。


解决方案: ?= 运算符使错误处理变得简单直观,保持代码线性,易于理解。


示例:


const [error, result] ?= await performAsyncTask();
if (error) handleError(error);

标准化错误处理:跨 API 保持一致性


问题: 不同的 API 通常需要不同的错误处理方法,导致代码不一致。


解决方案: ?= 运算符提供了一种统一的错误处理方式,使你的代码在各种 API 中保持一致。


提升安全性:每次都捕获所有错误


问题: 漏掉错误会导致 bug 和潜在的安全问题。


解决方案: ?= 运算符确保始终捕获错误,降低漏掉关键问题的风险。


Symbol.result 背后的奥秘


自定义错误处理变得简单


概述: 实现 Symbol.result 方法的对象可以使用 ?= 运算符定义自己的错误处理逻辑。


示例:


function customErrorHandler() {
return {
[Symbol.result]() {
return [new Error("Custom error message"), null];
},
};
}

const [error, result] ?= customErrorHandler();

轻松处理嵌套错误:平滑处理复杂场景


概述: ?= 运算符可以处理包含 Symbol.result 的嵌套对象,使复杂错误场景更容易管理。


示例:


const complexObj = {
[Symbol.result]() {
return [
null,
{ [Symbol.result]: () => [new Error("Nested error"), null] }
];
},
};

const [error, data] ?= complexObj;

与 Promise 和异步函数无缝集成


概述: ?= 运算符专门设计用于与 Promise 和 async/await 无缝协作,简化异步错误处理。


示例:


const [error, data] ?= await fetch("https://api.example.com/data");

使用 using 语句简化资源管理


概述:?= 运算符与 using 语句结合使用,可以更有效地管理资源。


示例:


await using [error, resource] ?= getResource();

优先处理错误:先处理错误,后处理数据


概述: 将错误放在 [error, data] ?= 结构的第一个位置,确保在处理数据之前先处理错误。


示例:


const [error, data] ?= someFunction();

让你的代码面向未来:简化填充


概述: 虽然无法直接填充 ?= 运算符,但你可以使用后处理器在旧环境中模拟其行为。


示例:


const [error, data] = someFunction[Symbol.result]();

汲取灵感:从 Go、Rust 和 Swift 中学习


概述: ?= 运算符借鉴了 Go、Rust 和 Swift 等语言的先进错误处理实践,这些语言以其强大的错误管理功能而闻名。


当前限制和未来方向


仍在发展: ?= 运算符仍在开发中。改进领域包括:



  • 命名: 为实现 Symbol.result 的对象提供更好的术语。

  • finally 代码块: 没有新的 finally 代码块语法,但传统用法仍然有效。


总结


安全赋值运算符 (?= ) 将通过使 JavaScript 错误处理更加直观和简洁来彻底改变 JavaScript 错误处理。随着该提案的不断发展,它将有望成为每个 JavaScript 开发人员工具箱中的必备工具。准备迎接更干净、更安全的代码吧!🚀


作者:前端宝哥
来源:juejin.cn/post/7413284830945493001
收起阅读 »

两个月写完的校园社交小程序,这是篇uniapp踩坑记录

web
人员搭配和两个舍友,一前两后,从开发到最终上线,耗时两个月,此篇文章记录一下整个开发流程中踩的坑......前置准备:资质核验。需要企业认证才能发布社交类的小程序。需要上传营业执照、法人信息等类目选择。小程序中有个类目选择,选择自己小程序涉及的类目,但这里就比...
继续阅读 »

人员搭配和两个舍友,一前两后,从开发到最终上线,耗时两个月,此篇文章记录一下整个开发流程中踩的坑......

前置准备:

  1. 资质核验。需要企业认证才能发布社交类的小程序。需要上传营业执照、法人信息等
  2. 类目选择。小程序中有个类目选择,选择自己小程序涉及的类目,但这里就比较抽象了(下文讲)
  3. 微信认证。300元,腾子的吃相很难看,但奈何寄人篱下
  4. 小程序备案。在前面流程完成之后才能进行小程序的备案

image.png

审核流程

整个审核流程给我的感觉就是跟便秘一样,交一次吐一句,交一次吐一句,然后打回来重改 这里记录一下几个需要注意的点,如果你和我一样也是做UGC类的小程序的话

  1. 微信审核。所有但凡涉及言论的,能展示出你的思想的、个性的,对不起,请通通接入微信审核。包括但不限于用户昵称、头像;发布的帖子内容、图片

文字审核算比较快,图片审核就有点慢,且图片审核不是很精准。为了避免等待检测而让用户在界面等待过久的问题,后面无奈自己搭了个后台管理。以致于流程就变成了:用户发点什么东西 -> 后端调用微信检测接口 -> 检测完甩到后台 -> 后台管理员做个二次审核 -> 通过后在小程序获取

  1. 不能使用微信icon图标。之前做微信快捷登录,想着搞个微信的icon图标好看点,结果给我审核失败的原因就是不能使用微信图标 image.png
  2. 请预留足够的时间和审核慢慢耗。以上讲到的还只是真正发布之前踩的坑,等所有条件都符合后,算可以真正的发布,然后提示你首次提交一般需要7个自然日,然后就真的是7*24小时

image.png

image.png 5. 第一次代码真正预上线之后,此后更新代码再发布新的包,一半就只需要1~2天

开发过程

  1. 文件上传。以往网页开发中涉及文件上传的业务都是new FormData,然后再append必要的字段。但是,小程序中使用FormData会报错,所以,得使用uniapp自带的uni.uoloadFile
  2. 消息提示。在帖子发布成功之后等场景都会涉及到消息提示,一般涉及消息提示、页面回退,执行顺序请按navigateBackuni.showToast,如果二者调用顺序反了的话,只会页面回退,不会显示提示内容
  3. 分享功能。小程序的分享功能需要在onShareAppMessage(分享至好友)或者onShareTimeline(分享至朋友圈)调用。这两个是和onLoad同级的,如果你的技术选型也是Vue3的话,可以从@dcloudio/uni-app中导入
  4. 消息订阅。小程序涉及到一个需求:当用户发布帖子,微信审核+后台审核都通过之后会通知用户,此时就需要进行消息订阅

先在微信公众号平台开通消息订阅模板,拿到模板ID,前端中再调用 uni.requestSubscribeMessage 传入拿到的模板ID就可以实现消息订阅

  1. webSocket。小程序中的树洞评论功能我们选用的是webSocket,小程序中我们没有使用三方库,调用的是uniapp的uni.connectSocket,创建一个webSocket实例,然后处理对应的回调。由于用户一段时间内如果不发送消息,服务端也就没东西推送过来,webSocket自己会断掉。所以我们引入了心跳机制和断线重连机制。但是在做短信重连的时候就发现一个问题:断线重连之后确实是会创建新的实例,心跳包也正常推送给服务端,但是就是接收不到服务端反推回来的东西,后面经过排查,是webSocket实例的onMessage事件应当写在onOpen中,而不是独立写到外面

独立写到外面进行处理就会出现:断线重连之后死活接不到最新的实例返回的消息

这里再次吐槽微信的内容审核机制。原先选用webSocket的原因就是看中了它的实时推送,但是接入了内容审核就变得很抽象,时而秒通过,时而得等一下,这也搞得失去了选用webSocket的意义

  1. 请求池。小程序中使用到了tabs组件,当tab在切换过程中,比如tabA切换至tabB,由于需要请求数据,所以页面会有短暂的白屏时间,这里采用的是请求池,在获取第一个tab的列表数据的时候,由请求池顺便把之后的tab的内容也请求回来,此后在进行tab切换时就可以避免白屏,优化用户体验

image.png

image.png

  1. 响应式布局。小程序在个人页模仿了小红书个人页的实现形式,也就是随着页面的滚动,页面的布局(主要是用户头像缩小并由透明度为0逐渐变为1)发生变化。一开始采用的是监听页面scroll事件。但是,scroll涉及大量的计算;后面采用Intersection Observer。但是注意,uniapp不能直接使用这个API,得调用uni.createIntersectionObserver,二者语法差不多
  2. 防抖节流的使用。页面滚动加载下一页使用防抖,按钮点击进行节流,常规操作。

大概暂时先能想到这么多,后面有想到再接着补充......

后记

其实校园小程序这个题材市面上早已烂大街,说不上有任何的创新。此前决定搞这个的原因有以下几点:

  1. 很多东西只停留于理论,没有实操。就像webSocket的心跳和断线重连,博客教你怎样怎样,始终不如你自己去亲手实现一下,这个算主要驱动原因
  2. 这个程序刚好拿去参加学校的比赛,拿奖有钱doge
  3. ......

然后整一个项目跟下来吧,给我的感觉就是:技术重要吗?重要;但也不太重要,技术的重要性就跟我这句废话一样。因为一个东西,推广不起来,没人用,你就没动力去更新、去维护。当然,有没有人用对我们此次的开发来说还不算是必选项。

大家觉得校园社交类的程序还能集成什么功能,也欢迎提出您的宝贵意见


作者:吃肉不吃皮
来源:juejin.cn/post/7412665439501844490
收起阅读 »

利用CSS延迟动画,打造令人惊艳的复杂动画效果!

web
动画在前端开发中是经常遇到的场景之一,加入动画后页面可以极大的提升用户体验。 绝大多数简单的动画场景可以直接通过CSS实现,对于一些特殊场景的动画可能会使用到JS计算实现,通过本文的学习,可以让你在一些看似需要使用JS实现的动画场景,使用纯CSS一样可以实...
继续阅读 »

动画在前端开发中是经常遇到的场景之一,加入动画后页面可以极大的提升用户体验。




绝大多数简单的动画场景可以直接通过CSS实现,对于一些特殊场景的动画可能会使用到JS计算实现,通过本文的学习,可以让你在一些看似需要使用JS实现的动画场景,使用纯CSS一样可以实现,并且更方便快捷。



先看一个简单的例子:一个方块的位置随着滑条滑动的位置改变
在这里插入图片描述


这个场景实现起来很简单,滑条值改变后,使用JS计算方块应该移动的距离,然后将方块定位到指定位置即可。代码如下:


.box {
height: 50px;
width: 50px;
background-color: aquamarine;
}
<div class="box">div>
<input type="range" min="0" max="1" step="0.01"/>



现在稍微增加一些动画效果:



  • 方块在中间位置时缩放为原来的一半大小

  • 方块在中间位置时变成球形

  • 方块从红色变为绿色


在这里插入图片描述


对于大小和圆角,同样可以使用简单的JS进行计算实现,但是对于颜色变化,使用JS计算将会是一个非常复杂的过程。


先抛开动画跟随滑条运动这个要求,如果使用CSS实现上面从0-1的动画过程是一个很简单的事:
在这里插入图片描述


.box {
height: 50px;
width: 50px;
background-color: aquamarine;
transform: translateX(0);
animation: run 1s linear forwards;
}
@keyframes run {
0% {
transform: translateX(0) scale(1);
border-radius: 0%;
background: red;
}
50% {
transform: translateX(100px) scale(.5);
border-radius: 50%;
}
100% {
transform: translateX(200px) scale(1);
border-radius: 0%;
background: green;
}
}

利用CSS动画帮我们可以很轻松的计算出每个时间点时的状态,现在的问题就变成如何让动画停留在指定的时间点,这就需要使用到动画的两个属性:


annimation-play-state:设置动画是运行还是暂停,有两个属性值runing、paused
annimation-delay:设置动画开始时间的偏移量,如果是正值,则动画会延迟开始;如果是负值(-d),动画会立即开始,开始位置在动画(d)s时所处的位置。


有了这两个属性,现在将上面的动画停留在50%的位置
在这里插入图片描述


假设整个动画过程需要1s,50%的位置则需要将延迟值设置为-0.5s,这样动画就会停留在0.5s的位置。


.box {
height: 50px;
width: 50px;
background-color: aquamarine;
transform: translateX(0);
animation: run 1s -0.5s linear forwards infinite paused;
}

接下来只需要将滑条的值与动画延迟的值关联起来即可,这里可以通过CSS变量来实现:


.box {
--duration: -0.5s; // 定义延迟变量
height: 50px;
width: 50px;
background-color: aquamarine;
transform: translateX(0);
animation: run 1s var(--duration) linear forwards infinite paused;
}

@keyframes run {
0% {
transform: translateX(0) scale(1);
border-radius: 0%;
background: red;
}
50% {
transform: translateX(100px) scale(.5);
border-radius: 50%;
}
100% {
transform: translateX(200px) scale(1);
border-radius: 0%;
background: green;
}
}



应用场景



利用CSS延迟动画可以轻松实现很多交互场景,例如:跟随鼠标滚动界面发生反馈动画、根据当天时间界面从日出到日落、根据不同分值出现不同表情变化等等。
在这里插入图片描述




作者:前端筱园
来源:juejin.cn/post/7363094767557378099
收起阅读 »

实现 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 是暴露出去的 box 的实时目标尺寸
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)
}
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 大小
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
在业务运营中,倒计时功能是常见的需求,尤其是在限时秒杀等促销活动中。为了确保时间的精确性和一致性,推荐使用服务器时间作为倒计时的基准。那么,如何在前端实现一个既准确又用户友好的倒计时组件的计时逻辑呢? 传统计时器实现 传统计时器实现倒计时的核心原理很简单,它使...
继续阅读 »

在业务运营中,倒计时功能是常见的需求,尤其是在限时秒杀等促销活动中。为了确保时间的精确性和一致性,推荐使用服务器时间作为倒计时的基准。那么,如何在前端实现一个既准确又用户友好的倒计时组件的计时逻辑呢?


传统计时器实现


传统计时器实现倒计时的核心原理很简单,它使用了 setIntervalsetTimeout 的对计时信息进行更新。类似于如下代码:


import React, { useState, useEffect } from 'react';

const CountdownTimerReact.FC<{ durationnumber }> = ({ duration }) => {
  const [secondsRemaining, setSecondsRemaining] = useState(duration);

  useEffect(() => {
    const intervalId = setInterval(() => {
      if (secondsRemaining > 0) {
        setSecondsRemaining(secondsRemaining - 1);
      } else {
        clearInterval(intervalId);
      }
    }, 1000);

    // 清理计时器
    return () => clearInterval(intervalId);
  }, [secondsRemaining]);

  return (
    <div>
      倒计时: {secondsRemaining} 秒
    </div>

  );
};

export default CountdownTimer;

上述代码实现很好地实现了倒计时逻辑,但是,还是存在一些问题。我们先来讨论一下浏览器事件循环关于延时队列的优先级。我们知道,为了有效地管理任务和事件,事件循环使用了一个队列系统。事件循环主要包含如下两个队列:



  1. 宏任务队列(Macro Task Queue) :包括如 setTimeoutsetInterval、I/O、UI 事件等。

  2. 微任务队列(Micro Task Queue) :包括Promise回调、MutationObserver 等。


在事件循环中,当一个宏任务执行完毕后,JavaScript 引擎会先清空所有微任务队列中的所有任务,然后再去检查是否需要执行下一个宏任务。这意味着微任务的优先级高于宏任务。


setTimeoutsetInterval 任务会在指定的延时后被加入到宏任务队列的末尾。当当前的宏任务执行完毕后,如果微任务队列不为空,JavaScript 引擎会先执行完所有微任务,然后才会执行下一个宏任务,也就是 setTimeoutsetInterval 中的回调函数。因此,setTimeoutsetInterval 的优先级是相对较低的,因为它们必须等待当前宏任务执行完毕以及所有微任务执行完毕后才能执行。


这种机制可能导致一个问题:如果页面上的其他微任务执行时间较长,倒计时显示可能会出现“跳秒”现象。例如,倒计时可能从 60 秒直接跳到 58 秒,而不是平滑地递减。


requestAnimationFrame 实现


针对上述“跳秒”问题,我们可以改用 requestAnimationFrame 去进行时间的更新逻辑执行。我们将上述代码修改为如下代码:


import React, { useState, useEffect } from 'react';

const CountdownTimerReact.FC<{ durationnumber }> = ({ duration }) => {
  const [secondsRemaining, setSecondsRemaining] = useState(duration);

  useEffect(() => {
    let animationFrameIdnumber;

    const updateTimer = () => {
      if (secondsRemaining > 0) {
        setSecondsRemaining(prev => prev - 1);
        animationFrameId = requestAnimationFrame(updateTimer);
      } else {
        cancelAnimationFrame(animationFrameId);
      }
    };

    // 启动动画帧
    animationFrameId = requestAnimationFrame(updateTimer);

    // 清理动画帧
    return () => cancelAnimationFrame(animationFrameId);
  }, [secondsRemaining]);

  return (
    <div>
      倒计时: {secondsRemaining} 秒
    </div>

  );
};

export default CountdownTimer;

在编写倒计时功能的代码时,我们应当确保在每次更新倒计时秒数后重新启动动画帧。这样做可以避免在动画帧完成后,倒计时逻辑停止更新,导致倒计时在减少一秒后不再继续。同时,为了确保资源的有效管理,我们还需要提供一个函数来清理动画帧,这样当组件不再需要时,可以停止执行动画帧,避免不必要的性能消耗。通过这些措施,我们可以保证倒计时功能的准确性和组件的高效卸载。


优势


要深入理解 requestAnimationFrame 在实现倒计时中的优势,我们首先需要探讨一个问题:在 requestAnimationFrame 中直接修改 DOM 是否合适?requestAnimationFrame 是一个专为动画效果设计的 Web API,它通过在浏览器的下一次重绘之前调用回调函数,帮助我们创建更加流畅且高效的动画。与传统的定时器方法(如 setTimeoutsetInterval)相比,requestAnimationFrame 提供了更优的性能和更少的资源消耗。


requestAnimationFrame 中修改 DOM 是合适的,尤其是当涉及到动画和视觉更新时。这是因为 requestAnimationFrame 的设计初衷就是为了优化动画性能,确保动画的流畅性和效率。总结来说,requestAnimationFrame 相较于传统的计时器方法,具有以下显著优势:



  • 性能优化:通过在浏览器的下一次重绘前调用回调,确保动画的流畅性。

  • 节能高效:当浏览器标签页不处于活跃状态时,requestAnimationFrame 会自动暂停,从而减少 CPU 的使用,延长设备电池寿命。

  • 同步刷新:能够与浏览器的刷新率同步,有效避免动画中的跳帧现象。


因此,requestAnimationFrame 不仅适用于复杂的动画场景,也非常适合实现需要精确时间控制的倒计时功能,提供了一种更加高效和节能的解决方案。


劣势


尽管 requestAnimationFrame 在动画制作方面表现出色,但在实现倒计时功能时,它也存在一些局限性:



  • 精确度问题requestAnimationFrame 并不适用于需要严格时间控制的场景。因为它的调用时机依赖于浏览器的重绘周期,这可能导致时间间隔的不稳定性。

  • 管理复杂性:使用 requestAnimationFrame 需要开发者手动管理动画状态和进行资源清理,这增加了实现的复杂度。


正因如此,许多现代前端框架和库,如 ahook 等,在选择实现倒计时功能时,倾向于采用传统的定时器(如 setTimeoutsetInterval),而非 requestAnimationFrame。这些传统方法虽然可能不如 requestAnimationFrame 在动画性能上优化,但它们提供了更稳定和可预测的时间控制,这对于倒计时这类功能来说至关重要。


总结


实现一个倒计时组件的计时逻辑,我们有如下的一些建议:



  1. 动画与浏览器同步:对于涉及动画或需要与浏览器重绘周期同步的任务,requestAnimationFrame 是一个理想的选择。它能够确保动画的流畅性和性能优化。

  2. 体验优化:为了进一步提升用户体验,可以利用 performance.now() 来提高时间控制的精度。这个高精度的时间戳 API 可以帮助你更准确地计算时间间隔,从而优化倒计时的显示效果。

  3. 时间控制与简易任务:如果你的应用场景需要精确的时间控制或涉及到简单的定时任务,传统的 setTimeoutsetInterval 方法可能更加方便和直观。它们提供了稳定的时间间隔,易于理解和实现。


总结来说,选择最合适的技术方案取决于你的具体需求。无论是 requestAnimationFrame 还是传统的定时器方法,都有其适用场景和优势。关键在于根据项目需求,做出明智的选择,以实现最佳的用户体验。


作者:You1i
来源:juejin.cn/post/7412951456549175306
收起阅读 »

多人开发小程序设置体验版的痛点

web
抛出痛点 在分配任务时,我们将需求分为三个分支任务,分别由前端A、B、C负责: 前端A: HCC-111-实现登录功能 前端B: HCC-112-实现用户注册 前端C: HCC-113-实现用户删除 相应地,我们创建三个功能分支: feature_HCC...
继续阅读 »

抛出痛点


在分配任务时,我们将需求分为三个分支任务,分别由前端A、B、C负责:



  1. 前端A: HCC-111-实现登录功能

  2. 前端B: HCC-112-实现用户注册

  3. 前端C: HCC-113-实现用户删除


相应地,我们创建三个功能分支:



  • feature_HCC-111-实现登录功能

  • feature_HCC-112-实现用户注册

  • feature_HCC-113-实现用户删除


当所有的前端都开发完成了他们的任务,我们就要开始测试小程序了。但是如果按照以往体验版的测试方式,我们就需要排个顺序。比如,前端 A 先将他的小程序设置为体验版,测试把他的功能测试完成之后,再把前端 B 的设置为体验版,以此类推。可以看出真的很麻烦,而且浪费开发时间,我想你肯定不想在开发的时候突然被叫把你的小程序版本设置为体验版。


解决方案


小程序开发助手 这是一个官方提供的小程序,里面有多个版本的小程序可供选择,很方便测试人员的测试,并且也会节省开发人员的时间。点击版本查看就可以看到所有开发人员提交的最近的一次版本了。这样也不用设置体验版就可以测试最新的提交了。


image.png


再次抛出痛点


如果前端 A 头上有三个任务单呢?任务单:HCC-121-实现框架搭建,HCC-122-实现在线录屏,HCC-123-实现画板。此时你可能想说, 为啥前端 A 这么多的任务单呢?他命苦啊!


这个时候就需要配合微信的机器人了,我们可以创建多个机器人作为我们提交版本的媒介,这样我们就不受限于微信账号了。


可以在微信的官方文档看到 robot 参数有30个机器人可供选择。


请添加图片描述


接下来看下微信的机器人的使用方式。


miniprogram-ci文档


微信官方是这样介绍这个工具的; miniprogram-ci 是从微信开发者工具中抽离的关于小程序/小游戏项目代码的编译模块。它其实是一个自动上传代码的工具,可以帮助我们自动化的编译代码并且上传到微信。


下面是一个大概得使用的示例,具体还是要参考官方文档。


const ci = require('miniprogram-ci');
(async () => {
const project = new ci.Project({
appid: 'wxsomeappid',
type: 'miniProgram',
projectPath: 'the/project/path',
privateKeyPath: 'the/path/to/privatekey',
ignores: ['node_modules/**/*'],
})
const previewResult = await ci.preview({
project,
desc: 'hello', // 此备注将显示在“小程序助手”开发版列表中
setting: {
es6: true,
},
qrcodeFormat: 'image',
qrcodeOutputDest: '/path/to/qrcode/file/destination.jpg',
onProgressUpdate: console.log,
// pagePath: 'pages/index/index', // 预览页面
// searchQuery: 'a=1&b=2', // 预览参数 [注意!]这里的`&`字符在命令行中应写成转义字符`\&`
})
console.log(previewResult)
})()

当我们使用这个脚本上传完代码就可以在小程序开发助手或者小程序管理平台看到以下内容。


微信管理后台
请添加图片描述
小程序开发助手页面
在这里插入图片描述


最后


我们可以使用 miniprogram-ci 配合 Jenkins 实现自动化部署,提交完成代码就可以自动部署了。以下是一个 github 的 actions 示例。当然也可以使用别的方式,例如本地提交,Jenkins提交等。


name: Feature Branch CI

on:
workflow_dispatch:
push:
branches: ['feature_*'] # 使用通配符匹配所有feature分支

jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'yarn'

- name: Install dependencies
run: |
npm install -g miniprogram-ci cross-env
yarn install --frozen-lockfile

- name: Build Package
run: yarn cross-env ENV=PROD uni build -p mp-weixin --mode PROD

- name: Create private key file
run: echo "${{ secrets.PRIVATE_KEY }}" > private.key

- name: Deploy Package
env:
APP_ID: ${{ secrets.APP_ID }}
run: |
COMMIT_MESSAGE=$(git log --format=%B -n 1 ${{ github.sha }})
if [[ $COMMIT_MESSAGE =~ VERSION-([A-Za-z0-9_]+-[A-Za-z0-9_-]+)_DEV ]]; then
VERSION=${BASH_REMATCH[1]}
echo "Extracted Version: $VERSION"
miniprogram-ci preview \
--pp ./dist/build/mp-weixin \
--pkp ./private.key \
--appid $APP_ID \
--uv "${VERSION}" \
-r 7 \
--desc "${COMMIT_MESSAGE}" \
--upload-description "${COMMIT_MESSAGE}" \
--enable-es6 true \
--enable-es7 true \
--enable-minifyJS true \
--enable-minifyWXML true \
--enable-minifyWXSS true \
--enable-minify true \
--enable-bigPackageSizeSupport true \
--enable-autoPrefixWXSS true
else
echo "No Version found in commit message. Skipping upload."
fi

作者:阿臻
来源:juejin.cn/post/7412854873439027240
收起阅读 »

仿树木生长开花的动画效果

web
效果介绍 使用 canvas 进行绘制树木生长的效果,会逐渐长出树干,长出树叶,开出花。当窗口大小发生变化时进行重新渲染。 实现效果展示 实现步骤 创建画布 import React, { useEffect, useRef } from 'react'...
继续阅读 »

效果介绍



使用 canvas 进行绘制树木生长的效果,会逐渐长出树干,长出树叶,开出花。当窗口大小发生变化时进行重新渲染。



实现效果展示


Dec-06-2023 14-39-01.gif


实现步骤


创建画布


import React, { useEffect, useRef } from 'react'

function TreeCanvas(props: {
width: number;
height: number;
}
) {
const { width = 400, height = 400 } = props;
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas?.getContext('2d');
if (!canvas || !context) return;
context.strokeStyle = '#a06249';
}, [])
return (
<canvas ref={canvasRef} width={width} height={height} />
)
}

export default TreeCanvas

封装创建树枝的方法



  • 树枝需要起点,终点,树枝宽度


 function lineTo(p1: PointType, p2: PointType, lineWidth: number) {
context?.beginPath();
context?.moveTo(p1.x, p1.y);
context?.lineTo(p2.x, p2.y);
context.lineWidth = lineWidth;
context?.stroke();
}


绘制树叶和花朵的方法封装



  • 提前生成图片实例

  • 传递图片和坐标进行绘制


   // 花的实例
const image = new Image();
image.src ='https://i.postimg.cc/D0LLWwKy/flower1.png';
// 叶子的实例
const imageLeaves = new Image();
imageLeaves.src = 'https://i.postimg.cc/PJShQmH6/leaves.png';

function drawTmg(imageUrl: any, p1: PointType) {
context?.drawImage(imageUrl, p1.x, p1.y, 20 * Math.random(), 20 * Math.random());
}

封装绘制处理



  • 提供绘制的起点,计算绘制的终点

  • 根据起点和终点进行绘制


// 计算终点
function getEnd(b: BranchType) {
const { start, theta, length } = b;
return {
x: start.x + Math.cos(theta) * length,
y: start.y + Math.sin(theta) * length
};
}
// 绘制整理
function drawBranch(b: BranchType) {
// 绘制树干
lineTo(b.start, getEnd(b), b.lineWidth);
if (Math.random() < 0.4) { // 绘制花朵的密度
drawTmg(image, getEnd(b));
}
if (Math.random() < 0.4) {
drawTmg(imageLeaves, b.start); // 绘制树叶的密度
}
}

绘制树的方法



  • 起点和终点的计算及绘制数的角度计算

  • 绘制左边树和右边树

  • 随机绘制


function step(b: BranchType, depth: number = 0) {
const end = getEnd(b);
drawBranch(b);
if (depth < depthNum || Math.random() < 0.5) {
step(
{
start: end,
theta: b.theta - 0.3 * Math.random(),
length: b.length + (Math.random() * 10 - 5),
lineWidth: depthNum - depth
},
depth + 1
);
}
if (depth < depthNum || Math.random() < 0.5) {
step(
{
start: end,
theta: b.theta + 0.3 * Math.random(),
length: b.length + (Math.random() * 10 - 5),
lineWidth: depthNum - depth
},
depth + 1
);
}
}

动画处理



  • 把所有绘制添加到动画处理中


const pendingTasks: Function[] = []; // 动画数组
function step(b: BranchType, depth: number = 0) {
const end = getEnd(b);
drawBranch(b);
if (depth < depthNum || Math.random() < 0.5) {
pendingTasks.push(() => { // 添加左侧动画
step(
{
start: end,
theta: b.theta - 0.3 * Math.random(), // 角度变化
length: b.length + (Math.random() * 10 - 5), // 长度变化
lineWidth: depthNum - depth
},
depth + 1
);
});
}
if (depth < depthNum || Math.random() < 0.5) {
pendingTasks.push(() => { // 添加右侧动画
step(
{
start: end,
theta: b.theta + 0.3 * Math.random(), // 角度变化
length: b.length + (Math.random() * 10 - 5),// 长度变化
lineWidth: depthNum - depth
},
depth + 1
);
});
}
}
function frame() {
const tasks = [...pendingTasks];
pendingTasks.length = 0;
tasks.forEach((fn) => fn());
}
let framesCount = 0;
function satrtFrame() {
requestAnimationFrame(() => {
framesCount += 1;
// if (framesCount % 10 === 0) {
frame();
satrtFrame();
// }
});
}

封装执行方法


useEffect(() => {
function init() {
step(startBranch);
}
satrtFrame();
init();
}, []);

添加常用场景封装



  • 宽高获取当前屏幕大小

  • 屏幕发生变化时进行重新渲染


export const TreeCanvasInner = () => {
const [innerSize, setInnerSize] = useState({ x: window.innerWidth, y: window.innerHeight });
useEffect(() => {
const resizeFunc = () => {
setInnerSize({ x: window.innerWidth, y: window.innerHeight });
};
window.addEventListener('resize', resizeFunc);
return () => {
window.removeEventListener('resize', resizeFunc);
};
}, []);
return (
<TreeCanvas
key={JSON.stringify(innerSize)}
width={innerSize.x}
height={innerSize.y}
startBranch={{ start: { x: 0, y: 0 }, theta: 20, length: 25, lineWidth: 3 }}
/>

);
};

完整代码


生长的树木和花朵


作者:前端了了liaoliao
来源:juejin.cn/post/7309061655095361571
收起阅读 »

前端纯css实现-一个复选框交互展示效果

web
纯手工写一个复选框前端交互,这样可以自由定制自己想要展示的字段和设计风格 写这篇文章主要是抛砖引玉,可能存在槽点,大家多多担待! 1.交互效果展示 用码上掘金在线简单的写了一下: 2.简要说明 $primary-color:#1e80ff; // 主题色-掘...
继续阅读 »

纯手工写一个复选框前端交互,这样可以自由定制自己想要展示的字段和设计风格
写这篇文章主要是抛砖引玉,可能存在槽点,大家多多担待!


1.交互效果展示


用码上掘金在线简单的写了一下:



2.简要说明


$primary-color:#1e80ff; // 主题色-掘金蓝


$primary-disable: #7ab0fd; // 只读或禁用色


可以根据实际需求更改主题色,这里的禁用变量色忘记使用了,sorry!!!


image.png


3.布局代码部分


  <!-- page start -->
<div class="ui-layout-page">
<h1>请选择关注类型</h1>
<div class="ui-checkbox">
<!-- 复选框 item start -->
<div
:class="{'ui-item-box':true,'ui-item-check': i.isCheck,'ui-item-disable':i.disable}"
v-for="(i,index) in list"
:key="index"
@click="doCheck(i)">

<img :src="i.icon"/>
<span class="span-bar">
<p class="label-bar">{{i.label}}</p>
<p class="desc-bar">{{i.desc}}</p>
</span>
<!-- 选中标识 start -->
<span
v-if="i.isCheck"
class="icon-check">

</span>
<!-- 选中标识 end -->
</div>
<!-- 复选框 item end -->
</div>
<p style="font-size:12px;color:#333">当前选择ids:{{ this.checked.join(',') }}</p>
</div>
<!-- page end -->

4.方法和数据结构部分



checked:['1','2'],
list:[
{
label:'JYM系统消息',
id:'1',
desc:'关注掘金系统消息',
isCheck:true,
icon:'https://gd-hbimg.huaban.com/6f3e3ff111c6c98be6785d9eddd5b13f8979ef9d1719e-Xwo8QB_fw658webp',
disable:true,
},{
label:'JYM后端',
id:'2',
isCheck:true,
desc:'关注后端讨论区新消息',
icon:'https://gd-hbimg.huaban.com/e2622fe339d655bd17de59fed3b0ae0afb9a16c31db25-YNpnGV_fw658webp',
disable:false,
},{
label:'JYM前端',
id:'3',
isCheck:false,
desc:'关注前端讨论区新消息',
icon:'https://gd-hbimg.huaban.com/80765200aa4ffb7683ddea51c3063b0801874fb86324-3OVCQN_fw1200',
disable:false,
},{
label:'JYM开发工具',
id:'4',
isCheck:false,
desc:'关注开发工具讨论区新消息',
icon:'https://gd-hbimg.huaban.com/ef1c0e1fb2eae73d674aae791526a331b45b26d2b78e-r4p1aq_fw1200',
disable:false,
}
]

/**
* 复选点击事件
* el.disable 禁用状态
* */

doCheck(el){
if(el.disable) return
if(this.checked.includes(el.id)){
el.isCheck = false
this.checked=this.checked.filter(item => item !== el.id);
} else{
el.isCheck = true
this.checked.push(el.id)
}
this.checked.join(',')
}

5.样式控制部分


.ui-layout-page{ 
padding:20px;
h1{
font-size:16px;
}

// 个性化复选框 css start -------------
.ui-checkbox{
width:100%;

$primary-color:#1e80ff; // 主题色-掘金蓝
$primary-disable: #7ab0fd; // 只读或禁用色

// 选中状态css
.ui-item-check{
border:1px solid $primary-color !important;
background:rgba($primary-color,0.05) !important;
}

// 禁用状态css
.ui-item-disable{
border:1px solid #d3d3d3 !important;
background: #f3f3f3 !important;
cursor:not-allowed !important;
.icon-check{
border-top:20px solid #ccc !important;
}
.label-bar{
color:#777 !important;
}
.desc-bar{
color:#a3a3a3 !important;
}
}

// 常规状态css
.ui-item-box{
position:relative;
display:inline-flex;
align-items: center;
width:220px;
height:70px;
border:1px solid #ccc;
cursor: pointer;
margin:0px 8px 8px 0px;
border-radius:4px;
overflow:hidden;

&:hover{
border:1px solid $primary-color;
background:rgba($primary-color,0.05);
}

img{
width:38px;
height:38px;
margin-left:15px;
}
p{
margin:0px;
}
.span-bar{
width:0px;
flex:1 0 auto;
padding:0px 10px;

.label-bar{
font-size:14px;
font-weight:700;
margin-bottom:4px;
color:#333;
}
.desc-bar{
font-size:12px;
color:#999;
}
}
// 绘制圆角斜三角形
.icon-check{
position:absolute;
width:0px;
height:0px;
top:2px;
right:2px;
border-top:20px solid $primary-color;
border-left:25px solid transparent;
border-radius: 5px 3px 5px 0px;
&:after{
content:'✓';
position: relative;
color:#fff;
font-size:12px;
left: -12px;
top: -26px;
}
}
}
}
// 个性化复选框 css end -------------
}

作者:黑白琴键
来源:juejin.cn/post/7412545166539128841
收起阅读 »

CSS 实现呼吸灯

web
引言 在现代前端开发中,为网站添加吸引人的动画效果是提高用户体验的一种常见方式。其中,呼吸灯效果是一种简单而又引人注目的动画,适用于各种应用场景。本文将深入研究如何使用 CSS 来实现呼吸灯效果,包括基本的实现原理、动画参数调整、以及一些实际应用案例。 第一部...
继续阅读 »

引言


在现代前端开发中,为网站添加吸引人的动画效果是提高用户体验的一种常见方式。其中,呼吸灯效果是一种简单而又引人注目的动画,适用于各种应用场景。本文将深入研究如何使用 CSS 来实现呼吸灯效果,包括基本的实现原理、动画参数调整、以及一些实际应用案例。


第一部分:基本的呼吸灯效果


1. 使用关键帧动画


呼吸灯效果的实现依赖于 CSS 的关键帧动画。我们可以使用 @keyframes 规则定义一个简单的呼吸灯动画。


@keyframes breathe {
0% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.5;
transform: scale(1);
}
}

.breathing-light {
animation: breathe 3s infinite;
}

在这个例子中,我们定义了一个名为 breathe 的关键帧动画,包含三个关键帧(0%、50%、100%)。在不同的关键帧,我们分别调整了透明度和缩放属性,从而形成了呼吸灯效果。


2. 应用到元素


接下来,我们将这个动画应用到一个元素上,例如一个 div


<div class="breathing-light"></div>

通过给这个元素添加 breathing-light 类,我们就能够观察到呼吸灯效果的实现。可以根据实际需求调整动画的持续时间、缓动函数等参数。


第二部分:调整动画参数


1. 调整动画持续时间


通过调整 animation 属性的第一个值,我们可以改变动画的持续时间。例如,将动画持续时间改为 5 秒:


.breathing-light {
animation: breathe 5s infinite;
}

2. 调整缓动函数


缓动函数影响动画过渡的方式。可以通过 animation-timing-function 属性来调整。例如,使用 ease-in-out 缓动函数:


.breathing-light {
animation: breathe 3s ease-in-out infinite;
}

3. 调整动画延迟时间


通过 animation-delay 属性,我们可以设置动画的延迟时间。这在创建多个呼吸灯效果不同步的元素时很有用。


.breathing-light {
animation: breathe 3s infinite;
animation-delay: 1s;
}

第三部分:实际应用案例


1. 页面标题的动态效果


在页面的标题上应用呼吸灯效果,使其在页面加载时引起用户的注意。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breathing Light Title</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1 class="breathing-light">Welcome to Our Website</h1>
</body>
</html>

@keyframes breathe {
0% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.5;
transform: scale(1);
}
}

.breathing-light {
animation: breathe 3s infinite;
}

2. 图片边框的动感效果


通过为图片添加呼吸灯效果,为静态图片增加一些生动感。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breathing Light Image</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="image-container">
<img src="example-image.jpg" alt="Example Image" class="breathing-light">
</div>
</body>
</html>

@keyframes breathe {
0% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.5;
transform: scale(1);
}
}

.breathing-light {
animation: breathe 3s infinite;
}

.image-container {
display: inline-block;
overflow: hidden;
border: 5px solid #fff; /* 图片边框 */
}

结语


通过本文,我们深入探讨了如何使用 CSS 实现呼吸灯效果。从基本原理、动画参数调整到实际应用案例,希望读者能够深刻理解呼吸灯效果的制作过程,并能够在实际项目中灵活运用这一技术,为用户呈现更加生动有趣的页面效果。不仅如此,这也是提升前端开发技能的一种乐趣。


作者:饺子不放糖
来源:juejin.cn/post/7315314479204581391
收起阅读 »

文本美学:text-image打造视觉吸引力

web
当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。 项目介绍 话不多说,我们先看下作者的demo效果: _202...
继续阅读 »

当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。


项目介绍


话不多说,我们先看下作者的demo效果:


微信截图_20240420194201.png


_20240420194201.jpg


text-image可以将文字、图片、视频进行「文本化」


只需要通过简单的配置即可使用。


虽然这个项目star数很少,但确实是一个很有意思的项目,使用起来很简单的项目。


_20240420194537.jpg


_20240420194537.jpg


github地址:https://github.com/Sunny-117/text-image


我也是使用这个项目做了一个简单的web页面,感兴趣的家人可以使用看下效果:


web地址:http://h5.xiuji.mynatapp.cc/text-image/


_20240420211509.jpg


_20240420211509.jpg


项目使用


这个项目使用起来相对简单,只需按作者的文档使用即可,虽然我前端属于小白的水平,但还是在ai的帮助下做了一个简单的html页面,如果有家人需要的话可以私信我,我发下文件。下边我们就介绍下:



  • 文字「文本化」


先看效果:


_20240420195701.jpg


_20240420195701.jpg


我们在这儿是将配置的一些参数在页面上做了一个可配置的表单,方便我们配置。


家人们想自己尝试的话可以试下以下这个demo。


demo.html


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
replaceText: '123',
source: {
text: '修己xj',
},
});
</script>
</body>
</html>


  • 图片「文本化」


_20240420200651.jpg


_20240420200651.jpg


demo.html


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

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

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 7,
isGray: true,
source: {
img: './assets/1.png',
},
});
</script>
</body>

</html>


  • 视频「文本化」


动画1.gif


1.gif


demo.html


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

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

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>

<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 8,
isGray: true,
source: {
video: './assets/1.mp4',
height: 700,
},
});
</script>
</body>

</html>

需要注意的是:作者在项目中提供的视频的demo这个属性值有错误,我们需要改正后方可正常显示:


_20240420211124.jpg


_20240420211124.jpg


总结


text-image 是一个强大的前端工具,可以帮助用户快速、轻松地将文本、图片、视频转换成文本化的图片,增强文本内容的表现力和吸引力。


作者:修己xj
来源:juejin.cn/post/7359510120248786971
收起阅读 »

排行榜--实现点击视图自动滚动到当前用户所在位置.

web
需求 我们今天来实现一下,点击当前用户的div, 自动滚动到用户在排行榜中的位置. 效果 大家可以先看一下下面的GIF, 所实现的效果. 实现 1. 准备DOM 结构 首先,我们在进行列表建设的时候, 需要准备好一个数据. 因为此处我们是使用的vue3来进行...
继续阅读 »

需求


我们今天来实现一下,点击当前用户的div, 自动滚动到用户在排行榜中的位置.


效果


大家可以先看一下下面的GIF, 所实现的效果.


QQ2024817-122950.gif


实现


1. 准备DOM 结构


首先,我们在进行列表建设的时候, 需要准备好一个数据. 因为此处我们是使用的vue3来进行编写. 对于列表我们使用的是v-for列表渲染来做的. 在渲染的时候, 我们需要给每一个列表项(当前就是每一个用户项)添加一个自定义属性. 具体的话, 可以看下 下方的关键代码.


image.png


核心代码就是


 <div v-for="(item, index) in rankingData" :key="item.user.id" :data-key="item.user.id"
</div>



因为数据是后端返回的, 是包含的user_id,而且这个user_id 是不可能重复的. 我们只要保证每个列表的自定义的属性是唯一的即可.



image.png


2. 绑定方法,实现方法


接下来,我们需要考虑的是,在点击的时候,如何获取到当前的dom. 这对我们目前来说就很容易了, 因为我们可以根据据user_id 拿到我们当前点击的dom.


添加一个方法


<!-- 当前用户排名情况 -->
<div class="text-white w-[100%] ...." @click="scrollToCurrentRankingPosition(userId)">

实现方法.


第一步: 拿到rankingList的dom实例.


这里我们通过vue3提供ref拿到dom. 可以看下
模板引用


<div v-else class=" overflow-auto bg-white" ref="rankingList">

const rankingList = ref(null);

第二步: 根据userId获取到具体的DOM


const currentItem = rankingList.value.querySelector(`[data-key="${id}"]`);

第三步: 使用scrollIntoView方法滚动视图到当前选中的元素


 // 平滑滚动到当前元素
currentItem.scrollIntoView({ behavior: 'smooth', block: 'center' });

scrollIntoView方法 讲解:



  • Element 接口的 scrollIntoView() 方法会滚动元素的父容器,使被调用 scrollIntoView() 的元素对用户可见。


简单来讲就是被调用的者的元素出现在用户的视线里面.
scrollIntoView() 方法有三种调用形式:



  1. scrollIntoView():无参数调用,元素将滚动到可视区域顶部,如果它是第一个可见元素。

  2. scrollIntoView(alignToTop):接受一个布尔值参数,决定元素是与滚动区的顶部还是底部对齐。

  3. scrollIntoView(scrollIntoViewOptions):接受一个对象作为参数,提供了更多的滚动选项。


参数



  • alignToTop(可选):布尔值,控制元素滚动到顶部还是底部对齐。默认为 true(顶部对齐)。

  • scrollIntoViewOptions(可选实验性):对象,包含以下属性:



    • behavior:定义滚动行为是平滑动画还是立即发生。可取值有 smooth(平滑动画)、instant(立即发生)或 auto(由CSS的 scroll-behavior 属性决定)。

    • block:定义垂直方向的对齐方式,可取值有 startcenterend 或 nearest。默认为 start

    • inline:定义水平方向的对齐方式,可取值有 startcenterend 或 nearest。默认为 nearest




目前我们实现了效果.


QQ2024817-13328.gif


但是我们发现,还可以继续改进, 目前我们虽然滚动到了屏幕的中间, 但是我们很难去发现. 所以我们可以继续完善一下这个方法. 就是滚动到视图的中间的同时高亮选中的DOM.


3. 额外扩展, 高亮当前的元素


定义一个两个方法,一个用于应用样式, 一个应用于移除样式.


const applyHighlightStyles = (element) => {
element.style.transition = 'background-color 1s ease, border-color 1s ease';
element.style.border = '1px solid transparent'; // 预定义边框样式
element.style.borderColor = '#006cfe'; // 设置边框颜色
element.style.backgroundColor = '#cfe5ff'; // 设置背景色为浅蓝色
};

const removeHighlightStyles = (element) => {
element.style.backgroundColor = ''; // 移除背景色
element.style.borderColor = 'transparent'; // 移除边框颜色
};

然后再在我们之前的方法的后面加入代码


            // 设置高亮显示的样式
applyHighlightStyles(currentItem);

// 清除之前的定时器(如果有)
if (currentItem._highlightTimer) {
clearTimeout(currentItem._highlightTimer);
}

// 设置定时器,2秒后移除高亮显示
currentItem._highlightTimer = setTimeout(() => {
removeHighlightStyles(currentItem);
currentItem._highlightTimer = null;
}, 2000);


然后在组件卸载前记得清除定时器.


onUnmounted(() => {
if (rankingList.value) {
// 遍历所有项目,清除定时器
rankingList.value.querySelectorAll('[data-key]').forEach(item => {
if (item._highlightTimer) {
clearTimeout(item._highlightTimer);
item._highlightTimer = null;
}
});
}
});

效果:
image.png


总结


整体下来的思路就是:



  1. v-for的时候, 给每个循环的元素添加一个自定义的属性.(value:user_id), 不重复且能标识每个元素.

  2. 点击之后,拿到id,透传给方法,然后通过id获取到当前的元素.

  3. 使用Element.scrollIntoView(), 将当前的选中的DOM自动滚动视图的中间.

  4. 高亮显示当前的元素之后(2s)进行取消高亮.


作者:心安事随
来源:juejin.cn/post/7403576996393910308
收起阅读 »

开箱即用的web打印和下载,自动分页不截断

web
哈喽哈喽🌈 哈喽大家好!我是小周🤪🤪🤪。相信各位前端小伙伴都知道可以用window.print()这个方法来调用打印机实现打印功能,但是直接下载功能window.print()还是无法实现的。今天我来介绍另外一种实现方式,真正的开箱即用,既可以实现打印和直接下...
继续阅读 »

哈喽哈喽🌈


哈喽大家好!我是小周🤪🤪🤪。相信各位前端小伙伴都知道可以用window.print()这个方法来调用打印机实现打印功能,但是直接下载功能window.print()还是无法实现的。今天我来介绍另外一种实现方式,真正的开箱即用,既可以实现打印和直接下载,也可以防止内容截断。


技术栈


1、html2canvas


html2canvas 一个可以将html转换成canvas的三方库


2、jsPDF


jsPDF 生成pdf文件的三方库


一些用到的方法介绍


1、canvas.getContext('2d').getImageData()


canvas.getContext('2d').getImageData() 是 HTML5 Canvas API 中用于获取指定区域的像素数据的方法。它返回一个 ImageData 对象,该对象包含了指定区域中每个像素的颜色和透明度信息。


canvas.getContext('2d').getImageData(x, y, width, height)

参数说明:



  • x: 采集的图像区域的左上角的水平坐标。

  • y: 采集的图像区域的左上角的垂直坐标。

  • width: 采集的图像区域的宽度(以像素为单位)。

  • height: 采集的图像区域的高度(以像素为单位)。


返回值:


返回一个 ImageData 对象,包含以下属性:



  • width: 图像数据的宽度。

  • height: 图像数据的高度。

  • data: 一个 Uint8ClampedArray 类型的数组,存储了区域中每个像素的颜色和透明度信息。每个像素占用 4 个元素,分别对应:



    • data[n]: 红色通道的值(0 到 255)

    • data[n+1]: 绿色通道的值(0 到 255)

    • data[n+2]: 蓝色通道的值(0 到 255)

    • data[n+3]: 透明度(alpha)通道的值(0 到 255),255 表示完全不透明,0 表示完全透明。




代码实现


1、设置 DomToPdf 类


export class DomToPdf {

_rootDom = null
_title = 'pdf' //生成的pdf标题
_a4Width = 595.266
_a4Height = 841.89
_pageBackground = 'rgba(255,255,255)' //页面背景色
_hex = [0xff, 0xff, 0xff] //用于检测分页的颜色标识

//初始化状态
constructor(rootDom, title, color = [255, 255, 255]) {
this._rootDom = rootDom
this._title = title
this._pageBackground = `rgb(${color[0]},${color[1]},${color[2]})`
this._hex = color
}

}

2、设置 pdf的生成函数


  async savePdf() {
const a4Width = this._a4Width
const a4Height = this._a4Height
const hex = this._hex

return new Promise(async (resolve, reject) => {
try {
const canvas = await html2canvas(this._rootDom, {
useCORS: true,
allowTaint: true,
scale: 0.8,
backgroundColor: this._pageBackground,
})
const pdf = new jsPDF('p', 'pt', 'a4')
let index = 1,
canvas1 = document.createElement('canvas'),
height
let leftHeight = canvas.height

let a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height)
let position = 0
let pageData = canvas.toDataURL('image/jpeg', 0.7)
pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')

function createImpl(canvas) {
if (leftHeight > 0) {
index++
let checkCount = 0
if (leftHeight > a4HeightRef) {
let i = position + a4HeightRef
for (i = position + a4HeightRef; i >= position; i--) {
let isWrite = true
for (let j = 0; j < canvas.width; j++) {
let c = canvas.getContext('2d').getImageData(j, i, 1, 1).data
if (c[0] !== hex[0] || c[1] !== hex[1] || c[2] !== hex[2]) {
isWrite = false
break
}
}
if (isWrite) {
checkCount++
if (checkCount >= 10) {
break
}
} else {
checkCount = 0
}
}
height =
Math.round(i - position) || Math.min(leftHeight, a4HeightRef)
if (height <= 0) {
height = a4HeightRef
}
} else {
height = leftHeight
}
canvas1.width = canvas.width
canvas1.height = height
let ctx = canvas1.getContext('2d')
ctx.drawImage(
canvas,
0,
position,
canvas.width,
height,
0,
0,
canvas.width,
height,
)
if (position !== 0) {
pdf.addPage()
}
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
canvas1.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4Width,
(a4Width / canvas1.width) * height,
)
leftHeight -= height
position += height
if (leftHeight > 0) {
setTimeout(createImpl, 500, canvas)
} else {
resolve(pdf)
}
}
}
if (leftHeight < a4HeightRef) {
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
pageData,
'JPEG',
0,
0,
a4Width,
(a4Width / canvas.width) * leftHeight,
)
resolve(pdf)
} else {
try {
pdf.deletePage(0)
setTimeout(createImpl, 500, canvas)
} catch (err) {
reject(err)
}
}
} catch (error) {
reject(error)
}
})
}

3、设置承接pdf的方法


//直接下载pdf
async downToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const title = this._title
newPdf.save(title + '.pdf')
setLoadParent(false)
}

//通过构造链接的形式去跳转打印页面
async printToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const pdfBlob = newPdf.output('blob')
const pdfUrl = URL.createObjectURL(pdfBlob)
setLoadParent(false)
window.open(pdfUrl)
}

完整代码


import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
export class DomToPdf {
_rootDom = null
_title = 'pdf'
_a4Width = 595.266
_a4Height = 841.89
_pageBackground = 'rgba(255,255,255)'
_hex = [0xff, 0xff, 0xff]

constructor(rootDom, title, color = [255, 255, 255]) {
this._rootDom = rootDom
this._title = title
this._pageBackground = `rgb(${color[0]},${color[1]},${color[2]})`
this._hex = color
}
async savePdf() {
const a4Width = this._a4Width
const a4Height = this._a4Height
const hex = this._hex

return new Promise(async (resolve, reject) => {
try {
const canvas = await html2canvas(this._rootDom, {
useCORS: true,
allowTaint: true,
scale: 0.8,
backgroundColor: this._pageBackground,
})
const pdf = new jsPDF('p', 'pt', 'a4')
let index = 1,
canvas1 = document.createElement('canvas'),
height
let leftHeight = canvas.height

let a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height)
let position = 0
let pageData = canvas.toDataURL('image/jpeg', 0.7)
pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')

function createImpl(canvas) {
if (leftHeight > 0) {
index++
let checkCount = 0
if (leftHeight > a4HeightRef) {
let i = position + a4HeightRef
for (i = position + a4HeightRef; i >= position; i--) {
let isWrite = true
for (let j = 0; j < canvas.width; j++) {
let c = canvas.getContext('2d').getImageData(j, i, 1, 1).data
if (c[0] !== hex[0] || c[1] !== hex[1] || c[2] !== hex[2]) {
isWrite = false
break
}
}
if (isWrite) {
checkCount++
if (checkCount >= 10) {
break
}
} else {
checkCount = 0
}
}
height =
Math.round(i - position) || Math.min(leftHeight, a4HeightRef)
if (height <= 0) {
height = a4HeightRef
}
} else {
height = leftHeight
}
canvas1.width = canvas.width
canvas1.height = height
let ctx = canvas1.getContext('2d')
ctx.drawImage(
canvas,
0,
position,
canvas.width,
height,
0,
0,
canvas.width,
height,
)
if (position !== 0) {
pdf.addPage()
}
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
canvas1.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4Width,
(a4Width / canvas1.width) * height,
)
leftHeight -= height
position += height
if (leftHeight > 0) {
setTimeout(createImpl, 500, canvas)
} else {
resolve(pdf)
}
}
}
if (leftHeight < a4HeightRef) {
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
pageData,
'JPEG',
0,
0,
a4Width,
(a4Width / canvas.width) * leftHeight,
)
resolve(pdf)
} else {
try {
pdf.deletePage(0)
setTimeout(createImpl, 500, canvas)
} catch (err) {
reject(err)
}
}
} catch (error) {
reject(error)
}
})
}
async downToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const title = this._title
newPdf.save(title + '.pdf')
setLoadParent(false)
}

async printToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const pdfBlob = newPdf.output('blob')
const pdfUrl = URL.createObjectURL(pdfBlob)
setLoadParent(false)
window.open(pdfUrl)
}

结束


好喽,开箱即用的打印和下载功能的实现就完成了。欢迎大家阅读,我是小周🤪🤪🤪


作者:KLin
来源:juejin.cn/post/7412672713376497727
收起阅读 »

基于 Letterize.js + Anime.js 实现炫酷文本特效

web
如上面gif动图所示,这是一个很炫酷的文字动画效果,文字的每个字符呈波浪式的扩散式展开。本次文章将解读如何实现这个炫酷的文字效果。基于以上的截图效果可以分析出以下是本次要实现的主要几点:文案呈圆环状扩散开,扩散的同时文字变小文字之间的间距从中心逐个扩散开,间距...
继续阅读 »

如上面gif动图所示,这是一个很炫酷的文字动画效果,文字的每个字符呈波浪式的扩散式展开。本次文章将解读如何实现这个炫酷的文字效果。

基于以上的截图效果可以分析出以下是本次要实现的主要几点:

  • 文案呈圆环状扩散开,扩散的同时文字变小
  • 文字之间的间距从中心逐个扩散开,间距变大
  • 文案呈圆环状扩散开,扩散的同时文字变大
  • 文字之间的间距从中心逐个聚拢,间距变小
  • 动画重复执行以上4个步骤

实现过程

核心代码实现需要基于一下两个库:

Letterize.js是一个轻量级的JavaScript库,它可以将文本内容分解为单个字母,以便可以对每个字母进行动画处理。这对于创建复杂的文本动画效果非常有用。

使用Letterize.js,你可以轻松地将一个字符串或HTML元素中的文本分解为单个字母,并为每个字母创建一个包含类名和数据属性的新HTML元素。这使得你可以使用CSS或JavaScript来控制每个字母的样式和动画。

anime.js是一个强大的JavaScript动画库,它提供了一种简单而灵活的方式来创建各种动画效果。它可以用于HTML元素、SVG、DOM属性和JavaScript对象的动画。

通过使用Letterize.js以便可以对每个字母进行动画处理,再结合anime.js即可创建各种动画效果。本文不对这两个库做更多的详细介绍,只对本次特效实现做介绍,有兴趣的可以看看官网完整的使用文档。

界面布局

html就是简单的本文标签,也不需要额外的样式,只需要在外层使用flex布局将内容居中,因为本文的长度都是一样的,所以完成后的文本内容就像一个正方形。

<div>
<div class="animate-me">
letterize.js&anime.js
div>
<div class="animate-me">
anime.js&letterize.js
div>
......
<div class="animate-me">
letterize.js&anime.js
div>
<div class="animate-me">
anime.js&letterize.js
div>
div>

动画实现

  1. 初始化 Letterize.js,只需要传入 targets 目标元素,元素即是上面的 .animate-me 文本标签。返回的 letterize 是包含所有选中的 .animate-me 元素组数。
const letterize = new Letterize({
targets: ".animate-me"
});
  1. 接下来初始化 anime 库的使用,下面的代码即创建了一个新的anime.js时间线动画。目标是Letterize对象的所有字母。动画将以100毫秒的间隔从中心开始,形成一个网格。loop: true 动画将无限循环。
const animation = anime.timeline({
targets: letterize.listAll,
delay: anime.stagger(100, {
grid: [letterize.list[0].length, letterize.list.length],
from: "center"
}),
loop: true
});

  1. 开始执行动画,首先设置 「文案呈圆环状扩散开,扩散的同时文字变小」,这里其实就是将字母的大小缩小。
animation
.add({
scale: 0.5
})

此时的效果如下所示:

  1. 继续处理下一步动画,「文字之间的间距从中心逐个扩散开,间距变大」,这里处理的其实就是将字母的间距加大,通过设置 letterSpacing 即可,代码如下:
animation
.add({
scale: 0.5
})
.add({
letterSpacing: "10px"
})

此时的效果如下所示:

  1. 后面还有2个步骤,「文案呈圆环状扩散开,扩散的同时文字变大;文字之间的间距从中心逐个聚拢,间距变小」,换做上面的思路也就是将文字变大和将文字间距变小,增加相应的代码如下:
  .add({
scale: 1
})
.add({
letterSpacing: "6px"
});

在线预览

码上掘金地址:

最后

本文通过 Letterize.js + Anime.js 实现了一个很炫酷的文字动画效果,文字的每个字符呈波浪式的扩散式展开和收起。anime.js还有很多的参数可以尝试,有兴趣的朋友可以尝试探索看看~

看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~

专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)

参考

动画效果发布者 Wojciech Krakowiak :https://codepen.io/WojciechWKROPCE/pen/VwLePLy


作者:南城FE
来源:juejin.cn/post/7300847292974071859
收起阅读 »

CSS萤火虫按钮特效

web
如图所示,这是一个很炫酷的按钮悬浮特效,鼠标悬停时,按钮呈现发光的效果,周边还出现类型萤火虫的效果。本文将解析如何实现这个按钮特效,基于这个动图可以分析出需要实现的要点: 有一个跟随鼠标移动的圆点 按钮悬停时有高亮发光的效果 悬停时按钮周边的萤火中效果 实...
继续阅读 »


如图所示,这是一个很炫酷的按钮悬浮特效,鼠标悬停时,按钮呈现发光的效果,周边还出现类型萤火虫的效果。本文将解析如何实现这个按钮特效,基于这个动图可以分析出需要实现的要点:



  • 有一个跟随鼠标移动的圆点

  • 按钮悬停时有高亮发光的效果

  • 悬停时按钮周边的萤火中效果


实现过程


跟随鼠标移动的圆点


这个部分需要基于JS实现,但不是最主要的实现代码


如果单纯做一个跟随鼠标移动的点很简单,只需要监听鼠标事件获取坐标实时更新到需要移动的元素上即可。但是仔细看这里的效果并不是这样,圆点是跟随在鼠标后面,鼠标移动停止后圆点才会和鼠标重合。这里是使用了一个名为 Kinet 的库来实现的这个鼠标移动动画效果,具体实现如下:



  1. 创建 Kinet 实例,传入了自定义设置:



  • acceleration: 加速度,控制动画的加速程度。

  • friction: 摩擦力,控制动画的减速程度。

  • names: 定义了两个属性 x 和 y,用于表示动画的两个维度。


 var kinet = new Kinet({
acceleration: 0.02,
friction: 0.25,
names: ["x", "y"],
});


  1. 通过 document.getElementById 获取页面中 ID 为 circle 的元素,以便后续进行动画处理。


var circle = document.getElementById('circle');


  1. 设置 Kinet 的 tick 事件处理:



  • 监听 tick 事件,每当 Kinet 更新时执行该函数。

  • instances 参数包含当前的 x 和 y 值及其速度。

  • 使用 style.transform 属性来更新圆形元素的位置和旋转:

  • translate3d 用于在 3D 空间中移动元素。

  • rotateXrotateY 用于根据当前速度旋转元素。


 kinet.on('tick', function(instances) {
circle.style.transform = `translate3d(${ (instances.x.current) }px, ${ (instances.y.current) }px, 0) rotateX(${ (instances.x.velocity/2) }deg) rotateY(${ (instances.y.velocity/2) }deg)`;
});


  1. 听 mousemove 事件,kinet.animate 方法用于更新 x 和 y 的目标值,计算方式是将鼠标的当前位置减去窗口的中心位置,使动画围绕窗口中心进行。


 document.addEventListener('mousemove', function (event) {
kinet.animate('x', event.clientX - window.innerWidth/2);
kinet.animate('y', event.clientY - window.innerHeight/2);
});

随着鼠标的移动这个圆点元素将在页面上进行平滑的动画。通过 Kinet 库的加速度和摩擦力设置,动画效果显得更加自然,用户体验更加生动。有兴趣的可以尝试调整参数解锁其他玩法,此时我们的页面效果如下:



按钮悬停时发光效果


这里主要通过悬停时设置transition过渡改变按钮的内外阴影效果,阴影效果通过伪元素实现,默认透明度为0,按钮样式代码如下:


.button {
z-index: 1;
position: relative;
text-decoration: none;
text-align: center;
appearance: none;
display: inline-block;
}

.button::before, .button::after {
content: "";
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
border-radius: 999px;
opacity: 0;
transition: opacity 0.3s;
}

.button::before {
box-shadow: 0px 0px 24px 0px #FFEB3B;
}

.button::after {
box-shadow: 0px 0px 23px 0px #FDFCA9 inset, 0px 0px 8px 0px #FFFFFF42;
}

当鼠标悬停在按钮上时,通过改变伪元素的透明度,使发光效果在鼠标悬停时变得可见:


.button-wrapper:hover .button::before, 
.button-wrapper:hover .button::after {
opacity: 1;
}

此时的按钮效果如下:



悬停时萤火中效果


如头部图片所展示,萤火虫效果是有多个圆点散开,所以这里我们添加多个圆点元素。


  class="dot dot-1">
<span class="dot dot-2">span>
<span class="dot dot-3">span>
<span class="dot dot-4">span>
<span class="dot dot-5">span>
<span class="dot dot-6">span>
<span class="dot dot-7">span>

设置元素样式,这里的CSS变量(如 --speed, --size, --starting-x, --starting-y, --rotatation)用于控制圆点的动画速度、大小、起始位置和旋转角度。


.dot {
display: block;
position: absolute;
transition: transform calc(var(--speed) / 12) ease;
width: var(--size);
height: var(--size);
transform: translate(var(--starting-x), var(--starting-y)) rotate(var(--rotatation));
}

给圆点设置动画效果,使用 @keyframes 定义了两个动画:dimFireflyhoverFirefly,为圆点添加了闪烁和移动效果:


@keyframes dimFirefly {
0% { opacity: 1; }
25% { opacity: 0.4; }
50% { opacity: 0.8; }
75% { opacity: 0.5; }
100% { opacity: 1; }
}

@keyframes hoverFirefly {
0% { transform: translate(0, 0); }
12% { transform: translate(3px, 1px); }
24% { transform: translate(-2px, 3px); }
37% { transform: translate(2px, -2px); }
55% { transform: translate(-1px, 0); }
74% { transform: translate(0, 2px); }
88% { transform: translate(-3px, -1px); }
100% { transform: translate(0, 0); }
}

在圆点的伪元素上关联动画效果:


.dot::after {
content: "";
animation: hoverFirefly var(--speed) infinite, dimFirefly calc(var(--speed) / 2) infinite calc(var(--speed) / 3);
animation-play-state: paused;
display: block;
border-radius: 100%;
background: yellow;
width: 100%;
height: 100%;
box-shadow: 0px 0px 6px 0px #FFEB3B, 0px 0px 4px 0px #FDFCA9 inset, 0px 0px 2px 1px #FFFFFF42;
}

给每个圆点设置不同的动画参数,通过使用 CSS 变量,开发者可以灵活地控制每个 .dot 元素的旋转角度,进一步丰富视觉效果。


.dot-1 {
--rotatation: 0deg;
--speed: 14s;
--size: 6px;
--starting-x: 30px;
--starting-y: 20px;
top: 2px;
left: -16px;
opacity: 0.7;
}

.dot-2 {
--rotatation: 122deg;
--speed: 16s;
--size: 3px;
--starting-x: 40px;
--starting-y: 10px;
top: 1px;
left: 0px;
opacity: 0.7;
}
...

此时只要在父元素.button-wrapper悬停时,则触发 .dot 元素的旋转效果,并使其伪元素的动画开始运行,此时萤火中悬停效果就会开始运行。


.button-wrapper:hover {
.dot {
transform: translate(0, 0) rotate(var(--rotatation));
}

.dot::after {
animation-play-state: running;
}
}

最后完成的悬停效果如下:



在线预览



最后


通过以上步骤,结合现代 CSS 的强大功能,我们实现了一个发光的萤火虫圆点悬停按钮效果。这样的效果不仅提升了视觉吸引力,还增强了用户的交互体验。利用 CSS 变量和动画,设计师可以灵活地控制每个元素的表现,使得网页更加生动和引人注目。有兴趣的可以调整相关参数体验其他的视觉效果。




作者:南城FE
来源:juejin.cn/post/7401144423563444276
收起阅读 »

我的第一个独立产品,废了,大家来看看热闹哈哈

web
产品想法萌生背景: 我孩子4岁,很喜欢画画,平常在家里,画在纸上,墙的画板上,学习机上,每一次画都很专注,而外出时,有时候会无聊,比如就餐等位,长时间坐高铁,等爸爸剪头发等等场景,一时之间也不好找东西给他玩,于是有了做一个画画小程序给他的想法,同时也觉得会有同...
继续阅读 »

产品想法萌生背景:


我孩子4岁,很喜欢画画,平常在家里,画在纸上,墙的画板上,学习机上,每一次画都很专注,而外出时,有时候会无聊,比如就餐等位,长时间坐高铁,等爸爸剪头发等等场景,一时之间也不好找东西给他玩,于是有了做一个画画小程序给他的想法,同时也觉得会有同样需求的家长,尽管需求场景很小,用的频率很低,但这小程序也许是可以救急的


产品实施:


1.梳理初步想要实现的功能


WX20240909-165957@2x.png


2.开发实现

需求想要的效果都实现了,可随意改变颜色在白板上随意画画,效果如下


WechatIMG2296.jpg


3.更多的想法

实现了画画功能,感觉太单一,于是想到涂色卡和字帖功能,具体如下


WX20240909-171147@2x.png
其实都是“画”这个功能的延伸,实现起来也比较顺利,实现效果如下


WechatIMG2297.jpg


WechatIMG2298.jpg


4.为什么废了?



  • 想要在外出时画画,可以买一个小小的画板,一键清除那种,这样既能画画,还不用看手机,蛮多家长介意看手机的

  • 需要场景很小,很多替代方案,各种小型的益智玩具,绘本等

  • 字帖功能,一般是打印纸质版,练习握笔和书写习惯

  • 欢迎补充哈哈哈


最后想说


虽然产品废了,但从0到1实现了自己的想法,收获还是很多的,我从事Java开发,因为实现这个想法,自学了小程序开发,AI抠图等,在开发过程中,解决了一个又一个开发障碍,最终达到想要的效果,对这个产品实现有任何疑问都可以留言哈,我都会解答!对了,搜索小程序“小乐画板”,就可以体验这款小程序


作者:Nero
来源:juejin.cn/post/7412505754382696448
收起阅读 »

人人都可配置的大屏可视化

web
大屏主要是为了展示数据和酷炫的效果,布局大部分是9宫格,或者在9宫格上做的延伸,现在介绍下 泛积木-低代码 提供的大屏可视化配置。 首先查看效果展示:泛积木-低代码大屏展示,泛积木-低代码大屏展示 此页面注册登录之后可编辑(会定期恢复至演示版本)。 创建页面...
继续阅读 »

大屏主要是为了展示数据和酷炫的效果,布局大部分是9宫格,或者在9宫格上做的延伸,现在介绍下 泛积木-低代码 提供的大屏可视化配置。


首先查看效果展示:泛积木-低代码大屏展示泛积木-低代码大屏展示 此页面注册登录之后可编辑(会定期恢复至演示版本)。


大屏布局组件


创建页面之后,点击进入编辑页面,在可视化编辑器左侧组件往下翻,找到自定义组件中的 大屏布局组件 ,将 大屏布局组件 拖入页面,可以看到下面的成果:


大屏布局组件


拖入的 大屏布局组件 将使用基础配置,并且已经自带了缩放容器组件。


缩放容器组件


缩放容器组件


缩放容器组件主要用于适配不同的尺寸大小,实现原理:缩放容器组件是以该组件的宽度和设计稿的宽度求比例,然后等比例缩放


缩放容器组件支持配置 设计稿宽度、设计稿高度、样式名称、背景颜色,当要适配不同尺寸的屏幕时,我们只需要修改 设计稿宽度、设计稿高度 为对应尺寸即可。样式名称是添加您需要设置的 样式 或添加唯一的classNameclassName作用的元素将作为后续全屏按钮点击时全屏的元素。


全屏按钮组件


全屏按钮组件


全屏按钮组件主要用于配置全屏按钮加全屏元素等。在全屏元素中配置 缩放容器组件 的 唯一className


全屏按钮组件还支持配置 样式名称、字体颜色、字体大小、间距。字体颜色未配置时,会默认从 大屏布局组件 的字体颜色继承。


说完上述两个小组件之后,我们再来说说关键的 大屏布局组件。


大屏布局组件


大屏布局组件


大屏布局组件的配置项可以概括为两部分:



  1. 总体配置:

    1. 总体内容:

      1. 样式名称;

      2. 字体颜色;

      3. 背景颜色;

      4. 背景图片(不想写链接,也可以直接上传);

      5. 是否显示头部;

      6. 模块样式模板;

      7. 样式覆盖;



    2. 页面内容:

      1. 样式名称;

      2. 内间距;





  2. 头部配置:

    1. 头部总体配置:

      1. 标题名称;

      2. 头部背景图片(支持上传);

      3. 样式名称;



    2. 头部左侧:

      1. 左侧内容;

      2. 样式名称;



    3. 头部右侧:

      1. 右侧内容;

      2. 样式名称;



    4. 头部时间:

      1. 是否显示;

      2. 字体大小;

      3. 显示格式。






样式覆盖 填入 css 之后,会自动在组件内创建 style 标签添加样式,这个时候需要使用 css 优先级去覆盖默认展示内容,例如:


.large-screen-layout .large-screen-layout-header {
height: 100px;
}

此时页面头部的高度将由默认的 80px 调整为 100px 。


头部背景图片 未设置时,头部高度默认为 80px ,设置之后,高度为背景图片按照宽度整体缩放之后的高度。


头部左/右侧内容 是配置 SchemaNode , SchemaNode 是指每一个 amis 配置节点的类型,支持模板、Schema(配置)以及SchemaArray(配置数组)三种类型。


例如:


{
...
"headerLeft": {
"type": "tpl",
"tpl": "公司名称",
"id": "u:3dc2c3411ae1"
},
"headerRight": {
"type": "fan-screenfull-button",
"targetClass": "largeScreenLayout",
"fontSize": "22px",
"id": "u:be46114da702"
},
...
}

模块样式模板 用于统一设置 大屏单块模板组件 的样式模板,样式模板是事先定义好的一些简单样式。


大屏单块模板组件


大屏单块模板组件


大屏单块模板组件 是用于配置大屏每块内容,大屏布局组件 和 大屏单块模板组件 之间还有一层 grid-2d 组件


grid-2d
grid-2d


grid-2d 组件 是使用 grid 布局,支持配置 外层 Dom 的类名、格子划分、格子垂直高度、格子间距、格子行间距,建议 大屏布局组件 -> 总体配置 -> 页面内容 -> 内边距 和格子间距设置一致,格子划分 指定 划分为几列,格子间距统一设置横向和纵向的间距,格子行间距可以设置横向间距,优先级高于格子间距。


格子垂直高度 = (缩放容器组件的设计稿高度 - 大屏布局组件头部高度 - 大屏布局组件头部高度页面内容内边距 * 2 - (格子行间距 || 格子间距) * 2) / 3


例如默认的: (1080 - 80 - 20 * 2 - 20 * 2) / 3 = 306.667px


大屏单块模板组件 支持如下配置:



  1. 总体内容:

    1. 样式名称;

    2. 样式模板;

    3. 位置配置;

      1. 起始位置X;

      2. 起始位置Y;

      3. 宽度W;

      4. 高度H;



    4. 是否显示头部;

    5. 样式覆盖;



  2. 模块标题:

    1. 标题名称;

    2. 标题样式;

    3. 字体颜色;



  3. 模块头部右侧:

    1. 右侧内容;

    2. 样式名称;



  4. 模块内容:

    1. 样式名称;

    2. 内边距。




样式覆盖 填入 css 之后,会自动在组件内创建 style 标签添加样式,这个时候需要使用 css 优先级去覆盖默认展示内容,例如:


.fan-screen-card .fan-screen-card-header {
height: 80px;
}

此时模块头部的高度将由默认的 50px 调整为 80px 。 css 会作用于符合 css 的所有DOM元素,如果需要唯一设置,请在前面添加特殊的前缀,例如:


特殊的前缀


.fan-screen-card-1.fan-screen-card .fan-screen-card-header {
height: 80px;
}

样式模板 可单独设置每个模块的样式。


模块头部右侧内容 是配置 SchemaNode , SchemaNode 是指每一个 amis 配置节点的类型,支持模板、Schema(配置)以及SchemaArray(配置数组)三种类型。


位置配置 每项的值都是数值,比如默认的 9 宫格就是 3 * 3,此时设置的值就是 1/2/3 ,宽度1就代表一列,高度1就代表一行。可以调整初始位置、宽度、高度等配置出多种布局方式。


大屏单块模板内容首先嵌套 Service 功能型容器 用于获取数据,再使用 Chart 图表 进行图表渲染。


如果需要轮流高亮 Chart 图表的每个数据,例如 大屏动态展示 可以使用如下配置:



  1. Chart 图表 上添加唯一的 className

  2. 配置 Chart 图表的 config

  3. 配置 Chart 图表的 dataFilter


dataFilter


const curFlag = 'lineCharts';

if (window.fanEchartsIntervals && window.fanEchartsIntervals.get(curFlag)) {
clearInterval(window.fanEchartsIntervals.get(curFlag)[0]);
window.fanEchartsIntervals.get(curFlag)[1] && window.fanEchartsIntervals.get(curFlag)[1].dispose();
}

const myChart = echarts.init(document.getElementsByClassName(curFlag)[0]);
let currentIndex = -1;
myChart.setOption({
...config,
series: [
{
...config.series[0],
data: data.line
}
]
});
const interval = setInterval(function () {
const dataLen = data.line.length;
// 取消之前高亮的图形
myChart.dispatchAction({
type: 'downplay',
seriesIndex: 0,
dataIndex: currentIndex
});
currentIndex = (currentIndex + 1) % dataLen;
// 高亮当前图形
myChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: currentIndex
});
// 显示 tooltip
myChart.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: currentIndex
});
}, 1000);

if (window.fanEchartsIntervals) {
window.fanEchartsIntervals.set(curFlag, [interval, myChart]);
} else {
window.fanEchartsIntervals = new Map();
window.fanEchartsIntervals.set(curFlag, [interval, myChart]);
}

return config;

修改高亮行 1 curFlag 设置为对应的 Chart 图表的 className,12-17 行是插入数据,22-39 为对应数据的切换展示方式。


增加大屏单块模板


当添加第二个 大屏单块模板 时,直接把第一个复制一份,调整位置、service组件的接口、dataFilter配置等。


至此大屏就配置完成了。


更详细的使用文档可以查看 泛积木-低代码


作者:fxss
来源:juejin.cn/post/7329767824200810534
收起阅读 »

前端实现:页面滚动时,元素缓慢上升效果

web
效果 实现方式 自定义指令 封装组件 两种方式均可以在SSR页面中使用 方式1:自定义指令实现 import Vue from 'vue'; const DISTANCE = 100; // y轴移动距离 const DURATION = 1000; ...
继续阅读 »

效果


2024-08-11 13.58.04.gif


实现方式



  1. 自定义指令

  2. 封装组件


两种方式均可以在SSR页面中使用


方式1:自定义指令实现


import Vue from 'vue';

const DISTANCE = 100; // y轴移动距离
const DURATION = 1000; // 动画持续时间
const THRESHOLD_FOR_TRIGGERING_ANIMATION = 0.1; // 当元素一部分可见时触发动画

const animationMap = new WeakMap();

function handleIntersection(entries, observer) { // IntersectionObserver 回调函数, 处理元素的可见性变化
for (const entry of entries) { // 遍历所有观察目标
if (entry.isIntersecting) { // 如果目标可见
const animation = animationMap.get(entry.target); // 获取动画对象
if (animation) {
animation.play(); // 播放动画
} else {
// 如果不支持 Web Animations API,则使用 CSS 动画回退方案
entry.target.classList.add('active');
}
observer.unobserve(entry.target); // 播放一次后停止监听
}
}
}

let ob;
if ('IntersectionObserver' in window) { // 如果浏览器支持 IntersectionObserver
ob = new IntersectionObserver(handleIntersection, { // 创建 IntersectionObserver 对象
threshold: THRESHOLD_FOR_TRIGGERING_ANIMATION // 当元素一部分可见时触发动画
});
} else {
// 回退机制:如果不支持 IntersectionObserver
ob = {
observe(el) { // IntersectionObserver 接口的 observe 方法
el.__onScroll__ = () => { // 监听元素的滚动事件
if (isInViewport(el)) { // 如果元素在视窗内
const animation = animationMap.get(el); // 获取动画对象
if (animation) {
animation.play();
} else {
// 如果不支持 Web Animations API,则使用 CSS 动画回退方案
el.classList.add('active');
}
window.removeEventListener('scroll', el.__onScroll__); // 停止监听
}
};
window.addEventListener('scroll', el.__onScroll__); // 监听元素的滚动事件
},
unobserve(el) { // IntersectionObserver 接口的 unobserve 方法
if (el.__onScroll__) { // 如果元素有滚动事件监听
window.removeEventListener('scroll', el.__onScroll__); // 停止监听
delete el.__onScroll__; // 清理引用
}
}
};
}

function isBelowViewport(el) { // 判断元素是否在视窗下方
const rect = el.getBoundingClientRect();
return rect.top > window.innerHeight;
}

function isInViewport(el) { // 判断元素是否在视窗内
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
}

const directive = {
name: 'slide-in',
inserted(el, binding) { // 元素插入到 DOM 时触发
if (!isBelowViewport(el)) { // 如果元素在视窗下方,则不执行动画
console.log('Element is not below viewport');
return;
}

const duration = binding.value && binding.value.duration ? binding.value.duration : DURATION; // 动画持续时间
const animationOptions = { // 动画选项: 目标位置、持续时间、缓动函数
duration: duration,
easing: binding.value && binding.value.easing ? binding.value.easing : 'ease'
};

// 检查是否支持 Web Animations API
let animation;
if (el.animate) { // 如果支持 Web Animations API
animation = el.animate([ // 创建动画对象
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0.5
},
{
transform: 'translateY(0)',
opacity: 1
}
], animationOptions);
animation.pause(); // 初始化时暂停动画
animationMap.set(el, animation); // 保存动画对象
} else {
// 如果不支持 Web Animations API,则添加 CSS 动画回退类
el.classList.add('animate-fallback'); // animate-fallback在下面SCSS中有定义
}

ob.observe(el); // 开始监听元素的可见性变化
},
unbind(el) { // 元素从 DOM 中移除时触发
ob.unobserve(el); // 停止监听元素的可见性变化
}
};

Vue.directive(directive.name, directive);


注册指令


image.png


directives/index.js


import './slide-in' // 元素缓慢上升效果

main.js


import './directives'

在页面中使用


<template>
<div class="boxs .scroll-container">
<div class="slide-box" v-slide-in="{ duration: 500, easing: 'ease-in-out' }">0 - slide-directives</div>
<div class="slide-box" v-slide-in>1 - slide-directives</div>
<div class="slide-box" v-slide-in>2 - slide-directives</div>
<div v-slide-in>3 - slide-directives</div>
<div v-slide-in="{ duration: 500, easing: 'linear' }">4 - slide-directives</div>
<div v-slide-in>5 - slide-directives</div>
<div v-slide-in="{ duration: 500 }">6 - slide-directives</div>
</div>
</template>

<style lang="scss" scoped>
.boxs {
div {
text-align: center;
width: 800px;
height: 300px;
background-color: #f2f2f2;
margin: 0 auto;
margin-top: 20px;
}
}

<!-- 兼容性处理(可放到全局style中) -->
.animate-fallback {
opacity: 0;
transform: translateY(100px);
transition: transform 1s ease, opacity 1s ease;
}

.animate-fallback.active {
opacity: 1;
transform: translateY(0);
}

@keyframes slideIn {
from {
opacity: 0;
transform: translateY(100px);
}

to {
opacity: 1;
transform: translateY(0);
}
}

.animate-fallback-keyframes {
opacity: 0;
animation: slideIn 1s ease forwards;
}
</style>

方式2: 封装为组件


<template>
<div ref="animatedElement" :style="computedStyle">
<slot></slot>
</div>
</template>

<script>
export default {
name: 'slideIn',
props: {
duration: { // 动画持续时间
type: Number,
default: 1000
},
easing: { // 动画缓动效果
type: String,
default: 'ease'
},
distance: { // 动画距离
type: Number,
default: 100
}
},
data() {
return {
hasAnimated: false // 是否已经动画过
}
},
computed: {
computedStyle() {
return {
opacity: this.hasAnimated ? 1 : 0,
transform: this.hasAnimated ? 'translateY(0)' : `translateY(${this.distance}px)`,
transition: `transform ${this.duration}ms ${this.easing}, opacity ${this.duration}ms ${this.easing}`
}
}
},
mounted() {
if (typeof window !== 'undefined' && 'IntersectionObserver' in window) { // 检测是否支持IntersectionObserver
this.createObserver() // 创建IntersectionObserver
} else {
// 如果不支持IntersectionObserver,则使用scroll事件来实现动画
this.observeScroll()
}
},
methods: {
createObserver() {
const observer = new IntersectionObserver(entries => { // IntersectionObserver回调函数
entries.forEach(entry => { // 遍历每个观察目标
if (entry.isIntersecting && !this.hasAnimated) { // 如果目标进入视口并且没有动画过
this.hasAnimated = true // 标记动画过
observer.unobserve(entry.target) // 停止观察
}
})
}, { threshold: 0.1 }) // 观察阈值,表示目标在视口的百分比
observer.observe(this.$refs.animatedElement) // 观察目标
},
observeScroll() {
const onScroll = () => { // scroll事件回调函数
if (this.isInViewport(this.$refs.animatedElement) && !this.hasAnimated) { // 如果目标在视口并且没有动画过
this.hasAnimated = true // 标记动画过
window.removeEventListener('scroll', onScroll) // 停止监听scroll事件
}
}
window.addEventListener('scroll', onScroll) // 监听scroll事件
},
isInViewport(el) { // 判断目标是否在视口
const rect = el.getBoundingClientRect()
return rect.top < window.innerHeight && rect.bottom > 0
}
}
}
</script>

页面使用


<div class="text-slide-in-vue">
<slide-comp v-for="(s ,idx) in list" :key="idx">
<p>{{ s.text }} - slide-comp</p>
</slide-comp>
</div>

<div class="level-slide">
<slide-comp v-for="(s, idx) in list" :key="idx" :duration="500 * idx + 500">
<p>{{ s.text }} - slide-comp</p>
</slide-comp>
</div>

<style>
.text-slide-in-vue {
p {
text-align: center;
width: 400px;
height: 200px;
background-color: goldenrod;
margin: 0 auto;
margin-top: 20px;
}
}

.level-slide {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 20px;
p {
text-align: center;
width: 200px;
height: 200px;
background-color: blueviolet;
margin: 0 auto;
margin-top: 20px;
}
}
</style>

作者:Blue啊
来源:juejin.cn/post/7401042923490836480
收起阅读 »

手把手教你打造一个“蚊香”式加载

web
前言 这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。 效果预览 从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。 HTML布局 首先我们通过1...
继续阅读 »

前言


这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。


效果预览



从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。


HTML布局


首先我们通过15span子元素来实现金钟罩的每一层,用于创建基本结构。从专业术语上讲,每个span元素都代表加载动画中的一个旋转的小点。通过添加多个span元素,可以创建出一串连续旋转的小点,形成一个加载动画的效果。


<div class="loader">
<span></span>
// 以下省略15span元素
</div>

CSS设计


完成了基本的结构布局,接下来就是为它设计CSS样式了。我们一步一步来分析:


首先是类名为loaderCSS类,相关代码如下。


.loader{
position: relative;
width: 300px;
height: 300px;
transform-style: preserve-3d;
transform: perspective(500px) rotateX(60deg);
}

我们将元素的定位方式设置为相对定位,使其相对于其正常位置进行定位。然后定义好宽度和高度之后,设置元素的变换样式为preserve-3d,这样可以元素的子元素也会受到3D变换的影响。除此之外,还需要transform属性来设置元素的变换效果。这里的perspective(500px)表示以500像素的视角来观察元素,rotateX(60deg)则表示绕X轴顺时针旋转60度。


这样就将一个宽高都定义好的元素进行了透视效果的3D旋转,使其以60度角度绕X轴旋转。


loader类可以理解为父容器,接下来就是loader类中的子元素span


.loader span{
position: absolute;
display: block;
border: 5px solid #fff;
box-shadow: 0 5px 0 #ccc,
inset 0 5px 0 #ccc;
box-sizing: border-box;
border-radius: 50%;
animation: animate 3s ease-in-out infinite;
}

通过以上样式,我们可以创建一个圆形的动画效果,边框有阴影效果,并且以动画的方式不断旋转。关于CSS部分大部分都是一样的,这里主要介绍一下这里定义的动画效果。名称为animate,持续时间为3秒,缓动函数为ease-in-out,并且动画无限循环播放。


@keyframes animate {
0%,100%{
transform: translateZ(-100px);
}
50%{
transform: translateZ(100px);
}
}

这是一个关键帧动画。关键帧是指动画在不同时间点上的状态或样式。首先该动画名为animate,它包含了三个时间点的样式变化:


0%100% 的时间点,元素通过transform: translateZ(-100px)样式将在Z轴上向后移动100像素,这将使元素远离视图。


50% 的时间点,元素通过transform: translateZ(100px)样式将在Z轴上向前移动100像素。这将使元素靠近视图。


通过应用这个动画,span元素将在动画的持续时间内以一定的速率来回移动,从而产生一个视觉上的动态效果。


最后就是单独为每个子元素span赋予样式了。


.loader span:nth-child(1){
top: 0;
left: 0;
bottom: 0;
right: 0;
animation-delay: 1.4s;
}
.loader span:nth-child(2){
top: 10px;
left: 10px;
bottom: 10px;
right: 10px;
animation-delay: 1.3s;
}
......
以下省略到第15span元素

第一个span元素的样式设置了top、left、bottom和right属性为0,这意味着它会填充父元素的整个空间。它还设置了animation-delay属性为1.4秒,表示在加载动画开始之后1.4秒才开始播放动画。


后面14span元素都是按照这个道理,以此类推即可。通过给span元素的动画延迟属性的不同设置,可以实现加载动画的错落感和流畅的过渡效果。


总结


以上就是整个效果的实现过程了,通过设计的动画来实现这个蚊香式加载,整体还是比较简单的。大家可以去码上掘金看看完整代码,然后自己去尝试一下,如果有什么创新的地方或者遇到了什么问题欢迎在评论区告诉我~


作者:一条会coding的Shark
来源:juejin.cn/post/7291951762948259851
收起阅读 »

2024 年排名前 5 的 Node.js 后端框架

web
自 2009 年以来,Node.js 一直是人们谈论的话题,大多数后端开发人员都倾向于使用 Node.js。在过去的几年里,它的受欢迎程度有所增加。它被认为是美国最受欢迎的网络开发工具,包括 Netflix 和 PayPal 等客户。 受欢迎程度增加的原因是...
继续阅读 »

自 2009 年以来,Node.js 一直是人们谈论的话题,大多数后端开发人员都倾向于使用 Node.js。在过去的几年里,它的受欢迎程度有所增加。它被认为是美国最受欢迎的网络开发工具,包括 Netflix 和 PayPal 等客户。



受欢迎程度增加的原因是加载时间的减少和性能的提高。因此,分析 2024 年排名前 5 的 Node.js 后端框架至关重要。本文将介绍 2024 年排名前 5 的 Node.js 后端框架、它们的功能和常见用例。


Express.js:久经考验的冠军


Express.js 是 Node.js 最著名的后端框架之一。它是一个开源 Web 应用程序框架,可免费使用并构建在 Node.js 平台上。由于它是一个最小的框架,新手和经验丰富的 Web 开发人员都倾向于使用 Express.js。它主要用于创建 Web 应用程序和 RESTful API。



高效路由:Express.js 提供了一种干净、简单的方法来管理各种 HTTP 请求并将它们分配给特定任务,让我们看一个例子。



中间件支持:Express.js 允许中间件支持处理 HTTP 请求。让我们看一个创建用于记录 HTTP 请求详细信息的中间件的简单示例。



轻松的数据库集成:Express.js 与数据库无关。它不强制执行特定的数据库选择。开发者可以选择自己喜欢的数据库。将数据库与 Express.js 集成很容易,因为它具有模块化和灵活的特性,以及提供数据库连接的丰富的 npm 包生态系统。


简单易学:Express.js 以其简单和简约的设计而闻名,这使得开发人员很容易学习,特别是如果他们已经熟悉 JavaScript 和 Node.js。


另外,您可以使用 Bit 等工具轻松开始使用 Express.js 。如果您以前没有使用过 Bit,那么它是可组合软件的下一代构建系统。Express.js 本身本质上是可组合的,您可以在应用程序中的任何位置即插即用组件。


Nest.js:现代且结构化的方法


Nest.js 是一个以构建可扩展且高效的 Node.js 服务器端应用程序而闻名的框架。它使用渐进式 JavaScript,并具有用 TypeScript 编写代码的能力。尽管它完全支持 TypeScript,但它可以用纯 JavaScript 编写代码,包括面向对象编程、函数式编程和函数式响应式编程。



模块化:Nest.js 允许将代码分解为单独的可管理模块,从而使其更易于维护。让我们看一下下面的模块。



这个 PaymentModule 可以导出到其他模块。在此示例中,我们在该模块内导出了通用的缓存模块。由于 Nest.js 具有模块结构,因此易于维护。


可扩展:Nest.js 通过将应用程序分解为可管理的模块、支持灵活的组件替换以及通过微服务和异步操作容纳高流量来实现无缝扩展。它确保有效处理增加的工作量,同时保持可靠性。


依赖注入:依赖注入只是向类添加外部依赖项的方法,而不是在类本身中创建它。让我们看一个例子。



我们创建 PaymentService 并添加了 @Injectable() 注释以使其可注入。我们可以使用创建的服务,如下所示。



类型安全:Nest.js 使用 TypeScript 提供类型安全,可用于捕获开发过程中潜在的错误并提高代码的可维护性。


Koa.js:优雅且轻量


Koa.js 是一个更小、更具表现力的 Web 框架,也是由 Express.js 团队设计的。它允许您通过利用异步函数来放弃回调并处理错误。



上下文对象(ctx):Koa.js 包含一个名为 ctx 的功能来捕获请求和响应详细信息。该上下文被传递给每个中间件。在此示例中,我们从 ctx 对象记录了method 和 request。



中间件组成:与 Express.js 非常相似,Koa 支持处理 HTTP 请求和响应的中间件功能。在此示例中,我们创建了一个简单的中间件。



async/await 支持:Koa 使用 async/await 语法以更同步的方式编写异步代码。下面的示例包含使用 async/await 关键字。



Hapi.js


Hapi.js 是 Http-API 的缩写,是一个用于开发可扩展 Web 应用程序的开源框架。Hapi.js 最基本的用例之一是构建 REST API。沃尔玛实验室创建了 hapi js 来处理黑色星期五等活动的流量,黑色星期五是美国日历上在线购物最繁忙的日子之一。



配置驱动设计:使用配置对象,我们可以在 Hapi.js 中配置路由、设置和插件。



强大的插件系统:Hapi.js 允许轻松集成插件,让我们看一个例子。在这个例子中,我们集成了两个插件,可以使用 key 将选项传递给插件 options



认证与授权:Hapi.js 提供了对各种身份验证策略的内置支持,并允许开发人员轻松定义访问控制策略。



输入验证:输入验证是 Hapi.js 的另一个重要方面。在 options 路由的对象中,我们可以定义哪些输入需要验证。默认 validate 对象由以下值组成。


Adonis.js


Adonis.js 是 Node.js 的全功能 MVC 框架。它具有构建可扩展且可维护的应用程序的能力。 Adonis.js 遵循与 Laravel 类似的结构,并包含 ORM、身份验证和开箱即用的路由等功能。



全栈 MVC 框架:Adonis.js 遵循 MVC 架构模式。拥有 MVC 框架有助于组织代码并使其更易于维护和扩展。



数据库集成 ORM:Adonis.js 有自己的 ORM,称为 Lucid。 Lucid 提供了一个富有表现力的查询构建器并支持各种数据库系统。在 Lucid 中,我们可以创建模型来读取和写入数据库。让我们看下面的例子。



认证系统:Adonis.js 内置了对用户身份验证和授权的支持。它提供了一组用于处理用户会话、密码散列和访问控制的方法和中间件。


结论


2024年,上述后端框架在市场上屹立不倒。无论您选择 Express.js 是为了简单,Nest.js 是为了结构,Adonis.js 是为了生产力,还是 Koa.js 是为了优雅,选择正确的框架至关重要。这始终取决于您的要求。


了解您的项目需求至关重要,并在此基础上选择合适的框架。此外,寻找最新趋势、现有框架的新功能和新框架对于 2024 年后端开发之旅的成功至关重要。


参考链接:blog.bitsrc.io/top-5-nodej…


作者:一纸忘忧
来源:juejin.cn/post/7350581011262373928
收起阅读 »

uni-app微信小程序动态切换tabBar,根据不同用户角色展示不同的tabBar

web
前言 在UniApp的开发小程序过程中,为了针对不同角色用户登录后的个性化需求。通过动态权限配置机制,能够根据用户的角色展示不同的TabBar。此项目是通过Uni-App命令行的方式搭建的Vue3+Vite+Ts+Pinia+Uni-ui的小程序项目 最终...
继续阅读 »

前言



在UniApp的开发小程序过程中,为了针对不同角色用户登录后的个性化需求。通过动态权限配置机制,能够根据用户的角色展示不同的TabBar。此项目是通过Uni-App命令行的方式搭建的Vue3+Vite+Ts+Pinia+Uni-ui的小程序项目



最终效果



  • 1、司机角色:
    在这里插入图片描述

  • 2、供应商角色:
    在这里插入图片描述

  • 3、司机且供应商角色:
    在这里插入图片描述


目前常规的实现方式,大多数都是封装一个tabbar组件,在需要显示tabbar的页面添加这个组件,在根据一个选中的index值来切换选中效果。



而我的实现方式:把所有有tabbar的页面全部引入在一个tabbarPage页面,根据角色userType,来动态显示页面



实现思路



1、常规登录:通过微信登录获取code
2、根据code获取openId
3、根据openId获取token,若token存在表:此用户已经登陆/绑定过,则根据token获取用户信息,根据角色直接进入项目页面;若token不存在,则跳转到登录页面
4、登录成功后,调用用户信息接口,根据角色直接进入项目页面



1、以下是封装了一个useLogin的hooks



export const useLogin = () => {
const { proxy } = getCurrentInstance() as any
//常规登录
const judgmentLogin = () => {
uni.login({
provider: 'weixin', //使用微信登录
success: async (loginRes) => {
// 根据微信登录的code获取openid
const res = await proxy.$api.getOpenid({ code: loginRes.code })
if (res.success) {
// console.log('res.data.openid----', res.data.openId)
// 根据openid获取token
openidLogin(res.data.openId)
// 存储openid
uni.setStorageSync('openId', res.data.openId)
}
}
});
}
// 登录过的用户再次进入根据openid获取token,有token则直接进入当前用户的页面,没有则进入登录页面
const openidLogin = (async (openId: string) => {
// console.log('openId----', openId)
const res = await proxy.$api.openIdLogin({ openId })
if (res.success) {
if (res.data) {
// 存储token
uni.
setStorageSync('token', res.data)
userInfo(openId)
}
else {
uni.
navigateTo({
url: '/pages/login/login'
}
)
}
}
}
)
// 登录成功后(有token后)根据openid获取用户信息
const userInfo = (async (openId: any) => {
const res = await proxy.$api.getUserInfo({ openId })
if (res.success) {
console.log('获取登陆用户信息', res.data)
uni.
setStorageSync('userInfo', JSON.stringify(res.data))
const userTypeList = ['scm_driver', 'scm_supplier', 'supplier_and_driver']
// 遍历角色数组来存储当前用户的角色。此角色为userTypeList中的某一个并且此数组只能存在一个userTypeList里面的角色,不会同时存在两个
res.
data.roles.map((item: any) => {
if (userTypeList.includes(item.roleKey)) {
uni.
setStorageSync('userType', item.roleKey)
}
}
)
// 判断角色数组中只要有一个角色在userTypeList中,则进入当前用户的角色页面,否则进入无权限页面
const flag = res.data.roles.some((item: any) => {
return userTypeList.includes(item.roleKey)
}
)
// console.log('flag----', flag)
if (flag && userTypeList.includes(uni.getStorageSync('userType'))) {
setTimeout(() => {
uni.
reLaunch({
url: '/pages/tabbarPage/tabbarPage'
}
)
},
500)
}
else {
uni.
showToast({
icon: 'none',
title: '当前用户角色没有权限!'
}
)
}
}
}
)
return {
judgmentLogin,
userInfo
}
}

2、修改page.json中的tabBar


"tabBar": {
"color": "#a6b9cb",
"selectedColor": "#355db4",
"list": [
{
"pagePath": "pages/supplierMyorder/supplierMyorder"
},
{
"pagePath": "pages/driverMyorder/driverMyorder"
},
{
"pagePath": "pages/mycar/mycar"
},
{
"pagePath": "pages/driverPersonal/driverPersonal"
}
]
},

3、关键页面tabbarPage.vue


<template>
<div class="tabbar_page flex-box flex-col">
<div
class="page_wrap"
v-if="userType === 'scm_supplier'"
v-show="active === 'supplierMyorder'"
>

<supplier-myorder
ref="supplierMyorder"
:show="active === 'supplierMyorder'"
/>

div>
<div
class="page_wrap"
v-if="userType === 'scm_supplier'"
v-show="active === 'supplierPersonal'"
>

<supplier-personal
ref="supplierPersonal"
:show="active === 'supplierPersonal'"
/>

div>
<div
class="page_wrap"
v-if="userType === 'scm_driver'"
v-show="active === 'driverMyorder'"
>

<driver-myorder ref="driverMyorder" :show="active === 'driverMyorder'" />
div>
<div
class="page_wrap"
v-if="userType === 'scm_driver'"
v-show="active === 'mycar'"
>

<mycar ref="mycar" :show="active === 'mycar'" />
div>
<div
class="page_wrap"
v-if="userType === 'scm_driver'"
v-show="active === 'driverPersonal'"
>

<driver-personal
ref="driverPersonal"
:show="active === 'driverPersonal'"
/>

div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'supplierMyorder'"
>

<supplier-myorder
ref="supplierMyorder"
:show="active === 'supplierMyorder'"
/>

div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'driverMyorder'"
>

<driver-myorder ref="driverMyorder" :show="active === 'driverMyorder'" />
div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'mycar'"
>

<mycar ref="mycar" :show="active === 'mycar'" />
div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'supplierPersonal'"
>

<supplier-personal
ref="supplierPersonal"
:show="active === 'supplierPersonal'"
/>

div>
<view class="tab">
<view
v-for="(item, index) in tabbarOptions"
:key="index"
class="tab-item"
@click="switchTab(item, index)"
>

<image
class="tab_img"
:src="currentIndex == index ? item.selectedIconPath : item.iconPath"
>
image>
<view
class="tab_text"
:style="{ color: currentIndex == index ? selectedColor : color }"
>
{{ item.text }}
view>
view>
div>
template>

<script lang="ts" setup>
import supplierMyorder from '@/pages/supplierMyorder/supplierMyorder.vue'
import supplierPersonal from '@/pages/supplierPersonal/supplierPersonal.vue'
import driverMyorder from '@/pages/driverMyorder/driverMyorder.vue'
import mycar from '@/pages/mycar/mycar.vue'
import driverPersonal from '@/pages/driverPersonal/driverPersonal.vue'
let color = ref('#666666')
let selectedColor = ref('#355db4')
let currentIndex = ref(0)

const active = ref('')
const switchTab = (item: any, index: any) => {
// console.log('tabbar----switchTab-----list', item, index)
currentIndex.value = index
active.value = item.name
}

onLoad((option: any) => {
currentIndex.value = option.index || 0
active.value = option.name || tabbarOptions.value[0].name
})
onShow(() => {
active.value = active.value || tabbarOptions.value[0].name
currentIndex.value = currentIndex.value || 0
})
const userType = computed(() => {
return uni.getStorageSync('userType')
})
const tabbarOptions = computed(() => {
return {
scm_supplier: [
{
name: 'supplierMyorder',
pagePath: '/pages/supplierMyorder/supplierMyorder',
iconPath: '/static/tabbar/order.png',
selectedIconPath: '/static/tabbar/order_active.png',
text: '我的订单'
},
{
name: 'supplierPersonal',
pagePath: '/pages/supplierPersonal/supplierPersonal',
iconPath: '/static/tabbar/my.png',
selectedIconPath: '/static/tabbar/my_active.png',
text: '个人中心'
}
],
scm_driver: [
{
name: 'driverMyorder',
pagePath: '/pages/driverMyorder/driverMyorder',
iconPath: '/static/tabbar/waybill.png',
selectedIconPath: '/static/tabbar/waybill_active.png',
text: '我的运单'
},
{
name: 'mycar',
pagePath: '/pages/mycar/mycar',
iconPath: '/static/tabbar/car.png',
selectedIconPath: '/static/tabbar/car_active.png',
text: '我的车辆'
},
{
name: 'driverPersonal',
pagePath: '/pages/driverPersonal/driverPersonal',
iconPath: '/static/tabbar/my.png',
selectedIconPath: '/static/tabbar/my_active.png',
text: '个人中心'
}
],
supplier_and_driver: [
{
name: 'supplierMyorder',
pagePath: '/pages/supplierMyorder/supplierMyorder',
iconPath: '/static/tabbar/order.png',
selectedIconPath: '/static/tabbar/order_active.png',
text: '我的订单'
},
{
name: 'driverMyorder',
pagePath: '/pages/driverMyorder/driverMyorder',
iconPath: '/static/tabbar/order.png',
selectedIconPath: '/static/tabbar/order_active.png',
text: '我的运单'
},
{
name: 'mycar',
pagePath: '/pages/mycar/mycar',
iconPath: '/static/tabbar/car.png',
selectedIconPath: '/static/tabbar/car_active.png',
text: '我的车辆'
},
{
name: 'supplierPersonal',
pagePath: '/pages/supplierPersonal/supplierPersonal',
iconPath: '/static/tabbar/my.png',
selectedIconPath: '/static/tabbar/my_active.png',
text: '个人中心'
}
]
}[userType.value]
})
script>

<style lang="scss" scoped>
.tabbar_page {
height: 100%;
.page_wrap {
height: calc(100% - 84px);
&.hidden {
display: none;
}
}
.tab {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background: white;
display: flex;
justify-content: center;
align-items: center;
padding-bottom: env(safe-area-inset-bottom); // 适配iphoneX的底部

.tab-item {
flex: 1;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;

.tab_img {
width: 45rpx;
height: 45rpx;
}

.tab_text {
font-size: 25rpx;
margin: 9rpx 0;
}
}
}
}
.flex-box {
display: -webkit-box;
display: -webkit-flex;
display: flex;
}
.flex-col {
flex-direction: column
}
style>

作者:wocwin
来源:juejin.cn/post/7372366198099886090
收起阅读 »

聊聊 CSS 的 ::marker

web
::marker 是一个 CSS 的另一个伪元素,有点类似于 CSS 的 ::before 和 ::after 伪元素。只不过,它常用于给列表标记框定制样式。简而言之,使用::marker伪元素,可以对列表做一些有趣的事情,在本文中,我们将深入的聊聊该伪元素。...
继续阅读 »


::marker 是一个 CSS 的另一个伪元素,有点类似于 CSS 的 ::before::after 伪元素。只不过,它常用于给列表标记框定制样式。简而言之,使用::marker伪元素,可以对列表做一些有趣的事情,在本文中,我们将深入的聊聊该伪元素。


初识 CSS 的 ::marker


::marker 是 CSS 的伪元素,现在被纳入到 CSS Lists Module Level 3 规范中。在该规范中涵盖了列表和计算数器相关的属性,比如我们熟悉的list-style-typelist-style-positionlist-stylelist-itemcounter-increment、counter-reset、counter()和counters()等属性。


在 CSS 中 display 设置 list-item 值之后就会生成一个 Markers 标记以及控制标记位置和样式的几个属性,而且还定义了计数器(计数器是一种特殊的数值对象),而且该计数器通常用于生成标记(Markers)的默认内容。


一时之间,估计大家对于Markers标记并不熟悉,但对于一个列表所涉及到的相关属性应该较为熟悉,对于一个CSS List,它可以涵盖了下图所涉及到的相关属性:




如果你对CSS List中所涉及到的属性不是很了解的话,可以暂时忽略,随着后续的知识,你会越来越清楚的。



解构一个列表


虽然我们在 Web 的制作中经常会用到列表,但大家可能不会过多的考虑列表相关的属性或使用。就 HTML语义化出发,如果遇到无序列表的时候会使用

    ,遇到有序列表的时候会使用
      ,但在有些场景(或不追求语义化的同学)会采用其他的标签元素,比如说
      。针对这个场景,会采用 display 设置为list-item。如此一来会创建一个块级别的框,以及一个附加的标记框。同时也会自动增加一个隐含列表项计算数器。


      ulol 元素默认情况之下会带有list-style-typelist-style-imagelist-style-position属性,可以用来设置列表项的标记样式。同样的,带有display:list-item的元素生成的标记框,也可以使用这几个属性来设置标记项样式。


      list-style-type的属性有很多个值:



      取值不同时,列表符号(也就是Marker标识符)会有不同的效果,比如下面这个示例所示:




      Demo 地址:codepen.io/airen/full/…



      在 CSS 中给列表项设置类型的样式风格可以通过 list-style-typelist-style-image 来实现,但这两个属性设置列表项的样式风格会有所限制。比如要实现类似下图的列表项样式风格:



      值得庆幸的是,CSS 的 ::marker 给予我们更大的灵活性,可以让我们实现更多的列表样式风格,而且灵活性也更大。


      创建 marker 标记框


      HTML 中的 ulol 元素会自动创建 marker 标记框。如果通过浏览器调试器来查看的话,你会发现,不管是 ul 还是 ol 的子元素 li 会自带 display:list-item 属性设计(客户端默认属性),另外会带有一个默认的list-style-type样式设置:



      这样一来,它自身就默认创建了一个 marker 标记框,同时我们可以通过 ::marker 伪元素来设置列表项的样式风格,比如下面这个示例:


      ul ::marker,
      ol ::marker {
      font-size: 200%;
      color: #00b7a8;
      font-family: "Comic Sans MS", cursive, sans-serif;
      }

      你会看到效果如下所示:




      Demo 地址:codepen.io/airen/full/…



      对于非列表元素,可以通过display: list-item来创建 Marker 标记,这样就可以在元素上使用 ::marker 伪元素来设置项目符号的样式。虽然通过display:list-item在形式上看上去像列表项,但在语义化上并没有起到任何的作用。


      在深入探讨 ::marker 使用之前,大家要知道,元素必须要具备一个Marker标记框,对于非列表项的元素需要显式的使用 display:list-item 来创建Marker标记框



      CSS的display属性是一个非常重要的属性,现在被纳入在CSS Display Module Level 3中。CSS的display属性可以改变任何一个元素的框模型。而且在Level 3规范中给display引用了两个值的语法,比如使用display: inline list-item可以创建一个内联列表项。

      ::marker 的基本使用


      前面的小示例中,其实我们已经领略到了::marker的魅力。在列表项li中,其实已经带有Marker标记框,可以借助::marker伪元素来设置列表标记的样式。


      我们先来回忆一下,CSS的::marker还未出现(或者说不支持的浏览器)时,要对列表项设置不同的样式,都是通过li上来控制(看上去继承了li上的样式)。虽然能设置列表样式,但还是具有一定的局限性,灵活度不够大 —— 特别是当列表项标记样式和内容要区分时


      CSS的::marker会让我们变得容易的多。从前面的示例中我们可以了解到, ::marker 伪元素和列表项内容是分开的,正因此,我们可以独立为两个部分设计不同的样式。这在以前的CSS版本中是不可能的(除非借助::before伪元素来模拟,稍后也会介绍这一部分)。比如说,我们更改ullicolorfont-size时也会更改标记的colorfont-size。为了达到两者的区分,往往需要在HTML中做一些结构上的调整,比如列表项用一个子元素来包裹(比如span元素或::before伪元素)。


      更了大家更易于理解::marker的作用,我们在上面的示例基础上做一些调整:


      .box:nth-child(odd) li {
      font-size: 200%;
      color: #00b7a8;
      font-family: "Comic Sans MS", cursive, sans-serif;
      }

      .box:nth-child(even) ::marker {
      font-size: 200%;
      color: #00b7a8;
      font-family: "Comic Sans MS", cursive, sans-serif;
      }

      代码中的具体作用不做介绍,很简单的代码,但效果却有很大的差异性,如下图所示:



      很神奇吧!在浏览器中查看源码,你会发现使用::marker和未使用::marker的差异性:



      虽然::marker易于帮助我们控制标记的样式风格,但有一点需要特别注意,如果显式的设置了list-style-type: none时,::marker标记内容就会丢失不可见。在这个时候,不管是否显式的设置了::marker的样式都将会看不到。比如:



      大家是否还记得,在::marker还没有出现之前,要对列表项设置别的标记符,比如Emoji。我们就需要通过别的方式来完成,最为常见的是修改HTML的结构,或者借助CSS伪元素::before和CSS的content属性,例如下面这个示例:




      Demo 地址:codepen.io/airen/full/…



      事实上,CSS的::marker和伪元素::before类似,也可以通过contentattr()一起来控制Marker标记的效果。需要记住,生成个性化Marker标记内容需要做到几下几点:



      • 非列表项li元素需要显式的设置display:list-item (内联列表项需要使用display: inline list-item

      • 需要显式设置list-style-typenone

      • 使用content添加内容(也可以通过attr()配合data-*来添加内容)


      来看一个小示例:


      li::marker {
      content: attr(data-emoji);
      }


      ::marker伪元素自从可以使用content来添加内容之后,让我们可操作的空间更大了。对于列表标记(即,带有Marker标记)的元素再也不需要额外的通过::before伪元素和content来生成标记内容。而且,我们还可以结合计算数器相关的特性,让列表标记可造性空间更大。如果你感兴趣的话,请继续往下阅读。


      ::marker 与计数器的结合


      对于无序列表,或者说统一使用同样的标记符,那么::markercontent结合就可以解决。但是如果面对的是一个有顺列表,那么我们就需要用到CSS计数器的相关特性。


      先来回忆一下CSS的计数器相关的特性。在CSS中计数器有三个属性:



      • counter-reset:设置一个计数器,定义计数器名称,用来标识计数器作用域

      • counter-set:将计数器设置为给定的值。它操作现有计数器的值,并且只在元素上还没有给定名称的计数器时才创建新的计数器

      • counter-increment:用来标识计数器与实际关联元素范围,可接受两个值,第一个值必须是counter-reset定义的标识符,第二个值是可选值,是一个整数值(正负值都可以),用来预设递增的值


      以及两个相关的函数:



      • counter() :主要配合content一起使用,用来调用定义好的计数器标识符

      • counters() :支持嵌套计数器,如果有指定计数器的当前值,则返回一个表示这些计数器的当前值的串联字符串。counters()有两种形式counters(name, string)counters(name, string, style)。通常和伪元素一起使用,但理论上可以支持值的任何地方使用



      一般情况之下,counter-resetcounter-incrementcounter()即可满足一个计数器的需求。



      CSS的计数器使用非常的简单。在元素的父元素上显式设置:


      body {
      counter-reset: section
      }

      使用counter-reset声明了一个计数器标识符叫section。然后再需要使用计算器的元素上(一般配合伪元素::before)使用counter-increment来调用counter-reset已声明的计数器标识符,然后使用counter(section)来计数:


      h3::before {
      counter-increment: section
      content: "Section " counter(section) ": "
      }

      下图会更详尽一些,把计数器可能会用的值都罗列出来了,可供参考:



      回到我们的列表设置中来。::marker还没有得到浏览器支持之前,一般都是使用CSS的计数器来实现一些带有个性化的有顺序列表,比如下面这样的效果:



      也可以借助计数器做一些其他的效果比如:




      Demo 地址:codepen.io/snookca/ful…



      更为厉害的是,CSS的计数器配合复选框或单选按钮还可以做一些小游戏,比如 @una教程中向我们展示的一个效果:




      Demo 地址:codepen.io/jak_e/full/…



      @kizmarh使用同样的原理,做了一个黑白棋的小游戏:



      是不是很有意思,有关于CSS计数器相关的特性暂且搁置。我们回到::marker的世界中来。


      ::marker配合content可以定制个性化Marker标记风格。借助CSS计数器,可以更轻易的构建带有顺序的Marker标记。同样可以让Marker标记和内容分离。更易于实现可定制化的样式风格。


      接下来,我们来看一个简单的示例,看看::marker生成的标记符和以往生成的标记符效果上有何差异没。


      结果很简单,这里使用的是一个无序列表:


      <ul>
      <li>
      Item1
      <ul>
      <li>Item 1-1li>
      <li>Item 1-2li>
      <li>Item 1-3li>
      ul>
      li>
      <li>Item2li>
      <li>Item3li>
      <li>Item4li>
      <li>Item5li>
      ul>

      你可以根据自己的爱好来选择标签元素。先来看::beforecontent配合counter()counters()的一个效果:


      /* counter() */ 
      .box:nth-child(1) {
      ul {
      counter-reset: item;
      }
      li {
      counter-increment: item;
      &::before{
      content: counter(item);
      /* ... */
      }
      }
      }

      /* counters() */
      .box:nth-child(2) {
      ul {
      counter-reset: item;
      }
      li {
      counter-increment: item;
      &::before{
      content: counters(item, '.');
      /* ... */
      }
      }
      }


      对于上面的效果,大家可能也猜到了。我们再来看一下::marker的使用:


      /* counter() */ 
      .box:nth-child(3) {
      ul {
      counter-reset: item;
      }
      li {
      counter-increment: item;
      }
      ::marker {
      content: counter(item);
      /* ... */
      }
      }

      /* counters() */
      .box:nth-child(4) {
      ul {
      counter-reset: item;
      }
      li {
      counter-increment: item;
      }
      ::marker {
      content: counters(item, '.');
      /* ... */
      }
      }

      可以看到::marker和前面::before效果是一样的:



      另外使用::marker还有一特殊之处。不管是列表元素还是设置了display:list-item的非列表元素,不需要显式的使用counter-reset声明计数器标识符,也无需使用counter-increment调用已声明的计数器标识符。它可以直接在 ::marker 伪元素的 content 中使用 counter(list-item) counters(list-item, '.')


      但是非列表元素,哪怕是设置了display:list-item,直接在::markercontent中使用counters(list-item, '.')所起的效果和我们预期的有所不同。如果在非列表元素的::markercontent中使用counters()达到我们想要的效果,需要使counter-reset先声明计数器标识符,然后counter-increment调用已声明的计数器标识符(回归到以前::before的使用)。具本的可以看下面的示例代码:


      ::marker {
      content: counter(list-item);
      padding: 5px 30px 5px 12px;
      background: linear-gradient(to right, #f36, #f09);
      font-size: 2rem;
      clip-path: polygon(0% 0%, 75% 0, 75% 51%, 100% 52%, 75% 65%, 75% 100%, 0 100%);
      border-radius: 5px;
      color: #fff;
      text-shadow: 1px 1px 1px rgba(#09f, .5);
      }
      .box:nth-child(2n) ::marker {
      content: counters(list-item, '.');
      }

      .box:nth-child(3) {
      section {
      counter-reset: item;
      }
      article {
      counter-increment: item;
      }
      ::marker {
      content: counters(item, '.');
      }
      }

      具体效果如下:



      是不是觉得::marker非常有意思,特别是给元素添加Marker标记的时候。换句话说,就是在定制个性化列表符号时,使用::marker伪元素要比::before之类的较为方便。而且::marker是元素原生的列表标记符(::marker)。


      一旦::marker伪元素得到所有浏览器支持之后,我们要让列表标记符和内容分离就会多了一种方案:



      • 调整HTML结构

      • 伪元素::beforecontent

      • 伪元素 ::marker content


      前面也向大家展示了,::marker也可以像::before一样,借助CSS计数器属性,可以更好的实现有序列表,甚至是嵌套的列表。


      写在最后


      虽然 ::marker 的出现允许我们为列表标记定制样式,但它也有一定的限制性,至少到目前为止是这样。比如,我们在 ::marker 伪元素上可控样式还是有限,要实现下面这样的个性化效果是不可能的:



      庆幸的是,CSS 中除了 ::marker 伪元素之外,还可以使用 ::before::after 来生成内容,然后通过 CSS 来实现更具个性化的列表标记样式。


      作者:大漠_w3cpluscom
      来源:juejin.cn/post/7358348786843959336
收起阅读 »

用Three.js搞个炫酷雷达扩散和扫描特效

web
1.画点建筑模型 添加光照,开启阴影 //开启renderer阴影 this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;...
继续阅读 »

1.画点建筑模型


添加光照,开启阴影


//开启renderer阴影
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;

//设置环境光
const light = new THREE.AmbientLight(0xffffff, 0.6); // soft white light
this.scene.add(light);

//夜晚天空蓝色,假设成蓝色的平行光
const dirLight = new THREE.DirectionalLight(0x0000ff, 3);
dirLight.position.set(50, 50, 50);
this.scene.add(dirLight);

平行光设置阴影


//开启阴影
dirLight.castShadow = true;
//阴影相机范围
dirLight.shadow.camera.top = 100;
dirLight.shadow.camera.bottom = -100;
dirLight.shadow.camera.left = -100;
dirLight.shadow.camera.right = 100;
//阴影影相机远近
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 200;
//阴影贴图大小
dirLight.shadow.mapSize.set(1024, 1024);


  • 平行光的阴影相机跟正交相机一样,因为平行光的光线是平行的,就跟视线是平行一样,切割出合适的阴影视角范围,用于计算阴影。

  • shadow.mapSize设置阴影贴图的宽度和高度,值越高,阴影的质量越好,但要花费计算时间更多。


增加建筑


//添加一个平面
const pg = new THREE.PlaneGeometry(100, 100);
//一定要用受光材质才有阴影效果
const pm = new THREE.MeshStandardMaterial({
color: new THREE.Color('gray'),
transparent: true,//开启透明
side: THREE.FrontSide//只有渲染前面
});
const plane = new THREE.Mesh(pg, pm);
plane.rotateX(-Math.PI * 0.5);
plane.receiveShadow = true;//平面接收阴影
this.scene.add(plane);

//随机生成建筑
this.geometries = [];
const helper = new THREE.Object3D();
for (let i = 0; i < 100; i++) {
const h = Math.round(Math.random() * 15) + 5;
const x = Math.round(Math.random() * 50);
const y = Math.round(Math.random() * 50);
helper.position.set((x % 2 ? -1 : 1) * x, h * 0.5, (y % 2 ? -1 : 1) * y);
const geometry = new THREE.BoxGeometry(5, h, 5);
helper.updateWorldMatrix(true, false);
geometry.applyMatrix4(helper.matrixWorld);
this.geometries.push(geometry);
}
//长方体合成一个形状
const mergedGeometry = BufferGeometryUtils.mergeGeometries(this.geometries, false);
//建筑贴图
const texture = new THREE.TextureLoader().load('assets/image.jpg');
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
const material = new THREE.MeshStandardMaterial({ map: texture,transparent: true });
const cube = new THREE.Mesh(mergedGeometry, material);
//形状产生阴影
cube.castShadow = true;
//形状接收阴影
cube.receiveShadow = true;
this.scene.add(cube);

image.png


效果就是很多高楼大厦的样子,为什么楼顶有窗?别在意这些细节,有的人就喜欢开天窗呢~


2.搞个雷达扩散和扫描特效


改变建筑材质shader,计算建筑的俯视uv


material.onBeforeCompile = (shader, render) => {
this.shaders.push(shader);
//范围大小
shader.uniforms.uSize = { value: 50 };
shader.uniforms.uTime = { value: 0 };
//修改顶点着色器
shader.vertexShader = shader.vertexShader.replace(
'void main() {',
` uniform float uSize;
varying vec2 vUv;
void main() {`

);
shader.vertexShader = shader.vertexShader.replace(
'#include <fog_vertex>',
`#include <fog_vertex>
//计算相对于原点的俯视uv
vUv=position.xz/uSize;`

);
//修改片元着色器
shader.fragmentShader = shader.fragmentShader.replace(
'void main() {',
`varying vec2 vUv;
uniform float uTime;
void main() {`

);
shader.fragmentShader = shader.fragmentShader.replace(
'#include <dithering_fragment>',
`#include <dithering_fragment>
//渐变颜色叠加
gl_FragColor.rgb=gl_FragColor.rgb+mix(vec3(0,0.5,0.5),vec3(1,1,0),vUv.y);`

);
};

image.png


然后你将同样的onBeforeCompile函数赋值给平面的时候,没有对应的效果。


因为平面没有z,只有xy,而且经过了-90度旋转后,坐标位置也要对应反转,由此可以得出平面的uv计算公式


vUv=vec2(position.x,-position.y)/uSize;

image.png


至此,建筑和平面的俯视uv一致了。


雷达扩散特效



  • 雷达扩散就是一段渐变的环,随着时间扩大。

  • 顶点着色器不变,改一下片元着色器,增加扩散环颜色uColor,对应shader.uniforms也要添加


shader.uniforms.uColor = { value: new THREE.Color('#00FFFF') };

const fragmentShader1 = `varying vec2 vUv;
uniform float uTime;
uniform vec3 uColor;
uniform float uSize;
void main() {`
;
const fragmentShader2 = `#include <dithering_fragment>
//计算与中心的距离
float d=length(vUv);
if(d >= uTime&&d<=uTime+ 0.1) {
//扩散圈
gl_FragColor.rgb = gl_FragColor.rgb+mix(uColor,gl_FragColor.rgb,1.0-(d-uTime)*10.0 )*0.5 ;
}`
;

shader.fragmentShader = shader.fragmentShader.replace('void main() {', fragmentShader1);
shader.fragmentShader = shader.fragmentShader.replace(
'#include <dithering_fragment>',
fragmentShader2);

//改变shader的时间变量,动起来
animateAction() {
if (this.shaders?.length) {
this.shaders.forEach((shader) => {
shader.uniforms.uTime.value += 0.005;
if (shader.uniforms.uTime.value >= 1) {
shader.uniforms.uTime.value = 0;
}
});
}
}

20240322_224153.gif


噔噔噔噔,完成啦!是立体化的雷达扩散,看起来很酷的样子。


雷达扫描特效


跟上面雷达扩散差不多,只要修改一下片元着色器



  • 雷达扫描是通过扇形渐变形成的,还要随着时间旋转角度


  const fragmentShader1 = `varying vec2 vUv;
uniform float uTime;
uniform vec3 uColor;
uniform float uSize;
//旋转角度矩阵
mat2 rotate2d(float angle)
{
return mat2(cos(angle), - sin(angle),
sin(angle), cos(angle));
}
//雷达扫描渐变扇形
float vertical_line(in vec2 uv)
{
if (uv.y > 0.0 && length(uv) < 1.2)
{
float theta = mod(180.0 * atan(uv.y, uv.x)/3.14, 360.0);
float gradient = clamp(1.0-theta/90.0,0.0,1.0);
return 0.5 * gradient;
}
return 0.0;
}
void main() {`
;


const fragmentShader2 = `#include <dithering_fragment>
mat2 rotation_matrix = rotate2d(- uTime*PI*2.0);
//将雷达扫描扇形渐变混合到颜色中
gl_FragColor.rgb= mix( gl_FragColor.rgb, uColor, vertical_line(rotation_matrix * vUv)); `
;

20240322_232006.gif


GitHub地址


https://github.com/xiaolidan00/my-earth


作者:敲敲敲敲暴你脑袋
来源:juejin.cn/post/7349837128508964873
收起阅读 »

震惊!🐿浏览器居然下毒!

web
发生什么事了 某天,我正在愉快的摸鱼,然后我看到测试给我发了条消息,说我们这个系统在UC浏览器中有问题,没办法操作,点了经常没反应(测试用得iPhone14,是一个h5的项目)。我直接懵了,这不是都测了好久了吗,虽然不是在uc上测得,chrome、safari...
继续阅读 »

发生什么事了


某天,我正在愉快的摸鱼,然后我看到测试给我发了条消息,说我们这个系统在UC浏览器中有问题,没办法操作,点了经常没反应(测试用得iPhone14,是一个h5的项目)。我直接懵了,这不是都测了好久了吗,虽然不是在uc上测得,chrome、safari、自带浏览器等,都没这个问题,代码应该是没问题的,uc上为啥会没有反应呢?难道是有什么隐藏的bug,需要一定的操作顺序才能触发?我就去找了测试,让他重新操作一下,看看是啥样的没反应。结果就是,正常进入列表页(首页),正常点某一项,正常进入详情页,然后点左上角返回,没反应。我上手试了下,确实,打开vconsole看了下,也没有报错。在uc上看起来还是必现的,我都麻了,这能是啥引起的啊。


找问题


在其他浏览器上都是好好的,uc上也不报错,完全看不出来代码有啥bug,完全没有头绪啊!那怎么办,刷新看看:遇事不决,先刷新,还不行就清空缓存刷新。刷新了之后,哎,好了!虽然不知道是什么问题,但现在已经好了,就当作遇到了灵异事件,我就去做其他事了。


过了一会,测试来找我了,说又出现了,不止是详情页,进其他页面也返回不了。这就难受住了呀,说明肯定是有问题的,只是还没找到原因。我就只好打开vconsole,一遍一遍的进入详情页点返回;刷新再进;清掉缓存,再进。


然后,我就发现,network中,出现了一个没有见过的请求


20240906-222447.png


20240906-222456.png
根据track、collect这些单词来判断,这应该是uc在跟踪、记录某些操作,requestType还是个ping;我就在想,难道是这个请求的问题?但是请求为啥会导致,我页面跳转产生问题?然后我又看到了intercept(拦截)、pushState(history添加记录)拦截了pushState?

这个项目确实使用的是history路由,问题也确实出在路由跳转的时候;而且出现问题的时候,路由跳转,浏览器地址栏中的地址是没有变化的,返回就g了(看起来是后退没有反应,实际是前进时G了)。这样看,uc确实拦截了pushState的操作。那它是咋做到的?


原来如此


然后,我想起来,前段时间在掘金上看到了一篇,讲某些第三方cdn投毒的事情,那么uc是不是在我们不知情的情况下,改了我们的代码。然后我切到了vconsole的element,展开head,发现了一个不属于我们项目的script,外链引入了一段js,就挂在head的最顶上。通过阅读,发现它在window的history上加了点料覆写了forward和pushState(forward和pushState是继承来的方法)

正常的history应该是这样:


image.png
复写的类似这样:


image.png
当然,有些系统或框架,为了实现某些功能,比如实现触发popstate的效果,也会复写

但uc是纯纯的为了记录你的操作,它这玩意主要还有bug,会导致路由跳转出问题,真是闹麻了


如何做


删掉就好了,只要删掉uc添加的,当我们调用相关方法时,history就会去继承里找


// 判断是否是uc浏览器
if (navigator.userAgent.indexOf('UCBrowser') > -1) {
if (history.hasOwnProperty('pushState')) {
delete window.history.forward
delete window.history.pushState
}
// 找到注入的script
const ucScript = document.querySelector('script[src*="ucbrowser_script"]')
if (ucScript) {
document.head.removeChild(ucScript)}
}
}

吐槽


你说你一个搞浏览器的,就不能在底层去记录用户行为吗,还不容易被发现。主要是你这玩意它有bug呀,这不是更容易被发现吗。(这是23年11月份遇到的问题,当时产品要求在qq/百度/uc这些浏览器上也都测一下才发现的,现在记录一下,希望能帮助到其他同学)


作者:忍者扔飞镖
来源:juejin.cn/post/7411358506048766006
收起阅读 »

CSS 终于在 2024 年增加了垂直居中功能

web
本文翻译自 CSS finally adds vertical centering in 2024,作者:James Smith, 略有删改。 在 2024 年的 CSS 原生属性中允许使用 1 个 CSS 属性 align-content: center进...
继续阅读 »

本文翻译自 CSS finally adds vertical centering in 2024,作者:James Smith, 略有删改。



在 2024 年的 CSS 原生属性中允许使用 1 个 CSS 属性 align-content: center进行垂直居中。


<div style="align-content: center; height: 100px;">
<code>align-content</code> 就是这么简单!
</div>



支持情况:


Chrome: 123Firefox: 125Safari: 17.4

CSS 对齐一般会使用 flexboxgrid 布局,因为 align-content 在默认的流式布局中不起作用。在 2024 年,浏览器实现了 align-content



  • 你不需要 flexbox 或 grid,只需要 1 个 CSS 属性就可以进行对齐。

  • 因此内容不需要包裹在 div 中。


<!-- 有效 -->
<div style="display: grid; align-content: center;">
内容。
</div>


<!-- 失败!-->
<div style="display: grid; align-content: center;">
包含 <em>多个</em> 节点的内容。
</div>


<!-- 包装div有效 -->
<div style="display: grid; align-content: center;">
<div> <!-- 额外的包装器 -->
包含 <em>多个</em> 节点的内容。
</div>
</div>


<!-- 无需包装div即可工作 -->
<div style="align-content: center;">
包含 <em>多个</em> 节点的内容。
</div>


令人惊讶的是,经过几十年的发展,CSS 终于有了 一个属性 来控制垂直对齐!


垂直居中的历史


浏览器很有趣,像对齐这样的基本需求长期以来都没有简单的答案。以下是在浏览器中垂直居中的方法(水平居中是另一个话题):


方法 1: 表格单元格


星级:★★★☆☆


有 4 种主要布局:流(默认)、表格、flexbox、grid。如何对齐取决于容器的布局。Flexbox 和 grid 相对较晚添加,所以表格是第一种方式。


<div style="display: table;">
<div style="display: table-cell; vertical-align: middle;">
内容。
</div>
</div>


方法 2: 绝对定位


星级:☆☆☆☆☆


通过绝对定位间接的方式来实现这个效果。


<div style="position: relative;">
<div style="position: absolute; top: 50%; transform: translateY(-50%);">
内容。
</div>
</div>


这个方式通过绝对定位来绕过流式布局:



  • position: relative 标记参考容器。

  • position: absolute; top: 50% 将内容的边缘放置在中心。

  • transform: translateY(-50%) 将内容中心偏移到边缘。


方法 3: 内联内容


星级:☆☆☆☆☆


虽然流布局对内容对齐没有帮助。它允许在一行内进行垂直对齐。那么为什么不使一行和容器一样高呢?


<div class="container">
::before
<div class="content">内容。</div>
</div>


.container::before {
content: '';
height: 100%;
display: inline-block;
vertical-align: middle;
}
.content {
display: inline-block;
vertical-align: middle;
}


这个方式有一个缺陷,需要额外创建一个伪元素。


方法 4: 单行 flexbox


星级:★★★☆☆


现在布局中的 Flexbox 变得广泛可用。它有两种模式:单行和多行。在单行模式(默认)中,行填充垂直空间,align-items 对齐行内的内容。


<div style="display: flex; align-items: center;">
<div>内容。</div>
</div>


或者调整行为列,并用 justify-content 对齐内容。


<div style="display: flex; flex-flow: column; justify-content: center;">
<div>内容。</div>
</div>


方法 5: 多行 flexbox


星级:★★★☆☆


在多行 flexbox 中,行不再填充垂直空间,所以行(只有一个项目)可以用 align-content 对齐。


<div style="display: flex; flex-flow: row wrap; align-content: center;">
<div>内容。</div>
</div>


方法 6: grid


星级:★★★★☆


Grid 出来的更晚,对齐变得更简单。


<div style="display: grid; align-content: center;">
<div>内容。</div>
</div>


方法 7: grid 单元格


星级:★★★★☆


注意与前一个方法的微妙区别:



  • align-content 将单元格居中到容器。

  • align-items 将内容居中到单元格,同时单元格拉伸以适应容器。


<div style="display: grid; align-items: center;">
<div>内容。</div>
</div>


似乎有很多方法可以做同一件事。


方法 8: margin:auto


星级:★★★☆☆


在流布局中,margin:auto 可以水平居中,但不是垂直居中。使用 margin-block: auto 可以设置垂直居中。


<div style="display: grid;">
<div style="margin-block: auto;">
内容。
</div>
</div>


方法 9: 这篇文章的开头


星级:★★★★★


为什么浏览器最初没有添加这个?


<div style="align-content: center;">
<code>align-content</code> 就是这么简单!
</div>


总结


CSS 的新特性 align-content 提供了一个简单且直接的方式来实现垂直居中,无需使用额外的div包装或复杂的布局模式即可完成垂直居中。但注意这个属性还存在一定的浏览器兼容性,在线上使用需谨慎。




看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~



专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)



作者:南城FE
来源:juejin.cn/post/7408097468796551220
收起阅读 »

写css没灵感,那是你没用到这几个开源库

web
你是否遇到过写css没灵感,写不出酷炫的效果,那这篇文章你一定要看完。知道这几个开源库,它能让你写出炸天的效果并且有效地增加你的摸鱼时长。 1.CSS Inspiration 网址: chokcoco.github.io/CSS-Inspira… CSS ...
继续阅读 »

你是否遇到过写css没灵感,写不出酷炫的效果,那这篇文章你一定要看完。知道这几个开源库,它能让你写出炸天的效果并且有效地增加你的摸鱼时长。


1.CSS Inspiration


网址:


chokcoco.github.io/CSS-Inspira…


图片


图片


CSS Inspiration 上面有各种天马行空的css教程,涵盖了css的许多常见的特效。以分类的形式展示不同的css属性或者不同的课题,例如布局方式、border、伪元素、滤镜、背景3D等。这些都是css里面十分重要的知识点,不管是用于学习还是项目中实际运用都是不错的选择。


当然你也可以用来巩固基础知识,可以利用此项目来制作一些常用的特效,可以看到有上百个经典案例供我们参考,重点是提供源代码,复制粘贴即可使用。


图片


2.Neumorphism


地址:


neumorphism.io/


图片


Neumorphism属于新拟态ui风格,是目前比较新颖的一种前端css设计风格。它的格调简单,基本颜色比较浅,如米白、浅灰、浅蓝等。再利用阴影呈现出凹凸效果,看起来很简单舒适且有3D效果,因此我们可以通过拟态设计出很多优美的页面,拖动效果控制条即可秒生成css样式。


图片


3.AnimXYZ


地址:


animxyz.com/


图片


如果说你热衷于动画,那animxyz绝对是你的不二之选。你可以使用animxyz组合和混合不同的动画来创建自己的高度可定制的css动画,而无需编写一个单一的关键帧。


图片


相比于animate css,它的强大之处在于你可以在这里根据自己的想法来手动配置动画。实现的动画代码实例,我们可以复制迁移到项目中使用。


4.CodePen


图片


图片


最后要推荐的则是我最常用也是我最推荐的,它就是codepen。codepen是一个完全免费的前端代码托管服务,上面云集了各路大神,拥有全世界前端达人经典项目进行展示,让你从中获取到很多的创作灵感。


它可以实现即时预览,你甚至可以在线修改并及时预览别人的作品。支持多种主流预处理器,快速添加外部资源文件,只需在输入框里输入库名,codepen就会从cdn上寻找匹配的css或js库。


作者:程序员Winn
来源:juejin.cn/post/7278238985448177704
收起阅读 »

为什么 2!=false 和 2!=true 返回的都是true

web
前言 今天突然想起一个奇怪的问题,记录一下,我在控制台执行内容如下: 由上图可见,2 != false 和 2 != true 返回的值竟然都是true,那么为什么呢,请看下文: 1 != 操作符的作用 != 是“不等于”操作符。它会在比较前执行类型转换,...
继续阅读 »

前言


今天突然想起一个奇怪的问题,记录一下,我在控制台执行内容如下:


image-20240821171734282


由上图可见,2 != false2 != true 返回的值竟然都是true,那么为什么呢,请看下文:


1 != 操作符的作用



  • != 是“不等于”操作符。它会在比较前执行类型转换,然后再比较两个值是否不相等。


    在 JavaScript 中,2 != false2 != true 返回 true 的原因涉及到 JavaScript 中的类型转换和比较规则。



2 类型转换


当使用 != 进行比较时,JavaScript 会尝试将比较的两个值转换为相同的类型,然后再进行比较。以下是 2 != false2 != true 的过程:


2 != false



  1. false 会被转换为数字类型。根据 JavaScript 的转换规则,false 被转换为 0

  2. 现在表达式变成了 2 != 0

  3. 20 不相等,因此返回 true


2 != true



  1. true 会被转换为数字类型。根据 JavaScript 的转换规则,true 被转换为 1

  2. 现在表达式变成了 2 != 1

  3. 21 不相等,因此返回 true


总结



  • 2 != false 返回 true 是因为 20 不相等。

  • 2 != true 返回 true 是因为 21 不相等。


这就是为什么 2 != false2 != true 都会返回 true


作者:潜心专研的小张同学
来源:juejin.cn/post/7411168461500563468
收起阅读 »

哦!该死的您瞧瞧这箭头

web
今天和大家分享一个小思路,用于实现箭头步骤条效果。 在我们项目中,有一个需求,想实现一个步骤条,默认的时候是 边框和文字 需要有特定的颜色,但是选中时,背景需要有特定颜色,边框颜色消失,文字显示白色,具体效果如下图: 可以看到,步骤一是默认样式,步骤二是选...
继续阅读 »

image.png


今天和大家分享一个小思路,用于实现箭头步骤条效果。


在我们项目中,有一个需求,想实现一个步骤条,默认的时候是 边框和文字 需要有特定的颜色,但是选中时,背景需要有特定颜色,边框颜色消失,文字显示白色,具体效果如下图:


image.png


image.png


可以看到,步骤一是默认样式,步骤二是选中样式,即选中背景颜色需要变成默认样式的边框颜色


使用div思路(无法实现默认效果)


当时第一次想的是使用div来实现这个逻辑,因为看到elementui有个差不多的(但是实现不了上面的效果,实现一个箭头倒是可以的,下面为大家简单介绍div的实现思路)



image.png
-- 饿了么效果图



搭建dom结构


首先我们先创建一个矩形


image.png


然后像这样使用一个伪元素,盖在矩形的开头,并修改其border-color的颜色即可,操作方式如下图


image.png


    <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
width: 100vw;
display: grid;
place-content: center;
overflow: hidden;
}
.arrow {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
max-width: max-content;
height: 30px;
background: salmon;
}

.arrow::after {
position: absolute;
content: "";
left: 0px;
border: 15px solid transparent;
border-left-color: white;
}

.arrow::before {
position: absolute;
content: "";
right: 0px;
border: 15px solid transparent;
border-top-color: white;
border-right-color: white;
border-bottom-color: white;
}

.content {
text-align: center;
padding: 0px 20px;
width: 140px;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.content-inner {
display: inline-block;
width: 100%;
transform: translateX(-5px);
}
</style>
</head>
<body>
<div class="arrow">
<div class="content">
<span class="content-inner">1</span>
</div>
</div>
</body>
</html>

这样就实现了一个箭头啦。


image.png



但是使用div实现箭头,并不太好实现我们开头想要的那种效果,如果非要实现也要费很大劲,得不偿失,所以接下来,介绍第二种方案



使用SVG标签(可缩放矢量图形)


实现思路即标签介绍


polyline


polyline元素是 SVG 的一个基本形状,用来创建一系列直线连接多个点。


使用到的属性:



  • stroke-width: 用于设置绘制的线段宽度

  • fill: 填充色

  • stroke: 线段颜色,

  • points:绘制一个元素点的数列 (0,0 22,22


接下来我们尝试使用该元素绘制一个箭头,先看看需要多少点位(如下图需6个,但是元素需要闭合,所以需要7个)


image.png


所以我们就可以很轻松的绘制出一个箭头,具体代码如下


  <svg
:viewBox="0 0 130 26"
width="130px"
height="26"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>

<polyline
class="polyline"
points="0,0 115,0 130,13 115,26 0,26 15,13 0,0"
fill="green"
stroke-width="1"
>
</polyline>
</svg>

image.png


此时我们得到了类似选中后的颜色,那默认颜色呢,只需要修改其 fill, stroke属性即可,具体逻辑如下


    <svg
:viewBox="0 0 130 26"
width="130px"
height="26"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>

<polyline
class="polyline"
points="0,0 115,0 130,13 115,26 0,26 15,13 0,0"
fill="transparent"
stroke="red"
stroke-width="1"
>
</polyline>
</svg>

image.png


此时那文中的内容怎么办,没法直接放标签内,此时需要借助另一个标签。


foreignObject



SVG中的  <foreignObject>  元素允许包含来自不同的 XML 命名空间的元素。在浏览器的上下文中,很可能是 XHTML / HTML。



所以我们可以使用该标签来作为放置内容的容器


属性介绍:



  • x:设置 foreignObject 的 x 坐标

  • y:设置 foreignObject 的 y 坐标

  • width:设置 foreignObject 的宽度

  • height:设置 foreignObject 的高度


具体代码如下


 <svg
:viewBox="0 0 130 26"
width="130px"
height="26"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>

<polyline
class="polyline"
points="0,0 115,0 130,13 115,26 0,26 15,13 0,0"
fill="transparent"
stroke="red"
stroke-width="1"
>


</polyline>
<foreignObject
x="0"
y="0"
width="130"
height="26"
>

<span
style="line-height: 26px; transform: translateX(14px); display: inline-block;"
>

步骤1111111
</span>
</foreignObject>
</svg>


image.png


这样就实现了默认样式,文字颜色可以自己调整


完整代码


由于需要遍历数据,所以完整代码是 vue3 风格


<template>
<div class="next-step-item" @click="stepClick">
<svg
:viewBox="`0 0 ${arrowStaticData.width} ${arrowStaticData.height}`"
:width="arrowStaticData.width"
:height="arrowStaticData.height"
:style="{
transform:
index === 0
? 'translate(0px,0px)'
: `translate(${arrowStaticData.offsetLeft * index}px,0)`,
}"

version="1.1"
xmlns="http://www.w3.org/2000/svg"
>

<polyline
class="polyline"
:points="points"
v-bind="color"
stroke-width="1"
>
</polyline>
<foreignObject
x="0"
y="0"
:width="arrowStaticData.width"
:height="arrowStaticData.height"
>

<span
class="svg-title"
:style="{
color: fontColor,
lineHeight: arrowStaticData.height + 'px',
}"

:title="title"
>

{{ title }}
</span>
</foreignObject>
</svg>
</div>
</template>

<script lang="ts" setup>
import { computed } from "vue";

const defaultFontColor = "#fff";
const defaultColor = "transparent";

// 主题颜色
const colorObj = Object.freeze({
finish: {
default: {
stroke: "#16BB60",
fill: defaultColor,
color: "#16BB60",
},
active: {
stroke: "#16BB60",
fill: "#16BB60",
color: defaultFontColor,
},
}, // 绿色
await: {
default: {
stroke: "#edf1f3",
fill: defaultColor,
color: "#333",
},
active: {
stroke: "#edf1f3",
fill: "#edf1f3",
color: "#333",
},
}, // 灰色
process: {
default: {
stroke: "#0A82E5",
fill: defaultColor,
color: "#0A82E5",
},
active: {
stroke: "#0A82E5",
fill: "#0A82E5",
color: defaultFontColor,
},
}, // 蓝色
});
const arrowStaticData = Object.freeze({
width: 130,
height: 26,
hornWidth: 15, // 箭头的大小
offsetLeft: -7, // step离左侧step的距离,-15则左间距为0
});

const emits = defineEmits(["stepClick"]);

const props = defineProps({
title: {
type: String,
default: "",
},
// 类型名称
typeName: {
type: String,
default: "",
},
// 是否点中当前的svg
current: {
type: Boolean,
default: false,
},
// 当前是第几个step
index: {
type: Number,
default: 0,
},
});

const points = computed(() => {
const { width, hornWidth, height } = arrowStaticData;
return props.index === 0
? `0,0 ${width - hornWidth},0
${width},${height / 2}
${width - hornWidth},${height}
0,${height} 0,0`

: `0,0 ${width - hornWidth},0
${width},${height / 2}
${width - hornWidth},${height}
0,${height}
${hornWidth},${height / 2} 0,0`
;
});

const color = computed(() => {
let color = {};
const currentStyleConfig: any = colorObj[props.typeName];
// 如果当前是被选中的,颜色需要区分
if (props.current) {
color = {
fill: currentStyleConfig.active.fill,
stroke: currentStyleConfig.active.stroke,
};
} else {
color = {
stroke: currentStyleConfig.default.stroke,
fill: currentStyleConfig.default.fill,
};
}
return color;
});

const fontColor = computed(() => {
const currentStyleConfig: any = colorObj[props.typeName];
let fontColor = "";
if (props.current) {
fontColor = currentStyleConfig.active.color;
} else {
fontColor = currentStyleConfig.default.color;
}
return fontColor;
});

const stepClick = () => {
emits("stepClick", props.index);
};
</script>

<style lang="scss" scoped>
.next-step-item {
cursor: pointer;
.polyline {
transition: 0.3s;
}
.svg-title {
padding: 0 15px;
display: block;
position: relative;
width: 100%;
text-align: center;
font-weight: bold;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: 0.3s;
box-sizing: border-box;
}
}
</style>


使用方式


<template>
<div>
<div class="arrow-container">
<arrow
v-for="item of arrowList"
:key="item.index"
v-bind="item"
:current="arrowCurrent === item.index"
@stepClick="changeStepCurrent"
/>

</div>
</div>
</template>

<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import Arrow from "components/Arrow/index.vue";

const arrowCurrent = ref<number>(0);
const arrowList = [
{
index: 0,
title: "步骤一一一一一一一一一",
typeName: "process",
},
{
index: 1,
title: "步骤二一一一一一一一一一",
typeName: "finish",
},
{
index: 2,
title: "步骤三",
typeName: "await",
},
];

</script>

<style lang="scss">
.arrow-container {
padding: 30px;
display: flex;
width: 800px;
height: 400px;
border: 1px solid #ccc;
margin-top: 100px;
box-sizing: border-box;
}
</style>



完整效果


image.png


作者:写代码真是太难了
来源:juejin.cn/post/7350695708074344498
收起阅读 »

uniapp实现背景颜色跟随图片主题色变化(多端兼容)

web
最近做uniapp项目时遇到一个需求,要求模仿腾讯视频app首页的背景颜色跟随banner图片的主题颜色变化,并且还要兼容H5、APP、微信小程序三端。 由于项目的技术栈为uniapp,所以以下使用uni-ui的组件库作为栗子。 需求分析 腾讯视频app效果如...
继续阅读 »

canvas 00_00_00-00_00_30.gif


最近做uniapp项目时遇到一个需求,要求模仿腾讯视频app首页的背景颜色跟随banner图片的主题颜色变化,并且还要兼容H5、APP、微信小程序三端。


由于项目的技术栈为uniapp,所以以下使用uni-ui的组件库作为栗子


需求分析


腾讯视频app效果如下:


fe5c83b7f461b47bd0d68a827a07412.jpg


从上图看出,大致分为两步:



1.获取图片主题色


2.设置从上到下的主题色to白色的渐变:


background: linear-gradient(to bottom, 主题色, 白色)



获取主题色主要采用canvas绘图,绘制完成后获取r、g、b三个通道的颜色像素累加值,最后再分别除以画布大小,得到每个颜色通道的平均值即可。


搭建页面结构


page.vue




<script>
import {
getImageThemeColor
}
from '@/utils/index'
export default {
data() {
return {
// 图片列表
list: [],
// 当前轮播图索引
current: 0,
// 缓存banner图片主题色
colors: [],
// 记录当前提取到第几张banner图片
count: 0
}
},
computed: {
// 动态设置banner主题颜色背景
getStyle() {
const color = this.colors[this.current]
return {
background: color ? `linear-gradient(to bottom, rgb(${color}), #fff)` : '#fff'
}
}
},
methods: {
// banner改变
onChange(e) {
this.current = e.target.current
},
getList() {
this.list = [
'https://img.zcool.cn/community/0121e65c3d83bda8012090dbb6566c.jpg@3000w_1l_0o_100sh.jpg',
'https://img.zcool.cn/community/010ff956cc53d86ac7252ce64c31ff.jpg@900w_1l_2o_100sh.jpg',
'https://img.zcool.cn/community/017fc25ee25221a801215aa050fab5.jpg@1280w_1l_2o_100sh.jpg',
]
},
// 获取主题颜色
getThemColor() {
getImageThemeColor(this, this.list[this.count], 'canvas', (color) => {
const colors = [...this.colors]
colors[
this.count] = color
this.colors = colors
this.count++
if (this.count < this.list.length) {
this.getThemColor()
}
})
}
},
onLoad() {
this.getList()
// banner图片请求完成后,获取主题色
this.getThemColor()
}
}
script>


<style>
.box {
display: flex;
flex-direction: column;
background-color: deeppink;
padding: 10px;
}

.tabs {
height: 100px;
color: #fff;
}

.swiper {
width: 95%;
height: 200px;
margin: auto;
border-radius: 10px;
overflow: hidden;
}

image {
width: 100%;
height: 100%;
}
style>


封装获取图片主题颜色函数


先简单讲下思路 (想直接看源码可直接跳到下面) 。先通过request请求图片地址,获取图片的二进制数据,再将图片资源其转换成base64,调用drawImage进行绘图,最后调用draw方法绘制到画布上。


CanvasContext.draw介绍


image.png
更多api使用方法可参考:uniapp官方文档


getImageThemeColor.js


/**
* 获取图片主题颜色
*
@param path 图片的路径
*
@param canvasId 画布id
*
@param success 获取图片颜色成功回调,主题色的RGB颜色值
*
@param fail 获取图片颜色失败回调
*/

export const getImageThemeColor = (that, path, canvasId, success = () => {}, fail = () => {}) => {
// 获取图片后缀名
const suffix = path.split('.').slice(-1)[0]
// uni.getImageInfo({
// src: path,
// success: (e) => {
// console.log(e.path) // 在安卓app端,不管src路径怎样变化,path路径始终为第一次调用的图片路径
// }
// })
// 由于getImageInfo存在问题,所以改用base64
uni.request({
url: path,
responseType: 'arraybuffer',
success: (res) => {
let base64 = uni.arrayBufferToBase64(res.data);
const img = {
path: `data:image/${suffix};base64,${base64}`
}
// 创建canvas对象
const ctx = uni.createCanvasContext(canvasId, that);

// 图片绘制尺寸
const imgWidth = 300;
const imgHeight = 150;

ctx.drawImage(img.path, 0, 0, imgWidth, imgHeight);

ctx.save();
ctx.draw(true, () => {
uni.canvasGetImageData({
canvasId: canvasId,
x: 0,
y: 0,
width: imgWidth,
height: imgHeight,
fail: fail,
success(res) {
let data = res.data;
let r = 1,
g = 1,
b = 1;
// 获取所有像素的累加值
for (let row = 0; row < imgHeight; row++) {
for (let col = 0; col < imgWidth; col++) {
if (row == 0) {
r += data[imgWidth * row + col];
g += data[imgWidth * row + col + 1];
b += data[imgWidth * row + col + 2];
} else {
r += data[(imgWidth * row + col) * 4];
g += data[(imgWidth * row + col) * 4 + 1];
b += data[(imgWidth * row + col) * 4 + 2];
}
}
}
// 求rgb平均值
r /= imgWidth * imgHeight;
g /= imgWidth * imgHeight;
b /= imgWidth * imgHeight;
// 四舍五入
r = Math.round(r);
g = Math.round(g);
b = Math.round(b);
success([r, g, b].join(','));
},
}, that);
});
}
});
}

主题色计算公式


计算图片主题色的公式主要有两种常见的方法:平均法和主成分分析法。


平均法:


平均法是最简单的一种方法,它通过对图片中所有像素点的颜色进行平均来计算主题色。具体步骤如下:



  • 遍历图片的每个像素点,获取其RGB颜色值。

  • 将所有像素点的R、G、B分量分别求和,并除以像素点的总数,得到平均的R、G、B值。

  • 最终的主题色即为平均的R、G、B值。


主成分分析法


主成分分析法是一种更复杂但更准确的方法,它通过对图片中的颜色数据进行降维处理,提取出最能代表整个图片颜色分布的主要特征。具体步骤如下:



  • 将图片的所有像素点的颜色值转换为Lab颜色空间(Lab颜色空间是一种与人眼感知相关的颜色空间)。

  • 对转换后的颜色数据进行主成分分析,找出相应的主成分。

  • 根据主成分的权重,计算得到最能代表整个图片颜色分布的主题色。


需要注意的是,计算图片主题色的方法可以根据具体需求和算法的实现方式有所不同,上述方法只是其中的两种常见做法。


结语


大家有更好的实现方式,欢迎评论区留言哦!




作者:vilan_微澜
来源:juejin.cn/post/7313979304513044531
收起阅读 »

和妹子逛完街,写了个 AI 智能穿搭系统

web
想直接看成品演示的可以直接划到文章底部 背景 故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个小时。虽然这个时间不多, 但这个时间黑神话悟空足矣让我打完虎先锋 回家我就灵光一闪,是不是可以搞一个AI智能穿搭,只需要上传自己的照片和对...
继续阅读 »

想直接看成品演示的可以直接划到文章底部



背景


故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个小时。虽然这个时间不多,
但这个时间黑神话悟空足矣让我打完虎先锋


回家我就灵光一闪,是不是可以搞一个AI智能穿搭,只需要上传自己的照片和对应的衣服图片就能实现在线试衣服呢?


说干就干,我就开始构思方案,画原型。
俗话说万事开头难,事实上这个构思到动工就耗费了我一个礼拜,因为一直在构思怎么样的交互场景会让用户使用起来比较丝滑,并且容易上手。


目前实现的功能有:



  • ✅ 用户信息展示

  • ✅ AI 生成穿搭

  • ✅ 风格大厅


待完成:



  • 私人衣柜

  • AI 换鞋


经过


1. 画产品原型


起初第一个版本的产品原型由于是自己构思没有任何参考,直接上手撸代码的,想到啥就画啥,所以布局非常传统,配色也非常普通(蚂蚁蓝),所以感觉没有太多的时尚气息(个人觉得丑的一逼,不像是互联网的产物)。因为重构掉了,老的现在没有了,我懒就不重新找回来截图了,直接画个当时的样子,大概长成下面这样:



丑的我忍不了,我就去设计师专门用的网站参(chao)考(xi)了一下,找来找去,终于有了下面的最终版原型图



2. 配色选择


大家知道,所有的UI设计,都离不开主题色的选择,比如:淘宝橙、飞猪橙、果粒橙...,目的一方面是为了打造品牌形象,另一方面也是为了提升品牌辨识度,让你看到这个颜色就会想起它


那我必须也得跟上时代的潮流,选了 #c1a57b 这款低调而又不失奢华的色值作为主题色,英雄不问出处,问就是借鉴。


3. 技术选型


我对技术的定义是:技术永远服务于产品,能高效全面帮助我开发出一款应用,并且能保证后续的稳定性和可维护性,啥技术我都行。当然如果这门技术我优先会从我属性的板块去找。


经过各种权衡和比较,最后敲定下来了技术选型方案:



  • 前端:taro (为了后续可能会有小程序端做准备)

  • 后端:koajs (实际使用的是midway,基于koajs,主要是比较喜欢koa的轻量化架构)

  • 数据库:mongodb (别问,问就是简单易上手)

  • 代码仓库:gitea

  • CI:gitea-runner

  • 部署工具:pm2

  • 静态文件托管:阿里云OSS


4. 撸代码


这里我只挑一些个人感觉相对需要注意的地方展开讲讲


4.1 图片转存


由于我生成图片的API图片链接会在一天之后失效,所以我需要在调用任务详情的时候,把这个文件转存到我自己的oss服务器,这里我总结出来的思路是:【1. 保存在本地暂存文件夹】-【2. 调用node流式读取接口】-【3. 保存到oss】-【4. 返回替换原来的链接】


具体代码参考如下:


const tempDir = path.join(tmpdir(), 'temp-upload-files')
const link = url.parse(src);
const fileName = path.basename(link.pathname)
const localPath = path.join(tempDir, `/${fileName}`); // 生成保存路径
let request
if (link.protocol === 'https:') {
request = https
} else {
request = http
}
request.get(src, async (response) => {
const fileStream = await fs.createWriteStream(localPath); // 保存到本地暂存路径

await response.pipe(fileStream);
fileStream.on("error", (error) => {
console.error("保存图片出错:", error);
reject(error)
});
fileStream.on('finish', async res => {
console.log('暂存完成,开始上传:', res)
let result = await this.ossService.put(`/${params.saveDir || 'tmp'}/${fileName}`, localPath);
if (!result) return
resolve(result)
});
});

这里的request因为我不想引入其它的库所以这样写,如果有更好的方案,可以在评论区告知一下。



这里需要注意的一个地方是,上传的这个 localPath 最好是自己做一下处理,我这边没有处理,因为可能两个用户同时上传,他们的文件名称相同的时候,可能会出现覆盖的情况,包括后面的oss保存也是。



4.2 文件流式上传中间件


因为默认的接口处理是不处理流式调用的,所以需要自己创建一个中间件来拦截处理一下,下面给出我的参考代码:



class SSE {
ctx: Context
constructor(ctx: Context) {
ctx.status = 200;
ctx.set('Content-Type', 'text/event-stream');
ctx.set('Cache-Control', 'no-cache');
ctx.set('Connection', 'keep-alive');
ctx.res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Transfer-Encoding': 'chunked'
});
ctx.res.flushHeaders();
this.ctx = ctx;
}
send(data: any) {
// string
if (typeof data === "string") {
this.push(data);
} else if (data.id) {
this.push(`id: ${data.id}\n`);
} else if (data.event) {
this.push(`event: ${data.event}\n`);
} else {
const text = JSON.stringify(data)
this.push(`data: ${text}\n\n`);
}
}
push(data: any) {
this.ctx.res.write(data);
this.ctx.res.flushHeaders();
}
close() {
this.ctx.res.end();
}
}

@Middleware()
export class StreamMiddleware implements IMiddleware<Context, NextFunction> {
// ?------------ 中间件处理逻辑 -----------------
resolve() {
return async (ctx: Context, next: NextFunction) => {

if (ctx.res.headersSent) {
if (!ctx.sse) {
console.error('[sse]: response headers already sent, unable to create sse stream');
}
return await next();
}

const sse = new SSE(ctx);
ctx.sse = sse;
await next();

if (!ctx.body) {
ctx.body = ctx.sse;
} else {
ctx.sse.send(ctx.body);
ctx.body = sse;
}
};
}

public match(ctx: Context): boolean {
// ?------------ 不带 stream 前缀默认都不是流式接口 -----------------
if (ctx.path.indexOf('stream') < 0) return false
}

static getName(): string {
return 'stream';
}
}

4.3 mongodb 数据库的权限


这里尽量不要使用root权限的数据库角色,可以创建一个只有当前数据库权限的角色,具体可以网上找相关文档,怎么为某个collection创建账户。


实机演示


1. 提交素材,创建任务



2. 获取生成图片



3. 展示大厅(待完善)



结语


当然现在目前这个还是内测版本,功能还不够健全,还有很多地方需要打磨,包括用户信息页面的展示是否合理,UI的排版,数据库表的设计等等


通过观察生活用现有的技术创造一些价值,对我来说就是一种幸福且有意义的事儿。



如果想要体验的可以后台私信我。如果你也有很棒的想法想交流一下,也可以私我。



我是dev,下期见(太懒了我,更新频率太低)


个人博客


灵感中心


作者:dev
来源:juejin.cn/post/7407374655109283851
收起阅读 »

多语言翻译你还在一个个改?我不允许你不知道这个工具

web
最近在做项目的多语言翻译,由于是老项目,里面需要翻译的文本太多了,如果一个个翻译的话,一个人可能一个月也做不完,因此考虑使用自动化工具实现。 我也从网上搜索了很多现有的自动化翻译工具,有vsc插件、webpack或vite插件、webpack loader等前...
继续阅读 »

最近在做项目的多语言翻译,由于是老项目,里面需要翻译的文本太多了,如果一个个翻译的话,一个人可能一个月也做不完,因此考虑使用自动化工具实现。


我也从网上搜索了很多现有的自动化翻译工具,有vsc插件、webpack或vite插件、webpack loader等前人实现的方案,但是安装之后发现,要不脱离不了一个个点击生成的繁琐,要不插件安装太麻烦,安装报错依赖报错等等问题层出不穷。因此,决定自己写一个,它需要满足:


1.不需要手动改代码,自动运行生成

2.不需要查翻译,自动调用翻译接口生成翻译内容

3.不需要安装,避免安装问题、环境问题等

i18n-cli 工具的产生


经过一个星期左右的开发和调试,i18n-cli自动化翻译工具实现了,详情可以看@tenado/i18n-cli。先来看看使用效果:


转换前:


<template>
<div class="empty-data">
<div class="name">{{ name }}</div>
<template>
<div class="empty-image-wrap">
<img class="empty-image" :src="emptyImage" />
</div>
<div class="empty-title">暂无数据</div>
</template>
</div>
</template>

<script lang="js">
import Vue from "vue";
export default Vue.extend({
data(){
return {
name: "测试"
}
},
});
</script>

转换后:


<template>
<div class="empty-data">
<div class="name">{{ name }}</div>
<template>
<div class="empty-image-wrap">
<img class="empty-image" :src="emptyImage" />
</div>
<div class="empty-title">{{ $t("zan-wu-shu-ju") }}</div>
</template>
</div>
</template>

<script lang="js">
import { i18n } from 'i18n';
import Vue from "vue";
export default Vue.extend({
data() {
return {
name: i18n.t('ce-shi')
};
}
});
</script>

@tenado/i18n-cli翻译,不受语言类型限制,目前vue、react、vue3等代码都能完美的支持,它通过分析语法树,自动匹配中文内容,并生成翻译后的代码。


如何使用 i18n-cli 工具


1.下载@tenado/i18n-cli项目代码,例如存作i18n-cli


2.将需要翻译的代码文件,拷贝到i18n-cli项目下的目录下


3.修改 i18n.config.js 配置,修改入口entry为你刚复制的文件的位置,修改你需要翻译的语言列表langs,例如英文、繁体['en-US', 'zh-TW'],修改引入i18n的方法i18nImport、i18nObject、i18nMethod,修改翻译的类型和秘钥,一个简单的配置如下:


module.exports = {
// 入口位置
entry: ['example/transform-i-tag'],
// 翻译后的文件存放位置
localPath: './example/transform-i-tag/locales',
// 需要翻译的语言列表
langs: ['en-US'],
// 引入i18n
i18nImport: "import { t } from 'i18n';",
i18nObject: '',
i18nMethod: 't',
// 翻译配置,例如百度
translate: {
type: 'baidu',
appId: '2023088292121',
secretKey: 'J1ArqOof1s8kree',
interval: 1000,
},
};

4.在i18n-cli项目下执行命令,npm run sync,将会修改你刚复制的文件里面的代码,并在locales下生成翻译内容,这里如果没有百度翻译api key,那你可以先收集,后面在翻译,先执行 npm run extract,再执行 npm run translate


5.将修改后的文件复制回你的项目下


当然,i18n-cli 的配置不是仅仅这些,更多配置你可以去 @tenado/i18n-cli 对应的 github 仓库上查看


i18n-cli 是怎么实现的


1、收集文件


根据入口,获取需要处理的文件列表,主要代码如下:


// 根据入口获取文件列表
const getSourceFiles = (entry, exclude) => {
return glob.sync(`${entry}/**/*.{js,ts,tsx,jsx,vue}`, {
ignore: exclude || [],
})
}
// 例如 getSourceFiles('src/components')
// 结果 ['src/components/Select/index.vue', 'src/components/Select/options.vue', 'src/components/Select/index.js']

2、转换文件


根据文件类型,生成不同文件的语法树,例如.vue文件分别解析vue的template、style、script三个部分,例如.ts、.tsx文件,例如html,解析成ast语法树后,针对不同类型的中文分别处理,如下是babel转换ast时候里面的一部分核心代码:


const { declare } = require("@babel/helper-plugin-utils");
const generate = require("@babel/generator").default;
module.exports = declare((api, options) => {
return {
visitor: {
// 针对不同类型的中文,进行转换
// 代码太多,这里不贴全部,具体的可以去github上查看源码
DirectiveLiteral() {},
StringLiteral() {},
TemplateLiteral() {},
CallExpression() {},
ObjectExpression() {},
},
};
});

3、调用接口翻译


根据locales文件存放位置,把收集到的中文都存在./locales/zh-CN.json里面,收集中文和key是在文件转换过程处理的。


这个过程,会根据生成的中文json,去请求接口,拿到中文对应语言的翻译,实现代码如下:


const fetch = require("node-fetch");
const md5 = require('md5');
const createHttpError = require('http-errors');
const langMap = require("./langMap.js");
const defaultOptions = {
from: "auto",
to: "en",
appid: "",
salt: "wgb236hj",
sign: "",
}
module.exports = async (text, lang, options) => {
const hostUrl = "http://api.fanyi.baidu.com/api/trans/vip/translate";
let _options = {
q: text,
...defaultOptions,
}
const { local } = options ?? {};
const { appId, secretKey } = options?.translate ?? {};
if(local) {
_options.from = langMap('baidu', local);
}
if(lang) {
_options.to = langMap('baidu', lang);
}
_options.appid = appId;
const str = `${_options.appid}${_options.q}${_options.salt}${secretKey}`;
_options.sign = md5(str);
const buildBody = () => {
return new URLSearchParams(_options).toString();
}
const buildOption = () => {
const opt = {};
opt.method = 'POST';
opt.headers = {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
}
opt.body = buildBody();
return opt;
}
const buildError = async (res) => {
const extractTooManyRequestsInfo = (html) => {
const ip = html.match(/IP address: (.+?)<br>/)?.[1] || '';
const time = html.match(/Time: (.+?)<br>/)?.[1] || '';
const url = (html.match(/URL: (.+?)<br>/)?.[1] || '').replace(/&amp;/g, '&');
return { ip, time, url };
}
if (res.status === 429) {
const text = await res.text();
const { ip, time, url } = extractTooManyRequestsInfo(text);
const message = `${res.statusText} IP: ${ip}, Time: ${time}, Url: ${url}`;
return createHttpError(res.status, message);
} else {
return createHttpError(res.status, res.statusText);
}
}
const buildText = ({ error_code, error_msg, trans_result }) => {
if(!error_code) {
return trans_result?.map(item => item.dst);
} else {
console.error(`百度翻译报错: ${error_code}, ${error_msg}`)
return '';
}
}
const fetchOption = buildOption();
const res = await fetch(hostUrl, fetchOption)
if(!res.ok) {
throw await buildError(res)
}
const raw = await res.json();
const _text = buildText(raw);
return _text;
}


总结


i18n-cli 是一个全自动的国际化插件,可以一键翻译多国语言,同时不会影响项目的业务代码,对于国际化场景是一个很强大的工具。


使用 i18n-cli 可以大大减少项目多语言翻译的工作量,这个插件已经在我们项目中使用很久了,是一个成熟的方案,欢迎大家使用,欢迎提交issues,欢迎star。


作者:是阿派啊
来源:juejin.cn/post/7327969921309065216
收起阅读 »

pnpm 的崛起:如何降维打击 npm 和 Yarn🫡

web
今天研究了一下 pnpm 的机制,发现它确实很强大,甚至可以说对 yarn 和 npm 形成了降维打击 我们从包管理工具的发展历史,一起看下到底好在哪里? npm2 在 npm 3.0 版本之前,项目的 node_modules 会呈现出嵌套结构,也就是说,我...
继续阅读 »

今天研究了一下 pnpm 的机制,发现它确实很强大,甚至可以说对 yarnnpm 形成了降维打击


我们从包管理工具的发展历史,一起看下到底好在哪里?


npm2


在 npm 3.0 版本之前,项目的 node_modules 会呈现出嵌套结构,也就是说,我安装的依赖、依赖的依赖、依赖的依赖的依赖...,都是递归嵌套的


node_modules
├─ express
│ ├─ index.js
│ ├─ package.json
│ └─ node_modules
│ ├─ accepts
│ │ ├─ index.js
│ │ ├─ package.json
│ │ └─ node_modules
│ │ ├─ mime-types
| | | └─ node_modules
| | | └─ mime-db
| │ └─ negotiator
│ ├─ array-flatten
│ ├─ ...
│ └─ ...
└─ A
├─ index.js
├─ package.json
└─ node_modules
└─ accepts
├─ index.js
├─ package.json
└─ node_modules
├─ mime-types
| └─ node_modules
| └─ mime-db
└─ negotiator

设计缺陷


这种嵌套依赖树的设计确实存在几个严重的问题



  1. 路径过长问题: 由于包的嵌套结构 , node_modules 的目录结构可能会变得非常深,甚至可能会超出系统路径长度上限 ,毕竟 windows 系统的文件路径默认最多支持 256 个字符

  2. 磁盘空间浪费: 多个包之间难免会有公共的依赖,公共依赖会被多次安装在不同的包目录下,导致磁盘空间被大量浪费 。比如上面 express 和 A 都依赖了 accepts,它就被安装了两次

  3. 安装速度慢:由于依赖包之间的嵌套结构,npm 在安装包时需要多次处理和下载相同的包,导致安装速度变慢,尤其是在依赖关系复杂的项目中


当时 npm 还没解决这些问题, 社区便推出了新的解决方案 ,就是 yarn。 它引入了一种新的依赖管理方式——扁平化依赖。


看到 yarn 的成功,npm 在 3.0 版本中也引入了类似的扁平化依赖结构


yarn


yarn 的主要改进之一就是通过扁平化依赖结构来解决嵌套依赖树的问题


具体来说铺平,yarn 尽量将所有依赖包安装在项目的顶层 node_modules 目录下,而不是嵌套在各自的 node_modules 目录中。


这样一来,减少了目录的深度,避免了路径过长的问题 ,也尽可能避免了依赖被多次重复安装的问题


|350


我们可以在 yarn-example 看到整个目录,全部铺平在了顶层 node_modules 目录下,展开下面的包大部分是没有二层 node_modules


然而,有些依赖包还是会在自己的目录下有一个 node_modules 文件夹,出现嵌套的情况,例如 yarn-example 下的http-errors 依赖包就有自己的 node_modules,原因是:


当一个项目的多个依赖包需要同一个库的不同版本时,yarn 只能将一个版本的库提升到顶层 node_modules 目录中。 对于需要这个库其他版本的依赖,yarn 仍然需要在这些依赖包的目录下创建一个嵌套的 node_modules 来存放不同版本的包


比如,包 A 依赖于 lodash@4.0.0,而包 B 依赖于 lodash@3.0.0。由于这两个版本的 lodash 不能合并,yarn 会将 lodash@4.0.0 提升到顶层 node_modules,而 lodash@3.0.0 则被嵌套在包 B 的 node_modules 目录下。


幽灵依赖


虽然 yarn 和 npm 都采用了扁平化的方案来解决依赖嵌套的问题,但这种方案本身也有一些缺陷,其中幽灵依赖是一个主要问题。


幽灵依赖,也就是你明明没有在 package.json 文件中声明的依赖项,但在项目代码里却可以 require 进来
这个也很容易理解,因为依赖的依赖被扁平化安装在顶层 node_modules 中,所以我们能访问到依赖的依赖


但是这样是有隐患的,因为没有显式依赖,未来某个时候这些包可能会因为某些原因消失(例如新版本库不再引用这个包了,然后我们更新了库),就会引发代码运行错误


浪费磁盘空间


而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题


那社区有没有解决这俩问题的思路呢? pnpm 就是其中最成功的一个


pnpm


pnpm 通过全局存储和符号链接机制从根源上解决了依赖重复安装和路径长度问题,同时也避免了扁平化依赖结构带来的幽灵依赖问题
pnpm 的优势概括来说就是“快、准、狠”:



  • 快:安装速度快

  • 准:安装过的依赖会准确复用缓存,甚至包版本升级带来的变化都只 diff,绝不浪费一点空间

  • 狠:直接废掉了幽灵依赖


执行 npm add express,我们可以在 pnpm-example 看到整个目录,由于只安装了 express,那 node_modules 下就只有 express


|400


那么所有的(次级)依赖去哪了呢? binggo,在node_modules/.pnpm/目录下,.pnpm/ 以平铺的形式储存着所有的包


|400


三层寻址



  1. 所有 npm 包都安装在全局目录 ~/.pnpm-store/v3/files 下,同一版本的包仅存储一份内容,甚至不同版本的包也仅存储 diff 内容。

  2. 顶层 node_modules 下有 .pnpm 目录以打平结构管理每个版本包的源码内容,以硬链接方式指向 pnpm-store 中的文件地址。

  3. 每个项目 node_modules 下安装的包以软链接方式将内容指向 node_modules/.pnpm 中的包。
    所以每个包的寻找都要经过三层结构:node_modules/package-a > 软链接 node_modules/.pnpm/package-a@1.0.0/node_modules/package-a > 硬链接 ~/.pnpm-store/v3/files/00/xxxxxx


    这就是 pnpm 的实现原理。官方给了一张原理图,可以搭配食用


    |600


    前面说过,npm 包都被安装在全局 pnpm store ,默认情况下,会创建多个存储(每个驱动器(盘符)一个),并在项目所在盘符的根目录


    所以,同一个盘符下的不同项目,都可以共用同一个全局 pnpm store,绝绝子啊👏,大大节省了磁盘空间,提高了安装速度


    |600



软硬链接


也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。


那么,这里的软连接、硬链接到底是什么东西?


硬链接是指向磁盘上原始文件所在的同一位置 (直接指向相同的数据块)


软连接可以理解为新建一个文件,它包含一个指向另一个文件或目录的路径 (指向目标路径)



总结


npm2 的嵌套结构: 每个依赖项都会有自己的 node_modules 目录,导致了依赖被重复安装,严重浪费了磁盘空间💣;在依赖层级比较深的项目中,甚至会超出 windows 系统的文件路径长度💣


npm3+ 和 Yarn 的扁平化策略: 尽量将所有依赖包安装在项目的顶层 node_modules 目录下,解决了 npm2 嵌套依赖的问题。但是该方案有一个重大缺陷就是“幽灵依赖”💣;而且依赖包有多个版本时,只会提升一个,那其余版本依然会被重复安装,还是有浪费磁盘空间的问题💣


pnpm全局存储和符号链接机制: 结合软硬链和三层寻址,解决了依赖被重复安装的问题,更加变态的是,同一盘符下的不同项目都可以共用一个全局 pnpm store。节省了磁盘空间,并且根本不存在“幽灵依赖”,安装速度还贼快💪💪💪


作者:柏成
来源:juejin.cn/post/7410923898647461938
收起阅读 »

代码与蓝湖ui颜色值一致!但页面效果出现色差问题?

web
前言 最近在开发新需求,按照蓝湖的ui图进行开发,但是在开发完部署后发现做出来的页面部分元素的颜色和设计图有出入,有色差!经过一步步的排查最终破案,解决。仅以此篇记录自己踩坑、学习的过程,也希望可以帮助到其他同学。 发现问题 事情是这样的,那是一个愉快的周五的...
继续阅读 »

前言


最近在开发新需求,按照蓝湖的ui图进行开发,但是在开发完部署后发现做出来的页面部分元素的颜色和设计图有出入,有色差!经过一步步的排查最终破案,解决。仅以此篇记录自己踩坑、学习的过程,也希望可以帮助到其他同学。


发现问题


事情是这样的,那是一个愉快的周五的下午,和往常一样我开心的提交了代码后进行打包发版,然后通知负责人查看我的工作成果。


但是,过了不久后,负责人找到了我,说我做出来的效果和ui有点出入,有的颜色有点不一样。我一脸懵逼,心想怎么可能呢,我是根据ui图来的,ui的颜色可是手把手从蓝湖复制到代码中的啊。


随后他就把页面和ui的对比效果图发了出来:


image.png


上图中左侧是蓝湖ui图,右侧是页面效果图。我定睛一看,哇趣!!!好像是有点不一样啊。 感觉右侧的比左侧的更亮一些。于是我赶紧本地查看我的页面和ui,果然也是同样问题! 开发时真的没注意,没发现这个问题!!!


排查问题


于是,我迅速开始进行问题排查,看看到底是什么问题,是值写错了?还是那里的问题。


ui、页面、代码对比


下图中:最上面部分是蓝湖ui图、下面左侧是我的页面、右侧是我的页面代码样式


image.png


仔细检查后发现颜色的值没错啊,我的代码中背景颜色、边框颜色的值都和ui的颜色值是一致的! 但这是什么问题呢??? 值都一样为什么渲染到页面会出现色差?


起初,我想到的是屏幕的问题,因为不同分辨率下展示出来的页面效果是会有差距的。但是经过查看发现同事的win10笔记本、我的mac笔记本、外接显示器上都存在颜色有色差这个问题!!!


ui、页面、源文件对比


通过对比ui、页面、颜色值,不同设备展示效果可以初步确认:和显示器关系不大。当我在百思不解的时候,我突然想到了ui设计师!ui提供的ui图是蓝湖上切出来的,那么她的源文件颜色是什么呢?


于是我火急火燎的联系到了公司ui小姐姐,让她发我源文件该元素的颜色值,结果值确实是一样的,但是!!! 源文件展示出来的效果好像和蓝湖上的不太一样!


然后我进行了对比(左侧蓝湖、右上页面、右下源文件):


image.png


可以看到源文件和我页面的效果基本一致!到这一步基本可以确定我的代码是没问题的!


尝试解决


首先去网上找了半天没有找到想要的答案,于是我灵光一现,想到了蓝湖客服!然后就询问了客服,为什么上传后的ui图内容和源文件有色差?


image.png


image.png


沟通了很久,期间我又和ui小姐姐在询问她的软件版本、电脑版本、源文件效果、设置等内容就不贴了,最终得到如下解答:


image.png


解决方式


下载最新版蓝湖插件,由于我们的ui小姐姐用的 sketch 切图工具,然后操作如下:


1.下载安装最新版蓝湖插件: lanhuapp.com/mac?formHea…


2.安装新版插件后--插件重置


3.后台程序退出 sketch,重新启动再次尝试打开蓝湖插件.


4.插件设置打开高清导出上传(重要!)


5.重新切图上传蓝湖


最终效果


左侧ui源文件、右侧蓝湖ui:
image.png


页面效果:


image.png


可以看到我的页面元素的border好像比ui粗一些,感觉设置0.5px就可以了,字体效果的话是因为我还没来得及下载ui对应的字体文件。


但是走到这一步发现整体效果已经和ui图到达了95%以上相似了,不至于和开始有那么明显的色差。


总结


至此,问题已经基本是解决。遇到问题不能怕,多想一想,然后有思路后就一步一步排查、尝试解决问题。当解决完问题后会发现心情舒畅!整个人都好起来了,也会增加自信心!


作者:尖椒土豆sss
来源:juejin.cn/post/7410712345226035200
收起阅读 »

数据可视化工具库比较与应用:ECharts、AntV、D3、Zrender

web
ECharts ECharts是一个由百度开发的强大的数据可视化库,它提供了丰富的图表类型和灵活的配置选项。以下是一个简单的示例,展示如何使用Echarts创建一个折线图: import * as echarts from 'echarts'; const ...
继续阅读 »

70500f24f666eb40cdc0ced971ce90d6.webp


ECharts


ECharts是一个由百度开发的强大的数据可视化库,它提供了丰富的图表类型和灵活的配置选项。以下是一个简单的示例,展示如何使用Echarts创建一个折线图:


import * as echarts from 'echarts';

const chartDom = document.getElementById('main');

const myChart = echarts.init(chartDom);


const option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line'
}
]
};

option && myChart.setOption(option);

image.png
如上步骤,简单易用,难点都封装好了,只需要配置数据即可。如果需要在网页中快速展示图表信息,刚好这个图表是比较常规的,不需要过多地调整和配置,就可以采用ECharts。


Antv


Antv是蚂蚁金服开发的数据可视化库,它基于G2和G6,提供了一系列强大的图表和可视化组件。下面是一个使用Antv使用G2产品创建折线图的示例:


import { Chart } from "@antv/g2";

const chart = new Chart({ container: "container" });

chart.options({
type: "view",
autoFit: true,
data: [
{ year: "1991", value: 3 },
{ year: "1992", value: 4 },
{ year: "1993", value: 3.5 },
{ year: "1994", value: 5 },
{ year: "1995", value: 4.9 },
{ year: "1996", value: 6 },
{ year: "1997", value: 7 },
{ year: "1998", value: 9 },
{ year: "1999", value: 13 },
],
encode: { x: "year", y: "value" },
scale: { x: { range: [0, 1] }, y: { domainMin: 0, nice: true } },
children: [
{ type: "line", labels: [{ text: "value", style: { dx: -10, dy: -12 } }] },
{ type: "point", style: { fill: "white" }, tooltip: false },
],
});

chart.render();

image.png
Antv提供了简单易用的API和丰富的图表组件,可以帮助开发者快速构建各种类型的数据可视化图表。在官网可以看到由七个模块产品,分别是:

G2|G2Plot:可视化图形语法和通用图表库

S2:多维可视分析表格

G6|Graphin:关系数据可视化分析工具和图分析组件

X6|XFlow:流程图相关分图表和组件

L7|L7Plot:地理空间数据可视化框架和地理图表

F2|F6:移动端的可视化解决方案

AVA:可视分析技术框架


image.png


D3


D3(d3js.org/getting-sta…


import * as d3 from "d3";
import {useRef, useEffect} from "react";

export default function LinePlot({
data,
width = 640,
height = 400,
marginTop = 20,
marginRight = 20,
marginBottom = 30,
marginLeft = 40
}) {
const gx = useRef();
const gy = useRef();
const x = d3.scaleLinear([0, data.length - 1], [marginLeft, width - marginRight]);
const y = d3.scaleLinear(d3.extent(data), [height - marginBottom, marginTop]);
const line = d3.line((d, i) => x(i), y);
useEffect(() => void d3.select(gx.current).call(d3.axisBottom(x)), [gx, x]);
useEffect(() => void d3.select(gy.current).call(d3.axisLeft(y)), [gy, y]);
return (
<svg width={width} height={height}>
<g ref={gx} transform={`translate(0,${height - marginBottom})`} />
<g ref={gy} transform={`translate(${marginLeft},0)`} />
<path fill="none" stroke="currentColor" strokeWidth="1.5" d={line(data)} />
<g fill="white" stroke="currentColor" strokeWidth="1.5">
{data.map((d, i) => (<circle key={i} cx={x(i)} cy={y(d)} r="2.5" />))}
</g>
</svg>
);
}

image.png
D3不是传统意义上的图表库,是由30个离散库或者模块组成的套件。如果你对其它高级图表库不满意,想使用SVG或Canvas、甚至WebGL滚动自己的图表,那么可以使用D3工具库。


ZRender


ZRender是2D绘图引擎。它提供Canvas、SVG、等多种渲染方式,也是ECharts的渲染器。


import zrender from 'zrender';
var zr = zrender.init(document.getElementById('main'));
var circle = new zrender.Circle({
shape: {
cx: 150,
cy: 50,
r: 40
},
style: {
fill: 'none',
stroke: '#F00'
}
});
zr.add(circle);
console.log(circle.shape.r); // 40
circle.attr('shape', {
r: 50 // 只更新 r。cx、cy 将保持不变。
});

通过 a = new zrender.XXX 方法创建了图形元素之后,可以用 a.shape 等形式获取到创建时输入的属性,但是如果需要对其进行修改,应该使用 a.attr(key, value) 的形式修改,否则不会触发图形的重绘。


从代码规范看,Echarts和D3官网的案例有用到es5的语法,Antv遵循了es6的语法规范,更专业。从灵活程度和使用难易程度来看,ECharts<Antv<D3<ZRender。还有使用到其它图表工具库的,欢迎留言探讨📒


作者:拾七视界
来源:juejin.cn/post/7345105846341648438
收起阅读 »

前端实现文件预览img、docx、xlsx、pptx、pdf、md、txt、audio、video

web
前言最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇具体的预览需求: 预览需要支持的文件类型有: png、jp...
继续阅读 »

前言

最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇

具体的预览需求: 预览需要支持的文件类型有: png、jpg、jpeg、docx、xlsx、pptx、pdf、md、txt、audio、video,另外对于不同文档还需要有定位的功能。例如:pdf 定位到页码,txtmarkdown定位到文字并滚动到指定的位置,音视频定位到具体的时间等等。


⚠️ 补充: 我的需求是需要先将文件上传到后台,然后我拿到url地址去展示,对于markdowntxt的文件需要先用fetch获取,其他的展示则直接使用url链接就可以。

不同文件的实现方式不同,下面分类讲解,总共分为以下几类:

  1. 自有标签文件:png、jpg、jpeg、audio、video
  2. 纯文字的文件: markdown 、txt
  3. office 类型的文件: docx、xlsx、pptx
  4. embed 引入文件:pdf
  5. iframe:引入外部完整的网站,例如:https://www.baidu.com/

自有标签文件:png、jpg、jpeg、audio、video

对于图片、音视频的预览,直接使用对应的标签即可,如下:

图片:png、jpg、jpeg

示例代码:

 <img src={url} key={docId} alt={name} width="100%" />;

预览效果如下:

截屏2024-04-30 11.18.01.png

音频:audio

示例代码:


预览效果如下:

截屏2024-04-30 11.18.45.png

视频:video

示例代码:


预览效果如下:

截屏2024-05-13 18.21.13.png

关于音视频的定位的完整代码:

import React, { useRef, useEffect } from 'react';

interface IProps {
type: 'audio' | 'video';
url: string;
timeInSeconds: number;
}

function AudioAndVideo(props: IProps) {
const { type, url, timeInSeconds } = props;
const videoRef = useRef(null);
const audioRef = useRef(null);

useEffect(() => {
// 音视频定位
const secondsTime = timeInSeconds / 1000;
if (type === 'audio' && audioRef.current) {
audioRef.current.currentTime = secondsTime;
}
if (type === 'video' && videoRef.current) {
videoRef.current.currentTime = secondsTime;
}
}, [type, timeInSeconds]);

return (

{type === 'audio' ? (

) : (

)}

);
}

export default AudioAndVideo;

纯文字的文件: markdown & txt

对于markdown、txt类型的文件,如果拿到的是文件的url的话,则无法直接显示,需要请求到内容,再进行展示。

markdown 文件

在展示markdown文件时,需要满足字体高亮、代码高亮,如果有字体高亮,需要滚动到字体所在位置,如果有外部链接,需要新开tab页面再打开。

需要引入两个库:

marked:它的作用是将markdown文本转换(解析)为HTML

highlight: 它允许开发者在网页上高亮显示代码。

字体高亮的代码实现:

高亮的样式,可以在行间样式定义

  const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `id='first-match' style="color: red;">${match}`;
}
return `style="color: red;">${match}`;
});
};

代码高亮的代码实现:

需要借助hljs这个库进行转换

marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `
class="hljs ${infostring}">${highlighted}
`;
}
},
});

链接跳转新tab页的代码实现:

marked.use({
renderer: {
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}`;
}
return `href="${href}" title="${title}">${text}`;
},
},
});

滚动到高亮的位置的代码实现:

需要配合上面的代码高亮的方法

const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

完整的代码如下:

入参的docUrl 是markdown文件的线上url地址,searchText 是需要高亮的内容。

import React, { useEffect, useState, useRef } from 'react';
import { marked } from 'marked';
import hljs from 'highlight.js';

const preStyle = {
width: '100%',
maxHeight: '64vh',
minHeight: '64vh',
overflow: 'auto',
};

// Markdown展示组件
function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
const [markdown, setMarkdown] = useState('');
const markdownRef = useRef<HTMLDivElement | null>(null);

const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `${match}`;
}
return `${match}`;
});
};

useEffect(() => {
// 如果没有搜索内容,直接加载原始Markdown文本
fetch(docUrl)
.then((response) => response.text())
.then((text) => {
const highlightedText = searchText ? highlightAndMarkFirst(text, searchText) : text;
setMarkdown(highlightedText);
})
.catch((error) => console.error('加载Markdown文件失败:', error));
}, [searchText, docUrl]);

useEffect(() => {
if (markdownRef.current) {
// 支持代码高亮
marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `
${infostring}">${highlighted}
`
;
},
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}`;
}
return `${href}" title="${title}">${text}`;
},
},
});
const htmlContent = marked.parse(markdown);
markdownRef.current!.innerHTML = htmlContent as string;
// 当markdown更新后,检查是否需要滚动到高亮位置
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [markdown]);

return (
<div style={preStyle}>
<div ref={markdownRef} />
div>
);
}

export default MarkdownViewer;

预览效果如下:

截屏2024-05-13 17.59.04.png

txt 文件预览展示

支持高亮和滚动到指定位置

支持文字高亮的代码:

  function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `style="color: red">$1`);
}

完整代码:

import React, { useEffect, useState, useRef } from 'react';
import { preStyle } from './config';

function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
const [paragraphs, setParagraphs] = useState<string[]>([]);
const targetRef = useRef<HTMLDivElement | null>(null);

function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `$1`);
}

useEffect(() => {
fetch(docurl)
.then((response) => response.text())
.then((text) => {
const highlightedText = highlightText(text);
const paras = highlightedText
.split('\n')
.map((para) => para.trim())
.filter((para) => para);
setParagraphs(paras);
})
.catch((error) => {
console.error('加载文本文件出错:', error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [docurl, searchText]);

useEffect(() => {
// 处理高亮段落的滚动逻辑
const timer = setTimeout(() => {
if (targetRef.current) {
targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);

return () => clearTimeout(timer);
}, [paragraphs]);

return (
<div style={preStyle}>
{paragraphs.map((para: string, index: number) => {
const paraKey = para + index;

// 确定这个段落是否包含高亮文本
const isTarget = para.includes(`>${searchText}<`);
return (
<p key={paraKey} ref={isTarget && !targetRef.current ? targetRef : null}>
<div dangerouslySetInnerHTML={{ __html: para }} />
p>
);
})}
div>
);
}

export default TextFileViewer;

预览效果如下:

截屏2024-05-13 18.34.27.png


office 类型的文件: docx、xlsx、pptx

docx、xlsx、pptx 文件的预览,用的是office的线上预览链接 + 我们文件的线上url即可。

关于定位:用这种方法我暂时尝试是无法定位页码的,所以定位的功能我采取的是后端将office 文件转成pdf,再进行定位,如果只是纯展示,忽略这个问题即可。

示例代码:


预览效果如下:

截屏2024-05-07 17.58.45.png


embed 引入文件:pdf

pdf文档预览时,可以采用embed的方式,这个httpsUrl就是你的pdf文档的链接地址

示例代码:

 src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;

关于定位,其实是地址上拼接的页码sourcePage,如下:

 const httpsUrl = sourcePage
? `${doc.url}#page=${sourcePage}`
: doc.url;

src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;

预览效果如下:

截屏2024-05-07 17.50.07.png


iframe:引入外部完整的网站

除了上面的各种文件,我们还需要预览一些外部的网址,那就要用到iframe的方式

示例代码:

 < iframe
title="网址"
width="100%"
height="100%"
src={doc.url}
allow="microphone;camera;midi;encrypted-media;"/>

预览效果如下:






课后附加题:



有些网站设置了X-Frame-Options不允许其他网站嵌入,X-Frame-Options 是一个 HTTP 响应头,用于控制浏览器是否允许一个页面在 <frame>、 <iframe>、 <embed>、 或 <object> 中被嵌入。



X-Frame-Options有以下三种配置:



  • DENY:完全禁止该页面被嵌入到任何框架中,无论嵌入页面的来源是什么。

  • SAMEORIGIN:允许同源的页面嵌入该页面。

  • ALLOW-FROM uri:允许指定的来源嵌入该页面。这个选项允许你指定一个 URI,只有来自该 URI 的页面可以嵌入当前页面。


但是无论是哪种配置,我们作为非同源的网站,都无法将其嵌入到页面中,且在前端也是拿不到这个报错的信息。


此时我们的解决方案是:



当文档为网址时,由后端服务去请求,检测响应头里是否携带X-Frame-Options字段,由后端将是否携带的信息返回前端,前端再根据是否可以嵌入进行页面的个性化展示。



预览效果如下:






总结: 到这里我们支持的所有文件都讲述完了,有什么问题,欢迎评论区留言!


作者:玖月晴空
链接:juejin.cn/post/7366432628440924170
收起阅读 »

30分钟搞懂JS沙箱隔离

web
什么是沙箱环境 在计算机安全中,沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行。 其实在前端世界里,沙箱环境无处不在! 例...
继续阅读 »

什么是沙箱环境


在计算机安全中,沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行。


其实在前端世界里,沙箱环境无处不在!


例如以下几个场景:



  1. Chrome本身就是一个沙箱环境


    Chrome 浏览器中的每一个标签页都是一个沙箱(sandbox)。渲染进程被沙箱(Sandbox)隔离,网页 web 代码内容必须通过 IPC 通道才能与浏览器内核进程通信,通信过程会进行安全的检查。


  2. 在线代码编辑器(码上掘金、CodeSandbox、CodePen等)


    在线代码编辑器在执行脚本时都会将程序放置在一个沙箱中,防止程序访问/影响主页面。


    image.png


  3. Vue的 服务端渲染


    在 Node.js 中有一个模块叫做 VM,它提供了几个 API,允许代码在 V8 虚拟机上下文中运行。


    const vm = require('vm');
    const sandbox = { a: 1, b: 2 };
    const script = new vm.Script('a + b');
    const context = new vm.createContext(sandbox);
    script.runInContext(context);

    vue的服务端渲染实现中,通过创建沙箱执行前端的bundle文件;在调用createBundleRenderer方法时候,允许配置runInNewContext为true或false的形式,判断是否传入一个新创建的sandbox对象以供vm使用。


  4. Figma 插件


    出于安全和性能等方面的考虑,Figma将插件代码分成两个部分:main 和 ui。其中 main 代码运行在沙箱之中,ui 部分代码运行在 iframe 之中,两者通过 postMessage 通信。


  5. 微前端


    典型代表是 Garfishqiankun


    image.png


    image.png



从0开始实现一个JS沙箱环境


1. 最简陋的沙箱(eval)


image.png


问题:



  • 要求源程序在获取任意变量时都要加上执行上下文对象的前缀

  • eval的性能问题

  • 源程序可以访问闭包作用域变量

  • 源程序可以访问全局变量


2. eval + with


image.png
问题:



  • eval的性能问题

  • 源程序可以访问闭包作用域变量

  • 源程序可以访问全局变量


3. new Function + with


image.png
问题:



  • 源程序可以访问全局变量


4. ES6 Proxy


我们先看Proxy的使用


image.png
Proxy{} 设置了属性访问拦截器,倘若访问的属性为 a 则返回 1,否则走正常程序。
Proxy 支持的拦截操作,一共 13 种:



  • get(target, propKey, receiver) :拦截对象属性的读取,比如proxy.fooproxy['foo']

  • set(target, propKey, value, receiver) :拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。

  • has(target, propKey) :拦截propKey in proxy的操作,返回一个布尔值。

  • deleteProperty(target, propKey) :拦截delete proxy[propKey]的操作,返回一个布尔值。

  • ownKeys(target) :拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。

  • getOwnPropertyDescriptor(target, propKey) :拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

  • defineProperty(target, propKey, propDesc) :拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。

  • preventExtensions(target) :拦截Object.preventExtensions(proxy),返回一个布尔值。

  • getPrototypeOf(target) :拦截Object.getPrototypeOf(proxy),返回一个对象。

  • isExtensible(target) :拦截Object.isExtensible(proxy),返回一个布尔值。

  • setPrototypeOf(target, proto) :拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

  • apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)

  • construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)


在沙箱环境中,对本身不存在的变量会追溯到全局变量上访问,此时我们可以使用 Proxy "欺骗" 程序,告诉它这个「不存在的变量」是存在的。


image.png
报错了,因为我们阻止了所有全局变量的访问。


image.png
继续改造:


image.png
Symbol.unscopables


Symbol 是 JS 的第七种数据类型,它能够产生一个唯一的值,同时也具备一些内建属性,这些属性可以用来进行元编程(meta programming),即对语言本身编程,影响语言行为。其中一个内建属性 Symbol.unscopables,通过它可以影响 with 的行为,从而造成沙箱逃逸。


image.png
对这种情况做一层加固,防止沙箱逃逸



到这一步,其实很多较为简单的场景就可以覆盖了(比如: Vue 的模板字符串)。


仍然有很多漏洞:



  • code 中可以提前关闭 sandboxwith 语境,如 '} alert(this); {'

  • code 中可以使用 evalnew Function 直接逃逸

  • code 中可以通过访问原型链实现逃逸

  • 更为复杂的场景,如何实现任意使用诸如 documentlocation 等全局变量且不会影响主页面。


5. iframe是天然的优质沙箱


iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离。在 iframe 中运行的脚本程序访问到的全局对象均是当前 iframe 执行上下文提供的,不会影响其父页面的主体功能,因此使用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法。


如果只考虑浏览器环境,可以用 With + Proxy + iframe 构建出一个比较好的沙箱:



  • 利用 iframe 对全局对象的天然隔离性,将 iframe.contentWindow 取出作为当前沙箱执行的全局对象

  • 将上述沙箱全局对象作为 with 的参数限制内部执行程序的访问,同时使用 Proxy 监听程序内部的访问。

  • 维护一个共享状态列表,列出需要与外部共享的全局状态,在 Proxy 内部实现访问控制。


image.png


6. 基于ShadowRealm 提案的实现


ShadowRealm API 是一个新的 JavaScript 提案,它允许一个 JS 运行时创建多个高度隔离的 JS 运行环境(realm),每个 realm 具有独立的全局对象和内建对象。



这项特性提案时间为 2021 年 12 月,目前在Stage 3阶段 tc39.es/proposal-sh…





  • evaluate(sourceText: string) 同步执行代码字符串,类似 eval()

  • importValue(specifier: string, bindingName: string) 异步执行代码字符串


7. Web Workers


Web Workers代码运行在独立的进程中,通信是异步的,无法获取当前程序一些属性或共享状态,且有一点无法不支持 DOM 操作,必须通过 postMessage 通知 UI 主线程来实现。



以上就是实现JS沙箱隔离的一些思考点。在真实的业务应用中,没有最完美的方案,只有最合适的方案,还需要结合自身业务的特性做适合自己的选型。


作者:木华
来源:juejin.cn/post/7410347763898597388
收起阅读 »

uniapp截取视频画面帧

web
前言 最近开发中遇到这么一个需求,上传视频文件的时候需要截取视频的一部分画面来供选择,作为视频的封面,截取画面可以使用canvas来实现,将视频画面画进canvas里,再通过canvas来生成文件对象和一个预览的url 逻辑层和视图层 想要将视频画进canva...
继续阅读 »

前言


最近开发中遇到这么一个需求,上传视频文件的时候需要截取视频的一部分画面来供选择,作为视频的封面,截取画面可以使用canvas来实现,将视频画面画进canvas里,再通过canvas来生成文件对象和一个预览的url


逻辑层和视图层


想要将视频画进canvas里就需要操作dom,但是在uniapp中我们是无法操作dom的,uniapp的app端逻辑层和视图层是分离的,renderjs运行的层叫【视图层】,uniapp原生script叫【逻辑层】,会产生一个副作用就是是在造成了两层之间通信阻塞


所以uniapp推出了renderjsrenderjs是一个运行在视图层的js,可以让我们在视图层操作dom,但是不能直接调用,需要在dom元素中绑定某个值,当值发生改变就会触发视图层的事件


// 视图层
// module为renderjs模块的名称,通过 模块名.事件 来调用事件
<script module="capture" lang="renderjs"></script>

// 逻辑层
// prop为绑定的值,名字可以随便起,但是要和change后面一致
// 当prop绑定的值发生改变就会触发capture模块的captures事件
<view style="display: none;" :prop="tempFilePath" :change:prop="capture.captures"></view>

<template>
<view class="container">
<view style="display: none;" :prop="tempFilePath" :change:prop="capture.captures"></view>
</view>

</template>

<script>
export default {
data() {
return {
tempFilePath: ''
}
},
mounted() {
this.tempFilePath = 'aaaaaaaaaaaaaaaa'
}
}
</script>


<script module="capture" lang="renderjs">
export default {
methods: {
captures(tempFilePath) {
console.log(tempFilePath);
},
}
}
</script>


image.png


截取画面帧


我们先获取到视频的信息,通过uni.chooseVideo(OBJECT)这个api我们可以拿到视频的临时文件路径,然后再将临时路径交给renderjs去进行截取操作


定义一个captureFrame方法,方法接收两个参数:视频的临时文件路径截取的时间。先创建video元素,设置video元素的currentTime属性(视频播放的当前位置)的值为截取的时间,由于video元素没有加到dom上,video播放到currentTime结束


并设置video元素的autoplay自动播放属性为true,但是由于浏览器的限制video无法自动播放,但是静音状态下可以自动播放,所以还要将video元素的muted属性设为true,最后再将src属性设置为视频的临时文件路径,当video元素可以播放的时候就可以将video元素画进canvas里了


captureFrame(vdoSrc, time = 0) {
return new Promise((resolve) => {
const vdo = document.createElement('video')
// video元素没有加到dom上,video播放到currentTime(当前帧)结束
// 定格时间,截取帧
vdo.currentTime = time
// 设置自动播放,不播放是黑屏状态,截取不到帧画面
// 静音状态下允许自动播放
vdo.muted = true
vdo.autoplay = true
vdo.src = vdoSrc
vdo.oncanplay = async () => {
const frame = await this.drawVideo(vdo)
resolve(frame)
}
})
},

然后再定义一个drawVideo方法用于绘制视频,接收一个video元素参数,在方法中创建一个canvas元素,将canvas元素的宽高设置为video元素的宽高,通过drawImage方法将视频画进canvas里,再通过toBlob方法创建Blob对象,然后通过URL.createObjectURL() 创建一个指向blob对象的临时url,blob对象可以用来上传,url可以用来预览


drawVideo(vdo) {
return new Promise((resolve) => {
const cvs = document.createElement('canvas')
const ctx = cvs.getContext('2d')
cvs.width = vdo.videoWidth
cvs.height = vdo.videoHeight
ctx.drawImage(vdo, 0, 0, cvs.width, cvs.height)

// 创建blob对象
cvs.toBlob((blob) => {
resolve({
blob,
url: URL.createObjectURL(blob),
})
})
})
}

最后我们就可以在触发视图层的事件里去使用这两个方法来截取视频画面帧了,最后将数据传递返回给逻辑层,通过this.$ownerInstance.callMethod() 向逻辑层发送消息并将数据传递过去


// 视图层
async captures(tempFilePath) {
let duration = await this.getDuration(tempFilePath)
let list = []
for (let i = 0; i < duration; i++) {
const frame = await this.captureFrame(tempFilePath, duration / 10 * i)
list.push(frame)
}
this.$ownerInstance.callMethod('captureList', {
list,
duration
})
},
getDuration(tempFilePath) {
return new Promise(resolve => {
const vdo = document.createElement('video')
vdo.src = tempFilePath
vdo.addEventListener('loadedmetadata', () => {
const duration = Math.floor(vdo.duration);
vdo.remove();
resolve(duration)
});
})
},

// 逻辑层
captureList({
list,
duration
}
) {
// 操作......
}

运行


最后运行起来,发现报了一个错误:Failed to execute 'toBlob' on 'HTMLCanvasElement': Tainted canvases may not be exported,这是因为由于浏览器的安全考虑,如果在使用canvas绘图的过程中,使用到了外域的资源,那么在toBlob()时会抛出异常,设置video元素的crossOrigin属性值为anonymous就行了


app端h5

code.png


作者:格斗家不爱在外太空沉思
来源:juejin.cn/post/7281912738863087656
收起阅读 »

面试必问,防抖函数的核心是什么?

web
防抖节流的作用是什么? 节流(throttle)与 防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。 防抖:是指在一定时间内,在动作被连续频繁触发的情况下,动作只会被执行一次,...
继续阅读 »

防抖节流的作用是什么?


节流(throttle)与 防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。


防抖:是指在一定时间内,在动作被连续频繁触发的情况下,动作只会被执行一次,也就是说当调用动作过n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间,所以短时间内的连续动作永远只会触发一次,比如说用手指一直按住一个弹簧,它将不会弹起直到你松手为止。


节流:是指一定时间内执行的操作只执行一次,也就是说即预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期,一个比较形象的例子是人的眨眼睛,就是一定时间内眨一次。


防抖函数应用场景:


就比如说这段代码:


   let btn = document.getElementById('btn')
btn.addEventListener('click', function() {
console.log('提交'); // 换成ajax请求
})

当你点击按钮N下,它就会打印N次“提交”,但如果把 console 换成 ajax 请求,可想而知后端接受到触发频率如此之高的请求,造成的页面卡顿甚至瘫痪的后果。


1722263640946.png


防抖函数的核心:


面对此种情形,我们必须在原有的基础上作出改进,做到在规定的时间内没有下一次的触发,才执行的效果。


那么首先我们要做的,就是创建一个防抖函数,这个函数的功能是设置一个定时器,每次点击都会触发一个定时器输出,但如果两次点击的间隔小于1s,则销毁上一个定时器,达到最后只有一个定时器输出的效果。



定时器:


在防抖节流中,最为重要的一个部分就是定时器,就比如下面这段代码,setTimeout的功能就是设置一个定时器,让setTimeout内部的代码延迟执行在 1000 毫秒后。


  setTimeout(function(){
console.log('提交');
}, 1000)

特别需要注意一点的是,定时器中回调函数里的 this 指向会更改成指向 window。



于是我们创建专门的debounce函数用于实现防抖,把handle交给debounce处理,再在debounce内部设置一个setTimeout定时器,handle的执行推迟到点击事件发生的一秒后,这样一来,我们就实现了初步的想法。


  let btn = document.getElementById('btn')

function handle(){
console.log('提交', this); // 换成ajax请求
}

// 创建专门的debounce函数用于防抖,把handle交给debounce处理
btn.addEventListener('click', debounce(handle))

// 将点击事件推迟一秒
function debounce(fn){
return function() {
// 设置定时器
setTimeout(fn, 1000)
}

}

那么关键来了,我们又在原基础上添加一个timer用于接收定时器返回的值(通常称为定时器的ID),然后设置clearTimeout(timer)通过timer取消之前通过 setTimeout 创建的定时器。


通过这段代码,我们便实现了如果在 1s 内频繁点击的话,上一次点击的事件都会被下一次点击取消,从而达到规定的时间内没有下一次的触发,再执行的防抖目的!


  let btn = document.getElementById('btn')

function handle(){
console.log('提交', this); // 换成ajax请求
}

// 创建专门的debounce函数用于防抖,把handle交给debounce处理
btn.addEventListener('click', debounce(handle))

// 防抖函数
function debounce(fn){
let timer = null; // 接收定时器返回的ID
return function() {
// 设置定时器
clearTimeout(timer); // 取消之前通过 `setTimeout` 创建的定时器
timer = setTimeout(fn, 1000);
}

}

但是别忘了,我们之前提到过,定时器改变了handle中 this 指向,要做到尽善尽美,我们必须通过显示绑定修正 this 的指向。


同时别忘记还原原函数的参数。


利用箭头函数不承认 this 的特性,我们将代码修改成这样:


  let btn = document.getElementById('btn')

function handle(e){
console.log('提交'); // 换成ajax请求
}

// 创建专门的debounce函数用于防抖,把handle交给debounce处理
btn.addEventListener('click', debounce(handle))

// 防抖函数
function debounce(fn){
let timer = null; // 接收定时器返回的ID
return function(e) {
// 设置定时器
clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this,e); // 修正this的同时归还原函数的参数
}, 1000)
}

}

至此,大功告成!


防抖函数核心机制:


同时需要理解的是:防抖函数的核心机制就是闭包,当每一次点击会产生debounce执行上下文,随后debounce执行完其上下文又被反复销毁,但是其中的变量timer又始终保持着对function外部的引用,于是由此形成了闭包。


关于 this 的指向可以参考这篇文章:juejin.cn/post/739763…


关于闭包概念可以参考这篇文章:juejin.cn/post/739762…


最后:


那么现在我们可以总结出这个防抖函数的核心理念四大要点


核心理念:点击按钮后,做到在规定的时间内没有下一次的触发,才执行



  1. 其中debounce返回一个函数体,跟debounce形成了一个闭包。

  2. 子函数体中每次先销毁上一个setTimeout,再创建一个新的setTimeout

  3. 最后需要 还原原函数的 this 指向。

  4. 最后需要 还原原函数的参数。


作者:木笙
来源:juejin.cn/post/7400253623790272547
收起阅读 »

关于微信小程序(uniapp)的优化

web
前言 开篇雷击 好害怕 怎么办 不要慌 仔细看完文章,彻底解决代码包大小超过限制 提示:以下是本篇文章正文内容,下面案例可供参考 一、微信小程序上传时的规则 微信小程序官方规定主包大小不能超过2M,单个分包大小也不能超过2M,多个分包总大小不能超过8M,文件...
继续阅读 »

在这里插入图片描述


前言


开篇雷击


好害怕


怎么办


不要慌


仔细看完文章,彻底解决代码包大小超过限制




提示:以下是本篇文章正文内容,下面案例可供参考


一、微信小程序上传时的规则


微信小程序官方规定主包大小不能超过2M,单个分包大小也不能超过2M,多个分包总大小不能超过8M,文件过大会增加启动耗时,对用户体验不友好。


官方解释:


在这里插入图片描述


二、分析、整理项目中的文件



1.正常来说一个小程序该有以下目录构成:




│——.hbuilderx

│——api // 接口路径及请求配置

│——components // 公共组件

│——config // 全局配置

│——node_modules // 项目依赖

│——pages // 项目主包

│——order // 项目分包

│——static // 静态资源
│ │
│ ├─scss // 主包css样式
│ │
│ ├─js // 全局js方法
│ │
│ └─image // tabBar图标目录

│——store // Vuex全局状态管理

│——utils // 封装的特定过滤器

│——error-log // 错误日志
│......



2.和自己本地的文件目录对比一下,分析后根据实际情况整理出规范的目录,相同文件规整至一起,删除多余的文件,检查每个页面中是否存在未使用的引用资源



三、按以下思路调整


1.图标资源建议只留下tabBar图标(注意:tabBar图标的大小,控制在30-40k左右最优),其余资源通过网络路径访问,有条件的就上个CDN加速好吧。


2.主包目录建议只留下tabBar相关的页面,其余文件按分包处理(注意:单个分包大小也不能超过2M,多个分包总大小不能超过8M,根据业务划分出合理的分包:例如:order、pay、login、setting、user...)


3.公共组件,公共方法的使用(建议:把分包理解成一个单独的项目,scss,js,components,小程序插件...这些都是仅限于这个分包内使用,后期也方便维护)


4.避免使用过大的js文件,或替换为压缩版或者mini版


5.检查是否存在冗余代码,抽出可复用的进行封装


6.小程序插件(建议:挂载在分包上使用,挂载主包上会影响体积)


	{
// 分包order
"root": "order",
"pages": [{
"path": "index",
"style": {
"navigationStyle": "custom",
"usingComponents": {
"healthcode": "plugin://xxxxx"
}
}
}
],
//插件引入
"plugins": {
"healthcode-plugin": {
"version": "0.2.3",
"provider": "插件appid"
}
}
}

7.根据官方建议开启按需引入、分包优化
manifest.json-源码视图


	"mp-weixin" : {
"appid" : "xxxxx",
"setting" : {
"urlCheck" : false,
"minified" : true
},
// 按需引入
"lazyCodeLoading" : "requiredComponents",
"permission" : {
"scope.userLocation" : {
"desc" : "获取您的位置信息,用于查询数据"
}
},
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ],
// 分包优化
"optimization" : {
"subPackages" : true
}
}

8.Hbuilderx工具点击发行-微信小程序 (注意:运行默认是不会压缩代码)


四、终极办法


如果按以上步骤下来,还是提示代码大小超过限制的话,不妨从微信开发工具上试试


按图勾选上相关配置(注意:不要勾选上传代码时样式自动补全,会增加代码体积)


在这里插入图片描述


五、写在最后


1.提升小程序首页渲染速度 官方给出的代码注入优化


首页代码避免出现复杂的逻辑,控制代码量,去除无用的引入,合理的接口数量


2.小程序加载分包时可能会遇到等待的情况,解决这个问题的办法:


pages.json文件开启分包预下载


    "preloadRule": {
"pages/index": { // 访问首页的时候就开始下载order、pay分包的内容
"network": "all", // 网络环境 all全部网络,wifi仅wifi下预下载
"packages": ["order","pay"] // 要下载的分包
}
}

总结


本文介绍了开发微信小程序时,遇到的代码包大小超过限制的问题,希望可以帮助到你。


作者:Rocky1
来源:juejin.cn/post/7325132133168185381
收起阅读 »

骚操作:如何让一个网页一直处于空白情况?

web
好了,周末闲来无事,突然有个诡异想法! 如题,惯性思路很简单,就是直接撸上一个空内容的html。 注:以下都是在现代浏览器中执行,主要为**Chrome 版本 120.0.6099.217(正式版本) (64 位)和Firefox123.0.1 (64 位) ...
继续阅读 »

好了,周末闲来无事,突然有个诡异想法!


如题,惯性思路很简单,就是直接撸上一个空内容的html。


注:以下都是在现代浏览器中执行,主要为**Chrome 版本 120.0.6099.217(正式版本) (64 位)和Firefox123.0.1 (64 位) **


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
</body>
</html>

؏؏☝ᖗ乛◡乛ᖘ☝؏؏~


但是,要优雅~咱玩的花一点,如果这个HTML中加入一行文字,比如下面这样,如何让这行文字一直不显示出来呢?


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<div>放我出去!!!</div>
</body>
</html>

思考几秒~有了,江湖一直传言,Javascrip代码执行不是影响Render树生成么,上循环!于是如下


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<div>放我出去!!!</div>
<script>
while (1) {
let a;
}
// 或者这样
/*(function stop() {
var message = confirm("我不想让文字出来!");

if (message == true) {
stop()
} else {
stop()
}
})()*/

</script>
</body>

</html>
```一下一下
bingo,可以实现,那再换个思路呢?加载资源?

说干就干,在开发者工具上,设置上下载速度为1kb/s,测试了以下三种类型资源

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<!-- <link rel="stylesheet" href="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/static/bytedesign.min.css" as="style"/> -->
<!-- <img src="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/static/bytedesign.min.css"/> -->
<div class="let-it-go">放我出去!!!</div>
<script src="https://lf3-cdn-tos.bytescm.com/obj/static/log-sdk/collect/5.1/collect.js"></script>
<style>
.let-it-go {
color: red;
}
</style>
</body>
</html>

总得来说,JS和CSS文件,需要排在.let-it-go元素前面或者样式前面,才会影响到渲染DOM或者CSSOM,图片或者影片之类的,不管放前面还是后面,都无影响。如果在css文件中,一直有import外部CSS,也是有很大影响!


但正如题目,这种只能影响一时,却不能一直影响,就算你在代码里写一个在头部不停插入脚本,也没有用,比如如下这么写,按,依旧无效:


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

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<link rel="stylesheet" href="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/static/bytedesign.min.css"
as="style" />

<!-- <img src="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/static/bytedesign.min.css"/> -->
<script>
// setInterval(()=>{
// 不停插入script脚本 或者css文件
let index = '';
(function fetchFile() {
var script = document.createElement('script');
script.src = `https://lf3-cdn-tos.bytescm.com/obj/static/log-sdk/collect/5.1/collect${index}.js`;
document.head.appendChild(script);
script.onload = () => {
fetchFile()
}
script.onerror = () => {
fetchFile()
}
index+=1

// 创建一个 link 元素
//var link = document.createElement('link');
// 设置 link 元素的属性
// link.rel = 'stylesheet';
// link.type = 'text/css';
// link.href = 'https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/app.f81e9f9${index}.css';
// 将 link 元素添加到文档的头部
//document.head.appendChild(link);
})()
// },1000)
</script>
<div class="let-it-go">放我出去!!!</div>
<style>
.let-it-go {
color: red;
}
</style>
<!-- <script src="https://lf3-cdn-tos.bytescm.com/obj/static/log-sdk/collect/5.1/collect.js"></script> -->
</body>

</html>

那么,还有别的方法吗?暂时没有啥想法了,等后续再在这篇上续接~


另外,在实验过程中,有一个方式让我很意外,以为以下代码也会造成页面一直空白,但好像不行。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<div id="appp"></div>
<script>
(function createElement() {
var parentElement = document.getElementById('appp');
// 创建新的子元素
var newElement = document.createElement('div');
// 添加文本内容(可选)
newElement.textContent = '这是新的子元素';
// 将新元素添加到父元素的子元素列表的末尾
parentElement.appendChild(newElement);
createElement()
})()
</script>
<div class="let-it-go">放我出去!!!</div>
</body>
</html>

这可以很好的证明,插入DOM元素这个任务,会在主HTML渲染之后再执行。


祝周末愉快~


作者:大怪v
来源:juejin.cn/post/7344164779629985818
收起阅读 »

js运算精度丢失,用这个库试试?

web
简述 当js进行算术运算时,有时候会遇到以下几个问题: // 控制台可以尝试以下代码 0.1 + 0.2 // 0.30000000000000004 0.3 - 0.1 // 0.19999999999999998 19.9 * 100 // 1989....
继续阅读 »

简述


js进行算术运算时,有时候会遇到以下几个问题:


// 控制台可以尝试以下代码
0.1 + 0.2 // 0.30000000000000004
0.3 - 0.1 // 0.19999999999999998
19.9 * 100 // 1989.9999999999998

为什么会遇到这个问题呢?


由于在计算机运算过程中,十进制的数会被转化为二进制来运算,有些浮点数用二进制表示是无穷的,浮点数运算标准(IEEE 754)64位双精度的小数部分最多支持53位二进制位,运算过程中超出的二进制位会被截断。运算完后再转为十进制。所以产生了精度问题。


为了解决此问题,整理了一些第三方的js库。


相关js库推荐


js库名称备注
Math.jsJavaScript 和 Node.js 的扩展数学库
decimal.jsjavaScript 任意精度的库
big.js一个轻量的任意精度库

big.js


版本介绍

本次用的big.js版本为6.2.1


页面引入

下载big.js:


访问以下页面,在网页中右键另存为即可


// 因为作为本地测试,就不下载压缩版本了
https:/
/cdn.jsdelivr.net/npm/big.js@6.2.1/big.js

// 若需要压缩版本
https:/
/cdn.jsdelivr.net/npm/big.js@6.2.1/big.min.js

引入到html页面:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Big js</title>
</head>
<body>
<!-- 引入页面 -->
<script src="./js/big.js"></script>
<script>
  // 尝试Big构造方法
  console.log('Big', Big)
</script>
</body>
</html>

工程化项目

npm install big.js

在所需页面引入:


// 现在一般用 es 模块引入
import Big from 'big.js';

使用

基本演示:


// 加
let a = new Big(0.1)
a = a.plus(0.2)

// 由于运算结果是个对象,所以展示以下值
console.log('a', a) // {s: 1, e: -1, c: Array(1)}
// 可以使用 Number || toNumber() 转为我们需要的数值
console.log('a', a.toNumber) || console.log('a', Number(a)) // 0.3

可以链式调用:


x.div(y).plus(z).times(9)

参考文档

// big.js 项目 github地址
https://mikemcl.github.io/big.js

// big.js 官方文档地址
https://mikemcl.github.io/big.js/

// 这篇文档将api写的很全了
https://blog.csdn.net/a123456234/article/details/132305810

作者:Allshadow
来源:juejin.cn/post/7356531073469825033
收起阅读 »

前端实现图片压缩方案总结

web
前文提要 在Web开发中,图片压缩是一个常见且重要的需求。随着高清图片和多媒体内容的普及,如何在保证图片质量的同时减少其文件大小,对于提升网页加载速度、优化用户体验至关重要。前端作为用户与服务器之间的桥梁,实现图片压缩的功能可以显著减轻服务器的负担,加快页面渲...
继续阅读 »


前文提要


在Web开发中,图片压缩是一个常见且重要的需求。随着高清图片和多媒体内容的普及,如何在保证图片质量的同时减少其文件大小,对于提升网页加载速度、优化用户体验至关重要。前端作为用户与服务器之间的桥梁,实现图片压缩的功能可以显著减轻服务器的负担,加快页面渲染速度。本文将探讨前端实现图片压缩的几种方法和技术。


1. 使用HTML5的<canvas>元素


HTML5的<canvas>元素为前端图片处理提供了强大的能力。通过JavaScript操作<canvas>,我们可以读取图片数据,对其进行处理(如缩放、裁剪、转换格式等),然后输出压缩后的图片。


步骤概述:



  1. 读取图片:使用FileReaderImage对象加载图片。

  2. 绘制到<canvas>:将图片绘制到<canvas>上,通过调整<canvas>的尺寸或绘图参数来控制压缩效果。

  3. 导出图片:使用canvas.toDataURL()方法将<canvas>内容转换为Base64编码的图片,或使用canvas.toBlob()方法获取Blob对象,以便进一步处理或上传。


示例代码:


function compressImage(file, quality, callback) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

// 设置canvas的尺寸,这里可以根据需要调整
const MAX_WIDTH = 800;
const MAX_HEIGHT = 600;
let width = img.width;
let height = img.height;

if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}

canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);

// 转换为压缩后的图片
canvas.toBlob(function(blob) {
callback(blob);
}, 'image/jpeg', quality);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}

// 使用示例
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
compressImage(file, 0.7, function(blob) {
// 处理压缩后的图片,如上传或显示
console.log(blob);
});
});

2. 利用第三方库(推荐)


除了原生JavaScript和HTML5外,还有许多优秀的第三方库可以帮助我们更方便地实现图片压缩,如image-magic-adapter、compressorjspica等。这些库通常提供了更丰富的配置选项和更好的兼容性支持。


特别推荐的库: image-magic-adapter
这个三方库是国内开发者提供的,他集成许多图片处理能力,包括“图片压缩”、“图片格式转换”、“图片加水印”等等,非常方便,而且这个库还有官网也可以直接使用这些功能.


库官网:http://www.npmjs.com/package/ima…


在线图片处理工具官网:luckycola.com.cn/public/dist…


使用 image-magic-adapter示例:


// 引入image-magic-adapter
import ImageMagicAdapter from 'image-magic-adapter';
let ImageCompressorCls = ImageMagicAdapter.ImageCompressorCls;

const imageCompressor = new ImageCompressorCls(); // 默认压缩质量

// -----------------------------------------图片压缩-----------------------------------------
document.getElementById('quality').addEventListener('input', () => {
const quality = parseFloat(document.getElementById('quality').value);
imageCompressor.quality = 1 - quality; // 更新压缩质量
console.log('更新后的压缩质量:', imageCompressor.quality);
});
document.getElementById('compress').addEventListener('click', async () => {
const fileInput = document.getElementById('upload');
if (!fileInput.files.length) {
alert('请上传图片');
return;
}

const files = Array.from(fileInput.files);
const progress = document.getElementById('progress');
const outputContainer = document.getElementById('outputContainer');
const downloadButton = document.getElementById('download');
const progressText = document.getElementById('progressText');

outputContainer.innerHTML = '';
downloadButton.style.display = 'none';
progress.style.display = 'block';
progress.value = 0;
progressText.innerText = '';
// compressImages参数说明:
// 第一个参数: files:需要压缩的文件数组
// 第二个参数: callback:压缩完成后的回调函数
// 第三个参数: 若是压缩png/bmp格式,输出是否保留png/bmp格式,默认为true(建议设置为false)
// 注意:如果 第三个参数设置true压缩png/bmp格式后的输出的文件为原格式(png/bmp)且压缩效果不佳,就需要依赖设置scaleFactor来调整压缩比例(0-1);如果设置为false,输出为image/jpeg格式且压缩效果更好。
// 设置caleFactor为0-1,值越大,压缩比例越小,值越小,压缩比例越大(本质是改变图片的尺寸),例: imageCompressor.scaleFactor = 0.5;
await imageCompressor.compressImages(files, (completed, total) => {
const outputImg = document.createElement('img');
outputImg.src = imageCompressor.compressedImages[completed - 1];
outputContainer.appendChild(outputImg);
progress.value = (completed / total) * 100;
progressText.innerText = `已完成文件数: ${completed} / 总文件数: ${total}`;
if (completed === total) {
downloadButton.style.display = 'inline-block';
}
}, false);

downloadButton.onclick = () => {
if (imageCompressor.compressedImages.length > 0) {
imageCompressor.downloadZip(imageCompressor.compressedImages);
}
};
});

<h4>图片压缩Demoh4>
<input type="file" id="upload" accept="image/*" multiple />
<br>
<label for="quality">压缩比率:(比率越大压缩越大,图片质量越低)label>
<input type="range" id="quality" min="0" max="0.9" step="0.1" required value="0.5"/>
<br>
<button id="compress">压缩图片button>
<br>
<progress id="progress" value="0" max="100" style="display: none;">progress>
<br />
<span id="progressText">span>
<br>
<div id="outputContainer">div>
<br>
<button id="download" style="display: none;">下载已压缩图片button>

3. gif图片压缩(拓展)


GIF(Graphics Interchange Format)图片是一种广泛使用的图像文件格式,特别适合用于显示索引颜色图像(如简单的图形、图标和某些类型的图片),同时也支持动画。尽管GIF图片本身可以具有压缩特性,但在前端和后端进行压缩处理时,存在几个关键考虑因素,这些因素可能导致在前端直接压缩GIF不如在后端处理更为有效或合理。


下面提供一个厚后端通过node实现gif压缩的方案:
1、下载imagemin、imagemin-gifsicle和image-size库
2、注意依赖的库的版本,不然可能会报错


	"image-size": "^1.1.1",
"imagemin": "7.0.1",
"imagemin-gifsicle": "^7.0.0",

node压缩gif实现如下:



const imagemin = require('imagemin');
const imageminGifsicle = require('imagemin-gifsicle');
const sizeOf = require('image-size');


// 压缩 GIF colors[0-256]
const compressGifImgFn = async (inputBase64, colors = 200, successFn = () => {}, failFn = () => {}) => {
try {
if (inputBase64.length <= 10) {
failFn && failFn('inputBase64 无效')
return;
}

// 获取输入 GIF 的尺寸
const originalSize = getBase64Size(inputBase64);
console.log('Original Size:', originalSize);
// 转换 Base64 为 Buffer
const inputBuffer = base64ToBuffer(inputBase64);
const outputBuffer = await imagemin.buffer(inputBuffer, {
plugins: [
imageminGifsicle({
// interlaced的作用 是,是否对 GIF 进行隔行扫描
interlaced: true,
// optimizationLevel的作用是,设置压缩的质量,0-3
optimizationLevel: 3,
// // progressive的作用是,是否对 GIF 进行渐进式压缩
// progressive: true,
// // palette的作用是,是否对 GIF 进行调色板优化
// palette: true,
// // colorspace的作用是,是否对 GIF 进行色彩空间转换
// colorspace: true,
colors
})
]
});
// 转换压缩后的 Buffer 为 Base64
const outputBase64 = bufferToBase64(outputBuffer);
// 获取压缩后 GIF 的尺寸
const compressedSize = getBase64Size(outputBase64);
console.log('Compressed Size:', compressedSize);
// 输出压缩后的 Base64 GIF
// console.log(outputBase64);
let gifCompressRes = {
outputBase64,
compressedSize,
originalSize
}
successFn && successFn(gifCompressRes);
} catch (error) {
console.error('Error compressing GIF:', error);
failFn && failFn(error)
}
};


// 将 Base64 字符串转换为 Buffer
function base64ToBuffer(base64) {
const base64Data = base64.split(',')[1]; // 如果是 data URL, 删除前缀
return Buffer.from(base64Data, 'base64');
}

// 将 Buffer 转换为 Base64 字符串
function bufferToBase64(buffer) {
return `data:image/gif;base64,${buffer.toString('base64')}`;
}

//获取base64图片大小,返回kb数字
function getBase64Size(base64url) {
try {
//把头部去掉
let str = base64url.replace('data:image/png;base64,', '');
// 找到等号,把等号也去掉
let equalIndex = str.indexOf('=');
if (str.indexOf('=') > 0) {
str = str.substring(0, equalIndex);
}
// 原来的字符流大小,单位为字节
let strLength = str.length;
// 计算后得到的文件流大小,单位为字节
let fileLength = parseInt(strLength - (strLength / 8) * 2);
// 由字节转换为kb
let size = "";
size = (fileLength / 1024).toFixed(2);
let sizeStr = size + ""; //转成字符串
let index = sizeStr.indexOf("."); //获取小数点处的索引
let dou = sizeStr.substr(index + 1, 2) //获取小数点后两位的值
if (dou == "00") { //判断后两位是否为00,如果是则删除00
return sizeStr.substring(0, index) + sizeStr.substr(index + 3, 2)
}
return size;
} catch (error) {
console.log('getBase64Size error:', error);
return 0;
}
};

注意事项



  • 压缩质量与文件大小:压缩质量越高,图片质量越好,但文件大小也越大;反之亦然。需要根据实际需求调整。

  • 兼容性:虽然现代浏览器普遍支持<canvas>和Blob等特性,但在一些老旧浏览器上可能存在问题,需要做好兼容性处理。

  • 性能考虑:对于大图片或高频率的图片处理,前端压缩可能会占用较多CPU资源,影响页面性能。


作者:六月的可乐
来源:juejin.cn/post/7409869765176475686
收起阅读 »

vue3连接mqtt

web
什么是MQTT? MQTT(Message Queuing Telemetry Transport)是一种轻量级的、基于发布/订阅模式的通信协议,通常用于连接物联网设备和应用程序之间的通信。它最初由IBM开发,现在由OASIS(Organization for...
继续阅读 »

什么是MQTT?


MQTT(Message Queuing Telemetry Transport)是一种轻量级的、基于发布/订阅模式的通信协议,通常用于连接物联网设备和应用程序之间的通信。它最初由IBM开发,现在由OASIS(Organization for the Advancement of Structured Information Standards)进行标准化。


MQTT的工作原理很简单:它采用发布/订阅模式,其中设备(称为客户端)可以发布消息到特定的主题(topics),而其他设备可以订阅这些主题以接收消息。这种模式使得通信非常灵活,因为发送者和接收者之间的耦合度很低。MQTT还支持负载消息(payload message)的传输,这意味着可以发送各种类型的数据,如传感器读数、控制指令等。


MQTT的轻量级设计使其在网络带宽和资源受限的环境中表现出色,因此在物联网应用中得到了广泛应用。它可以在低带宽、不稳定的网络环境下可靠地运行,同时保持较低的能耗。MQTT也有许多开源实现和客户端库,使得它在各种平台上都能方便地使用。


MQTT在项目的运用


官网使用指南:docs.emqx.com/zh/cloud/la…


(1)安装MQTT


npm install mqtt

(2)本项目Vite和Vue版本(包括但不限于)


"vue":"^3.3.11"
"vite": "^5.0.10"

(3)引入MQTT文件


import mqtt from "mqtt";

(4)MQTT的具体使用
本文将使用 EMQ X 提供的 免费公共 MQTT 服务器,该服务基于 EMQ X 的 MQTT 物联网云平台 创建。服务器接入信息如下:


Broker: broker.emqx.io

Port: 8083


export const connectMqtt = ({host, name, pwd, theme},onMessageArrived) => {
let client = null
let url = `${host}/mqtt`
let options={
username: name, // 用户名字
password: pwd, // 密码
// clientId: 'clientId'
}
try {
client = mqtt.connect(url, options)
}catch (error) {
console.log('mqtt.connect error', error)
}
// 订阅主题
client.subscribe(theme, (topic) => {
console.log(topic); // 此处打印出订阅的主题名称
});
// 推送消息
// client.publish(theme, JSON.stringify({one: '1', two: '2'}));
//接受消息
client.on("message", (topic, data) => {
// 这里有可能拿到的数据格式是Uint8Array格式,所以可以直接用toString转成字符串
let dataArr = data.toString();
console.log('mqtt收到的消息', dataArr);
onMessageArrived(data)
});
// 重连
client.on("reconnect", (error) => {
console.log("正在重连mqtt:", error);
});
// 错误回调
client.on("error", (error) => {
console.log("MQTT连接发生错误已关闭");
});
}

参考链接:


juejin.cn/post/735925…


docs.emqx.com/zh/cloud/la…


作者:hodawa
来源:juejin.cn/post/7410017851626913833
收起阅读 »

前端如何实现图片伪防盗链,保护页面图片

web
在前端开发中,实现图片防盗链通常涉及到与后端服务器的交互,因为防盗链机制主要是在服务器端实现的。然而,前端也可以采取一些措施来增强图片保护,并与服务器端的防盗链策略配合使用。以下是前端可以采用的一些方法: 一、使用 Token 保护图片资源 动态生成 To...
继续阅读 »

在前端开发中,实现图片防盗链通常涉及到与后端服务器的交互,因为防盗链机制主要是在服务器端实现的。然而,前端也可以采取一些措施来增强图片保护,并与服务器端的防盗链策略配合使用。以下是前端可以采用的一些方法:


image.png


一、使用 Token 保护图片资源



  1. 动态生成 Token


    在用户请求图片时,可以在前端生成一个包含时间戳的 token,然后将其附加到图片 URL 中。这个 token 可以在服务器端验证。


    前端代码示例(使用 JavaScript):


    // 生成当前时间戳作为 token
    function generateToken() {
    return Date.now();
    }

    // 获取图片 URL
    function getImageUrl() {
    const token = generateToken();
    return `https://example.com/images/photo.jpg?token=${token}`;
    }

    // 设置图片 src
    document.getElementById('image').src = getImageUrl();

    解释:



    • generateToken() 函数生成一个时间戳作为 token。

    • getImageUrl() 函数将 token 附加到图片 URL 中,以便进行验证。



  2. 在图片请求中使用 Token


    在图片加载时,确保 URL 中包含有效的 token。前端可以在页面加载时动态设置图片 URL。


    前端代码示例(使用 Vue.js):


    <template>
    <img :src="imageUrl" alt="Protected Image" />
    </template>

    <script>
    export default {
    data() {
    return {
    imageUrl: ''
    };
    },
    methods: {
    generateToken() {
    return Date.now(); // 或使用其他方法生成 token
    }
    },
    created() {
    const token = this.generateToken();
    this.imageUrl = `https://example.com/images/photo.jpg?token=${token}`;
    }
    };
    </script>

    解释:



    • 使用 Vue 的生命周期钩子 created 来生成 token 并设置图片 URL。




二、设置图片加载控制



  1. 防止右键下载


    在前端,你可以通过 CSS 或 JavaScript 来禁用图片的右键菜单,从而防止用户通过右键菜单下载图片。


    前端代码示例(使用 CSS):


    <style>
    .no-right-click {
    pointer-events: none;
    }
    </style>

    <img class="no-right-click" src="https://example.com/images/photo.jpg" alt="Protected Image" />

    前端代码示例(使用 JavaScript):


    document.addEventListener('contextmenu', function (e) {
    if (e.target.tagName === 'IMG') {
    e.preventDefault();
    }
    });

    解释:



    • 使用 CSS 属性 pointer-events: none 来禁用右键菜单。

    • 使用 JavaScript 事件监听器来阻止右键菜单弹出。



  2. 使用水印


    在图片上添加水印是另一种保护图片的方式。前端可以通过 Canvas 绘制水印,但通常这在图片生成或处理阶段进行更为合适。


    前端代码示例(使用 Canvas):


    <canvas id="myCanvas" width="600" height="400"></canvas>
    <script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    const img = new Image();

    img.src = 'https://example.com/images/photo.jpg';
    img.onload = function() {
    ctx.drawImage(img, 0, 0);
    ctx.font = '30px Arial';
    ctx.fillStyle = 'red';
    ctx.fillText('Watermark', 10, 50);
    };
    </script>

    解释:



    • 使用 Canvas 绘制图片并添加水印文本。




三、与服务器端防盗链机制配合



  1. 验证 Referer


    在前端代码中,可以通过设置 Referer 头(这通常由浏览器自动处理)来帮助服务器验证请求来源。


    前端代码示例(使用 Fetch API):


    fetch('https://example.com/images/photo.jpg', {
    method: 'GET',
    headers: {
    'Referer': 'https://yourwebsite.com'
    }
    }).then(response => response.blob())
    .then(blob => {
    const url = URL.createObjectURL(blob);
    document.getElementById('image').src = url;
    });

    解释:



    • 使用 fetch 请求图片,手动设置 Referer 头部(尽管大多数浏览器自动设置)。




总结


前端在实现图片防盗链方面,主要通过动态生成 Token、设置图片加载控制(如禁用右键菜单和添加水印)以及与服务器端防盗链机制配合来保护图片资源。虽然真正的防盗链逻辑通常是在服务器端实现,但前端可以采取这些措施来增强保护效果。结合前端和后端的策略,可以有效地防止未经授权的图片访问和盗用。
image.png


作者:不爱说话郭德纲
来源:juejin.cn/post/7410224960298041394
收起阅读 »

【算法】最小覆盖子串

web
难度:困难 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。 注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。 如...
继续阅读 »

难度:困难


给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""


注意:



  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。

  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。


示例 1:


输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A''B''C'

示例 2:


输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

示例 3:


输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

提示:



  • m == s.length

  • n == t.length

  • 1 <= m, n <= 105

  • st 由英文字母组成


解题思路:



  1. 初始化计数器:创建两个哈希表,一个用于存储字符串t中每个字符的出现次数,另一个用于存储当前窗口内每个字符的出现次数。

  2. 设定窗口:初始化窗口的左边界left和右边界right,以及一些辅助变量如required(表示t中不同字符的数量)、formed(表示当前窗口内满足t字符要求的数量)、windowCounts(表示当前窗口内字符的计数)。

  3. 扩展窗口:将right指针从0开始向右移动,直到窗口包含了t中的所有字符。在每次移动right指针时,更新窗口内的字符计数和formed变量。

  4. 收缩窗口:一旦窗口包含了t中的所有字符,开始移动left指针以尝试缩小窗口,同时更新窗口内的字符计数和formed变量。记录下最小覆盖子串的信息。

  5. 重复步骤3和4:继续移动right指针,重复上述过程,直到right指针到达s的末尾。

  6. 返回结果:最后返回最小覆盖子串。


JavaScript实现:


/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
const need = {}, windowCounts = {};
let left = 0, right = 0;
let valid = 0;
let start = 0, length = Infinity;

// Initialize the need counter with characters from t.
for(let c of t){
need[c] ? need[c]++ : need[c] = 1;
}

// Function to check if the current window satisfies the need.
const is_valid = () => Object.keys(need).every(c => (windowCounts[c] || 0) >= need[c]);

while(right < s.length){
const c = s[right];
right++;

// Increment the count in the windowCounts if the character is needed.
if(need[c]){
windowCounts[c] ? windowCounts[c]++ : windowCounts[c] = 1;
if(windowCounts[c] === need[c])
valid++;
}

// If the current window is valid, try to shrink it from the left.
while(valid === Object.keys(need).length){
if(right - left < length){
start = left;
length = right - left;
}

const d = s[left];
left++;

// Decrement the count in the windowCounts if the character is needed.
if(need[d]){
if(windowCounts[d] === need[d])
valid--;
windowCounts[d]--;
}
}
}

return length === Infinity ? '' : s.substring(start, start + length);
};、

作者:时清云
来源:juejin.cn/post/7410299130280722470
收起阅读 »

如何去实现浏览器多窗口互动

web
前段时间看到了一张神奇的 gif,如下: 感觉特别不可思议,而且是本地运行的环境,于是想自己实现一个但是碍于自己太菜了缺乏对球体、粒子和物理的3D技能,然后去了解了一下如何使一个窗口对另一个窗口的位置做出反应。 于是我做了一个极简的丑陋的版本: 首先,我们...
继续阅读 »

前段时间看到了一张神奇的 gif,如下:


1_vCKb_XLed3eD9y4h-yjdKQ.gif


感觉特别不可思议,而且是本地运行的环境,于是想自己实现一个但是碍于自己太菜了缺乏对球体、粒子和物理的3D技能,然后去了解了一下如何使一个窗口对另一个窗口的位置做出反应。


于是我做了一个极简的丑陋的版本:


1_KJHO9DmEDcTISWuCcvDpMQ.gif


首先,我们看一下在多个客户端之间共享信息的所有方法:


1. 服务器


显然,拥有服务器(使用轮询或Websockets)会简化问题。然而,我们能不能在不使用服务器的情况下去实现呢?


2. 本地存储


本地存储本质上是一个浏览器键值存储,通常用于在浏览器会话之间保持信息的持久性。虽然通常用于存储身份验证令牌或重定向URL,但它可以存储任何可序列化的内容。可以在这里了解更多信息


最近发现了一些有趣的本地存储API,包括storage事件,该事件在同一网站的另一个会话更改本地存储时触发。


image.png


我们可以通过将每个窗口的状态存储在本地存储中来利用这一点。每当一个窗口改变其状态时,其他窗口将通过存储事件进行更新。


这是我最初的想法,但是后来发现还有其他的方式可以实现


3. 共享 Workers


简单来说,Worker本质上是在另一个线程上运行的第二个脚本。虽然它们没有访问DOM,因为它们存在于HTML文档之外,但它们仍然可以与您的主脚本通信。 它们主要用于通过处理后台作业来卸载主脚本,比如预取信息或处理诸如流式日志和轮询之类的较不重要的任务。


我这有一篇关于web Worker 的文章 没了解过的可以先去看看。


image.png


共享的 Workers 是一种特殊类型的 WebWorkers,可以与多个相同脚本的实例通信。


image.png


4. 建立 Workers


我使用的是Vite和TypeScript,所以我需要一个worker.ts文件,并将@types/sharedworker作为开发依赖进行安装。我们可以使用以下语法在我的主脚本中创建连接:


new SharedWorker(new URL("worker.ts", import.meta.url));

接下来需要考虑的就是以下几方面:



  • 确定每个窗口

  • 跟踪所有窗口的状态

  • 当一个窗口改变其状态时,通知其他窗口重新绘制


type WindowState = {  
screenX: number; // window.screenX
screenY: number; // window.screenY
width: number; // window.innerWidth
height: number; // window.innerHeight
};

最关键的信息是window.screenXwindow.screenY,因为它们可以告诉我们窗口相对于显示器左上角的位置。


将有两种类型的消息:



  • 每个窗口在改变状态时,将发布一个windowStateChanged消息,带有其新状态。

  • 工作者将向所有其他窗口发送更新,以通知它们其中一个已更改。工作者将使用sync消息发送所有窗口的状态。


// worker.ts  
let windows: { windowState: WindowState; id: number; port: MessagePort }[] = [];

onconnect = ({ ports }) => {
const port = ports[0];

port.onmessage = function (event: MessageEvent) {
console.log("We'll do something");
};
};

我们与 SharedWorker 的基本连接将如下所示。我编写了一些基本函数来生成一个ID,并计算当前窗口状态,同时我对我们可以使用的消息类型进行了一些类型定义,称为 WorkerMessage


// main.ts  
import { WorkerMessage } from "./types";
import {
generateId,
getCurrentWindowState,
} from "./windowState";

const sharedWorker = new SharedWorker(new URL("worker.ts", import.meta.url));
let currentWindow = getCurrentWindowState();
let id = generateId();

一旦启动应用程序,应该立即通知工作者有一个新窗口,因此需要发送一条消息:


// main.ts  
sharedWorker.port.postMessage({
action: "windowStateChanged",
payload: {
id,
newWindow: currentWindow,
},
} satisfies WorkerMessage);

然后可以在工作者端监听此消息,并相应地更改 onmessage。基本上,一旦接收到 windowStateChanged 消息,它要么是一个新窗口,我们将其追加到状态中,要么是一个旧窗口发生了变化。然后,我们应该通知所有窗口状态已经改变:


// worker.ts  
port.onmessage = function (event: MessageEvent) {
const msg = event.data;
switch (msg.action) {
case "windowStateChanged": {
const { id, newWindow } = msg.payload;
const oldWindowIndex = windows.findIndex((w) => w.id === id);
if (oldWindowIndex !== -1) {
// old one changed
windows[oldWindowIndex].windowState = newWindow;
} else {
// new window
windows.push({ id, windowState: newWindow, port });
}
windows.forEach((w) =>
// send sync here
);
}
break;
}
};

要发送同步消息,实际上我需要一个小技巧,因为“port”属性无法被序列化,所以我将其转换为字符串,然后再解析回来。因为我比较懒,我不会只是将窗口映射到一个更可序列化的数组:


w.port.postMessage({  
action: "sync",
payload: { allWindows: JSON.parse(JSON.stringify(windows)) },
} satisfies WorkerMessage);

接下来就是绘制内容了。


5. 使用Canvas 绘图


在每个窗口的中心画一个圆圈,并用一条线连接这些圆圈,将使用 HTML Canvas 进行绘制


const drawCenterCircle = (ctx: CanvasRenderingContext2D, center: Coordinates) => {  
const { x, y } = center;
ctx.strokeStyle = "#eeeeee";
ctx.lineWidth = 10;
ctx.beginPath();
ctx.arc(x, y, 100, 0, Math.PI * 2, false);
ctx.stroke();
ctx.closePath();
};

要绘制线条,需要进行一些数学计算(我保证,不是很多 🤓),将另一个窗口中心的相对位置转换为当前窗口上的坐标。 基本上,正在改变基底。使用以下数学公式来实现这个功能。首先,将更改基底,使坐标位于显示器上,并通过当前窗口的 screenX/screenY 进行偏移。


image.png


const baseChange = ({  
currentWindowOffset,
targetWindowOffset,
targetPosition,
}: {
currentWindowOffset: Coordinates;
targetWindowOffset: Coordinates;
targetPosition: Coordinates;
}
) => {
const monitorCoordinate = {
x: targetPosition.x + targetWindowOffset.x,
y: targetPosition.y + targetWindowOffset.y,
};

const currentWindowCoordinate = {
x: monitorCoordinate.x - currentWindowOffset.x,
y: monitorCoordinate.y - currentWindowOffset.y,
};

return currentWindowCoordinate;
};

现在有了相同相对坐标系上的两个点,可以画线了!


const drawConnectingLine = ({  
ctx,
hostWindow,
targetWindow,
}: {
ctx: CanvasRenderingContext2D;
hostWindow: WindowState;
targetWindow: WindowState;
}
) => {
ctx.strokeStyle = "#ff0000";
ctx.lineCap = "round";
const currentWindowOffset: Coordinates = {
x: hostWindow.screenX,
y: hostWindow.screenY,
};
const targetWindowOffset: Coordinates = {
x: targetWindow.screenX,
y: targetWindow.screenY,
};

const origin = getWindowCenter(hostWindow);
const target = getWindowCenter(targetWindow);

const targetWithBaseChange = baseChange({
currentWindowOffset,
targetWindowOffset,
targetPosition: target,
});

ctx.strokeStyle = "#ff0000";
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(origin.x, origin.y);
ctx.lineTo(targetWithBaseChange.x, targetWithBaseChange.y);
ctx.stroke();
ctx.closePath();
};

现在,只需要对状态变化做出反应即可。


// main.ts  
sharedWorker.port.onmessage = (event: MessageEvent) => {
const msg = event.data;
switch (msg.action) {
case "sync": {
const windows = msg.payload.allWindows;
ctx.reset();
drawMainCircle(ctx, center);
windows
.forEach(({ windowState: targetWindow }) => {
drawConnectingLine({
ctx,
hostWindow: currentWindow,
targetWindow,
});
});
}
}
};

最后一步,只需要定期检查窗口是否发生了变化,如果是,则发送一条消息。


setInterval(() => {setInterval(() => {  
const newWindow = getCurrentWindowState();
if (
didWindowChange({
newWindow,
oldWindow: currentWindow,
})
) {
sharedWorker.port.postMessage({
action: "windowStateChanged",
payload: {
id,
newWindow,
},
} satisfies WorkerMessage);
currentWindow = newWindow;
}
}, 100);



作者:StriveToY
来源:juejin.cn/post/7329753721018269711
收起阅读 »

改进菜单栏动态展示样式,我被评上优秀开发!

web
精彩新文章:拿客户电脑,半小时完成轮播组件开发!被公司奖励500 背景 我们公司的导航菜单是动态可配置的,有的页面菜单数量比较多,有的比较少。 由于大多页面菜单都是比较少的,因此当菜单非常多时, 我们采用了朴实无华的滚动条:当横向超出的时候,滚动展示。 但很...
继续阅读 »

精彩新文章:拿客户电脑,半小时完成轮播组件开发!被公司奖励500


背景


我们公司的导航菜单是动态可配置的,有的页面菜单数量比较多,有的比较少。



由于大多页面菜单都是比较少的,因此当菜单非常多时, 我们采用了朴实无华的滚动条:当横向超出的时候,滚动展示。


但很快,客户就打回来了:说我们的样式太丑,居然用滚动条!还质问我们产品这合理吗?产品斩钉截铁的告诉客户,我让开发去优化...


于是,领导让我们想解决方案。(我真谢谢产品!


很快,我想到一个方案(从其他地方看到的交互),我告诉领导:



我们可以做成动态菜单栏,如果展示不下了,出现一个更多按钮,多余的菜单都放到更多里面去:




领导说这个想法不错啊,那就你来实现吧!


好家伙,我只是随便说说,没想到,自己给自己挖了个大坑啊!



不过,我最后也是顺利的完成了这个效果的开发,还被评上了本季度优秀开发!分享一下自己的实现方案吧!



技术方案


基础组件样式开发


既然要开发这个效果,干脆就封装一个通用组件AdaptiveMenuBar.vue吧。我们先写一下基本样式,如图,灰色区域就是我们的组件内容,也就是我们菜单栏动态展示的区域。



AdaptiveMenuBar.vue


<template>
<div class="adaptive-menu-bar">
</div>

</template>

<style lang="less" scoped>
.adaptive-menu-bar {
width: 100%;
height: 48px;
background: gainsboro;
display: flex;
position: relative;
overflow: hidden;
}
</style>




我们写点假数据


<template>
<div class="adaptive-menu-bar">
<div class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
{{ item.name }}
</div>
</div>

<div>更多</div>
</div>

</template>

<script setup>
const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];
</script>


<style lang="less" scoped>
.adaptive-menu-bar {
width: 100%;
height: 48px;
background: gainsboro;
display: flex;
position: relative;
overflow: hidden;
.origin-menu-item-wrap{
width: 100%;
display: flex;
}
}
</style>


如图,由于菜单数量比较多,一部分已经隐藏在origin-menu-item-wrap这个父元素里面了。



实现思路


那我们要如何才能让多余的菜单出现在【更多】按钮里呢?原理很简单,我们只要计算出哪个菜单超出展示区域即可。假设如图所示,第12个菜单被截断了,那我们前11个菜单就可以展示在显示区域,剩余的菜单就展示在【更多】按钮里。


更多按钮的展示逻辑


更多按钮只有在展示区域空间不够的时候出现,也就是origin-menu-item-wrap元素的滚动区域宽度scrollWidth 大于其宽度clientWidth的时候。


用代码展示大致如下


<template>
<div ref="menuBarRef" class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
<m-button type="default" size="small">{{ item.name }}</m-button>
</div>
</div>

</template>
<script setup>
const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];

// 是否展示更多按钮
const showMoreBtn = ref(false);

onMounted(() => {
const menuWrapDom = menuBarRef.value;
if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
showMoreBtn.value = true;
}
});
</script>


截断位置的计算


要计算截断位置,我们需要先渲染好菜单。



然后开始对menu-item元素宽度进行加和,当相加的宽度大于菜单展示区域的宽度clientWidth时,计算终止,此时的menu-item元素就是我们要截断的位置。



菜单截断的部分,我们此时放到更多里面展示就可以了。


大致代码如下:


<template>
<div ref="menuBarRef" class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
<m-button type="default" size="small">{{ item.name }}</m-button>
</div>
</div>

</template>
<script setup>
const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];

// 是否展示更多按钮
const showMoreBtn = ref(false);

onMounted(() => {
const menuWrapDom = menuBarRef.value;
if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
showMoreBtn.value = true;
}
// 计算截断菜单的索引位置
let sliceIndex = 0
// 获取menu-item元素dom的集合
const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item');
// 将NodeList转换成数组
const nodeArray = Array.prototype.slice.call(menuItemNodeList);
let addWidth = 0;
for (let i = 0; i < nodeArray.length; i++) {
const node = nodeArray[i];
// clientWidth不包含菜单的margin边距,因此我们手动补上12px
addWidth += node.clientWidth + 12;
// 76是更多按钮的宽度,我们也要计算进去
if (addWidth + 76 > menuWrapDom.clientWidth) {
sliceIndex.value = i;
break;
} else {
sliceIndex.value = 0;
}
}

});
</script>


样式重整


当被截断的元素计算完毕时,我们需要重新进行样式渲染,但是注意,我们原先渲染的菜单列不能注销,因为每次浏览器尺寸变化时,我们都是基于原先渲染的菜单列进行计算的。


所以,我们实际需要渲染两个菜单列:一个原始的,一个样式重新排布后的



如上图,黄色就是原始的菜单栏,用于计算重新排布的菜单栏,只不过,我们永远不在页面上展示给用户看!


<template>
<div class="adaptive-menu-bar">
<!-- 原始渲染的菜单栏 -->
<div ref="menuBarRef" class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
<m-button type="default" size="small">{{ item.name }}</m-button>
</div>
</div>

<!-- 计算优化显示的菜单栏 -->
<div v-for="(item, index) in menuList" :key="index" class="menu-item">
<m-button type="default" size="small">{{ item.name }}</m-button>
</div>
<div >更多</div>
</div>

</template>

代码实现


基础功能完善


为了我们的菜单栏能动态的响应变化,我们需要再每次resize事件触发时,都重新计算样式


const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];

// 是否展示更多按钮
const showMoreBtn = ref(false);

const setHeaderStyle = () => {
// ....
}

window.addEventListener('resize', () => setHeaderStyle());

onMounted(() => {
setHeaderStyle();
});
</script>

完整代码


完整代码剥离了一些第三方UI组件,便于大家理解。


<template>
<div class="adaptive-menu-bar">
<!-- 原始渲染的菜单栏 -->
<div ref="menuBarRef" class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
{{ item.name }}
</div>
</div>
<!-- 计算优化显示的菜单栏 -->
<div v-for="(item, index) in menuList" :key="index" class="menu-item">
{{ item.name }}
</div>

<!-- 更多按钮 -->
<div v-if="showMoreBtn" class="dropdown-wrap">
<span>更多</span>
<!-- 更多里面的菜单 -->
<div class="menu-item-wrap">
<div v-for="(item, index) in menuOriginData.slice(menuList.length)" :key="index">{{ item.name }}</div>
</div>
</div>
</div>

</template>

<script setup>
import { IconMeriComponentArrowDown } from 'meri-icon';

const menuBarRef = ref();

const open = ref(false);

const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];

const menuList = ref(menuOriginData);

// 是否展示更多按钮
const showMoreBtn = ref(false);

const setHeaderStyle = () => {
const menuWrapDom = menuBarRef.value;
if (!menuWrapDom) return;
if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
showMoreBtn.value = true;
} else {
showMoreBtn.value = false;
}
const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item');
if (menuItemNodeList) {
let addWidth = 0,
sliceIndex = 0;
// 将NodeList转换成数组
const nodeArray = Array.prototype.slice.call(menuItemNodeList);
for (let i = 0; i < nodeArray.length; i++) {
const node = nodeArray[i];
addWidth += node.clientWidth + 12;
if (addWidth + 64 + 12 > menuWrapDom.clientWidth) {
sliceIndex = i;
break;
} else {
sliceIndex = 0;
}
}
if (sliceIndex > 0) {
menuList.value = menuOriginData.slice(0, sliceIndex);
} else {
menuList.value = menuOriginData;
}
}
};

window.addEventListener('resize', () => setHeaderStyle());

onMounted(() => {
setHeaderStyle();
});
</script>

<style lang="less" scoped>
.adaptive-menu-bar {
width: 100%;
height: 48px;
background: gainsboro;
display: flex;
position: relative;
align-items: center;
overflow: hidden;
.origin-menu-item-wrap {
width: 100%;
display: flex;
position: absolute;
top: 49px;
display: flex;
align-items: center;
left: 0;
right: 0;
bottom: 0;
height: 48px;
z-index: 9;
}
.menu-item {
margin-left: 12px;
}
.dropdown-wrap {
width: 64px;
display: flex;
align-items: center;
cursor: pointer;
justify-content: center;
height: 28px;
background: #fff;
border-radius: 4px;
overflow: hidden;
border: 1px solid #c4c9cf;
background: #fff;
margin-left: 12px;
.icon {
width: 16px;
height: 16px;
margin-left: 4px;
}
}
}
</style>

代码效果


可以看到,非常丝滑!



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