注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

哦!该死的您瞧瞧这箭头

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
收起阅读 »

Linux新系统正式发布,易用性直接向Windows看齐!

提到 Linux Mint 这个系统,相信不少喜欢折腾Linux系统的小伙伴可能之前有尝试过。 该系统旨在为普通用户提供一个免费、易用、舒适、优雅的桌面操作系统。 就在不久前,备受期待的 Linux Mint 22(代号为“Wilma”)正式官宣发布,这一消...
继续阅读 »

提到 Linux Mint 这个系统,相信不少喜欢折腾Linux系统的小伙伴可能之前有尝试过。


该系统旨在为普通用户提供一个免费、易用、舒适、优雅的桌面操作系统。



就在不久前,备受期待的 Linux Mint 22(代号为“Wilma”)正式官宣发布,这一消息也在对应的爱好者圈子里引起了一阵关注和讨论。


作为Linux Mint系列的一个重要里程碑,Linux Mint 22不仅继承了Ubuntu 24.04 LTS的稳定性和安全性,还在此基础上进行了大量的改进和优化,为用户带来了全新的桌面体验。



长期支持


本次发布的全新Linux Mint 22作为一个难得的长期支持版(LTS),将更新支持到2029年,期间将会定期推送安全更新。



这意味着在未来几年中,用户可以享受到稳定且持续的安全更新。


内核升级


新版Linux Mint 22与时俱进,同样采用了Linux 6.8内核,这一更新不仅提升了与现代硬件、应用程序和软件包的兼容性,还带来了更好的系统性能和稳定性。


此外,内核的升级也为后续的维护和升级提供了更广阔的空间。


桌面环境


Linux Mint 22默认搭载了Cinnamon 6.2桌面环境,为用户带来更加流畅、智能和高效的桌面体验,并且同时提供了Xfce和MATE版本供用户选择。



Cinnamon 6.2带来了诸多新特性和改进,从而进一步提升用户体验。



  • 启动应用管理更便捷:添加启动应用时,搜索栏默认显示,方便用户快速定位所需应用。

  • 工作区管理更灵活:工作区切换器支持用鼠标中键删除工作区,操作更加直观;Cornerbar小程序允许自定义点击操作,提升效率。

  • 快捷键和Spices功能增强:支持可配置的快捷键绑定,键盘快捷方式编辑器新增搜索功能,设置更便捷。

  • 界面优化:用户小程序可在面板显示个人头像,提升个性化程度;Cinnamon会话界面新增欢迎徽章,提升用户体验;屏幕键盘添加关闭按钮,使用更方便,等等。


软件管理器


Linux Mint 22的一大重点更新就是针对mintinstall软件管理器的改进。


新版本不仅提升了加载速度,还增加了多线程支持、新的偏好设置页面和横幅幻灯片。



新的软件管理器默认禁用未验证的Flatpak软件包,以提高系统的安全性。同时,已验证的Flatpak软件包会显示维护者姓名,以增加用户信任度,而如果要启用未经验证的 Flatpak 软件包,它们将被清楚地标记出来。




其他更新


Linux Mint 22还带来了其他诸多更新,比如:



  • 高分辨率屏幕支持得到增强,确保在不同分辨率下都能获得最佳显示效果;

  • 默认音频服务器切换为了PipeWire,以提供更好的音频处理和兼容性;

  • 所有使用libsoup2的软件均迁移到libsoup3;

  • 支持GTK4;

  • Matrix加持;

  • ……等等。



此外,Linux Mint 22还包含了大量底层的Bug修复、稳定性提升和性能优化,这些都是为了确保系统运行更加流畅、稳定。


后记


总而言之,这次Linux Mint 22的发布,使得Linux桌面系统的易用性又进了一步。



感兴趣的小伙伴也可以直接去官网下载ISO镜像来安装使用。


我也特地看了一下新版本安装要求,对机器配置要求还真不高,最近有时间我也准备收拾出来一台老电脑来安装试试。



文章的最后也期待Linux桌面系统在未来能百花齐放,发展得越来越好。


作者:CodeSheep
来源:juejin.cn/post/7411032557074710543
收起阅读 »

多语言翻译你还在一个个改?我不允许你不知道这个工具

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
收起阅读 »

移动开发者终于失去了往日荣耀?

部分原因在于AI浪潮. 但可能还有其他原因. 作为一名资深移动开发者, 我渴望了解苹果, 安卓和跨平台生态系统的最新趋势. 然而, 最近业界发出的信号并不令人振奋. 这篇文章并不是要讨论在最新的 WWDC 或 Google I/O 上开发者们被灌输了什么. 我...
继续阅读 »

部分原因在于AI浪潮. 但可能还有其他原因.


作为一名资深移动开发者, 我渴望了解苹果, 安卓和跨平台生态系统的最新趋势. 然而, 最近业界发出的信号并不令人振奋.


这篇文章并不是要讨论在最新的 WWDC 或 Google I/O 上开发者们被灌输了什么. 我们要讨论的是当前的趋势是如何影响开发者的职业生涯的.


移动开发人员的工作岗位正在迅速消失 -- 至少我在 脉脉 上搜索到的情况是这样. 关于这一点, 还有一个有趣的Reddit讨论, 以及StackOverflow 开发人员调查开发人员数量统计: 移动开发调查受访者人数从 12.45% 降至 3.38%.


虽然行业领先论坛的结果不足以详细反映真实问题, 但线上+线下论坛的集体反馈也能反映些许蛛丝马迹.


我们是如何走到这一步的?


就在不到十年前, 移动开发还是最酷的技术. 每家领先的科技公司都在努力征服 iOS 和 Android 这两个平台.


近十年来, 原生平台和跨平台之争持续不断. 在此之前, Cordova, Xamarin 和 Titanium 是每个技术Manager口中的热门词汇. 这些框架在技术上并不出众, 但它们成功地维持了业界最喜欢的“一次开发, 到处发布, 节省资金”的信念.


同类产品的第二波浪潮以 React Native 和 Flutter 等跨平台框架的形式出现. Facebook 和谷歌称赞它们是完全原生的.


但是, 只有每天与它们打交道的程序员才知道, 在开发具有流畅性, 高性能和可玩性(利用传感器功能--使移动体验更加深入和个性化)用户体验(智能手机人机交互的最大组成部分)的大型应用时, 它们是多么的力不从心.


虽然用户体验是由 Facebook 和谷歌开发的, 但它们的整个发展历程都是由那些必须向大客户推销开发人员的代理商传播的. 在以降低开发成本为价值主张时, 他们开始声嘶力竭地高唱跨平台的大戏. 为了提高代理公司的投资组合, 他们还通过开发组件来扩大各自框架的 GitHub 代码仓库. Facebook 和谷歌的开发人员看着自己的边缘项目蓬勃发展, 乐得合不拢嘴. 大家都很高兴.


实际上, 从长远来看, 跨平台项目让拥有项目的公司付出了更大的代价, 因为这种方法存在明显的缺陷:



  • 只提供两个平台的最大公约数.

  • 开发人员疲劳(除了《Hello World》, 开发人员无论如何都得学习本地程序)

  • 开源(因此没有问责制)开发.


在完全原生的跨平台工具(Unity 及其朋友)与忠实于原生的 XCode 和 Eclipse 之间也发生了有趣的采用战争, 不过, 根据设计, 这场战争仅限于游戏开发.


这些战争是否削弱了移动开发事业? 也不尽然. 但它们确实割裂了普通开发人员对行业的认知. 新手急于跳槽, “得学那个东西(跨平台)”, 这将是他们进入IT行业的单程票, 但后来却失望了. 老手们还在坚持使用那些经过时间考验的东西(C++, Java, 以及后来的 Swift, Kotlin), 但他们常常发现, 由于这样或那样的原因, 他们很难在不断摇摆不定的市场中立足.


尤其是, 摇摆不定.


是AI, 还是其他原因?


似乎随着 GenAI 的到来, 关于移动开发的讨论已经酝酿成型.


然而, 移动开发的第一块多米诺骨牌倒在了 2017 年, 当时剑桥分析的宝贝竟然是从 Facebook 的壁橱里走出来的.


西方世界的民主理念与隐私紧密相连. 未经同意追踪用户成了至今不被承认的罪过. 剑桥分析丑闻引发了政府在全球范围内对大型科技公司的攻击.


Facebook就是最直接的受害者. 谷歌作为智能手机市场最大的利益相关者, 同时也是全球著名的雇主和政府服务提供商(GCP, 谷歌教育等), 成功地争取到了时间. 为了遵守规定, 谷歌对其广告产品和货币化 SDK 进行了多次修改. 安卓开发者的收入来源主要是广告, 他们不得不拖着不做, 否则就会失去市场.


苹果公司站在隐私保护的制高点上, 选择成为手握大棒的人, 将广告追踪的权力交到用户手中. Facebook 成为此举的最大受害者. 为了支持这一立场, 苹果开始将其备受推崇的订阅模式奉为应用开发的黄金标准. 2020 年是苹果开始改进其备受诟病的订阅 API 支持的第一年.


隐私合规对移动开发行业的冲击是前所未有的. 独立开发者受到的冲击最大, 但公司级的开发也放慢了脚步. 开发人员本可用于创建新框架的时间开始被浪费在与支持和法律专业人士进行无休止的问答, 快速修复以及与企业主进行无用的讨论上.


与此同时, 无代码也在兴起. 虽然它的成功还远未得到验证, 但它已成为管理讨论中的一个主要观点, 并成为主流开发的有力竞争者.


一些小型开发公司选择转向无代码/网页产品开发上. 大公司不再在移动交付方面冒大的风险. 预算不再流向开源项目. 维护商店评级成了新的焦点, 为此, 预先开发的功能大多已经足够了.


不要破坏已有的工作. 不要冒险去创造什么了不起的东西, 因为我们不知道什么叫了不起."成了新的口头禅.


移动开发人员无法应对这种对他们有利的转变. 为什么? 这就引出了我们的下一个问题.


移动开发者的真正力量在哪里?


这要看什么是优秀移动开发者的真正定义.



优秀开发人员的标准定义(“设计师, 编码员, 测试员”)并不能完全定义优秀的移动开发人员.



关于“怎样才能成为一名优秀的移动开发人员”, 人们几乎没有达成共识. 移动开发技能与网页开发技能几乎没有区别.


一个优秀的移动应用应该更多地与UE和UI有关, 还 应该与智能手机传感器的巧妙使用有关. 优秀开发人员的标准定义(“程序设计师, 编码员, 测试员”)并不能完全定义优秀的移动开发人员. 一个人首先必须是一名优秀的开发人员, 但还不止于这些.


绑定(UI + 数据库 + API)必须弄清楚. 必须选择在架构上最合适的. MVC 是否足够? 还是需要ViewModel? Coordinator等顽固模式呢?


设计的坏处在于没有经过验证的验证器. 衡量一个好设计的唯一指标是, 未来的开发人员是否能在其能力和主动性的基础上进行改进.


说到移动开发, 还必须掌握硬件集成的工作流程. 例如, 蓝牙, 加速计, 光传感器, 陀螺仪等. 这正是嵌入式工程师的优势所在. 金融技术(苹果和谷歌支付)增加了另一个维度: NFC.


虽然成功集成所有这些功能需要付出大量的精力和时间, 但从管理/领导的角度来看, 它们只是具有相同即插即用接口的简单盒子. 尽管 “Mobile First”的口号已经深入人心, 个人体验也越来越丰富, 但在公司的 IT 战略中, 移动仍然 只是一个辅助出口.


一个像样的移动应用代码库需要一个专门的架构师, 但小型团队很少有这样的人. 即使是大公司的团队也会因为平台分散而在此方面偷工减料.


只有大公司才有能力开发可重复使用的库和框架. 中小型公司的团队没有护城河, 无法证明在平台巨头随时可能破坏任何东西的情况下, 他们的长期努力是有道理的.


与网络不同的是, 网络上的伟大感知来自于个人创作者, 而移动应用的伟大则是由平台决定的: 苹果和谷歌. 《人机界面指南》和《材料设计》标准引导着业界的期望.


然而, 在大多数情况下, 这些标准仍未得到充分展示. 这是因为定义优秀应用的标准是在没有此类范例的情况下制定的. 它们的示例大多是开发人员必须在此基础上构建的简易版.


这种设计是一种简化主义, 阻碍了许多可能的创新, 只有那些获得平台奖励(安卓和苹果编辑选择的舞台)的创新才会不断被复制.


超越标准的杰出移动体验只是一个例外(设计师在其中同样发挥着重要作用). 规则就是要萧规曹随, 即使是最杰出的移动开发者也不得不这样做.


如果一个独立移动开发者向世界展示了他/她能为这些平庸的平台带来惊喜, 那么这些平台就会刺探他/她的作品, 并将其打上自己的烙印.


总结一下


AI(特别是 GenAI)正在吸走其他各个部门的资金. Manager们想证明自己没有错过潮流,即使他们知道这只是错失良机的焦虑。.


在移动端大语言模型成为现实之前, 移动开发人员几乎不可能再次大放异彩. 在那里, 他们将不得不与数据科学家分享荣耀.


当形势一片大好时, 所有开发人员都会有机地成长为镇上最大的赢家. 当经济不景气时, 有能力的开发人员会通过创建框架, 库和可重复使用的组件来倍增他们的影响力. 中低技能的开发人员受到顶级开发人员的启发, 开始提升自己的技能. 当他们再也找不到这条路时, 他们就会转换领域. 他们离开狭窄的溪流(移动开发), 开始在海洋中游泳, 模式如下:



同一平台的所有设备(Mac, Vision pro) => 跨平台(iOS + Android) => 网页开发(+web).



当情况变得更糟时, 大多数水手都会下船. 船长会留下来. 但当海盗占据上风时, 船长们也会被屠杀. 如果船还算幸运, 海盗们将会考虑重建.


入门级开发人员或来自边缘领域的开发人员就是这些海盗. 他们受雇充当临时工, 让这艘船保持漂浮, 直到老手重新发现它.


这就是今天移动开发的现状. 它还没有被毁灭. 也永远不会. 中级入门级开发人员将移动开发保持在 2015 年的水平.


这些人热衷于通过构建组合应用来炫耀自己的技能, 他们不想购买域名/托管服务, 并且对 500 多个 NPM/Yarn 软件包望而却步.


这些新手能否夺回平台? 更重要的是, 他们能找到值得热爱的创作领域吗? 唯一的办法就是超越单纯的软件开发, 成为硬件 + 设计的大师.


在AI重塑每个人期望的时代, 这种可能性并不存在.


一家之言, 欢迎拍砖!


Happy Coding! Stay GOLDEN!


作者:bytebeats
来源:juejin.cn/post/7410989416866955291
收起阅读 »

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
收起阅读 »

只因把 https 改成 http,带宽减少了 70%!

起因 是一个高并发的采集服务上线后,100m的上行很快就被打满了。 因为这是一条专线,并且只有这一个服务在使用,所以可以确定就是它导致的。 但是!这个请求只是一个 GET 请求,同时并没有很大的请求体,这是为什么呢? 于是使用 charles 重新抓包后发现,...
继续阅读 »

起因


是一个高并发的采集服务上线后,100m的上行很快就被打满了。

因为这是一条专线,并且只有这一个服务在使用,所以可以确定就是它导致的。


但是!这个请求只是一个 GET 请求,同时并没有很大的请求体,这是为什么呢?


于是使用 charles 重新抓包后发现,一个 request 的请求居然要占用 1.68kb 的大小!


其中TLS Handshake 就占了 1.27kb。


图片.png


这种情况下,需要的上行带宽就是:1.68*20000/1024*8=262.5mbps


也就说明100mbps的上行为何被轻松打满


TLS Handshake是什么来头,竟然如此大?


首先要知道HTTPS全称是:HTTP over TLS,每次建立新的TCP连接通常需要进行一次完整的TLS Handshake。在握手过程中,客户端和服务器需要交换证书、公钥、加密算法等信息,这些数据占用了较多的字节数。


TLS Handshake的内容主要包括:



  • 客户端和服务器的随机数

  • 支持的加密算法和TLS版本信息

  • 服务器的数字证书(包含公钥)

  • 用于生成对称密钥的“Pre-Master Secret”


这个过程不仅耗时,还会消耗带宽和CPU资源。


因此想到最粗暴的解决方案也比较简单,就是直接使用 HTTP,省去TLS Handshake的过程,那么自然就不会有 TLS 的传输了。


那么是否真的有效呢?验证一下就知道。


将请求协议改成 http 后:


图片.png
可以看到请求头确实不包含 TLS Handshake了!


整个请求只有 0.4kb,节省了 70% 的大小


目标达成


因此可以说明:在一些不是必须使用 https 的场景下,使用 http 会更加节省带宽。


同时因为减少了加密的这个过程,可以观察到的是,在相同的并发下,服务器的负载有明显降低。


那么问题来了


如果接口必须使用 https那怎么办呢?


当然还有另外一个解决方案,那就使用使用 Keep-Alive

headers 中添加 Connection: keep-alive 即可食用。


通过启用 Keep-Alive,

可以在同一TCP连接上发送多个HTTPS请求,

而无需每次都进行完整的TLS Handshake,

但第一次握手时仍然需要传输证书和完成密钥交换。


对于高并发的场景也非常适用。


要注意的是


keep-alive 是有超时时间的,超过时间连接会被关闭,再次请求需要重新建立链接。


Nginx 默认的 keep-alive 超时是 75 秒,

Apache HTTP 服务器 通常默认的 keep-alive 超时是 5 秒。



ps:

如果你的采集程序使用了大量的代理 ip那么 keep-alive 的效果并不明显~~

最好的还是使用 http



作者:麦麦麦造
来源:juejin.cn/post/7409138396792881186
收起阅读 »

数据可视化工具库比较与应用: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
收起阅读 »

作为技术Leader如何带散一个团队

大家好,我是程序员凌览。 这个话题本身就很有趣——如何有效地带散一个团队,精选了两位网友的回答让我们一起来看看。 第一位网友的回答 1938年10月14日,毛泽东谈了如何把团队带好。你反着来,肯定能把团队带散。 毛泽东说,要带好团队,必须善于爱护干部。爱护的办...
继续阅读 »

大家好,我是程序员凌览。


这个话题本身就很有趣——如何有效地带散一个团队,精选了两位网友的回答让我们一起来看看。


第一位网友的回答


1938年10月14日,毛泽东谈了如何把团队带好。你反着来,肯定能把团队带散。


毛泽东说,要带好团队,必须善于爱护干部。爱护的办法是:



“第一,指导他们。这就是让他们放手工作,使他们敢于负责;同时,又适时地给以指示,使他们能在党的政治路线下发挥其创造性。”



你反着来,你就处处摆你下属的谱,不管自己会不会,都要装着自己会的样子。同时要求团队的人不能有任何主观能动性,什么事都要跟你汇报。谁敢改变你的任何看法,就处理谁。说一不二。



“第二,提高他们。这就是给以学习的机会,教育他们,使他们在理论上在工作能力上提高一步。”



你反着来,不要给他们任何学习机会,也不进行任何业务培训,绝不多花一分钱在他们的学习上,因为学好了,他们就跳槽了,这不是浪费吗?



“第三,检查他们的工作,帮助他们总结经验,发扬成绩,纠正错误。有委托而无检查,及至犯了严重的错误,方才加以注意,不是爱护干部的办法。”



你反着来,平时不管不顾,听之任之,一旦发生了问题,就把犯错误的人骂得狗血喷头。该扣工资的扣工资,该开除的开除。谁让你们自己要犯错呢。



“第四,对于犯错误的干部,一般地应采取说服的方法,帮助他们改正错误。只有对犯了严重错误而又不接受指导的人们,才应当采取斗争的方法。在这里,耐心是必要的;轻易地给人们戴上‘机会主义’的大帽子,轻易地采用‘开展斗争’的方法,是不对的。”



你反着来,犯了错误就扣帽子,就人身攻击,就骂人家蠢,“这点事都能搞砸,你干什么吃的。”



“第五,照顾他们的困难。干部有疾病、生活、家庭等项困难问题者,必须在可能限度内用心给以照顾。”



你反着来,生病的只要没有三甲医院医生开的证明,就不能请假,家里有事的,能克服也要克服,不能克服的也要克服,遇到重要的工作,即使孕妇要生孩子,也要她晚几天生,工作重要。


你要这么干下去,你团队不散,你就去法院告我。


本文来源:《毛泽东选集》第二卷文章《中国共产党在民族战争中的地位》(1938年10月14日)


第二位网友的回答


1、开会!


早会,汇报会,进度会,总结会,推进会复盘会,总之不要闲着。


不管什么会,在中午吃饭前,下午下班前开! 晚上回家后再整个线上会议就可着吃饭点,线上会议还要开摄像头,效果满分!


2、做表


统计表,进度表,复盘报告,问题报告,项目总结。


不管什么事,就一句话先做张表给我,要抓重点,也能看出细节,同时手上事情不能停!


效果最好的就是同样的内容用不同的模板来做,还要突出不一样的重点!


3、打官腔


维度,抓手,组合拳,底层逻辑,赛道,载体。


总之就是不说人话! 这个不好总结举个例子


项目管理底层逻辑是打通信息屏障,创建项目新生态,顶层实际是聚焦用户感知赛道,通过差异化和颗粒度达到引爆点,交付价值是在采用复用打法达成持久受益,抽离透传归因分析作为抓手为产品赋能,体验度量作为闭环的评判标准,亮点为载体,优势为链路,思考整个项目生命周期,完善逻辑考虑资源倾斜,是组合拳,最终达到平台标准化


4、吃饼


薪资翻倍,奖金十万,三年买房。


激励政策一发放,人人都打鸡血,月中发政策大家努力来不及。


次月大家拼命干,公司一看卧槽要给这么多奖金?老板签批,同意发放但不是现在。然后就没有然后了


5、突发情况。


项目重大问题,大老板要审查,明天xxx要看项目进度。


别管在干嘛,吃饭?睡觉?OOXX? 也别管几点钟,就一句话,赶紧把ppt做好,明天要。赶紧去客户现场安抚客户。


这种事情越多效果越好。


6、团建


团建这个就有讲究了。


一定要选在节假日,周末随便选一天,三天选中间,五天搞长点,七天去外地。


胡吃海喝肯定达不到效果,所以什么马拉松?爬山?参观什么展览?等等等。 形式不限重点是时间。


比如十一放假,10.2早上出发去XXX,下午布置场地,10.3早上跑个十几二十公里健身,跑完开个会做个动员,下午统一服装去xxx观光。10.4上午大合照,下午回家。


我补充一下


1、制度、制度、制度

别管是什么日常琐事,一率立制度。从工作流程到个人习惯,如用餐和使用洗手间。不是在立制度就是在立制度的路上。


2、Pua

Pua的P要懂的12种不同的写法。做错就扣帽子,就人身攻击,就骂人家蠢;做对就打压,就贬低,就泼冷水。


3、日报、周报、月报、双周报、季度报等等


日报、周报、月报、双周报、季度报等等必不可少,要求写个四五千字的。至于我作为领导看不看报告?那我当然看的啦,你管我啥时候看呢


作者:程序员凌览
来源:juejin.cn/post/7410710728783413299
收起阅读 »

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
收起阅读 »

多人游戏帧同步策略

介绍解决该问题的基本概念和常见解决方案。 Lockstep state update 锁步状态更新 Client prediction 客户端预测 server reconcilation 服务端和解 多人游戏的运作方式 游戏程序的玩家当前状态随时间和玩家...
继续阅读 »

数据库如何快速查询数据-cover.jpeg
介绍解决该问题的基本概念和常见解决方案。



  • Lockstep state update 锁步状态更新

  • Client prediction 客户端预测

  • server reconcilation 服务端和解


多人游戏的运作方式


游戏程序的玩家当前状态随时间和玩家的输入会进行变化。也就是说游戏是有状态的程序。多人游戏也不例外,但由于多人玩家之间存在交互,复杂性会更高。


例如贪吃蛇游戏,我们假设它的操作会发送到服务器,那它的核心游戏逻辑应该是:



  1. 客户端读取用户输入改变蛇的方向,也可以没有输入,然后发送给服务端

  2. 服务端接收消息,根据消息改变蛇的方向,将蛇的“头”移动一个单位空间

  3. 服务端检查蛇是否撞到了墙壁或者自己,如果撞到了游戏结束,给客户端发送响应消息,更新客户端的画面。如果没有撞到,则继续接收客户端发送的消息,同时也要响应给客户端消息,告诉客户端,蛇目前的状态。


服务端接收该消息做出对应的动作,这个过程会以固定的间隔运行。每一次循环都被称为 frame 或 tick。


客户端将解析服务端发送的消息,也就是每一帧的动作,渲染到游戏华中中。


锁步状态更新


为了确保所有客户端都同步帧,最简单的方法是让客户端以固定的间隔向服务器发送更新。发送的消息包含用户的输入,当然也可以发送 no user input。


服务器收集“所有用户”的输入后,就可以生成下一次 frame 帧。



上图演示了客户端与服务端的交互过程。T0 ~ T1 时间段,客户端保持等待,或者说空闲状态,直到服务器响应 frame,等待时间的大小取决于网络质量,约 50 毫秒到 500 毫秒,人眼能够注意到任何超过 100 毫秒的延迟,因此这个等待时间对于某些游戏来说是不可接受的。


锁步状态更新,还有一个问题。游戏的延迟来自最慢的用户



上图有两个客户端。客户端 B 的网络比较差,A 和 B 都在 T0 时间点向服务器发送了用户输入,A 的请求在 T1 到达服务端,B 的请求在 T2 到达服务端,前面我们提到,服务器需要收集“所有用户”的请求后才开始工作,因此需要到 T2 时间点才开始生成 frame。


因为 Client B 比较慢,我们“惩罚”了所有的玩家。


假如我们不等待所有客户端的用户输入,低延迟玩家又会获得优势,因为它的输入到达服务器的时间更短,会更快处理。例如,两个玩家 A、B 同时互相射击预期是同时死亡,但是 A 玩家延迟比 B 玩家更低,因此在处理 B 玩家的用户输入时,A 玩家已经干掉 B 玩家了。


小结一下,锁步状态更新存在的问题,如下。



  • 游戏画面是否卡顿,取决于最慢的玩家

  • 客户端需要等待来自服务器的响应,否则不会渲染画面

  • 连接非常活跃,客户端需要定期发送一些无用的心跳包,以便服务器可以确定它拥有生成 frame 所需的所有信息


回合制类型的游戏大多数使用这种方法,因为玩家确实需要等待,例如《炉石传说》。


对于慢节奏的游戏,少量延迟也是可以接受的,例如《QQ农场》。


但是对于快节奏的游戏,锁步状态更新的这些问题都是致命的,不可能操纵游戏人物进入某一个建筑,500 毫秒后,我才能进入。我们一起来看看下一种方法。


客户端预测


客户端预测,在玩家的计算机上,运行游戏逻辑,来模拟游戏的行为,而不是等待服务器更新。


例如我们生成 Tn 时间点的游戏状态,我们需要 Tn-1 时间点的所有玩家状态和 Tn-1 时间点所有玩家的输入。


假设,我们现在的固定频率为 1 s,每 1s 需要给服务器发送一个请求,获取玩家状态并更新玩家的状态。



在 T0 时间点,客户端将用户的输入发送到服务器,用于获取 T1 时间点的游戏状态。在 T1 时间点,客户端已经可以渲染画面了,实际上客户端的响应是在 T3 时刻,也就是说客户端没有等待来自服务器的响应。


使用这个方法,需要满足一些前置条件:



  • 客户端拥有游戏运行逻辑所需的所有条件

  • 玩家状态的更新逻辑是确定性的,即没有随机性,或者可以以某种方式保证确定性,例如客户端和服务器使用同样的公式以及随机种子,可以保证具有随机性的同时,产生的结果具有确定性。这样保证了客户端和服务器在给定相同输入的情况下产生相同的游戏状态


满足这两点,客户端预测的结果也不一定总是对的。就比如刚提到的,使用相同的公式以及相同的随机种子,进行伪随机算法,但不同平台的浮点计算,可能会存在微小的差异。


再设想一个场景,如下图。



客户端 A 尝试使用 T0 时间点的信息模拟 T1 时间点上的游戏状态,但客户端 B 也在 T0 时间点提交了用户输入,客户端 A 并不知道这个用户输入。


这意味着客户端 A 对 T1 时间的预测将是错误的是,但!由于客户端 A 仍然从服务器接收 T1 时间点的状态,因此客户端有机会在 T3 时间点修正错误。


客户端需要知道,自己的预测是否正确,以及如何修正错误。


修正错误通常叫做 Reconcilation 和解。


需要根据上下文来实现和解部分,下面我们通过一个简单的例子来理解这个概念。这个例子只是抛弃我们的预测,并将其游戏状态替换为服务器响应的正确状态。



  • 客户端需要维护 2 个缓冲区,一个用于预测 PredictionBuffer,一个用于用户输入 InputBuffer 。它们是预测这个行为需要的上下文,请记住,预测 Tn 时刻,需要 Tn-1 的状态和 Tn-1 时刻的用户输入。它们一开始都为空



  • 玩家点击鼠标,移动游戏角色到下一个位置。此时,玩家输入的移动信息 Input 0 存储在 InputBuffer 中,客户端将生成预测 Prediction 1,存储在 PredictionBuffer 中,预测将展示在玩家画面中



  • 客户端收到服务器响应的 State0 ,发现与客户端的预测不匹配,我们将Prediction 1 替换为 State 0,并使用 Input 0 和 State 0 重新计算,得到 Prediction 2,这个重新计算的过程,就是 Reconcilation 和解



  • 和解后,我们从缓冲区中删除 State 0 和 Input 0




这种和解的方式有一个明显的缺点,如果服务器响应的游戏状态和客户端预测差异太大,则游戏画面可能会出现错误。例如我们预测敌人在 T0 时间点向南移动,但在 T3 时间点,我们意识到它在向北移动,然后通过使用服务器的响应进行和解,敌人将从北“飞到”正确的位置。


有一些方法可以解决此问题,这里不展开讨论,感兴趣可以搜一下实体插值 Entity Interpolation。


小结一下,客户端预测技术,让客户端以自己的更新频率运行,与服务器的更新频率无关,所以服务器如果出现阻塞,不会影响客户端的帧。


但它也带来复杂性,如下。



  • 需要在客户端处理更多的状态和逻辑,比如我们前面提到的缓冲区和预测逻辑

  • 需要和解来自服务器的状态(正确的游戏状态)与预测之前的冲突


还给我们带来了敌人从南飞到北的问题。


目前为止,我们都在讨论客户端,接下来看看服务端如何解决帧同步。


服务端和解


利用服务端解决帧同步问题,首先需要解决的是网络延迟带来的问题。如下图。



用户 A 在 T 处进行了操作(比如按下了一个技能键),该操作应该在 T+20ms 处理,但由于延迟,服务器在 T+120ms 才接收到输入。


在游戏中,用户做出指定操作后,应该立即有反应。立即有反应,这个立即是多久,取决于游戏的类型,比如之前我们提到的回合制,它的立即可能是几十秒。我们可以通过 T + X,表示立即反应的时间,T 代表用户的输入时刻,X 代表的是延迟。X 可以为 0,这代表真正的立即 :-)


解决这个问题的思路,与之前客户端预测中使用的办法类似,就是通过客户端的用户输入,来和解服务器中的玩家游戏状态。


所有的用户输入,都需要时间戳进行标记,该时间戳用于告诉服务器,什么时刻处理此用户输入。



为什么在同一水平线上,Client A 的时间是 Time X,而 Server 的时间是 Time Y?

因为客户端和服务端独立运行,通常时间会有所不同,在多人游戏中,我们可以特殊处理其中的差异。在特殊处理时,我们应该使客户端的时间大于服务端的时间,因为这样可以存在更大的灵活性


上图演示了一个客户端与服务端之间的交互。



  1. 客户端发送带有时间戳的输入。客户端告诉服务器在 X 时间点应该发生用户输入的效果

  2. 服务端在 Y 时间点收到请求

  3. 在 Y+1 时间点,即红色框的地方,服务端开始和解,服务端将 X 时间点的用户输入应用于最新的游戏状态,以保证 X 的 Input 发生在 X 时间点

  4. 服务端发送响应,该响应中包含时间戳


服务端和解部分(上图红色底色部分),主要维护 3 个部分,如下。



  • GameStateHistory,在一定时间范围内玩家在游戏中的状态

  • ProcessedUserInput,在一定时间范围内处理的用户输入的历史记录

  • UnprocessedUserInput,已收到但未处理的用户输入,也是在一定的时间内


服务端和解过程,如下。



  1. 当服务端收到来自用户的输入时,首先将其放入 UnprocessedUserInput 中

  2. 等待服务端开始同步帧,检查 UnprocessedUserInput 中是否存在任何早于当前帧的用户输入

  3. 如果没有,只需要将最新的 GameState 更新为当前用户的输入,并执行游戏逻辑,然后广播到客户端

  4. 如果有,则表示之前生成的某些游戏状态由于缺少部分用户输入而出错,需要和解,也就是更正。首先需要找到最早的,未处理的用户输入,假设它在时间 N 上,我们需要从 GameStateHistory 中获取时间 N 对应的 GameState 以及从 ProcessedUserInput 获取时间 N 上用户的输入

  5. 使用这 3 条数据,就可以创建一个准确的游戏状态,然后将未处理的输入 N 移动到 ProcessingUserInput,用于之后的和解

  6. 更新 GameStateHistory 中的游戏状态

  7. 重复步骤 4 ~ 6,直到从 N 的时间点到最新的游戏状态

  8. 服务端将最新帧广播给所有玩家


我并没有做过这些工作,分享的知识都是我对它感兴趣,在网上看了许多经验后整理的。


作者:龚国玮
来源:juejin.cn/post/7277489569958821900
收起阅读 »

我用这10招,能减少了80%的BUG

前言 对于大部分程序员来说,主要的工作时间是在开发和修复BUG。 有可能修改了一个BUG,会导致几个新BUG的产生,不断循环。 那么,有没有办法能够减少BUG,保证代码质量,提升工作效率? 答案是肯定的。 如果能做到,我们多出来的时间,多摸点鱼,做点自己喜欢的...
继续阅读 »

前言


对于大部分程序员来说,主要的工作时间是在开发和修复BUG。


有可能修改了一个BUG,会导致几个新BUG的产生,不断循环。


那么,有没有办法能够减少BUG,保证代码质量,提升工作效率?


答案是肯定的。


如果能做到,我们多出来的时间,多摸点鱼,做点自己喜欢的事情,不香吗?


这篇文章跟大家一起聊聊减少代码BUG的10个小技巧,希望对你会有所帮助。



1 找个好用的开发工具


在日常工作中,找一款好用的开发工具,对于开发人员来说非常重要。


不光可以提升开发效率,更重要的是它可以帮助我们减少BUG。


有些好的开发工具,比如:idea中,对于包没有引入,会在相关的类上面标红



并且idea还有自动补全的功能,可以有效减少我们在日常开发的过程中,有些单词手动输入的时候敲错的情况发生。


2 引入Findbugs插件


Findbugs是一款Java静态代码分析工具,它专注于寻找真正的缺陷或者潜在的性能问题,它可以帮助java工程师提高代码质量以及排除隐含的缺陷。


Findbugs运用Apache BCEL 库分析类文件,而不是源代码,将字节码与一组缺陷模式进行对比以发现可能的问题。


可以直接在idea中安装FindBugs插件:



之后可以选择分析哪些代码:



分析结果:



点击对应的问题项,可以找到具体的代码行,进行修复。



Findbugs的检测器已增至300多条,被分为不同的类型,常见的类型如下:



  • Correctness:这种归类下的问题在某种情况下会导致bug,比如错误的强制类型转换等。

  • Bad practice:这种类别下的代码违反了公认的最佳实践标准,比如某个类实现了equals方法但未实现hashCode方法等。

  • Multithreaded correctness:关注于同步和多线程问题。

  • Performance:潜在的性能问题。

  • Security:安全相关。

  • Dodgy:Findbugs团队认为该类型下的问题代码导致bug的可能性很高。


3 引入CheckStyle插件


CheckStyle作为检验代码规范的插件,除了可以使用配置默认给定的开发规范,如Sun、Google的开发规范之外,还可以使用像阿里的开发规范的插件。


目前国内用的比较多的是阿里的代码开发规范,我们可以直接通过idea下载插件:



如果想检测某个文件:



可以看到结果:



阿里巴巴规约扫描包括:



  1. OOP规约

  2. 并发处理

  3. 控制语句

  4. 命名规约

  5. 常量定义

  6. 注释规范


Alibaba Java Coding Guidelines 专注于Java代码规范,目的是让开发者更加方便、快速规范代码格式。


该插件在扫描代码后,将不符合规约的代码按 Blocker、Critical、Major 三个等级显示出来,并且大部分可以自动修复。


它还基于Inspection机制提供了实时检测功能,编写代码的同时也能快速发现问题。


4 用SonarQube扫描代码


SonarQube是一种自动代码审查工具,用于检测代码中的错误,漏洞和代码格式上的问题。


它可以与用户现有的工作流程集成,以实现跨项目分支和提取请求的连续代码检查,同时也提供了可视化的管理页面,用于查看检测出的结果。


SonarQube通过配置的代码分析规则,从可靠性、安全性、可维护性、覆盖率、重复率等方面分析项目,风险等级从A~E划分为5个等级;


同时,SonarQube可以集成pmd、findbugs、checkstyle等插件来扩展使用其他规则来检验代码质量。



一般推荐它跟Jenkins集成,做成每天定时扫描项目中test分支中的代码问题。


5 用Fortify扫描代码


Fortify 是一款广泛使用的静态应用程序安全测试(SAST)工具。


它具有代码扫描、漏斗扫描和渗透测试等功能。它的设计目的是有效地检测和定位源代码中的漏洞。


它能帮助开发人员识别和修复代码中的安全漏洞。


Fortify的主要功能:



  • 静态代码分析:它会对源代码进行静态分析,找出可能导致安全漏洞的代码片段。它能识别多种类型的安全漏洞,如 SQL 注入、跨站脚本(XSS)、缓冲区溢出等。

  • 数据流分析:它不仅分析单个代码文件,还跟踪应用程序的数据流。这有助于找到更复杂的漏洞,如未经验证的用户输入在应用程序中的传播路径。

  • 漏洞修复建议:发现潜在的安全漏洞时,它会为开发人员提供修复建议。

  • 集成支持:它可以与多种持续集成(CI)工具(如 Jenkins)和应用生命周期管理(ALM)工具(如 Jira)集成,实现自动化的代码扫描和漏洞跟踪。

  • 报告和度量:它提供了丰富的报告功能,帮助团队了解项目的安全状况和漏洞趋势。



使用Fortify扫描代码的结果:



一般推荐它跟Jenkins集成,定期扫描项目中test分支中的代码安全问题。


6 写单元测试


有些小伙伴可能会问:写单元测试可以减少代码的BUG?


答案是肯定的。


我之前有同事,使用的测试驱动开发模式,开发一个功能模块之前,先把单元测试写好,然后再真正的开发业务代码。


后面发现他写的代码速度很快,而且代码质量很高,是一个开发牛人。


如果你后期要做系统的代码重构,你只是重写了相关的业务代码,但业务逻辑并没有修改。


这时,因为有了之前写好的单位测试,你会发现测试起来非常方便。


可以帮你减少很多BUG。


7 功能自测


功能自测,是程序员的基本要求。


但有些程序员自测之后,BUG还是比较多,而有些程序员自测之后,BUG非常少,这是什么原因呢?


可能有些人比较粗心,有些人比较细心。


其实更重要的是测试的策略。


有些人喜欢把所有相关的功能都开发完,然后一起测试。


这种情况下,相当于一个黑盒测试,需要花费大量的时间,梳理业务逻辑才能测试完整,大部分情况下,开发人员是没法测试完整的,可能会有很多bug测试不出来。


这种做法是没有经过单元测试,直接进行了集成测试。


看似节省了很多单元测试的时间,但其实后面修复BUG的时间可能会花费更多。


比较推荐的自测方式是:一步一个脚印。


比如:你写了一个工具类的一个方法,就测试一下。如果这个方法中,调用了另外一个关键方法,我们可以先测试一下这个关键方法。


这样可以写出BUG更少的代码。


最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。


8 自动化测试


有些公司引入了自动化测试的功能。


每天都会自动测试,保证系统的核心流程没有问题。


因为我们的日常开发中,经常需要调整核心流程的代码。


不可能每调整一次,都需要把所有的核心流程都测试一遍吧,这样会浪费大量的时间,而且也容易遗漏一些细节。


如果引入了自动化测试的功能,可以帮助我们把核心流程都测试一下。


避免代码重构,或者修改核心流程,测试时间不够,或者测试不完全的尴尬。


自动化测试,可以有效的减少核心流程调整,或者代码重构中的BUG。


9 代码review


很多公司都有代码review机制。


我之前也参与多次代码review的会议,发现代码review确实可以找出很多BUG。


比如:一些代码的逻辑错误,语法的问题,不规范的命名等。


这样问题通过组内的代码review一般可以检查出来。


有些国外的大厂,采用结对编程的模式。


同一个组的两个人A和B一起开发,开发完之后,A reivew B的代码,同时B review A的代码。


因为同组的A和B对项目比较熟,对对方开发的功能更有了解,可以快速找出对外代码中的一些问题。


能够有效减少一些BUG。


10 多看别人的踩坑分享


如果你想减少日常工作中的代码BUG,或者线上事故,少犯错,少踩坑。


经常看别人真实的踩坑分享,是一个非常不错的选择,可以学到一些别人的工作经验,帮助你少走很多弯路。


最后说一句,本文总结了10种减少代码BUG的小技巧,但我们要根据实际情况选择使用,并非所有的场景都适合。


作者:卷积殉铁子
来源:juejin.cn/post/7359083483237859367
收起阅读 »

如何让你喜欢的原神角色陪你写代码

如何让你喜欢的原神角色陪你写代码 每天上班,脑子里面就想着一件事,原神,啊不对,VsCode!启动!(doge 狗头保命),那么如何将这两件事情结合起来呢?特别是原神里面有那么多我喜欢的角色。 最终效果预览 右下角固定一只小可爱 全屏一只小可爱 ...
继续阅读 »

如何让你喜欢的原神角色陪你写代码



每天上班,脑子里面就想着一件事,原神,啊不对,VsCode!启动!(doge 狗头保命),那么如何将这两件事情结合起来呢?特别是原神里面有那么多我喜欢的角色。



最终效果预览



  • 右下角固定一只小可爱



    • image-2.png



  • 全屏一只小可爱



    • image-3.png




省流直接抄作业版



  1. vscode 下载 background 插件



    • image.png

    • 第一次使用这个插件会提示 vscode 已损坏让你 restart vscode,不要慌,因为插件底层是改 vscode 的样式

    • 需要管理员权限,如果你安装到了 C 盘



  2. copy 下面的配置到你的 settings.json 文件


    2.1 右下角固定图片配置



{
"background.customImages": [
"https://upload-bbs.miyoushe.com/upload/2023/02/24/196231062/8540249f2c0dd72f34c8236925ef45bc_3880538836005347690.png",
"https://user-images.githubusercontent.com/41776735/163673800-0e575aa3-afab-405b-a833-212474a51adc.png"
],
"background.enabled": true,
"background.style": {
"content": "''",
"width": "30%",
"height": "30%",
"opacity": 0.3,
"position": "absolute",
"bottom": "0",
"right": "7%",
"z-index": "99999",
"background-repeat": "no-repeat",
"background-size": "contain",
"pointer-events": "none"
},
"background.useDefault": false,
"background.useFront": true
}

2.2 全屏图片配置


{
"background.customImages": ["https://upload-bbs.miyoushe.com/upload/2024/01/15/196231062/1145c3a2f56b2f789a9be086b546305d_3972870625465222006.png"],
"background.enabled": true,
"background.style": {
"content": "''",
"pointer-events": "none",
"position": "absolute",
"z-index": "99999",
"height": "100%",
"width": "100%",
"background-repeat": "no-repeat",
"background-size": "cover",
"background-position": "content",
"opacity": 0.3
},
"background.useDefault": false,
"background.useFront": true
}

详细说明版


第一步,你还是需要下载 background 这个插件。



第二步,配置解析


我们看官方文档里面,有两个配置是需要说明一下的



  • background.customImages:



    • 是一个数组,默认是用第一张图片做背景图,在分屏的时候第二屏就用的是数组的第二个元素,以此类推

    • 支持 https 协议和本地的 file 协议

      • 因为我有公司机器和自己电脑,所以本地协议不太适用我,但是我之前一直用的是本地协议,并且下面会教大家如何白嫖图床





  • background.style:



    • 作为一个前端,看到上面的内容是不是十分的熟悉,其实这个插件的底层原理就是去改 vscode 的 css,所以你可以想想成一个需求,在一个界面上显示一张图片,css 可以怎么写,背景图就可以是啥样的

    • 如果你看不懂 css 代码,你可以直接复制我上面提供的两种配置,一个全屏的,一个是在右下角固定一小只的



  • 如果你觉得背景图太亮了,有点看不清代码,你可以修改 background.style.opacity这个属性,降低图片的透明度。

  • 发现最新版本支持 background.interval 来轮播背景图了


高清社死图片


OK,其实到这就没啥了,但是到这里和原神好像并没有什么太大的关系,那现在我就教你怎么白嫖米游社的图床,并且获取高清的原神图片



  • 第一步,你需要有一个米游社的账号,当然,玩原神的不能没有吧

  • 第二步,随便找个帖子进行评论回复,如果你怕麻烦别人,可以自己发一贴



    • image-1.png



  • 第三步:新标签页打开



    • image-4.png



  • 到这里,你就能白嫖米游社的图床了,如果你是评论的自己的图片,新标签页打开之后,删除 url 中 ? 后面所有的内容,就可以得到原图 https 的链接了

  • 但是又遇到一个问题,我就是很喜欢米游社的图片,但是图片的分辨率太低了怎么办?



    • 提供两个用 ai 来放大图片的网站

    • waifu2x.udp.jp/

    • bigjpg.com/

    • 把图片放大之后,在用上面白嫖图床的方法就可以了




最后


水一期,求三连 + 可以在评论区分享你的背景图嘛


作者:既见君子
来源:juejin.cn/post/7324143986759303194
收起阅读 »

NestJs: 定时任务+redis实现阅读量功能

抛个砖头 不知道大家用了这么久的掘金,有没有对它文章中的阅读量的实现有过好奇呢? 想象一下,你每次打开一篇文章时,都会有一个数字告诉你多少人已经读过这篇文章。那么这个数字是怎么得出来的呢? 有些人可能会认为,每次有人打开文章,数字就加1,不是很简单吗? ...
继续阅读 »

抛个砖头


不知道大家用了这么久的掘金,有没有对它文章中的阅读量的实现有过好奇呢?


image.png



想象一下,你每次打开一篇文章时,都会有一个数字告诉你多少人已经读过这篇文章。那么这个数字是怎么得出来的呢?



有些人可能会认为,每次有人打开文章,数字就加1,不是很简单吗? 起初我也以为这样,哈哈(和大家站在同一个高度),但(好了,我不说了,继续往下看吧!


引个玉


文章阅读量的统计看似简单,实则蕴含着巧妙的逻辑和高效的技术实现。我们想要得到的阅读量,并非简单的页面刷新次数,而是真正独立阅读过文章的人数。因此,传统的每次页面刷新加1的方法显然不够准确,它忽略了用户的重复访问。


同时,阅读作为一个高频操作,如果每次都直接写入数据库,无疑会给数据库带来巨大的压力,甚至可能影响到整个系统的性能和稳定性。这就需要我们寻找一种既能准确统计阅读量,又能减轻数据库压力的方法。


Redis,这个高性能的内存数据库,为我们提供了解决方案。我们可以利用Redis的键值对存储特性,将用户ID和文章ID的组合作为键,设置一个短暂的过期时间,比如15分钟。当用户首次访问文章时,我们在Redis中为这个键设置一个值,表示该用户已经阅读过这篇文章。如果用户在15分钟内再次访问,我们可以直接判断该键是否存在,如果存在,则不再增加阅读量,否则进行增加。


这种方法的优点在于,它能够准确地统计出真正阅读过文章的人数,而不是简单的页面刷新次数。同时,通过将阅读量先存储在Redis中,我们避免了频繁地写入数据库,从而大大减轻了数据库的压力。


最后,我们还需要考虑如何将Redis中的阅读量最终写入数据库。由于数据库的写入操作相对较重,我们不宜频繁进行。因此,我们可以选择在业务低峰期,比如凌晨2到4点,使用定时任务将Redis中的阅读量批量写入数据库。这样,既保证了阅读量的准确统计,又避免了频繁的数据库写入操作,实现了高效的系统运行。


思路梳理



  1. 😎Redis 助力阅读量统计,方法超好用!✨

  2. 🧐在 Redis 存用户和文章关系,轻松解决多次无效阅读!👏

  3. 💪定时任务来帮忙,Redis 数据写入数据库,不再
    那么接下来就是实现环节


代码层面


项目使用的后端框架为NestJS


配置下redis


一、安装redis plugin


npm install --save redis


二、创建redis模块


image.png


三、初始化连接redis相关配置


@Module({
providers: [
RedisService,
{
provide: 'REDIS_CLIENT',
async useFactory(configService: ConfigService) {
console.log(configService.get('redis_server_host'));
const client = createClient({
socket: {
host: configService.get('redis_server_host'),
port: configService.get('redis_server_port'),
},
database: configService.get('redis_server_db'),
});
await client.connect();
return client;
},
inject: [ConfigService],
},
],
exports: [RedisService],
})

Redis是一个Key-Value型数据库,可以用作数据库,所有的数据以Key-Value的形式存在服务器的内存中,其中Value可以是多种数据结构,如字符串(String)、哈希(hashes)、列表(list)、集合(sets)和有序集合(sorted sets)等类型


在这里会用到字符串和哈希两种。


创建文章表和用户表


我的项目中创建有post.entity和user.entity这两个实体表,并为post文章表添加以下


image.png
这三个字段,在这里我们只拿 阅读量 说事。


访问文章详情接口-阅读量+1


  /**
* @description 增加阅读量
* @param id
* @returns
*/

@Get('xxx/:id')
@RequireLogin()
async frontIncreViews(@Param('id') id: string, @Req() _req: any,) {
console.log('frontFindOne');
return await this.postService.frontIncreViews(+id, _req?.user);
}

前文已经说过,同一个用户多次刷新,如果不做处理,就会产生多次无效的阅读量。 因此,为了避免这种情况方式,我们需要为其增加一个用户文章id组合而成的标记,并设置在有效时间内不产生多次阅读量。



那么,有的掘友可能会产生一个疑问,如果用户未登录,那么以游客的身份去访问文章就不产生阅读记录了吗?



其实同理!


在我的项目中只是,要求需要用户登录后才能访问,
那么我这就会以 userID_postID_ 来组成标识区分用户和文章罢了。
而如果想以游客身份,我们可以获取用户 IP_postID 这样的组合来做标识即可


接下来说下postService中调用的frontIncreViews方法
直接贴代码:


 const res = await this.redisService.hashGet(`post_${id}`);

if (res.viewCount === undefined) {
const post = await this.postRepository.findOne({ where: { id } });

post.viewCount++;

await this.postRepository.update(id, { viewCount: post.viewCount });

await this.redisService.hashSet(`post_${id}`, {
viewCount: post.viewCount,
likeCount: post.likeCount,
collectCount: post.collectCount,
});
// 在用户访问文章的时候在 redis 存一个 10 分钟过期的标记,有这个标记的时候阅读量不增加
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);

return post.viewCount;
} else {
const flag = await this.redisService.get(`user_${user.id}_post_${id}`);
console.log(flag);
if (flag) {
return res.viewCount;
}

await this.redisService.hashSet(`post_${id}`, {
...res,
viewCount: +res.viewCount + 1,
});

await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
return +res.viewCount + 1;
}
}


  1. 从Redis获取文章阅读量:


const res = await this.redisService.hashGet(`post_${id}`);

使用Redis的哈希表结构,从键post_${id}中获取文章的信息,其中可能包含阅读量(viewCount)、点赞数(likeCount)和收藏数(collectCount)。

2. 检查Redis中是否存在阅读量:


if (res.viewCount === undefined) {

如果Redis中没有阅读量数据,说明这篇文章的阅读量还没有被初始化。

3. 从数据库中获取文章并增加阅读量:


const post = await this.postRepository.findOne({ where: { id } });
post.viewCount++;
await this.postRepository.update(id, { viewCount: post.viewCount });

从数据库中获取文章,然后增加阅读量,并更新数据库中的文章阅读量。

4. 将更新后的文章信息存回Redis:


await this.redisService.hashSet(`post_${id}`, {
viewCount: post.viewCount,
likeCount: post.likeCount,
collectCount: post.collectCount,
});

将更新后的文章信息(包括新的阅读量、点赞数和收藏数)存回Redis的哈希表中。

5. 设置用户访问标记:


await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);

在用户访问文章时,在Redis中设置一个带有10分钟过期时间的标记,用于防止在10分钟内重复增加阅读量。

6. 返回阅读量:


return post.viewCount;

返回更新后的阅读量。

7. 如果Redis中存在阅读量:


} else {
const flag = await this.redisService.get(`user_${user.id}_post_${id}`);
console.log(flag);

如果Redis中存在阅读量数据,则检查用户是否已经访问过该文章。

8. 检查用户访问标记:


if (flag) {
return res.viewCount;
}

如果用户已经访问过该文章(标记存在),则直接返回当前阅读量,不增加。

9. 如果用户未访问过文章:


await this.redisService.hashSet(`post_${id}`, {
...res,
viewCount: +res.viewCount + 1,
});
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
return +res.viewCount + 1;

如果用户未访问过该文章,则增加阅读量,并重新设置用户访问标记。然后返回更新后的阅读量。


简而言之,目的是在用户访问文章时,确保文章阅读量只增加一次,即使用户在短时间内多次访问。


NestJS使用定时任务包,实现redis数据同步到数据库中


有的掘友可能疑问,既然已经用redis来做阅读量记录了,为什么还要同步到数据库中,前文开始的时候,就已经提到过了,一旦我们的项目重启, redis 数据就没了,而数据库却有着“数据持久性的优良品质”。不像redis重启后,又是个新生儿。但是它们的互补,又是1+1大于2的那种。


好了,不废话了


一、引入定时任务包 @nestjs/schedule


npm install --save @nestjs/schedule

app.module.ts 引入


image.png


二、创建定时任务模块和服务


nest g module task 
nest g service task

image.png


你可以在同一个服务里面声明多个定时任务方法。在 NestJS 中,使用 @nestjs/schedule 库时,你只需要在服务类中为每个定时任务方法添加 @Cron() 装饰器,并指定相应的 cron 表达式。以下是一个示例,展示了如何在同一个服务中声明两个定时任务:


import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class TasksService {
// 第一个定时任务,每5秒执行一次
@Cron(CronExpression.EVERY_5_SECONDS)
handleEvery5Seconds() {
console.log('Every 5 seconds task executed');
}

// 第二个定时任务,每10秒执行一次
@Cron(CronExpression.EVERY_10_SECONDS)
handleEvery10Seconds() {
console.log('Every 10 seconds task executed');
}
}

三、实现定时任务中同步文章阅读量的任务


更新文章的阅读数据


await this.postService.flushRedisToDB();


  // 查询出 key 对应的值,更新到数据库。 做定时任务的时候加上
async flushRedisToDB() {
const keys = await this.redisService.keys(`post_*`);
console.log(keys);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];

const res = await this.redisService.hashGet(key);

const [, id] = key.split('_');

await this.postRepository.update(
{
id: +id,
},
{
viewCount: +res.viewCount,
},
);
}
}


  1. 从 Redis 获取键


    const keys = await this.redisService.keys(post_*);: 使用 Redis 服务的 keys 方法查询所有以 post_ 开头的键,并将这些键存储在 keys 数组中。
    console.log(keys);: 打印出所有查询到的键。


  2. 遍历 Redis 键


    使用 for 循环遍历所有查询到的键。


  3. 从 Redis 获取哈希值


    const res = await this.redisService.hashGet(key);: 对于每一个键,使用 Redis 服务的 hashGet 方法获取其对应的哈希值,并将结果存储在 res 中。


  4. 解析键以获取 ID


    const [, id] = key.split('_');: 将键字符串按照 _ 分割,并取出第二个元素(索引为 1)作为 id。这假设键的格式是 post_<id>


  5. 更新数据库


    使用 postRepository.update 方法更新数据库中的记录。
    { id: +id, }: 指定要更新的记录的 id+id 是将 id 字符串转换为数字。
    { viewCount: +res.viewCount, }: 指定要更新的字段及其值。这里将 viewCount 字段更新为 Redis 中存储的值,并使用 +res.viewCount 将字符串转换为数字。



等到第二天,哈,数据就同步来了


访问:


gh_db79ec2f6f73_860.jpg

而产生的后台数据:


image.png


抛出问题


如果能看到这里的掘友,若能接下这个问题,说明你已经掌握了吖


问题1:


如何实现一个批量返回redis键值对的方法(这个方法问题2需要用到)


问题2:


用户查询文章列表的时候,如何整理数据后返回文章阅读量呈现给用户查看


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

骚操作:如何让一个网页一直处于空白情况?

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
收起阅读 »

了解这四个心理陷阱,让你摆脱心理上的“贫穷”

背景 Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。 我发现自己的经济情况正在处于一个紧张的状态。上个月的信用卡需要还款8600块钱,仔细看了下账单,分期支付的却有3000多,占比将近4成,这很可怕。 明明我已经每个月都尽量的省吃俭用,克制消费...
继续阅读 »

背景


Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。


我发现自己的经济情况正在处于一个紧张的状态。上个月的信用卡需要还款8600块钱,仔细看了下账单,分期支付的却有3000多,占比将近4成,这很可怕。


image.png


明明我已经每个月都尽量的省吃俭用,克制消费,但看到月底还款金额仍然居高不下,便会不由自主的有一些焦虑的情绪,甚至影响到日常的消费。


比如消耗更多精力去克制自己买想要东西的欲望,消费时去对比不同渠道的价格,变得对价格更加敏感。


我意识到我陷入到了“稀缺”的情绪中,并且这个情绪总是出现,并且影响着我做事的思路和方式。我想摆脱它,因为我们有很多重要的事情等着我去做,比如去看书学习更多的东西,又或者持续输出写下一篇篇文章。


上周看了《稀缺》这本书,罗振宇评价说:“我们的肉身刚步入富足的时代,但我们的精神还滞留在稀缺的恐惧之中。穷人思维,根植于人类的基因。率先用理性击碎它的人,也将率先获得心与灵的富足。”


我已经陷入了金钱稀缺的状态,这是是因为之前的各类分期导致的。那么稀缺是什么,它是如何影响我们的,又该如何摆脱它?


看完这篇文章,相信你会有答案。


稀缺


什么是稀缺


先说第一点,什么是稀缺?稀缺是长期缺乏,而导致的一种稀缺心态,是“拥有”少与“需要”的感觉。



  • 我们觉着积蓄太少,我们需要更好的房、车,永远都有还不完的账单

  • 我们觉着时间太少,我们需要时间旅游、健身、陪伴家人

  • 我们觉着精力太少,我们需要关注工作、关注个人成长 、关注国家大事


举个例子,老板给你安排了一项周五要完成的任务,然而到了周四还没有做完,这时候你会变的心急如焚,这是你就会全神贯注的地开始工作,相比于其他事情,你会优先处理这项紧急任务,只关注手头的工作,这就是进入到了一种稀缺状态。


幸运的是,稀缺带给你了专注红利,让你你在老板问你进度之前,完成了工作并顺利交差,你长舒一口气。就像开学前最后一天写完了暑假作业,像毕业设计答辩前终于写完了论文一般,这种场景似乎已经经历了很多次。



专注红利:就是短时间内集中精力爆发出高度的注意力,让我们高产出地工作,我们会在专注红利的帮助下把剩下的资源用得淋漓尽致。



话又说回来,虽然这样的专注在短时间内能够带来好处,但如果长时间处于稀缺心态中,并不是什么好事。它会把一个人拖向“贫穷”,进入一个匮乏的恶性循环。要理解这个循环,我们需要看看稀缺会对我们产生什么样的影响。


管窥


第一种叫做管窥效应。管窥就好像你通过一根管子看东西,这时你只能看到管子里面的东西,而管子外面的什么都看不见。中国有句成语:一叶障目,不见泰山。


管窥效应会改变我们的决策方式,举个例子,程序员日常的工作很忙碌,不但要写代码、改bug,还要参加需求评审的各种各样的会议。我们平常都会在工作疲惫的时候,选择站起来走做,接杯水,活动一下身体。但是在忙碌的时候,就会觉着喝水、 散步也没有这么重要了,赶紧把手头的工作做完吧,半天不喝水,一坐一整天似乎也没什么关系。(你多久没有起来活动活动身体了?)


我们虽然都知道,久坐在电脑前,对颈椎、眼睛的伤害很大,工作一个小时我们至少要起来活动五分钟,眼睛远眺一下。长远来看,你很明显知道对身体的投资是重要的,但是你在稀缺状态下,你就会做出损害长期价值的决定。


这是一段我自己很真实的经历,在上家公司时我接到了一个比较大的项目,面临新入职和换语言的情况,项目的排期显得尤其紧张,那段时间我几乎白天不怎么站起来活动,即使是在眼睛干涩的情况下,我也不得不写下一行行代码,改一个个难以排查的bug。


我的眼睛很早之前做过激光手术,其实并不适合一直盯着电脑工作。但那段时间在高压的工作环境下,最终项目上线了,却对眼睛造成了很大的伤害。


借用


再说稀缺导致的第二种影响,是借用。借用非常好理解,就是习惯性地透支未来的资源。


比如说,开头提到的我的情况,信用卡。我通过信用卡分期来解决之前遇到的现金问题,但造成影响却是长期的。每次收到工资,我就需要去还房贷、信用卡等一系列的债务。


这时候,如果我的一个同学或者朋友需要结婚,我需要给他们包一个大红包,那这个月就会更加紧张。而且银行不当人,每个月在账单出来之后,都会打电话问是否需要办理分期。


我们说到的借用,其实不止是钱。忙碌的时候,我们忙碌时也会对时间做借用。比如说,这周的工作没做完,我们就放到下一周。但下一周会有下一周的工作,所以我们会长期处于一种稀缺状态里。


再比如说,白天繁忙的工作让我们缺少娱乐时间,这时候我们就会选择熬夜。但熬夜会导致第二天的工作效率更低,结果我们的工作时间可能会更长。


没有余闲


稀缺导致的第三个影响是没有余闲,余闲就是我们剩余下来的,没有利用的时间和空间。


举一个例子,之前在外地工作的时候,假期结束前,总会带不少东西回北京,临走之前,我会把所有需要装的东西放进去,比如鞋、衣服、数码装备,甚至是一些吃吃喝喝。如果我这次用一个大的箱子,那么装完必须的用品之后,我们可能还会发现有很多剩余的空间。我们就可以放进去一些不那么需要的东西,比如珍藏的几本小说,甚至是一些下个季度才需要穿的衣物。在这个过程中,我们的心情会非常舒畅,有极大的”富裕感“。


但是,如果我们只有一个背包,那就不能像刚刚那样舒服了。我们需要开始权衡和比较:鞋子到底带哪一双?换洗的衣服要带多少?游戏机和书本要不要带呢?为了保证空间的使用,我们甚至需要我妈把里面的东西全部拿出来,通过分门别类的方式再塞回去。这样反反复复地收拾几次,直到包变的满满当当。


也许你会说,这样是不是更高效呀?毕竟我们用一个并不大的背包装完了所有需要的行李。从某种角度上来说是这样的,但是在这个过程中,我耗费了大量的精力和时间,去权衡一些无关紧要的东西。这种思维看起来很高效,但会让人产生大量的心智负担。这些心智负担会消耗注意力和精力,从而进一步产生管窥效应,只让你注重当前的事情,而忽略真正重要的事情。


带宽不足


稀缺导致的第四个影响,也是我认为最重要的一个点,那就是带宽不足。这里的带宽,指的是我们的认知能力和执行控制力。


我们从处理日常问题到思考问题都需要带宽,但是我们一般最多只能关注七件事。比如日常使用的APP不会超过七个,经常交往的联系人也不会超过七个。“七”通常是人类认知所能承受的一个临界点。超过这个数字,一个人就会产生严重的带宽负担,这个时候就会感觉精力不够用。


如果说在物质不够丰富的时代,养家糊口让人的带宽降低。可目前科技、社会发展这么快,我们这一代年轻人,需要考虑的事情也变得越来越多,比如,工作的压力,结婚,房车,再到各种风口什么小红书、AI、国家政策。


我们或许不贫穷了,但是我们的带宽一样被占的很满。



作者在书中提到了一个实验,实验者进行了两次测试,让被测试者去做测试题。


第一次测试,让他们什么都不想的去做测试题。第二次测试,提前对他们进行诱导,让他们思考自己的经济状况和他们关注自己缺乏的东西。这两组测试结果显示,第一组的分数要高很多,基本上是第二组的两倍,表明带宽会影响我们的智力水平。



比如说,不少年轻人喜欢熬夜(我也是),晚上睡不好,白天可能就会进入一种游离迟钝的状态。遇到难题的时候,选择去去抽烟喝酒,然后让自己陷入更大的问题中。其实绝大部分人缺的不是时间,而是带宽。


你有多长时间没有做一份自己的长期规划了?最近有学习什么新的技能吗?有去定期投资、理财吗?你需要更多的注意力放在长远的事情上,这样才能最终跳出稀缺的怪圈。


跳出稀缺陷阱


我们要明白稀缺并非个人特质,而是自身创造的环境条件所引发的结果,而这些条件是可以进行管理的。我们越是深入了解稀缺在大脑中的发展历程,就越有可能找到办法去避免稀缺陷阱,或至少去减轻稀缺陷阱的影响程度。


节约带宽


如何节约带宽?这里介绍几个我在生活中亲身实践并感觉有所收益的方式。


减少决策


马克·扎克伯格的标志性穿衣风格是灰色T恤和拉链连帽衫。‌‌


他这样的穿衣风格主要是为了节省时间,避免每天早上花费时间选择衣服。他曾经在接受采访时表示,他希望生活尽可能简单,减少不必要的决策,以便将更多的精力集中在重要的事情上。


我的穿衣风格都是休闲类,所以没有太大的选择空间,鞋子的话空军一号有两双且都是白色,买新衣服和新鞋我也会尽量从已有的风格里去挑选,这样无论是买东西还是日常穿衣,都不需要做太多决策。


减少信息过载


控制社交媒体和新闻资讯的浏览时间,过多的信息会占据我们的大脑,消耗带宽。我个人实践比较有效的方式是手机上卸载抖音,但是在iPad保留,想刷的话只能等下班孩子睡了再刷一会。这样白天即使是碎片时间也不会被浪费,其余时间专注于其他事情。


关闭大部分APP通知,屏蔽不重要的群。目前我的手机通知开启的APP大概只有银行类、微信、短信等的通知。微信里面不重要的微信群都会屏蔽掉,甚至是工作群,如果实在怕错过信息,微信群可以设置特别关注的用户,这样也能帮你过滤掉大部分信息。


缓解压力


定期进行体育锻炼,不开心的事情,和朋友们交流,可以减轻心理压力,释放带宽。


有一些心里的事情,也可以通过文字的方式记录下来,比如我很多想说的话就会通过写作的方式写下来,分享出来,心里的带宽也就释放了。


留有余闲


在金钱上,再没有钱,也要留出一小部分来投资、储蓄,投资也可以是投资自己。定期储蓄如果你总是忘记,银行的APP都提供了自动储蓄的功能,每当发工资就自动把一部分钱自动存起来。


其实现在的公积金、五险一金也是同理,自动帮我们对未来进行储蓄,让忽视变成默许


在时间上,当天的工作当天完成,未来还有未来的事情要做。


这里分享一个小技巧,之前在字节工作的时候,如果你的日历上面没有日程的话,很容易就会被别人约会或者约面试。你可以在需要余闲的时间,自己给自己约一个2个小时的会议,这段时间就可以确保不会有人来打扰你。


设置提醒


现在的打工人,都会因为工作的繁忙,白天一坐一整天,甚至连喝水的时间都没有,直到体检时候颈椎生理曲度变直,发现有肾结石才开始重视身体。


晚上不停的熬夜,10点钟打开抖音,回过神来时已经凌晨1点,第二天上班昏昏欲睡才懊悔不已。


善用提醒, 比如类似Eye Monitor的监控工具,可以监控你使用电脑的时常,在你疲劳的时候提醒你,或者给自己配一个智能手表,久坐时给予提示。


image.png


手机上设置睡眠时间,短视频上设置休息时间,那么过了那个时间,就会不断的提醒你,该睡觉了。


本质上提醒就是让你从管窥的视角中拽出来,让更多重要的事情进入到你的视角里,让你无法忽视那些更重要的事情。


说在最后


好了,文章到这里就要结束了,感谢你能看到最后。


总结一下,稀缺是一种心态,短暂的稀缺有一定好处,会让我们产生专注红利,但是长期的稀缺,这种稀缺心态就会掉进稀缺的陷阱里。让我们产生管窥效应,就是只关注紧急的东西,而忽视重要的东西。它会让我们没有余闲,让自己的工作、生活缺乏弹性;而且它会让我们容易借用,去透支未来的资源;它还会减少我们的带宽,增加做出错误决定的几率,最终让我们进入一个稀缺怪圈的恶性循环。


如果想跳出这个怪圈,方法有三个:一是节约带宽,减少权衡式的思维;二是留有余闲,让自己的效率更高;三是设置提醒,让重要的事情及时出现在视野当中。


作者:东东拿铁
来源:juejin.cn/post/7410220431821111296
收起阅读 »

前端实现图片压缩方案总结

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
收起阅读 »

领导问我:为什么一个点赞功能你做了五天?

公众号:【可乐前端】,每天3分钟学习一个优秀的开源项目,分享web面试与实战知识。 前言 可乐是一名前端切图仔,最近他们团队需要做一个文章社区平台。由于人手不够,前后端部分都是由前端同学来实现,后端部分用的技术栈是 nest.js 。 某一个周一,领导希望做...
继续阅读 »

公众号:【可乐前端】,每天3分钟学习一个优秀的开源项目,分享web面试与实战知识。



前言


可乐是一名前端切图仔,最近他们团队需要做一个文章社区平台。由于人手不够,前后端部分都是由前端同学来实现,后端部分用的技术栈是 nest.js


某一个周一,领导希望做一个给文章点赞的功能,在文章列表页与文章详情页需要看到该文章的点赞总数,以及当前登录的用户有没有对该文章点赞,即用户与文章的点赞关系。


交代完之后,领导就去出差了。等领导回来时已是周五,他问可乐:这期的需求进展如何?


可乐回答:点赞的需求我做完了,其他的还没开始。


领导生气的说:为什么点赞这样的一个小功能你做了五天才做完???


可乐回答:领导息怒。。请听我细细道来


往期文章


仓库地址



初步设计


对于上面的这个需求,我们提炼出来有三点最为重要的功能:



  1. 获取点赞总数

  2. 获取用户的点赞关系

  3. 点赞/取消点赞


所以这里容易想到的是在文章表中冗余一个点赞数量字段 likes ,查询文章的时候一起把点赞总数带出来。


idcontentlikes
1文章A10
2文章B20

然后建一张 article_lile_relation 表,建立文章点赞与用户之间的关联关系。


idarticle_iduser_idvalue
1100120011
2100120020

上面的数据就表明了 id2001 的用户点赞了 id1001 的文章; id2002 的用户对 id1001 的文章取消了点赞。


这是对于这种关联关系需求最容易想到的、也是成本不高的解决方案,但在仔细思考了一番之后,我放弃了这种方案。原因如下:



  1. 由于首页文章流中也需要展示用户的点赞关系,这里获取点赞关系需要根据当前文章 id 、用户 id 去联表查询,会增加数据库的查询压力。

  2. 有关于点赞的信息存放在两张表中,需要维护两张表的数据一致性。

  3. 后续可能会出现对摸鱼帖子点赞、对用户点赞、对评论点赞等需求,这样的设计方案显然拓展性不强,后续再做别的点赞需求时可能会出现大量的重复代码。


基于上面的考虑,准备设计一个通用的点赞模块,以拓展后续各种业务的点赞需求。


表设计


首先来一张通用的点赞表, DDL 语句如下:


CREATE TABLE `like_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`target_id` int(11) DEFAULT NULL,
`type` int(4) DEFAULT NULL,
`created_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`value` int(4) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `like_records_target_id_IDX` (`target_id`,`user_id`,`type`) USING BTREE,
KEY `like_records_user_id_IDX` (`user_id`,`target_id`,`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

解释一下上面各个字段的含义:



  • id :点赞记录的主键 id

  • user_id :点赞用户的 id

  • target_id :被点赞的文章 id

  • type :点赞类型:可能有文章、帖子、评论等

  • value :是否点赞, 1 点赞, 0 取消点赞

  • created_time :创建时间

  • updated_time :更新时间


前置知识


在设计好数据表之后,再来捋清楚这个业务的一些特定属性与具体实现方式:



  1. 我们可以理解这是一个相对来说读比写多的需求,比如你看了 10 篇掘金的文章,可能只会对 1 篇文章点赞

  2. 应该设计一个通用的点赞模块,以供后续各种点赞需求的接入

  3. 点赞数量与点赞关系需要频繁地获取,所以需要读缓存而不是读数据库

  4. 写入数据库与同步缓存需考虑数据一致性


所以可乐针对这样的业务特性上网查找了一些资料,发现有一些前置知识是他所欠缺的,我们一起来看看。


mysql事务


mysql 的事务是指一系列的数据库操作,这些操作要么全部成功执行,要么全部失败回滚。事务是用来确保数据库的完整性、一致性和持久性的机制之一。


mysql 中,事务具有以下四个特性,通常缩写为 ACID



  1. 原子性: 事务是原子性的,这意味着事务中的所有操作要么全部成功执行,要么全部失败回滚。

  2. 一致性: 事务执行后,数据库从一个一致的状态转换到另一个一致的状态。这意味着事务执行后,数据库中的数据必须满足所有的约束、触发器和规则,保持数据的完整性。

  3. 隔离性: 隔离性指的是多个事务之间的相互独立性。即使有多个事务同时对数据库进行操作,它们之间也不会相互影响,每个事务都感觉到自己在独立地操作数据库。 mysql 通过不同的隔离级别(如读未提交、读已提交、可重复读和串行化)来控制事务之间的隔离程度。

  4. 持久性: 持久性指的是一旦事务被提交,对数据库的改变将永久保存,即使系统崩溃也不会丢失。 mysql 通过将事务的提交写入日志文件来保证持久性,以便在系统崩溃后能够恢复数据。


这里以商品下单创建订单并扣除库存为例,演示一下 nest+typeorm 中的事务如何使用:


import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';
import { Order } from './order.entity';
import { Product } from './product.entity';

@Injectable()
export class OrderService {
constructor(
@InjectEntityManager()
private readonly entityManager: EntityManager,
) {}

async createOrderAndDeductStock(productId: number, quantity: number): Promise<Order> {
return await this.entityManager.transaction(async transactionalEntityManager => {
// 查找产品并检查库存是否充足
const product = await transactionalEntityManager.findOne(Product, productId);
if (!product || product.stock < quantity) {
throw new Error('Product not found or insufficient stock');
}

// 创建订单
const order = new Order();
order.productId = productId;
order.quantity = quantity;
await transactionalEntityManager.save(order);

// 扣除库存
product.stock -= quantity;
await transactionalEntityManager.save(product);

return order;
});
}
}


this.entityManager.transaction 创建了一个事务,在异步函数中,如果发生错误, typeorm 会自动回滚事务;如果没有发生错误,typeorm 会自动提交事务。


在这个实例中,尝试获取库存并创建订单和减库存,如果任何一个地方出错异常抛出,则事务就会回滚,这样就保证了多表间数据的一致性。


分布式锁



分布式锁是一种用于在分布式系统中协调多个节点并保护共享资源的机制。在分布式系统中,由于涉及多个节点并发访问共享资源,因此需要一种机制来确保在任何给定时间只有一个节点能够访问或修改共享资源,以防止数据不一致或竞争条件的发生。



对于同一个用户对同一篇文章频繁的点赞/取消点赞请求,可以加分布式锁的机制,来规避一些问题:



  1. 防止竞态条件: 点赞/取消点赞操作涉及到查询数据库、更新数据库和更新缓存等多个步骤,如果不加锁,可能会导致竞态条件,造成数据不一致或错误的结果。

  2. 保证操作的原子性: 使用分布式锁可以确保点赞/取消点赞操作的原子性,即在同一时间同一用户只有一个请求能够执行操作,从而避免操作被中断或不完整的情况发生。

  3. 控制并发访问: 加锁可以有效地控制并发访问,限制了频繁点击发送请求的数量,从而减少系统负载和提高系统稳定性。


redis 中实现分布式锁通常使用的是基于 SETNX 命令和 EXPIRE 命令的方式:



  1. 使用 SETNX 命令尝试将 lockKey 设置为 lockValue ,如果 lockKey 不存在,则设置成功并返回 1;如果 lockKey 已经存在,则设置失败并返回 0

  2. 如果 SETNX 成功,说明当前客户端获得了锁,可以执行相应的操作;如果 SETNX 失败,则说明锁已经被其他客户端占用,当前客户端需要等待一段时间后重新尝试获取锁。

  3. 为了避免锁被永久占用,可以使用 EXPIRE 命令为锁设置一个过期时间,确保即使获取锁的客户端在执行操作时发生故障,锁也会在一定时间后自动释放。


  async getLock(key: string) {
const res = await this.redis.setnx(key, 'lock');
if (res) {
// 10秒锁过期
await this.redis.expire(key, 10);
}
return res;
}

async unLock(key: string) {
return this.del(key);
}

redis中的set结构


redis 中的 set 是一种无序集合,用于存储多个不重复的字符串值,set 中的每个成员都是唯一的。


我们存储点赞关系的时候,需要用到 redis 中的 set 结构,存储的 keyvalue 如下:


article_1001:[uid1,uid2,uid3]


这就表示文章 id1001 的文章,有用户 iduid1uid2uid3 这三个用户点赞了。


常用的 set 结构操作命令包括:



  • SADD key member [member ...]: 将一个或多个成员加入到集合中。

  • SMEMBERS key: 返回集合中的所有成员。

  • SISMEMBER key member: 检查成员是否是集合的成员。

  • SCARD key: 返回集合元素的数量。

  • SREM key member [member ...]: 移除集合中一个或多个成员。

  • SPOP key [count]: 随机移除并返回集合中的一个或多个元素。

  • SRANDMEMBER key [count]: 随机返回集合中的一个或多个元素,不会从集合中移除元素。

  • SUNION key [key ...]: 返回给定所有集合的并集。

  • SINTER key [key ...]: 返回给定所有集合的交集。

  • SDIFF key [key ...]: 返回给定所有集合的差集。


下面举几个点赞场景的例子



  1. 当用户 iduid1 给文章 id1001 的文章点赞时:sadd 1001 uid1

  2. 当用户 iduid1 给文章 id1001 的文章取消点赞时:srem 1001 uid1

  3. 当需要获取文章 id1001 的点赞数量时:scard 1001


redis事务


redis 中,事务是一组命令的有序序列,这些命令在执行时会被当做一个单独的操作来执行。即事务中的所有命令要么全部执行成功,要么全部执行失败,不存在部分执行的情况。


以下是 redis 事务的主要命令:



  1. MULTI: 开启事务,在执行 MULTI 命令后,后续输入多个命令来组成一个事务。

  2. EXEC: 执行事务,在执行 EXEC 命令时,redis 会执行客户端输入的所有事务命令,如果事务中的所有命令都执行成功,则事务执行成功,返回事务中所有命令的执行结果;如果事务中的某个命令执行失败,则事务执行失败,返回空。

  3. DISCARD: 取消事务,在执行 DISCARD 命令时,redis 会取消当前事务中的所有命令,事务中的命令不会被执行。

  4. WATCH: 监视键,在执行 WATCH 命令时,redis 会监听一个或多个键,如果在执行事务期间任何被监视的键被修改,事务将会被打断。


比如说下面的代码给集合增加元素,并更新集合的过期时间,可以如下使用 redis 的事务去执行它:


  const pipeline = this.redisService.multi();
const setKey = this.getSetKey(targetId, type);
if (value === ELike.LIKE) {
pipeline.sadd(setKey, userId);
} else {
pipeline.srem(setKey, userId);
}
pipeline.expire(setKey, this.ttl);
await pipeline.exec();

流程图设计


在了解完这些前置知识之后,可乐开始画一些实现的流程图。


首先是点赞/取消点赞接口的流程图:


image.png


简单解释下上面的流程图:



  1. 先尝试获取锁,获取不到的时候等待重试,保证接口与数据的时序一致。

  2. 判断这个点赞关系是否已存在,比如说用户对这篇文章已经点过赞,其实又来了一个对此篇文章点赞的请求,直接返回失败

  3. 开启 mysql 的事务,去更新点赞信息表,同时尝试去更新缓存,在缓存更新的过程中,会有3次的失败重试机会,如果缓存更新都失败,则回滚mysql事务;整体更新失败

  4. mysql 更新成功,缓存也更新成功,则整个操作都成功


然后是获取点赞数量和点赞关系的接口


image.png


简单解释下上面的流程图:



  1. 首先判断当前文章 id 对应的点赞关系是否在 redis 中存在,如果存在,则直接从缓存中读取并返回

  2. 如果不存在,此时加锁,准备读取数据库并更新 redis ,这里加锁的主要目的是防止大量的请求一下子打到数据库中。

  3. 由于加锁的时候,可能很多接口已经在等待,所以在锁释放的时候,再加多一次从 redis 中获取的操作,此时 redis 中已经有值,可以直接从缓存中读取。


代码实现


在所有的设计完毕之后,可以做最后的代码实现了。分别来实现点赞操作与点赞数量接口。这里主要关注 service 层的实现即可。


点赞/取消点赞接口


  async toggleLike(params: {
userId: number;
targetId: number;
type: ELikeType;
value: ELike;
}
) {
const { userId, targetId, type, value } = params;
const LOCK_KEY = `${userId}::${targetId}::${type}::toggleLikeLock`;
const canGetLock = await this.redisService.getLock(LOCK_KEY);
if (!canGetLock) {
console.log('获取锁失败');
await wait();
return this.toggleLike(params);
}
const record = await this.likeRepository.findOne({
where: { userId, targetId, type },
});
if (record && record.value === value) {
await this.redisService.unLock(LOCK_KEY);
throw Error('不可重复操作');
}

await this.entityManager.transaction(async (transactionalEntityManager) => {
if (!record) {
const likeEntity = new LikeEntity();
likeEntity.targetId = targetId;
likeEntity.type = type;
likeEntity.userId = userId;
likeEntity.value = value;
await transactionalEntityManager.save(likeEntity);
} else {
const id = record.id;
await transactionalEntityManager.update(LikeEntity, { id }, { value });
}
const isSuccess = await this.tryToFreshCache(params);

if (!isSuccess) {
await this.redisService.unLock(LOCK_KEY);
throw Error('操作失败');
}
});
await this.redisService.unLock(LOCK_KEY);
return true;
}

private async tryToFreshCache(
params: {
userId: number;
targetId: number;
type: ELikeType;
value: ELike;
},
retry = 3,
) {
if (retry === 0) {
return false;
}
const { targetId, type, value, userId } = params;
try {
const pipeline = this.redisService.multi();
const setKey = this.getSetKey(targetId, type);
if (value === ELike.LIKE) {
pipeline.sadd(setKey, userId);
} else {
pipeline.srem(setKey, userId);
}
pipeline.expire(setKey, this.ttl);
await pipeline.exec();
return true;
} catch (error) {
console.log('tryToFreshCache error', error);
await wait();
return this.tryToFreshCache(params, retry - 1);
}
}


可以参照流程图来看这部分实现代码,基本实现就是使用 mysql 事务去更新点赞信息表,然后去更新 redis 中的点赞信息,如果更新失败则回滚事务,保证数据的一致性。


获取点赞数量、点赞关系接口


  async getLikes(params: {
targetId: number;
type: ELikeType;
userId: number;
}
) {
const { targetId, type, userId } = params;
const setKey = this.getSetKey(targetId, type);
const cacheExsit = await this.redisService.exist(setKey);
if (!cacheExsit) {
await this.getLikeFromDbAndSetCache(params);
}
const count = await this.redisService.getSetLength(setKey);
const isLike = await this.redisService.isMemberOfSet(setKey, userId);
return { count, isLike };
}

private async getLikeFromDbAndSetCache(params: {
targetId: number;
type: ELikeType;
userId: number;
}
) {
const { targetId, type, userId } = params;
const LOCK_KEY = `${targetId}::${type}::getLikesLock`;
const canGetLock = await this.redisService.getLock(LOCK_KEY);
if (!canGetLock) {
console.log('获取锁失败');
await wait();
return this.getLikeFromDbAndSetCache(params);
}
const setKey = this.getSetKey(targetId, type);
const cacheExsit = await this.redisService.exist(setKey);
if (cacheExsit) {
await this.redisService.unLock(LOCK_KEY);
return true;
}
const data = await this.likeRepository.find({
where: {
targetId,
userId,
type,
value: ELike.LIKE,
},
select: ['userId'],
});
if (data.length !== 0) {
await this.redisService.setAdd(
setKey,
data.map((item) => item.userId),
this.ttl,
);
}
await this.redisService.unLock(LOCK_KEY);
return true;
}

由于读操作相当频繁,所以这里应当多使用缓存,少查询数据库。读点赞信息时,先查 redis 中有没有,如果没有,则从 mysql 同步到 redis 中,同步的过程中也使用到了分布式锁,防止一开始没缓存时请求大量打到 mysql


同时,如果所有文章的点赞信息都同时存在 redis 中,那 redis 的存储压力会比较大,所以这里会给相关的 key 设置一个过期时间。当用户重新操作点赞时,会更新这个过期时间。保障缓存的数据都是相对热点的数据。


通过组装数据,获取点赞信息的返回数据结构如下:


image.png


返回一个 map ,其中 key 文章 idvalue 里面是该文章的点赞数量以及当前用户是否点赞了这篇文章。


前端实现


文章流列表发生变化的时候,可以监听列表的变化,然后去获取点赞的信息:


useEffect(() => {
if (!article.list) {
return;
}
const shouldGetLikeIds = article.list
.filter((item: any) => !item.likeInfo)
.map((item: any) => item.id);
if (shouldGetLikeIds.length === 0) {
return;
}
console.log("shouldGetLikeIds", shouldGetLikeIds);
getLikes({
targetIds: shouldGetLikeIds,
type: 1,
}).then((res) => {
const map = res.data;
const newList = [...article.list];
for (let i = 0; i < newList.length; i++) {
if (!newList[i].likeInfo && map[newList[i].id]) {
newList[i].likeInfo = map[newList[i].id];
}
}
const newArticle = { ...article };
newArticle.list = newList;
setArticle(newArticle);
});
}, [article]);

image.png


点赞操作的时候前端也需要加锁,接口执行完毕了再把锁释放。


   <Space
onClick={(e) => {
e.stopPropagation();
if (lockMap.current?.[item.id]) {
return;
}
lockMap.current[item.id] = true;
const oldValue = item.likeInfo.isLike;
const newValue = !oldValue;
const updateValue = (value: any) => {
const newArticle = { ...article };
const newList = [...newArticle.list];
const current = newList.find(
(_) => _.id === item.id
);
current.likeInfo.isLike = value;
if (value) {
current.likeInfo.count++;
} else {
current.likeInfo.count--;
}
setArticle(newArticle);
};
updateValue(newValue);
toggleLike({
targetId: item.id,
value: Number(newValue),
type: 1,
})
.catch(() => {
updateValue(oldValue);
})
.finally(() => {
lockMap.current[item.id] = false;
});
}}
>
<LikeOutlined
style={
item.likeInfo.isLike ? { color: "#1677ff" } : {}
}
/>

{item.likeInfo.count}
</Space>

Kapture 2024-03-23 at 22.49.08.gif


解释


可乐:从需求分析考虑、然后研究网上的方案并学习前置知识,再是一些环境的安装,最后才是前后端代码的实现,领导,我这花了五天不过份吧。


领导(十分无语):我们平台本来就没几个用户、没几篇文章,本来就是一张关联表就能解决的问题,你又搞什么分布式锁又搞什么缓存,还花了那么多天时间。我不管啊,剩下没做的需求你得正常把它正常做完上线,今天周五,周末你也别休息了,过来加班吧。


最后


以上就是本文的全部内容,如果你觉得有意思的话,点点关注点点赞吧~


作者:可乐鸡翅kele
来源:juejin.cn/post/7349437605858066443
收起阅读 »

2024字节跳动“安全范儿”高校挑战赛报名开启,三大赛道等你来战!

近日,第五届字节跳动“安全范儿”高校挑战赛正式开启。赛事由字节跳动安全与风控团队主办,设置三大赛道,为高校人才提供多样化的能力展示平台和竞技之路。“安全范儿”高校挑战赛自2019年首届举办以来,共吸引了10,000+战队、近30,000名网安高手同台竞技,为高...
继续阅读 »

近日,第五届字节跳动“安全范儿”高校挑战赛正式开启。赛事由字节跳动安全与风控团队主办,设置三大赛道,为高校人才提供多样化的能力展示平台和竞技之路。

“安全范儿”高校挑战赛自2019年首届举办以来,共吸引了10,000+战队、近30,000名网安高手同台竞技,为高校新生力量进入职场打好前站,开启向上成长新空间。

本届“安全范儿”高校挑战赛8月29日开启报名,面向全日制高校本科生/研究生,不限专业与院校(可跨校组队),累计提供80多万元专项奖励,分ByteCTF大师赛、ByteAI安全挑战赛、ByteHACK三大赛道。

ByteCTF大师赛为定向邀请制,赛题深度结合字节跳动业务场景,覆盖AI、Web、逆向工程等7大方向,助力选手提升工程实战能力和思考深度。ByteAI赛道聚焦大模型安全,考察选手如何在大模型时代进行创新性攻击,以“攻击突破”促进大模型防御。ByteHACK赛道为选手提供丰富且真实的漏洞挖掘场景,单个漏洞最高可获得200,000元奖金。

本次大赛也得到北京理工大学、北京邮电大学、电子科技大学、福州大学、华中科技大学、南京大学、上海交通大学、西安交通大学、中国人民大学等23所国内高校的大力支持。“安全范儿”高校挑战赛相关负责人表示,希望通过搭建学术与工业的沟通桥梁,助力产学研高效合作。(作者:李双)

收起阅读 »

很多人不懂什么是优势

大家好,我卡颂。 上周和10个左右同学交流了如何发展第二曲线,他们中有国企员工、宝妈、4S店管理、程序员... 在交流中,我发现个普遍现象 —— 应试教育以及职场带来的思维惯势,对人的影响非常大,最直观的影响就是 —— 很多人不懂什么是优势。 如果你一路从学校...
继续阅读 »

大家好,我卡颂。


上周和10个左右同学交流了如何发展第二曲线,他们中有国企员工、宝妈、4S店管理、程序员...


在交流中,我发现个普遍现象 —— 应试教育以及职场带来的思维惯势,对人的影响非常大,最直观的影响就是 —— 很多人不懂什么是优势。


如果你一路从学校到职场,直到最近才由于各种原因(比如裁员压力、内卷、年龄焦虑...)考虑发展第二曲线,那这篇文章也许会改变你对优势的固有认知。


说真的,什么是优势?


学校与公司都是服务于具体目的的组织



  • 学校:服务于升学,表现形式是学习

  • 公司:服务于盈利,表现形式是工作


所以,在学校与公司的语境下,优势通常指那些能为达成具体目的提供帮助的能力或性格,比如:



  • 学校最看重应试能力,因为这跟升学息息相关

  • 公司鼓励争强好胜,因为这跟盈利相关


那些与达成具体目的关联不大,甚至背道而驰的能力或性格,就会被冠以劣势的名头。


比如在公司中,不善沟通、讨好型人格就是劣势。


我们会发现,决定一个能力或性格是否是优势,取决于为了达成什么目的


但由于长久以来的思维惯势,很多人已经慢慢忽略了为了达成什么目的这个前缀。


他们已经习惯性认为不善沟通、讨好型人格就是劣势,不管什么情况下都是劣势。


这就是思维惯势对我们带来的第一个影响 —— 不考虑目的的情况下否定自己的能力与性格。


让我们暂停思考一下 —— 当我在表达上述观点时,你有没有觉察到什么不对劲的地方?


在上面的观点中,我提到 —— 优势是那些“能为达成具体目的提供帮助的能力性格”。


优势是能力或性格,这个表述你觉得有问题么?


在学校或公司中,这种表述是没问题的,因为在这样的组织内,个体都是螺丝钉,只要发挥你的能力或性格做好本职工作即可。


比如,一个程序员可能只看重技术能力而不看重运营能力,因为本职工作不需要后者。


这就是思维惯势对我们带来的第二个影响 —— 认为优势是单一维度的(能力或性格)。


当考虑第二曲线时


当考虑发展第二曲线时,上述两个影响会对我们的选择造成不利的结果。


带着优势是单一维度的思维惯势,我们思考第二曲线的模式会倾向于 —— 我有什么比别人厉害的能力?


再结合第一条思维惯势不考虑目的的情况下否定自己的能力与性格,最终思考的模式会倾向于 —— 我有什么拿得出手的职场能力或性格特质?


比如,当程序员考虑第二曲线时,最常见的选择是:



  1. 接外包

  2. 独立开发

  3. 远程工作


都是这种思维惯势下的产物。


对于更广大的普通人,正是因为职场能力不出众、性格没优势,才在职场混不下去,从而考虑第二曲线。


如果还顺着思维惯势思考,不又陷入:



  1. 弱所以混不下去

  2. 混不下去所以找出路

  3. 弱所以找不到出路


这样的死局了么?


第二曲线应该如何思考优势


对于发展第二曲线的同学,我们需要跳出应试教育以及职场带来的思维惯势。


首先,回归本质 —— 评判一个能力或性格是优势还是劣势,取决于为了达到什么目的


有个找我咨询的女生,由于是讨好型人格,所以在职场混的很痛苦,处理同事关系时很内耗。


但她的第二曲线方向是线下组织人参与有趣的活动。作为组织者,她需要在活动期间照顾到所有参与者。


正是由于讨好型人格,使得她在做这件事时心情无比愉悦,也收获了满满的成就感。


再比如一个找我咨询的男程序员,小镇做题家,喜欢文史,人前内向,网上又有极强表达欲,喜欢输出知识和自嘲。


他在职场就是被领导PUA的老实人,但他非常适合做ip,因为ip最重要的就是面相。


换一个目的,之前的劣势就都是优势了。


既然目的、能力、性格互相羁绊,那显然评价优势的标准并非单一维度。


实际上,当考虑第二曲线时,我们应该考虑优势领域,而非优势本身。


所谓优势领域,是指下面三者的交集:



  1. 你熟悉的领域

  2. 你擅长的能力

  3. 你的性格特质


比如,一个找我咨询的4S店管理的情况如下:



  1. 熟悉的领域:汽车、育儿(家有一女)

  2. 擅长的能力:销售能力、学习能力、英语能力

  3. 性格特质:同理心强、敏感、有小孩缘


最后,结合他辅导女儿学英语的经历(他女儿在会说中文前就能用英文单词表达需求了,比如渴了会说water),最终选择了一个优势领域 —— 教小朋友学英语。


注意,教小朋友学英语学英语是两个完全不同的领域,后者竞争者众多,而前者完全是根据他的能力、性格特质出发定制的领域。


这是个集合了亲子关系、儿童心理学、英语教学、幼儿教育等学科的交叉领域。


在他的优势领域中:



  • 比他英语厉害的可能没他有小孩缘

  • 比他有小孩缘的可能育儿没他厉害

  • 比他育儿厉害的可能英语没他厉害


总之,在自己选择的优势领域中,我们普通人也能处于绝对的优势地位。


总结


由于应试教育以及职场带来的思维惯势,大部分人对优势的理解有两个误区:



  1. 脱离目的谈优势能力、优势性格

  2. 认为优势是单一维度的


在考虑发展第二曲线时,这种思维惯势会影响我们的最终选择。


更好的方式是:考虑优势领域,而非优势本身,即找到下面三者的交集:



  1. 你熟悉的领域

  2. 你擅长的能力

  3. 你的性格特质


交集所在的就是你的优势领域,也就是你应该探索第二曲线的方向。


作者:魔术师卡颂
来源:juejin.cn/post/7385108486517112883
收起阅读 »

改进菜单栏动态展示样式,我被评上优秀开发!

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
收起阅读 »

Cesium为军工助力!动态绘制各类交互式态势图

web
态势图(Situation Map)是一种用于表示空间环境中动态或静态信息的地图,它能够展示事件、资源、威胁和其他关键因素的地理位置及其变化情况 前言 什么是态势图 态势图(Situation Map)是一种用于表示空间环境中动态或静态信息的地图,它能够展示...
继续阅读 »

态势图(Situation Map)是一种用于表示空间环境中动态或静态信息的地图,它能够展示事件、资源、威胁和其他关键因素的地理位置及其变化情况



前言


什么是态势图


态势图(Situation Map)是一种用于表示空间环境中动态或静态信息的地图,它能够展示事件资源威胁其他关键因素的地理位置及其变化情况。



通过可视化的方式,态势图帮助决策者在复杂环境中迅速获取关键信息,从而做出及时而准确的决策。


随着地理信息系统(GIS)的不断发展,态势图军事应急管理地理规划等领域中扮演着越来越重要的角色。


军工领域



在军工领域,态势图是军事指挥控制系统中的核心组件。


它们能够实时展示战场上的动态信息,如部队的部署位置、敌军动向、武器系统状态等。这种可视化工具对于战术指挥、作战计划制定和战场态势感知至关重要。


应急管理



在应急管理领域,态势图能够帮助管理者协调资源和人员应对自然灾害、山林火灾、事故或突发事件。通过态势图,可以清晰地看到灾害影响范围救援力量分布资源需求逃生路线等关键信息,从而实现有效的应急响应和资源调配。


地理规划


在地理规划中,态势图用于展示和分析区域开发土地利用交通网络等方面的信息。能帮助规划者更清晰的理解地理空间关系、评估环境影响,并做出科学的规划决策。


Cesium中绘制态势图


OK,接下来我们主要介绍一下在Cesium中如何绘制态势图,主要包括各种箭头类型的绘制,如直线箭头攻击箭头钳击箭头等。



源码地址在文末。


箭头绘制的核心算法


algorithm.js是实现复杂箭头绘制的核心脚本。


这里定义了多种箭头类型的绘制算法,如双箭头(doubleArrow)、三箭头(threeArrow)以及带尾攻击箭头(tailedAttackArrow)。


这些算法通过接收用户点击的多个点,并计算出箭头的控制点和多边形点来实现箭头形状的生成。


以下是doubleArrow函数的部分代码与解析:


xp.algorithm.doubleArrow = function (inputPoint) {
// 初始化结果对象
var result = {
controlPoint: null,
polygonalPoint: null
};

// 根据输入点数量决定不同的箭头形状
var t = inputPoint.length;
if (!(2 > t)) {
if (2 == t) return inputPoint;

// 获取关键点
var o = this.points[0],
e = this.points[1],
r = this.points[2];

// 计算连接点和临时点位置
3 == t ? this.tempPoint4 = xp.algorithm.getTempPoint4(o, e, r) : this.tempPoint4 = this.points[3];
3 == t || 4 == t ? this.connPoint = P.PlotUtils.mid(o, e) : this.connPoint = this.points[4];

// 根据点的顺序计算箭头的左右侧点位
P.PlotUtils.isClockWise(o, e, r)
? (n = xp.algorithm.getArrowPoints(o, this.connPoint, this.tempPoint4, !1), g = xp.algorithm.getArrowPoints(this.connPoint, e, r, !0))
: (n = xp.algorithm.getArrowPoints(e, this.connPoint, r, !1), g = xp.algorithm.getArrowPoints(this.connPoint, o, this.tempPoint4, !0));

// 生成最终的箭头形状并返回
result.controlPoint = [o, e, r, this.tempPoint4, this.connPoint];
result.polygonalPoint = Cesium.Cartesian3.fromDegreesArray(xp.algorithm.array2Dto1D(f));
}
return result;
};

该函数首先根据输入点的数量确定是否继续进行箭头的绘制,接着计算关键点的位置,并通过getArrowPoints函数计算出箭头形状的多个控制点,最终生成一个包含箭头形状顶点的数组。


基于Cesium的箭头实体管理


arrowClass.js定义了具体的箭头类(如StraightArrow)和其行为管理。


通过结合Cesium的ScreenSpaceEventHandler事件处理机制,开发者可以方便地在地图上绘制、修改和删除箭头实体。


以下是StraightArrow类的部分代码与解析:


StraightArrow.prototype.startDraw = function () {
var $this = this;
this.state = 1;

// 单击事件,获取点击位置并创建箭头起点
this.handler.setInputAction(function (evt) {
var cartesian = getCatesian3FromPX(evt.position, $this.viewer);
if (!cartesian) return;

// 处理点位并开始绘制箭头
if ($this.positions.length == 0) {
$this.firstPoint = $this.creatPoint(cartesian);
$this.floatPoint = $this.creatPoint(cartesian);
$this.positions.push(cartesian);
}
if ($this.positions.length == 3) {
$this.firstPoint.show = false;
$this.floatPoint.show = false;
$this.handler.destroy();
$this.arrowEntity.objId = $this.objId;
$this.state = -1;
}
$this.positions.push(cartesian.clone());
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

// 鼠标移动事件,实时更新箭头形状
this.handler.setInputAction(function (evt) {
if ($this.positions.length < 1) return;
var cartesian = getCatesian3FromPX(evt.endPosition, $this.viewer);
if (!cartesian) return;

$this.floatPoint.position.setValue(cartesian);
if ($this.positions.length >= 2) {
if (!Cesium.defined($this.arrowEntity)) {
$this.positions.push(cartesian);
$this.arrowEntity = $this.showArrowOnMap($this.positions);
} else {
$this.positions.pop();
$this.positions.push(cartesian);
}
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
};

startDraw函数中,通过设置单击和鼠标移动事件,开发者可以实时捕获用户的操作,并根据点击位置动态绘制箭头。


最终的箭头形状会随着鼠标的移动而更新,当点击完成时箭头形状被确定。


工具类与辅助函数


plotUtil.js提供了一些用于计算几何关系的实用工具函数。


例如,distance函数计算两个点之间的距离,而getThirdPoint函数根据给定的两个点和角度,计算出第三个点的位置。 这些工具函数被广泛用于箭头的绘制逻辑中,以确保箭头的形状符合预期。


以下是distancegetThirdPoint函数的代码示例:


P.PlotUtils.distance = function (t, o) {
return Math.sqrt(Math.pow(t[0] - o[0], 2) + Math.pow(t[1] - o[1], 2));
};

P.PlotUtils.getThirdPoint = function (t, o, e, r, n) {
var g = P.PlotUtils.getAzimuth(t, o),
i = n ? g + e : g - e,
s = r * Math.cos(i),
a = r * Math.sin(i);
return [o[0] + s, o[1] + a];
};

这些函数都是为复杂箭头形状的计算提供了基础,确保在地图上绘制的箭头具有精确的几何形态。


总结


以上主要介绍了在Cesium中实现态势图的一些关键代码以及解释,更多细节请参考项目源码,如果有帮助也请给一个免费的star


【项目开源地址】:github.com/tingyuxuan2…


作者:攻城师不浪
来源:juejin.cn/post/7409181068597919781
收起阅读 »

什么是混入,如何正确地使用Mixin

web
在 Vue.js 中,mixins 是一种代码复用机制,允许我们将多个组件共享的逻辑提取到一个独立的对象中,从而提高代码的可维护性和重用性。下面将详细介绍 mixin 的概念,并通过示例代码来说明它的用法。 什么是 Mixin? Mixin 是一种在 Vue ...
继续阅读 »

在 Vue.js 中,mixins 是一种代码复用机制,允许我们将多个组件共享的逻辑提取到一个独立的对象中,从而提高代码的可维护性和重用性。下面将详细介绍 mixin 的概念,并通过示例代码来说明它的用法。


什么是 Mixin?


Mixin 是一种在 Vue 组件中复用代码的方式。我们可以将一个对象(即 mixin 对象)中的数据、方法和生命周期钩子等混入到 Vue 组件中。这样,多个组件就可以共享同一份逻辑代码。


image.png


Mixin 的基本用法


1. 定义 Mixin


首先,我们定义一个 mixin 对象,其中包含数据、方法、计算属性等。比如,我们可以创建一个 commonMixin.js 文件来定义一个 mixin:


// commonMixin.js
export default {
data() {
return {
message: 'Hello from mixin!'
};
},
methods: {
greet() {
console.log(this.message);
}
},
created() {
console.log('Mixin created hook called.');
}
};

2. 使用 Mixin


在 Vue 组件中,我们可以通过 mixins 选项来引入上述定义的 mixin。例如,我们可以在 HelloWorld.vue 组件中使用这个 mixin:


<template>
<div>
<p>{{ message }}</p>
<button @click="greet">Greet</button>
</div>
</template>

<script>
// 导入 mixin
import commonMixin from './commonMixin';

export default {
name: 'HelloWorld',
mixins: [commonMixin], // 使用 mixin
mounted() {
console.log('Component mounted hook called.');
}
};
</script>

在上面的示例中,HelloWorld 组件通过 mixins 选项引入了 commonMixin。这意味着 HelloWorld 组件将拥有 commonMixin 中定义的数据、方法和生命周期钩子。


3. Mixin 的冲突处理


如果组件和 mixin 中都定义了相同的选项,Vue 将遵循一定的优先级规则来处理这些冲突:



  • 数据:如果组件和 mixin 中有相同的 data 字段,组件中的 data 会覆盖 mixin 中的 data

  • 方法:如果组件和 mixin 中有同名的方法,组件中的方法会覆盖 mixin 中的方法。

  • 生命周期钩子:如果组件和 mixin 中有相同的生命周期钩子(如 created),它们都会被调用,且 mixin 中的钩子会在组件中的钩子之前调用。


// commonMixin.js
export default {
data() {
return {
message: 'Hello from mixin!'
};
},
methods: {
greet() {
console.log('Mixin greet');
}
},
created() {
console.log('Mixin created hook called.');
}
};

// HelloWorld.vue
<template>
<div>
<p>{{ message }}</p>
<button @click="greet">Greet</button>
</div>
</template>


<script>
import commonMixin from './commonMixin';

export default {
name: 'HelloWorld',
mixins: [commonMixin],
data() {
return {
message: 'Hello from component!'
};
},
methods: {
greet() {
console.log('Component greet');
}
},
created() {
console.log('Component created hook called.');
}
};
</script>


在这个例子中,HelloWorld 组件中 message 的值会覆盖 mixin 中的值,greet 方法中的实现会覆盖 mixin 中的方法,created 钩子的调用顺序是 mixin 先调用,然后组件中的 created 钩子调用。


4. 使用 Mixin 的注意事项



  • 命名冲突:为了避免命名冲突,建议使用明确且独特的命名方式。

  • 复杂性:过度使用 mixin 可能会导致代码难以跟踪和调试。可以考虑使用 Vue 的组合式 API 来替代 mixin,以提高代码的可读性和可维护性。


在 Vue.js 开发中,mixin 主要用于以下场景,帮助我们实现代码复用和逻辑共享:


1. 共享功能和逻辑


当多个组件需要使用相同的功能或逻辑时,mixin 是一个有效的解决方案。通过将共享的逻辑提取到一个 mixin 中,我们可以避免重复代码。例如,多个组件可能都需要处理表单验证或数据格式化,这时可以将这些功能封装到一个 mixin 中:


// validationMixin.js
export default {
methods: {
validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
return re.test(email);
}
}
};

// UserForm.vue
<template>
<form @submit.prevent="handleSubmit">
<input v-model="email" placeholder="Enter your email" />
<button type="submit">Submit</button>
</form>
</template>


<script>
import validationMixin from './validationMixin';

export default {
mixins: [validationMixin],
data() {
return {
email: ''
};
},
methods: {
handleSubmit() {
if (this.validateEmail(this.email)) {
alert('Email is valid!');
} else {
alert('Email is invalid!');
}
}
}
};
</script>


2. 封装重复的生命周期钩子


有时候,多个组件可能需要在相同的生命周期阶段执行某些操作。例如,所有组件都需要在 created 钩子中初始化数据或进行 API 请求。可以将这些操作封装到 mixin 中:


// dataFetchMixin.js
export default {
created() {
this.fetchData();
},
methods: {
async fetchData() {
// 假设有一个 API 请求
try {
const response = await fetch('https://api.example.com/data');
this.data = await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
}
}
},
data() {
return {
data: null
};
}
};

// DataComponent.vue
<template>
<div>
<pre>{{ data }}</pre>
</div>
</template>


<script>
import dataFetchMixin from './dataFetchMixin';

export default {
mixins: [dataFetchMixin]
};
</script>


3. 跨组件通信


在 Vue 2 中,mixin 可以用来管理跨组件通信。例如,多个子组件可以通过 mixin 共享父组件传递的数据或方法:


// communicationMixin.js
export default {
methods: {
emitEvent(message) {
this.$emit('custom-event', message);
}
}
};

// ParentComponent.vue
<template>
<div>
<ChildComponent @custom-event="handleEvent" />
</div>
</template>


<script>
import communicationMixin from './communicationMixin';
import ChildComponent from './ChildComponent.vue';

export default {
components: { ChildComponent },
mixins: [communicationMixin],
methods: {
handleEvent(message) {
console.log('Received message:', message);
}
}
};
</script>


// ChildComponent.vue
<template>
<button @click="sendMessage">Send Message</button>
</template>


<script>
import communicationMixin from './communicationMixin';

export default {
mixins: [communicationMixin],
methods: {
sendMessage() {
this.emitEvent('Hello from ChildComponent');
}
}
};
</script>


4. 封装组件的默认行为


对于有相似默认行为的多个组件,可以将这些默认行为封装到 mixin 中。例如,处理表单提交、数据清理等:


// formMixin.js
export default {
methods: {
handleSubmit() {
console.log('Form submitted');
// 处理表单提交逻辑
},
clearForm() {
this.$data = this.$options.data();
}
}
};

// LoginForm.vue
<template>
<form @submit.prevent="handleSubmit">
<!-- 表单内容 -->
<button type="submit">Login</button>
</form>
</template>


<script>
import formMixin from './formMixin';

export default {
mixins: [formMixin]
};
</script>


结论


Vue 的 mixin 机制提供了一种简单有效的方式来复用组件逻辑,通过将逻辑封装到独立的 mixin 对象中,可以在多个组件中共享相同的代码。理解和正确使用 mixin 可以帮助我们构建更清晰、可维护的代码结构。不过,随着 Vue 3 的引入,组合式 API 提供了更强大的功能来替代 mixin,在新的开发中,可以根据需要选择合适的方案。


作者:不爱说话郭德纲
来源:juejin.cn/post/7409110408991768587
收起阅读 »

uniapp 授权登录、页面跳转及弹窗问题

web
uniapp 弹框 主要介绍了 uniapp 弹框使用的一些问题,例如 uni.showModal 中的 content 换行显示实现、uni.showToast()字数超过 7 个显示问题、以及 uni-popup 自定义弹层处理 1. uni.showMo...
继续阅读 »

uniapp 弹框


主要介绍了 uniapp 弹框使用的一些问题,例如 uni.showModal 中的 content 换行显示实现、uni.showToast()字数超过 7 个显示问题、以及 uni-popup 自定义弹层处理


1. uni.showModal 中的 content 换行显示实现


// 注意:\r\n在微信模拟器中无效,真机才行
const content =
"学校名:光谷一小\r\n" +
"班级名:501\r\n" +
"教师名:张哈哈\r\n"
uni.showModal({
title: "确认操作吗?",
content: content,
success: (res) => {
if (res.confirm) {
} else if (res.cancel) {
}
},
});

2. uniapp 解决 showToast 字数超过 7 个显示问题


使用 uni-app 开发小程序,不管是使用微信小程序的 wx.showToast() API 或 uni-app 的 uni.showToast() API 显示消息提示框,显示图标 title 文本最多显示 7 个汉字长度,在不显示图标的情况下,大于两行不显示。


解决方案一:如果要显示超过两行的文本,使用 uview-ui 框架的 Toast 消息提示组件。


// 先在html中引入组件
<u-toast ref="uToast" />;
// 然后调用
this.$refs.uToast.show({
title: "请学生绑定图书后再布置任务!",
duration: 1500,
});

解决方案二:不显示图标,就可以显示两行了


uni.showToast({
title: "请学生绑定图书后再布置任务!",
icon: "none",
duration: 1500,
});

3. uniapp 自定义弹框 uni-popup


复杂弹框,需要自定义内容的话,只能自己写了,可以使用 uni-popup 弹出层进行实现


<template>
<uni-popup ref="popup" class="lm-popup">
<view class="lm-popup-content">
<uni-icons
class="close"
type="closeempty"
size="24"
color="#ccc"
@click="hide"
>
</uni-icons>
<button class="btn confirm" @click="handleAuth">微信授权登录</button>
<button class="btn cancel" @click="handleLogin">手机验证码登录</button>
</view>
</uni-popup>
</template>

<script>
export default {
name: "login-popup",
methods: {
show() {
this.$refs.popup.open("center");
},
hide() {
this.$refs.popup.close();
},
},
};
</script>

uniapp 授权登录



主要介绍了uniapp授权的几种方式,分别为临时登录授权、手机号授权、用户信息授权



1. 微信登录授权


微信小程序的登录逻辑发生了变化,要求开发者使用静默登录,即在用户无感知的情况下进行登录操作,不需要弹出授权窗口了。


如下所示,获取微信的临时登录凭证 code,不会弹出授权窗口了。


uni.getProvider({
// 类型为oauth,用于获取第三方登录提供商
service: "oauth",
success: (res) => {
// 输出支持的第三方登录提供商列表
if (~res.provider.indexOf("weixin")) {
// 发起登录请求,获取临时登录凭证 code
uni.login({
// 登录提供商,如微信
provider: "weixin",
success: (loginRes) => {
// 获取用户登录凭证
this.handleLogin(loginRes.code);
},
});
}
},
});

2. 微信手机号授权


对于一般的用户信息,如头像、昵称等,被视为非敏感信息,以静默登录的方式进行获取。而用户的手机号等敏感信息,是需要授权的,可以通过 open-type="getPhoneNumber"属性来触发获取手机号码的授权弹框。


getPhoneNumber:获取用户手机号,可以从@getphonenumber 回调中获取到用户信息,该接口一直会弹出授权弹框,具体可查看官网:uniapp.dcloud.net.cn/component/b…


<button type="default" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">获取手机号</button>

// 打开获取用户手机号的授权窗口
getPhoneNumber(e) {
console.log('getPhoneNumber', e.detail)
console.log(e.detail.encryptedData); // 获取加密数据
console.log(e.detail.iv); // 获取加密算法的初始向量

const { detail:{ code, encryptedData, iv, errMsg } } = e;
if(errMsg === 'getPhoneNumber:ok') {
// 获取成功,做对应操作
}
}

uni-1.png


2.1 拦截默认弹框


如下图所示:一般情况下,需要先勾选隐私协议,才能弹窗。


但是@getphonenumber事件弹窗无法拦截的,就算事件中写了判断,还是会先弹窗的,没找到拦截的方法。


截屏2024-03-19 11.17.34.png


目前的解决方法,用了两个按钮来判断实现


<button v-if="!agree" class="login-btn" hover-class='zn-btn-green-hover' @click="handleSubmit('auth')">手机号验证登录</button>
<button v-else class="login-btn" hover-class='zn-btn-green-hover' open-type="getPhoneNumber" @getphonenumber="(e) => handleSubmit('auth', e)">手机号验证登录</button>

handleSubmit(type, e) {
if (!this.agree) {
uni.showToast({ title: '请勾选用户服务协议', icon: 'none' });
return;
}
}

3. 微信用户信息(头像、昵称)授权


可以使用uni.getUserProfile获取用户信息,如头像、昵称


该 API 对于低版本(基础库 2.10.4-2.27.0 版本),每次触发 uni.getUserProfile 才会弹出授权窗口;


我开发时,最新的基础库为 3.3.1,不会弹出授权窗口,直接获取到值了,也是静默授权状态。


<button type="default" size="mini" @click="getUserInfo">获取用户信息</button>

getUserInfo(e) {
// 获取用户信息
uni.getUserProfile({
desc: '获取你的昵称、头像、地区及性别',
success: res => {
console.log('获取你的昵称、头像',res);
},
fail: err => {
console.log("拒绝了", err);
}
});
}

uni-2.png


uniapp 跳转



主要介绍了 uniapp 小程序跳转的三种方式,分别为内部页面跳转、外部链接跳转、其他小程序跳转。



1. 内部页面


内部页面的跳转,可以通过如下方式:navigateTo、reLaunch、switchTab


// 保留当前页面,跳转到应用内的某个页面
uni.navigateTo({ url: "/pages/home/home" });

// 关闭所有页面,打开到应用内的某个页面
uni.reLaunch({ url: "/pages/home/home" });

// 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
uni.switchTab({ url: "/pages/home/home" });

2. 外部链接


通过 webview 打开外部网站链接,web-view 是一个 web 浏览器组件,可以用来承载网页的容器。


// 1. 新建pages/webview/webview.vue页面
<template>
<web-view :src="url"></web-view>
</template>
<script>
export default {
data() {
return {
url: ''
}
},
onLoad(e) {
// 使用web-view标签进行跳转
this.url = decodeURIComponent(e.url)
}
}
</script>


// 2. 外链跳转使用
uni.navigateTo({url: "https://www.taobao.com"})

3. 小程序 appId


通过navigateToMiniProgram可以打开其他小程序


// 打开其他小程序
uni.navigateToMiniProgram({
appId: "AppId", // 其他小程序的AppId
path: "pages/index/index", // 其他小程序的首页路径
extraData: {}, // 传递给其他小程序的数据
envVersion: "release", // 其他小程序的版本(develop/trial/release)
success(res) {
// 打开其他小程序成功的回调函数
},
fail(err) {
// 打开其他小程序失败的回调函数
},
});

作者:时光足迹
来源:juejin.cn/post/7331717626059817023
收起阅读 »

纯css实现无限循环滚动logo墙

web
一、需求 在许多网站的合作伙伴一栏,常常会看到一排排无限地循环滚动的logo墙。 不久前,接到一个类似的需求。需求如下: 1、无限循环滚动; 2、鼠标hover后,暂停滚动,鼠标离开后,继续滚动; 3、支持从左往右和从右往左滚动; 4、滚动速度需要可配置。 简...
继续阅读 »

完整示例.gif


一、需求


在许多网站的合作伙伴一栏,常常会看到一排排无限地循环滚动的logo墙。
不久前,接到一个类似的需求。需求如下:
1、无限循环滚动;
2、鼠标hover后,暂停滚动,鼠标离开后,继续滚动;
3、支持从左往右和从右往左滚动;
4、滚动速度需要可配置。


简单动画,我们先尝试只使用css实现。


二、实现


1、marquee标签

说到无限循环滚动,很久以前marquee标签可以实现类似的功能,它可以无限循环滚动,并且可以控制方向和速度。但是该标签在HTML5中已经被弃用,虽然还可以正常使用,但w3c不再推荐使用该特性。作为一个标签,只需要负责布局,而不应该有行为特性。


了解marquee标签:marquee标签


2、css3动画

说到无限循环滚动我们会想到使用css3动画。


把animation-iteration-count设置为infinite,代表无限循环播放动画。


为了使动画运动平滑,我们把animation-timing-function设置为linear,代表动画匀速运动。


<!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 {
padding-top: 200px;
}

@keyframes scrolling {
to {
transform: translateX(100vw);
}
}

.wall-item {
height: 90px;
width: 160px;
background-image: linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%);
animation: scrolling 4s linear infinite;
}
</style>
</head>
<body>
<div class="wall-item"></div>
</body>
</html>

我们上面实现了一个元素的滚动,在多个元素的时候,我们只需为每个元素设置不同的动画延迟时间(animation-delay),让每一项错落开来,就可以实现我们想要的效果了。
至于鼠标hover后暂停动画,我们只需在滚动元素hover时把animation-play-state设置为暂停即可。


有了以上思路,我很快就可以写出一个纯css实现logo墙无限循环滚动的效果。


完整示例代码:

<!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;
}

:root {
--wall-item-height: 90px;
--wall-item-width: 160px;
--wall-item-number: 9;
--duration: 16s;
}

body {
padding-top: 300px;
}

@keyframes scrolling {
to {
transform: translateX(calc(var(--wall-item-width) * -1));
}
}

.wall {
margin: 30px auto;
height: var(--wall-item-height);
width: 80vw;
position: relative;
mask-image: linear-gradient(90deg, hsl(0 0% 0% / 0),
hsl(0 0% 0% / 1) 20%,
hsl(0 0% 0% / 1) 80%,
hsl(0 0% 0% / 0));
}

.wall .wall-item {
position: absolute;
top: 0;
left: 0;
transform: translateX(calc(var(--wall-item-width) * var(--wall-item-number)));
height: var(--wall-item-height);
width: var(--wall-item-width);
background-image: linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%);
animation: scrolling var(--duration) linear infinite;
cursor: pointer;
}

.wall[data-direction="reverse"] .wall-item {
animation-direction: reverse;
background-image: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
}

.wall .wall-item:nth-child(1) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 1) * -1);
}

.wall .wall-item:nth-child(2) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 2) * -1);
}

.wall .wall-item:nth-child(3) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 3) * -1);
}

.wall .wall-item:nth-child(4) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 4) * -1);
}

.wall .wall-item:nth-child(5) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 5) * -1);
}

.wall .wall-item:nth-child(6) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 6) * -1);
}

.wall .wall-item:nth-child(7) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 7) * -1);
}

.wall .wall-item:nth-child(8) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 8) * -1);
}

.wall .wall-item:nth-child(9) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 9) * -1);
}

.wall:has(.wall-item:hover) .wall-item {
animation-play-state: paused;
}
</style>
</head>

<body>
<div class="wall" style="--duration:10s">
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
</div>
<div class="wall" data-direction="reverse" style="--wall-item-number:9">
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
</div>
</body>

</html>


  • 可配置项:


    --wall-item-height: 滚动项高度
    --wall-item-width: 滚动项宽度
    --wall-item-number: 滚动元素个数
    --duration: 动画时长


    我们也可以向上面示例代码一样,在.wall标签上,通过style属性,给每个滚动墙单独配置宽高、数量、动画时间。


  • 滚动方向:


    动画默认从左往右运动。我们可以在.wall标签上设置data-direction="reverse",让动画从右往左运动。



三、局限性



  1. 滚动元素(wall-item)太少,不足以填满包装元素(wall)时,会达不到预期效果;


    (解决办法:用js把所有子元素复制一份push到最后面)


  2. --wall-item-number默认为9,每次子元素数量变化,需手动修改--wall-item-number值。


选择器.png


(解决办法:使用js计算赋值。说到要用js,那么这一点局限性或许就可以忍受了。)

3. 需要手动为每一个滚动元素设置动画延迟时间(animation-delay)。


(解决办法:可用js计算赋值。)

作者:JasmineKii
来源:juejin.cn/post/7408441793790410804
收起阅读 »

对象有顺序吗?

web
前言 对象有顺序吗?换句话说,如果我们遍历一个对象,获取属性的顺序是和属性添加时的顺序一样吗?这靠谱吗?这篇文章将为你揭晓答案。 JavaScript 对象基础 在 JavaScript 中,一个对象是一个无序的键值对集合,键通常是字符串或者 Symbol,而...
继续阅读 »

前言


对象有顺序吗?换句话说,如果我们遍历一个对象,获取属性的顺序是和属性添加时的顺序一样吗?这靠谱吗?这篇文章将为你揭晓答案。


JavaScript 对象基础


在 JavaScript 中,一个对象是一个无序的键值对集合,键通常是字符串或者 Symbol,而值可以是任何类型的数据。对象的基本创建方式如下:


const obj = {
key1: 'value1',
key2: 'value2',
}

虽然我们通常认为对象的属性是无序的,但实际上,JavaScript 对对象属性的排列有其特定的规则。


属性顺序的规则


根据 ECMAScript 规范,JavaScript 对象属性的顺序受以下规则的影响:


整数属性,会按照升序排列


何为整数属性?指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串,也就是可以被解析为整数的字符串。


const obj = {
2: 'two',
1: 'one',
'3': 'three',
'a': 'alpha',
}

console.log(Object.keys(obj)); // -> ['1', '2', '3', 'a']

所有普通的字符串属性,按照其插入的顺序排列。


const obj = {
b: 'beta',
a: 'alpha',
c: 'gamma',
}

console.log(Object.keys(obj)); // -> ['b', 'a', 'c']

Symbol 类型的属性总是放在最后,并且保留其插入顺序。


const sym1 = Symbol('key1');
const sym2 = Symbol('key2');

const obj = {
[sym1]: 'value1',
1: 'one',
[sym2]: 'value2',
'a': 'alpha',
}

console.log(Object.keys(obj)); // -> ['1', 'a']
console.log(Object.getOwnPropertySymbols(obj)); // -> [sym1, sym2]

结合上述规则,我们再看一个综合示例。


const sym1 = Symbol("key1");
const sym2 = Symbol("key2");

const obj = {
[sym2]: "value2",
z: "last",
3: "three",
2: "two",
a: "alpha",
1: "one",
b: "beta",
[sym1]: "value1",
0: "zero",
};

console.log(Reflect.ownKeys(obj)); // -> ["0", "1", "2", "3", "z", "a", "b", Symbol("key2"), Symbol("key1")];

可以看到,整数属性按升序排列,普通属性按插入顺序排列,符号则放在最后并保留插入顺序。


最后


虽然 JavaScript 对象的属性被称为“无序”,但实际上它们有“特别的顺序”。整数属性会按升序排列,普通属性按插入顺序,Symbol 类型的属性总是排在最后并保留插入时的顺序


如果文中有错误或者不足之处,欢迎大家在评论区指正。


你的点赞是对我最大的鼓励!感谢阅读~


作者:掘金最后一个老实人
来源:juejin.cn/post/7409668839199883314
收起阅读 »

Flutter GPU 是什么?为什么它对 Flutter 有跨时代的意义?

Flutter 3.24 版本引入了 Flutter GPU 概念的新底层图形 API flutter_gpu ,还有 flutter_scene 的 3D 渲染支持库,它们目前都是预览阶段,只能在 main channel 上体验,并且依赖 Impel...
继续阅读 »

Flutter 3.24 版本引入了 Flutter GPU 概念的新底层图形 API flutter_gpu ,还有 flutter_scene 的 3D 渲染支持库,它们目前都是预览阶段,只能在 main channel 上体验,并且依赖 Impeller 的实现。



Flutter GPU 是 Flutter 内置的底层图形 API,它可以通过编写 Dart 代码和 GLSL 着色器在 Flutter 中构建和集成自定义渲染器,而无需 Native 平台代码。


目前 Flutter GPU 处于早期预览阶段并只提供基本的光栅化 API,但随着 API 接近稳定,会继续添加和完善更多功能。



详细说,Flutter GPU 是 Impeller 对于 HAL 的一层很轻的包装,并搭配了关于着色器和管道编排的自动化能力,也通过 Flutter GPU 就可以使用 Dart 直接构建自定义渲染器。


Flutter GPU 和 Impeller 一样,它的着色器也是使用 impellerc 提前编译,所以 Flutter GPU 也只支持 Impeller 的平台上可用。



Impeller 的 HAL 和 Flutter GPU 都没打算成为类似 WebGPU 这样的正式标准,相反,Flutter GPU 主要是由 Flutter 社区开发和发展,专职为了 Flutter 服务,所以不需要考虑「公有化」的兼容问题。



在 Flutter GPU 上,可直接从 Dart 与 Impeller 的 HAL 对话,甚至 Impeller Scene API(3D)也将作为重写的一部分出现。



说人话就是,可以用 Dart 通过 Flutter GPU 直接构建自定义渲染效果,未来直接支持 3D



可能有的人对于 Impeller 的整体结构和 HAL 还很模式无法理解,那么这里我们简单过一下:



  • 在 Framework 上层,我们知道 Widget -> Element -> RenderObject -> Layer 这样的过程,而后其实流程就来到了 Flutter 自定义抽象的 DisplayList

  • DisplayList 帮助 Flutter 在 Engine 做了接耦,从而让 Flutter 可以在 skia 和 Impeller 之间进行的切换

  • 之后 Impeller 架构的顶层是 Aiks,这一层主要作为绘图操作的高级接口,它接受来自 Flutter 框架的命令,例如绘制路径或图像,并将这些命令转换为一组更精细的 “Entities”,然后转给下一层。

  • Entities Framework,它是 Impeller 架构的核心组件,当 Aiks 处理完命令时生成 Entities 后,每一个 Entity 其实就是渲染指令的独立单元,其中包含绘制特定元素的所有必要信息(编码位置、旋转、缩放、content object),此时还不能直接作用于 GPU

  • HAL(Hardware Abstraction Layer) 则为底层图形硬件提供了统一的接口,抽象了不同图形 API 的细节,该层确保了 Impeller 的跨平台能力,它将高级渲染命令转换为低级 GPU 指令,充当 Impeller 渲染逻辑和设备图形硬件之间的桥梁。


所以 HAL 它包装了各种图形 API,以提供通用的设备作业调度接口、一致的资源管理语义和统一的着色器创作体验,而对于 Impeller , Entities (2D renderer) 和 Scene (3D renderer) 都是直接通过 HAL 对接,甚至可以认为,Impeller 的 HAL 抽象并统一了 Metal 和 Vulkan 的常见用法和相似结构。



Unity 现在也有在 C# 直接向用户公开其 HAL 版本,称为 "Scriptable Render Pipeline" ,并提供了两个基于该 API 构建的默认渲染器 "Universal RP" / "High Definition RP" 用于服务不同的场景,所以 Unity 开发可以从使用这些渲染器去进行修改或扩展一些特定渲染需求。





而在 Flutter 的设计上,Flutter GPU 会作为 Flutter SDK 的一部分,并以 flutter_gpu 的 Dart 包的形式提供使用。


当然,Flutter GPU 由 Impeller 支持,但重要的是要记住它不是 Impeller ,Impeller 的 HAL 是私有内部代码与 Flutter GPU 的要求非常不同, Impeller 的私有 HAL 和 Flutter GPU 的公共 API 设计之间是存在一定差异化实现,而前面的流程,如 Scene (3D renderer) ,也可以被调整为基于 Flutter GPU 的全新模式实现。


而通过 Flutter GPU,如曾经的 Scene (3D renderer) 支持,也可以被调整为基于 Flutter GPU 的全新模式实现,因为 Flutter GPU 的 API 允许完全控制渲染通道附件、顶点阶段和数据上传到 GPU。这种灵活性对于创建复杂的渲染解决方案(从 2D 角色动画到复杂的 3D 场景)至关重要。



Flutter GPU 支持的自定义 2D 渲染器的一个很好的用例:依赖于骨骼网格变形的 2D 角色动画格式。


Spine 2D 就是一个很好的例子,骨骼网格解决方案通常具有动画剪辑,可以按层次结构操纵骨骼的平移、旋转和缩放属性,并且每个顶点都有几个相关的“bone weights”,这些权重决定了哪些骨骼应该影响顶点以及影响程度如何。



使用像 drawVertices 这样的 Canvas 解决方案,需要在 CPU 上对每个顶点应用骨骼权重变换,而 使用 Flutter GPU,骨骼变换可以用统一数组或纹理采样器的形式发送到顶点着色器,从而允许根据骨架状态和每个顶点的 “bone weights” 在 GPU 上并行计算每个顶点的最终位置。


使用 Flutter GPU


首先你需要在最新的 main channel 分支,然后通过 flutter pub add flutter_gpu --sdk=flutter 将 flutter_gpu SDK 包添加到你的 pubspec。


为了使用 Flutter GPU 渲染内容,你会需要编写一些 GLSL 着色器,Flutter GPU 的着色器与 Flutter 的 fragment shader 功能所使用的着色器具有不同的语义,特别是在统一绑定方面,还需要定义一个顶点(vertex)着色器来与 fragment shader 一起使用,然后配合 gpu.ShaderLibrary 等 API 就可以直接实现 Flutter GPU 渲染。


当然,本篇不会介绍详细的 API 使用 ,这里只是单纯做一个简单的介绍,目前 Flutter GPU 进行光栅化的简单流程如下:



  • 获取 GPUContext。

  • GpuContext.createCommandBuffer 创建一个 CommandBuffer

  • CommandBuffer.createRenderPass 创建一个 RenderPass

  • 使用各种方法设置状态/管道并绑定资源 RenderPass

  • 附加绘图命令 RenderPass.draw

  • CommandBuffer 使用 CommandBuffer.submit (异步)提交绘制,所有 RenderPass 会按照其创建顺序进行编码


·····
///导入 flutter_gpu
import 'package:flutter_gpu/gpu.dart' as gpu;

ByteData float32(List<double> values) {
return Float32List.fromList(values).buffer.asByteData();
}

ByteData float32Mat(Matrix4 matrix) {
return Float32List.fromList(matrix.storage).buffer.asByteData();
}

class TrianglePainter extends CustomPainter {
TrianglePainter(this.time, this.seedX, this.seedY);

double time;
double seedX;
double seedY;

@override
void paint(Canvas canvas, Size size) {
/// Allocate a new renderable texture.
final gpu.Texture? renderTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate, 300, 300,
enableRenderTargetUsage: true,
enableShaderReadUsage: true,
coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture);
if (renderTexture == null) {
return;
}

final gpu.Texture? depthTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient, 300, 300,
format: gpu.gpuContext.defaultDepthStencilFormat,
enableRenderTargetUsage: true,
coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture);
if (depthTexture == null) {
return;
}

/// Create the command buffer. This will be used to submit all encoded
/// commands at the end.
final commandBuffer = gpu.gpuContext.createCommandBuffer();

/// Define a render target. This is just a collection of attachments that a
/// RenderPass will write to.
final renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: renderTexture),
depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthTexture),
);

/// Add a render pass encoder to the command buffer so that we can start
/// encoding commands.
final encoder = commandBuffer.createRenderPass(renderTarget);

/// Load a shader bundle asset.
final library = gpu.ShaderLibrary.fromAsset('assets/TestLibrary.shaderbundle')!;

/// Create a RenderPipeline using shaders from the asset.
final vertex = library['UnlitVertex']!;
final fragment = library['UnlitFragment']!;
final pipeline = gpu.gpuContext.createRenderPipeline(vertex, fragment);

encoder.bindPipeline(pipeline);

/// (Optional) Configure blending for the first color attachment.
encoder.setColorBlendEnable(true);
encoder.setColorBlendEquation(gpu.ColorBlendEquation(
colorBlendOperation: gpu.BlendOperation.add,
sourceColorBlendFactor: gpu.BlendFactor.one,
destinationColorBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha,
alphaBlendOperation: gpu.BlendOperation.add,
sourceAlphaBlendFactor: gpu.BlendFactor.one,
destinationAlphaBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha));

/// Append quick geometry and uniforms to a host buffer that will be
/// automatically uploaded to the GPU later on.
final transients = gpu.HostBuffer();
final vertices = transients.emplace(float32(<double>[
-0.5, -0.5, //
0, 0.5, //
0.5, -0.5, //
]));
final color = transients.emplace(float32(<double>[0, 1, 0, 1])); // rgba
final mvp = transients.emplace(float32Mat(Matrix4(
1, 0, 0, 0, //
0, 1, 0, 0, //
0, 0, 1, 0, //
0, 0, 0.5, 1, //
) *
Matrix4.rotationX(time) *
Matrix4.rotationY(time * seedX) *
Matrix4.rotationZ(time * seedY)));

/// Bind the vertex data. In this case, we won't bother binding an index
/// buffer.
encoder.bindVertexBuffer(vertices, 3);

/// Bind the host buffer data we just created to the vertex shader's uniform
/// slots. Although the locations are specified in the shader and are
/// predictable, we can optionally fetch the uniform slots by name for
/// convenience.
final mvpSlot = pipeline.vertexShader.getUniformSlot('mvp')!;
final colorSlot = pipeline.vertexShader.getUniformSlot('color')!;
encoder.bindUniform(mvpSlot, mvp);
encoder.bindUniform(colorSlot, color);

/// And finally, we append a draw call.
encoder.draw();

/// Submit all of the previously encoded passes. Passes are encoded in the
/// same order they were created in.
commandBuffer.submit();

/// Wrap the Flutter GPU texture as a ui.Image and draw it like normal!
final image = renderTexture.asImage();

canvas.drawImage(image, Offset(-renderTexture.width / 2, 0), Paint());
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

class TrianglePage extends StatefulWidget {
const TrianglePage({super.key});

@override
State<TrianglePage> createState() => _TrianglePageState();
}

class _TrianglePageState extends State<TrianglePage> {
Ticker? tick;
double time = 0;
double deltaSeconds = 0;
double seedX = -0.512511498387847167;
double seedY = 0.521295573094847167;

@override
void initState() {
tick = Ticker(
(elapsed) {
setState(() {
double previousTime = time;
time = elapsed.inMilliseconds / 1000.0;
deltaSeconds = previousTime > 0 ? time - previousTime : 0;
});
},
);
tick!.start();
super.initState();
}

@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Slider(
value: seedX,
max: 1,
min: -1,
onChanged: (value) => {setState(() => seedX = value)}),
Slider(
value: seedY,
max: 1,
min: -1,
onChanged: (value) => {setState(() => seedY = value)}),
CustomPaint(
painter: TrianglePainter(time, seedX, seedY),
),
],
);
}
}

GpuContext 是分配所有 GPU 资源并调度 GPU 的存在,而 GpuContext 仅有启用 Impeller 时才能访问。


DeviceBuffer 和 Texture 就是 GPU 拥有的资源,可以通过 GPUContext 创建获取,如 createDeviceBuffercreateTexture



  • DeviceBuffer 简单理解就是在 GPU 上分配的简单字节串,主要用于存储几何数据(索引和顶点属性)以及统一数据

  • Texture 是一个特殊的设备缓冲区


CommandBuffer 用于对 GPU 上的异步执行进行排队和调度工作。


RenderPass 是 GPU 上渲染工作的顶层单元。


RenderPipeline 提供增量更改绘制所有状态以及附加绘制调用的方法如 RenderPass.draw()


可以想象,通过 Flutter GPU,Flutter 开发者可以更简单地对 GPU 进行更精细的控制,通过与 HAL 直接通信,创建 GPU 资源并记录 GPU 命令,从而最大限度的发挥 Flutter 的渲染能力。


另外,对于 3D 支持的 Flutter Scene , 可以通过使用 native-assets 来设置 Flutter Scene 的 3D 模型自动导入,通过导入编译模型 .model 之后,就可以通过 Dart 实现一些 3D 的渲染。


import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_scene/camera.dart';
import 'package:flutter_scene/node.dart';
import 'package:flutter_scene/scene.dart';
import 'package:vector_math/vector_math.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatefulWidget {
const MyApp({super.key});

@override
MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
double elapsedSeconds = 0;
Scene scene = Scene();

@override
void initState() {
createTicker((elapsed) {
setState(() {
elapsedSeconds = elapsed.inMilliseconds.toDouble() / 1000;
});
}).start();

Node.fromAsset('build/models/DamagedHelmet.model').then((model) {
model.name = 'Helmet';
scene.add(model);
});

super.initState();
}

@override
Widget build(BuildContext context) {
final painter = ScenePainter(
scene: scene,
camera: PerspectiveCamera(
position: Vector3(sin(elapsedSeconds) * 3, 2, cos(elapsedSeconds) * 3),
target: Vector3(0, 0, 0),
),
);

return MaterialApp(
title: 'My 3D app',
home: CustomPaint(painter: painter),
);
}
}

class ScenePainter extends CustomPainter {
ScenePainter({required this.scene, required this.camera});
Scene scene;
Camera camera;

@override
void paint(Canvas canvas, Size size) {
scene.render(camera, canvas, viewport: Offset.zero & size);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}



目前 Flutter GPU 和 Flutter Scene 的支持还十分有限,但是借助 Impeller ,Flutter 开启了新的可能,可以说是,Flutter 团队完全掌控了渲染堆栈,在除了自定义更丰富的 2D 场景之外,也为 Flutter 开启了 3D 游戏的可能,2023 年 Flutter Forward 大会的承诺,目前正在被落地实现




详细 API 使用例子可以参看 :medium.com/flutter/get…



如果你对 Flutter Impeller 和其着色器感兴趣,也可以看:



作者:恋猫de小郭
来源:juejin.cn/post/7399985723673821193
收起阅读 »

常见呼吸灯闪烁动画

web
最近在需求里遇到了一个手指引导交互的动画需求。这篇文章就来讲讲如何CSS实现这个动画,如下图所示: 简单分析了一下效果,是一个手指移动到某处位置,然后会触发呼吸灯闪烁的效果,所有实现整个动画可以分两步: 呼吸灯闪烁动画 这里介绍下我遇到过得几种呼吸灯闪烁动画...
继续阅读 »

最近在需求里遇到了一个手指引导交互的动画需求。这篇文章就来讲讲如何CSS实现这个动画,如下图所示:


bms7p-ri1ho.gif
简单分析了一下效果,是一个手指移动到某处位置,然后会触发呼吸灯闪烁的效果,所有实现整个动画可以分两步:


呼吸灯闪烁动画


这里介绍下我遇到过得几种呼吸灯闪烁动画


第一种效果


2024-08-31 09.57.37.gif


    @keyframes twinkling {
0% {
opacity: 0.2;
transform: scale(1);
}

50% {
opacity: 0.5;
transform: scale(1.12);
}

100% {
opacity: 0.2;
transform: scale(1);
}
}
.circle {
border-radius: 50%;
width: 12px;
height: 12px;
background: green;
position: absolute;
top: 36px;
left: 36px;

&::after {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
border-radius: 50%;
background: greenyellow;
animation: twinkling 1s infinite ease-in-out;
animation-fill-mode: both;
}
}

第二种效果


2024-08-31 10.07.28.gif


    @keyframes scale {
0% {
transform: scale(1)
}

50%,
75% {
transform: scale(3)
}

78%,
100% {
opacity: 0
}
}

@keyframes scales {
0% {
transform: scale(1)
}

50%,
75% {
transform: scale(2)
}

78%,
100% {
opacity: 0
}
}

.circle {
position: absolute;
width: 12px;
height: 12px;
background-color: pink;
border-radius: 50%;
top: 36px;
left: 36px;
}

.circle:before {
content: '';
display: block;
width: 12px;
height: 12px;
border-radius: 50%;
opacity: .4;
background-color: pink;
animation: scale 1s infinite cubic-bezier(0, 0, .49, 1.02);
}

.bigcircle {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
opacity: .4;
background-color: pink;
top: 36px;
left: 36px;
animation: scales 1s infinite cubic-bezier(0, 0, .49, 1.02);
}


第三种效果


2024-08-31 10.17.25.gif


 @keyframes scaless {
0% {
transform: scale(1)
}

50%,
75% {
transform: scale(3)
}

78%,
100% {
opacity: 0
}
}

.circle {
position: absolute;
width: 12px;
height: 12px;
background-color: transparent;
border-radius: 50%;
top: 36px;
left: 36px;
}

.circle:before {
content: '';
display: block;
width: 12px;
height: 12px;
border-radius: 50%;
opacity: .7;
border: 3px solid hotpink;
background-color: transparent;
animation: scaless 1s infinite cubic-bezier(0, 0, .49, 1.02);
}

小手移动动画


    @keyframes animation-hand-move {
0% {
transform: translate(0, 0);
}

20% {
transform: translate(-80px, -50px);
}

25% {
transform: translate(-80px, -50px) scale(0.92) rotate(-3deg);
opacity: 1;
}

75% {
transform: translate(-80px, -50px) scale(0.92) rotate(-3deg);
opacity: 1;
}

90% {
transform: translate(-50px, -40px) scale(1) rotate(0deg);
opacity: 0.6;
}

100% {
opacity: 0;
}
}
.hard {
position: absolute;
top: 200px;
left: 318px;
width: 61px;
height: 69px;
background-size: contain;
background-repeat: no-repeat;
background-image: url('./img//174ba1f81a4d0d91a7dc45567b59fd8b.png');
animation: animation-hand-move 4s infinite linear;
}

完整动画代码


  <div class="card">
<View class="round circle"></View>
<View class="round small-circle"></View>
<View class="round less-circle"></View>
<div class="hard"></div>
</div>

.card {
margin: 100px auto;
width: 480px;
height: 300px;
background-color: #333333;
border-radius: 16px;
position: relative;
}

.hard {
position: absolute;
top: 200px;
left: 318px;
width: 61px;
height: 69px;
background-size: contain;
background-repeat: no-repeat;
background-image: url('./img/174ba1f81a4d0d91a7dc45567b59fd8b.png');
animation: animation-hand-move 4s infinite linear;
}

.round {
position: absolute;
top: 144px;
left: 227px;
width: 32px;
height: 32px;
border-radius: 50%;
background-color: transparent;

&::before {
content: '';
position: absolute;
border-radius: 50%;
background: transparent;
width: 100%;
height: 100%;
top: 50%;
left: 50%;
border: 6px solid rgba(255, 255, 255, 0.5);
pointer-events: none;
transform: translate(-50%, -50%) scale(0);
opacity: 1;
}
}

.circle {
&::before {
animation: animation-wave 4s infinite linear;
}
}

.small-circle {
&::before {
animation: animation-small-wave 4s infinite linear;
}
}

.less-circle {
&::before {
animation: animation-less-wave 4s infinite linear;
}
}

@keyframes animation-wave {

0%,
20%,
100% {
opacity: 0;
}

25% {
width: 0px;
height: 0px;
transform: translate(-50%, -50%);
opacity: 1;
}

75% {
width: 64px;
height: 64px;
transform: translate(-50%, -50%);
opacity: 0;
}
}

@keyframes animation-small-wave {

0%,
20%,
100% {
opacity: 0;
}

42% {
width: 0px;
height: 0px;
transform: translate(-50%, -50%);
opacity: 1;
}

75% {
width: 64px;
height: 64px;
transform: translate(-50%, -50%);
opacity: 0;
}
}

@keyframes animation-less-wave {

0%,
20%,
100% {
opacity: 0;
}

59% {
width: 0px;
height: 0px;
transform: translate(-50%, -50%);
opacity: 1;
}

75% {
width: 64px;
height: 64px;
transform: translate(-50%, -50%);
opacity: 0;
}
}

@keyframes animation-hand-move {
0% {
transform: translate(0, 0);
}

20% {
transform: translate(-80px, -50px);
}

25% {
transform: translate(-80px, -50px) scale(0.92) rotate(-3deg);
opacity: 1;
}

75% {
transform: translate(-80px, -50px) scale(0.92) rotate(-3deg);
opacity: 1;
}

90% {
transform: translate(-50px, -40px) scale(1) rotate(0deg);
opacity: 0.6;
}

100% {
opacity: 0;
}
}

作者:天动万象
来源:juejin.cn/post/7408795408861921290
收起阅读 »

如何写出让同事崩溃的代码

web
    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。 一、方法或变量名字随便取     首先,让同事看不懂自己代码的第一步就是,想尽办法让他看...
继续阅读 »



    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。


一、方法或变量名字随便取


    首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或者方法到底是干嘛用的。哎!对,就是让他去猜,哈哈哈。
来来来,空说没意思,举个栗子图片


    假设现在想要点击某个input框时,显示一个自定义的组件用于选择选择时间。


    正常的写法如下:定义一个 toggleDatePicker 方法
image.png


    这个一看就知道是时间选择器的显示切换方法。


    但是呢,我偏不,定义成下面这样:让同事摸不着头脑,哈哈哈


image.png
当看到很多这样的方法名或变量名时,同事的表情估计时这样的图片图片图片


接下来,第二招图片图片图片


二、方法体尽可能的长,长到不能在长


    这一步至关重要,将所有逻辑全部放在一个方法中写完,坚决不分步骤,不按逻辑一步步拆分方法。让同事为我的超长方法体感到叹为观止,默默流泪。


    老规矩,上栗子


    假设现在有个方法需要处理比较复杂(需要递归,而且每层数据有不同的类型)的json格式的数据回显到页面上(这是用于拼一些条件)。数据格式大概是这样的


[
{
type: "group",
relation: "or",
conditions: [
{
type: "condition",
field: {
name: "员工状态",
id: 12345678
},
logic: "=",
val: 1,
relation: "and"
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2011-07-09",
relation: "and"
}
]
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2001-07-09",
relation: "and"
}
]

    由于上面的 json 数组格式中是一个对象,对象都有 type 属性,一种是 group 类型(表示是一组条件),一种是 condition 类型(表示是真正的条件内容)。


    因此,正常情况下,我们应该是遍历数组,不同的类型分别用不同的方法去处理,并且在处理条件中各个部分的时候分别处理。如下所示:


image.png


image.png


    但是呢?咱主题是啥,让同时崩溃啊,怎么能把方法拆成这样清晰的逻辑呢。图片图片图片


    来给你看看怎么让同事对你的代码叹为观止


image.png


image.png


image.png


image.png


image.png


image.png


    怎么样,牛不牛,一个方法写完所以逻辑,从1825行一口气写到2103行,足足... 2103 - 1825 是多少来着,3减5 不够,向前借位 ,嗯。。。278 行。
****怎么样,有没有被哥的机智震惊到,如此代码。同事看到肯定心生敬佩,连连称绝。此时想到得到同事应该是这样的表情 图片图片图片


    同事还没进到方法体里面,就已经被我的 迷之方法名 和 超长方法体 所折服,接下来就让他在方法体里面快乐的遨游吧
    


    接下来,继续让同时崩溃。


三、坚决不定义统一的变量


    这个怎么说呢,就是因为有点懒,所有很多代码直接复制粘贴,多么的方便快捷。


    正常情况下,如果某个我们需要的对象在是其他对象的属性,并且层次很深,我们先定义一个变量来接收这个对象,再对这个对象操作。


例如:


let a = {
b: {
c: {
d: {
name: "我是最里面的对象"
}
}
}
}

    我们要对d对象进行很多次的操作时,一般先将d赋值给一个变量,然后对变量操作。如下:


var dOfA = a.b.c.d;
dOfA.name = "我现在被赋值给dOfA ";
dOfA.data = 1;
dOfA.other = false;

    但是呢,我就不这么干,就是要写得整整齐齐


a.b.c.d.name = "就要这么干,你打我呀";
a.b.c.d.data = 1;
a.b.c.d.other = false;

    老规矩,没有 实际的 栗子 怎么能说的形象呢,上图


    正常写法:


image.png


    我偏要这么写


image.png


    多么的整齐划一,


    全场动作必须跟我整齐划一


    来左边儿 跟我一起画个龙


    在你右边儿 画一道彩虹


    来左边儿 跟我一起画彩虹...


    咋突然哼起歌来了,不对,咱是要整同事的,怎么能偏题。


    继续,此时同事应该是这个表情图片图片图片


    然后,方法体里面只有这么点东西怎么够玩,继续 come on


四、代码能复制就复制,坚决不提成公用的方法


    代码能 CV ,干嘛费劲封装成方法,而且这样不是显得我代码行数多吗?图片图片图片


    就是玩儿,就是不封装


    来,上栗子


image.png


image.png


image.png


    看到没有,相同的代码。我在 1411行 - 1428行 写了一遍, 后面要用,在1459行-1476行复制一遍


    这还不够?那我在1504-1521行再复制一遍


    这下,爽了吧,哈哈哈


    就是不提方法,就是玩儿,哎! 有意思


    这个时候同事估计是这样的吧图片图片图片


    怎么样,是不是很绝?不不不,这算啥


    虽然以上这些会让看我代码的同事头疼,但是,只要我在公司,他们还会让我改啊。怎么能搞自己呢。


    最后一步


五、离职


    洋洋洒洒的写完代码,尽早离开。够不够绝,哈哈哈


六、申明:


    以上场景纯属个人虚构的,单纯为了给文章增加点乐趣。写这个文章的目的是让各位程序员兄弟尽量避免写这种难以维护的代码。真的太痛苦了!代码质量、代码重构真的是编程过程中很重要的一个步骤。不能抱着能用就行的心态。还是要对自己有一定的要求。只有你看得起自己的代码,别人才有可能看得起你的代码。加油吧!各位


作者:猩兵哥哥
来源:juejin.cn/post/7293888785400856628
收起阅读 »

我花了一天时间,做了一个图片上传组件,看起来很酷实际上确实很酷

web
今天,我花了一天的时间做了一个图片上传组件。效果如下: 可能有人觉得,这个组件很简单,没什么技术含量,其实确实也啥技术含量。但是,我是想借这个组件,来表达一种封装的思想在里面,希望可以帮助到一些朋友。 简单的描述下这个组件的功能: 用户可以点击下面颜色比较...
继续阅读 »

今天,我花了一天的时间做了一个图片上传组件。效果如下:


1721809724352-1721805313079-1721805282575-1721805255491-20240724_151251.gif


可能有人觉得,这个组件很简单,没什么技术含量,其实确实也啥技术含量。但是,我是想借这个组件,来表达一种封装的思想在里面,希望可以帮助到一些朋友。


简单的描述下这个组件的功能:



  • 用户可以点击下面颜色比较绚丽的上传按钮,选择本地图片进行上传,也可以直接点击图片区域进行上传。

  • 上传过程中,会有一个上传中的进度条,上传完成后,会有一个上传成功的提示,如果失败了,会有一个上传失败的提示,而且支持重试。

  • 可以点击图片右上角的删除按钮,删除图片。

  • 并发控制,最多只能同时上传 N 张图片,也就是所谓的限频,这里是 2 张。


是不是看了这么多功能之后,就开始有点头皮发麻了?哈哈,不要怕,这就带你了解下,如何拆解这种功能,而且,学会了这种拆解的办法,后面你遇到更加复杂的,也可以得心应手。


拆解功能,逐步实现


首先,我们思考,我们该使用自底向上的思路,还是自顶向下的思路来拆解这个功能呢?我的建议自顶向下的思路去思考架构,然后自底向上的去实现功能。


架构图


因为我们这个图片上传组件是支持多长图片同时上传的,而且,我们还需要支持上传失败重试的功能,所以,为了让功能更加聚焦,我们把关注点放在 PhotoItem 上,没一个 PhotoItem 就是一个图片上传的单元。他可以独立的上传,独立的删除,独立的重试。


那么,为了让 PhotoItem 这个组件更加简洁,我们把上传逻辑放在hooks useUpload中,这样,PhotoItem 只需要关注自己的展示逻辑即可。


这样做的目的是做到关注点分离,通常来讲,也是符合单一职责原则的。写出来的组件维护性必定大大提升。


代码实现


我们先来看下 useUpload 的代码,因为PhotoItem 依赖他,我们先实现它。


"use client";
export const useUploader = (uploadAction) => {
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState(null);
const upload = useCallback(async (file) => {
setIsUploading(true);
setError(null);
try {
return await uploadAction(file);
} catch (err) {
setError(err.message || 'Upload failed');
} finally {
setIsUploading(false);
}
}, [uploadAction]);

const reset = useCallback(() => {
setIsUploading(false);
setError(null);
}, []);

return { upload, isUploading, error, reset };
};

可以看到,我们的 hooks 非常之简单,就是暴露了一个实现图片上传的狗子 upload,然而,他替我们的组件管理了上传中,上传失败,的状态,因此,接下来看,我们的PhotoItem 组件将会有多清晰。


export const PhotoItem = ({
file,
onRemove,
onUploadComplete,
onUploadError,
}
) => {
const { upload, isUploading, error, reset } = useUploader();

const startUpload = useCallback(async () => {
try {
const url = await upload(file);
onUploadComplete(url);
} catch (err) {
onUploadError();
}
}, [file, upload, onUploadComplete, onUploadError]);

useEffect(() => {
startUpload();
}, [queueUpload, startUpload]);

const handleRetry = () => {
reset();
startUpload();
};

return (
<div className="relative w-full h-20">
<img
src={URL.createObjectURL(file)}
/>

{!isUploading && !error(
Uploaded
)}
{isUploading && (
<Progress />
)}
{error && (
<span>Failed</span>
)}
</div>

);
};

OK,到目前为止,还是极其简单的,但是我们貌似忘记了一个很核心的功能,限制并发数。为什么要限制并发数,因为我们自己的服务器或者三方的服务器,可能会有并发数的限制,如果我们不限制并发数,可能会导致一次传多张图片是卡住。


思考,如何限制并发数


我们想一样,是谁触发了上传的呢?是不是 PhotoItem 组件呢?是的,我们可以在 PhotoItem 组件中,去控制并发数,但是,这样做,会导致 PhotoItem 组件的逻辑变得复杂,因为他不仅要关注自己的展示逻辑,还要关注并发数的控制逻辑。这就显的不太合适了。所以,我们应该把他丢出去对吧,截止到目前为止,我们的PhotoUploader 这个组件似乎并没有干任何事情,我们思考下,并发控制的逻辑是否应该是他来呢?


答案是显然的,我们应该把并发控制的逻辑放在 PhotoUploader 组件中,因为他是整个上传组件的入口,他应该关注并发控制,而不是 PhotoItem 组件,而且最本质的原因是,PhotoItem 也不关心是否有其他的 PhotoItem 。


那么,问题来了,并发控制怎么写呢?使用什么数据结构较为合适呢?不卖关子了,我们知道,队列是最合适的数据结构,因为他是先进先出的,我们可以把上传任务放在队列中,然后,每次上传完成,就从队列中取出一个任务,继续上传。


好,我们改造一下,我们的 PhotoItem 组件,让他不要直接执行上传逻辑,而是把他做成一个任务,然后,把任务放在队列中,然后,我们在 PhotoUploader 组件中,去控制并发数。



export const PhotoItem = ({
file,
onRemove,
...
queueUpload // 加一个队列操作器
}
) => {
const { upload, isUploading, error, reset } = useUploader();
...

useEffect(() => {
queueUpload(startUpload); // 修改这里
}, [queueUpload, startUpload]);


const handleRetry = () => {
reset();
queueUpload(startUpload);//修改这里
};

// .... 其他几乎不变


在来看看我们的 PhotoUploader 组件,他是如何控制并发数的。很简单,我们只需要维护一个队列,然后,每次上传完成,就从队列中取出一个任务,继续上传。


  const processQueue = useCallback(() => {
while (activeUploadsRef.current < MAX_CONCURRENT_UPLOADS && uploadQueueRef.current.length > 0) {
const nextUpload = uploadQueueRef.current.shift();
activeUploadsRef.current++;
nextUpload();
}
}, []);

const queueUpload = useCallback((startUpload) => {
if (activeUploadsRef.current < MAX_CONCURRENT_UPLOADS) {
activeUploadsRef.current++;
startUpload();
} else {
uploadQueueRef.current.push(startUpload);
}
}, []);


这里,只给出最最核心的逻辑,实际上就是维护的了一个任务队列,然后,每次上传完成,就判断下队列中是否还有任务,并且是否超过了并发数,如果没有超过,并且队列中还有任务,就继续上传。仅此而已。


总结一下


这个图片上传组件,看似简单,但是,他涉及到了很多的知识点,比如并发控制,上传失败重试,组件拆解,自顶向下的架构设计,自底向上的功能实现。我们在实现这个组件的过程中。有过很多的思考,比如:



  • 如何拆解功能,让组件更加聚焦,做到关注点分离。

  • 控制并发数,使用队列是最合适的数据结构。

  • 如何设计一个 hooks,让组件更加简洁。

  • 以及自顶向下的架构设计,自底向上的功能实现。


只有建立起这些系统性的思维,我们才能在遇到更加复杂的问题时,得心应手。希望这篇文章对你有所帮助。


欢迎关注我老码沉思录,获取我最新的知识分享。


作者:brzhang
来源:juejin.cn/post/7394854112510951443
收起阅读 »

Flutter 七年之约:2024 年将带来哪些惊喜?

Flutter 作为目前最出色的跨平台开发框架之一,自2017年Google向公众发布它以来,已经走过了七年的历程。在这段时间里,成千上万的开发者和团队选择了使用 Flutter 来开发他们的应用程序。时至今日,Google 仍然坚定地支持着Flutter的发...
继续阅读 »

flutter-2024-roadmap 2.webp



Flutter 作为目前最出色的跨平台开发框架之一,自2017年Google向公众发布它以来,已经走过了七年的历程。在这段时间里,成千上万的开发者和团队选择了使用 Flutter 来开发他们的应用程序。时至今日,Google 仍然坚定地支持着Flutter的发展。接下来就让我们通过Flutter的2024年的路线图,一起来了解 Flutter 的未来规划吧。




  • 作为一个开源项目,Flutter 团队希望通过公开计划来增加透明度,并邀请社区成员共同参与项目的开发。这份路线图涵盖了核心框架和引擎的改进、对移动和桌面平台的支持,以及生态系统的扩展

  • 这份路线图充满了前瞻性,展示了 Flutter 和 Dart 社区中最活跃的贡献者今年计划进行的工作。通常情况下,很难对所有的工程工作做出明确的承诺,尤其是对于一个拥有数百名贡献者的开源项目来说。


核心框架和引擎


Flutter 将继续专注于通过 Impeller 提升质量和性能。Flutter 的目标是完成 iOS 向 Impeller 的迁移,这包括移除 iOS 上的 Skia 后端。在 Android 上,Flutter 预计 Impeller 将支持 Vulkan 和 OpenGLES。不过短期内,用户也可以选择继续使用 Skia。此外,Flutter 还计划改进 Impeller 的测试基础设施,以减少生产中的回归问题。


对于核心框架,Flutter 计划全面支持 Material 3。Flutter 也在研究如何让核心框架更通用化,以更好地支持苹果设备的设计需求,比如应用栏和选项卡栏的优化。


Flutter 还将继续推进 blankcanvas项目的工作。


移动平台(Android 和 iOS)


在 2023 年,Flutter 启动了支持多个 Flutter 视图的计划。到 2024 年,Flutter计划将这一支持扩展到 Android 和 iOS 平台。Flutter也在努力提高平台视图的性能,以及测试覆盖率和可测试性。


Flutter将继续通过支持最新的苹果标准(如 隐私清单 和 Swift 包管理器)来提升 iOS 产品的现代化水平。同时,Flutter也在评估对未来 Android 版本的支持需求。


在 Android 平台上,Flutter会研究如何在构建文件中支持 Kotlin。


互操作性对 Dart 与本地代码的交互非常重要。Flutter计划完成从 Dart 直接调用 Objective C 代码的支持工作,并探索如何支持直接调用 Swift 代码。同样,Flutter将继续改进 调用 Java 和 Android 的功能。Flutter还在研究如何更好地支持只能在主操作系统或平台线程上调用的 API。


越来越多的大型 Flutter 应用开始作为混合应用(包含 Flutter 代码和部分 Android/iOS 平台代码或 UI)开发。Flutter将研究如何在性能、资源开销和开发体验方面更好地支持这种模式。


Web 平台


Flutter 将继续专注于提升 Web 平台的性能和质量。这包括研究如何减少应用程序的整体大小、更好地利用多线程、支持平台视图、改善应用加载时间、将 CanvasKit 设为默认渲染器、改进文本输入,并探索支持 Flutter web 的 SEO 的选项


Flutter计划完成将 Dart 编译为 WasmGC 的工作,以支持 Wasm 编译 Flutter web 应用。这还包括一个新的 Dart 的 JS 互操作 机制,支持 JS 和 Wasm 编译。


Flutter还计划恢复对 web 上的热重载 的支持。


桌面平台


虽然 Flutter 的主要精力将继续放在移动和 Web 平台上,但Flutter也计划在桌面平台上进行一些改进:



  • Flutter希望在 macOS 和 Windows 上支持平台视图,以便支持例如 webview 的功能。

  • 在 Linux 上,Flutter的重点是支持 GTK4 和提升无障碍性。

  • 在所有平台上,Flutter将继续探索如何从一个 Dart isolate 支持多个视图,最终实现从一个小部件树渲染多个窗口。


生态系统


Flutter 计划与 AI 框架合作,推动 AI 驱动的 Flutter 应用的新时代。


Flutter不会扩大Flutter维护的 flutter.dev 插件 集合,而是专注于提升现有插件的质量,并解决核心功能差距(例如,改进 shared_preferences API,使其更好地支持隔离使用和集成到应用场景中)。Flutter还将支持社区倡议,如 Flutter Favorites。


Flutter还将继续与 Flame 社区合作,支持使用 Flutter 构建休闲游戏。


工具和 AI


Flutter 希望通过集成 AI 解决方案,为开发者提供编程任务的 AI 支持。


Flutter也将继续与谷歌的 IDX 团队 合作,探索与设计工具的集成。


编程语言


Dart 团队计划完成对在 Dart 中支持 宏 的可行性评估,并在 2024 年启动对宏的初步支持。如果发现不可克服的架构问题,Flutter将重新评估这一努力。宏的关键应用场景包括序列化/反序列化、数据类和通用扩展性。


Flutter将研究多个渐进的语言特性,例如减少冗长的语法(如 主构造函数 和 导入语法简写),以及更好地支持静态检查的类型差异。


最后,Flutter将探索如何在更多地方重用 Dart 业务逻辑,并为 Dart 提供更多的插件化和扩展性(例如在 DevTools 和分析器中)。


发布


Flutter 计划在 2024 年进行四次稳定版发布和 12 次测试版发布,这与 2023 年的节奏相似。


最后


最后,大家最关心的Flutter热更新合适支持? 在2024 Flutter团队依然没有支持热更新的计划。



作者:CrazyCodeBoy
来源:juejin.cn/post/7408848095615713295
收起阅读 »

改善生活的 50 个秘诀

Happiness 幸福You don’t have to love your job. Jobs are a way to make money. Many people live fine lives in okay jobs by using the m...
继续阅读 »

Happiness 幸福

  1. You don’t have to love your job. Jobs are a way to make money. Many people live fine lives in okay jobs by using the money they make on things they care about.
    1.你不必热爱你的工作。工作是赚钱的一种方式。许多人通过将赚到的钱花在自己关心的事情上,从事着不错的工作,过着美好的生活。

2. Sturgeon’s law states that 90% of everything is crap. If you dislike poetry, or fine art, or anything, it’s possible you’ve only ever seen the crap. Go looking!
2 .斯特金定律指出,90% 的事物都是垃圾。如果你不喜欢诗歌、美术或任何东西,那么你可能只见过垃圾。去寻找吧!

3. People don’t realize how much they hate commuting. A nice house farther from work is not worth the fraction of your life you are giving to boredom and fatigue.
3 .人们没有意识到他们有多么讨厌通勤。远离工作地点的好房子不值得你把生活中的一小部分时间花在无聊和疲劳上。

4. There’s some evidence that introverts and extroverts both benefit from being pushed to be more extroverted. Consider this the next time you aren’t sure if you feel like going out.
4 .有一些证据表明,内向者和外向者都可以从被迫变得更加外向中受益。下次当您不确定是否想出去时请考虑这一点。

Success 成功

5. History remembers those who got to market first. Getting your creation out int0 the world is more important than getting it perfect.
5 .历史会记住那些最先进入市场的人。将您的创作推向世界比使其完美更重要。

6. Are you on the fence about breaking up or leaving your job? You should probably go ahead and do it. People, on average, end up happier when they take the plunge.
6 .您是否在分手或离职方面犹豫不决?你或许应该继续去做。平均而言,人们在冒险时最终会更快乐。

7. Done is better than perfect.
7 .完成比完美更好。

  1. Discipline is superior to motivation. The former can be trained, the latter is fleeting. You won’t be able to accomplish great things if you’re only relying on motivation.
    8.纪律优于激励。前者是可以训练的,后者是转瞬即逝的。如果你仅仅依靠动力,你将无法完成伟大的事情。
  2. You can improve your communication skills with practice much more effectively than you can improve your intelligence with practice. If you’re not that smart but can communicate ideas clearly, you have a great advantage over everybody who can’t communicate clearly.
    9.通过练习提高你的沟通技巧比通过练习提高你的智力更有效。如果你不那么聪明,但可以清晰地表达想法,那么你比那些不能清晰表达的人有很大的优势。
  3. You do not live in a video game. There are no pop-up warnings if you’re about to do something foolish, or if you’ve been going in the wrong direction for too long. You have to create your own warnings.
    10.你不是生活在电子游戏中。如果您要做一些愚蠢的事情,或者您已经走错方向太久了,那么不会弹出警告。您必须创建自己的警告。

11. If you listen to successful people talk about their methods, remember that all the people who used the same methods and failed did not make videos about it.
11 .如果您听成功人士谈论他们的方法,请记住,所有使用相同方法但失败的人都没有制作有关该方法的视频。

  1. The best advice is personal and comes from somebody who knows you well. Take broad-spectrum advice like this as needed, but the best way to get help is to ask honest friends who love you.
  2. 最好的建议是针对个人的,来自熟悉你的人。根据需要接受此类广泛的建议,但获得帮助的最佳方法是询问爱你的诚实朋友。
  3. Make accomplishing things as easy as possible. Find the easiest way to start exercising. Find the easiest way to start writing. People make things harder than they have to be and get frustrated when they can’t succeed. Try not to.
  4. 让完成事情尽可能简单。找到开始锻炼的最简单方法。找到开始写作的最简单方法。人们让事情变得比他们必须的更困难,当他们不能成功时就会感到沮丧。尽量不要这样做。
  5. Cultivate a reputation for being dependable. Good reputations are valuable because they’re rare (easily destroyed and hard to rebuild). You don’t have to brew the most amazing coffee if your customers know the coffee will always be hot.
  6. 培养可靠的声誉。良好的声誉之所以珍贵,是因为它们很稀有(容易被摧毁,很难重建)。如果您的顾客知道咖啡永远是热的,您就不必煮出最美味的咖啡。

15. How you spend every day is how you spend your life.
15 .你怎样度过每一天,你就怎样度过一生。

Relationships 人际关系

  1. In relationships look for somebody you can enjoy just hanging out near. Long-term relationships are mostly spent just chilling.
  2. 在人际关系中寻找一个你可以享受在一起闲逛的人。长期的关系大多只是在寒冷中度过。
  3. Don’t complain about your partner to coworkers or online. The benefits are negligible and the cost is destroying a bit of your soul.
  4. 不要向同事或网上抱怨你的伴侣。好处是微不足道的,代价是摧毁你的灵魂。
  5. After a breakup, cease all contact as soon as practical. The potential for drama is endless, and the potential for a good friendship is negligible. Wait a year before trying to be friends again.
  6. 分手后,尽快停止所有联系。戏剧的潜力是无穷无尽的,而建立良好友谊的潜力则可以忽略不计。等一年再尝试再次成为朋友。

19. When dating, de-emphasizing your quirks will lead to 90% of people thinking you’re kind of alright. Emphasizing your quirks will lead to 10% of people thinking you’re fascinating and fun. Those are the people interested in dating you. Aim for them.
19 .约会时,淡化你的怪癖会让 90% 的人认为你还不错。强调你的怪癖会让 10% 的人认为你迷人且有趣。这些人有兴趣和你约会。瞄准他们。

  1. There are two red flags to avoid almost all dangerous people: 1. The perpetually aggrieved ; 2. The angry.
  2. 几乎所有危险人物都有两个危险信号需要避开: 1. 永远受委屈的人; 2. 永远受委屈的人; 2. 生气的人。
  3. Those who generate anxiety in you and promise that they have the solution are grifters. See: politicians, marketers, new masculinity gurus, etc. Avoid these.
    21.那些让你感到焦虑并承诺他们有解决办法的人都是骗子。请参阅:政客、营销人员、新男性气概大师等。避免这些。

Body 身体

  1. The 20-20-20 rule: Every 20 minutes of screenwork, look at a spot 20 feet away for 20 seconds. This will reduce eye strain and is easy to remember (or program reminders for).
  2. 20-20-20 规则:每 20 分钟的屏幕工作,注视 20 英尺外的一个地点 20 秒。这将减轻眼睛疲劳并且易于记住(或设置提醒)。

23. Exercise is the most important lifestyle intervention you can do. Even the bare minimum (15 minutes a week) has a huge impact. Start small.
23 .锻炼是您可以采取的最重要的生活方式干预措施。即使是最低限度(每周 15 分钟)也会产生巨大的影响。从小处开始。

  1. Phones have gotten heavier in the last decade and they’re actually pretty hard on your wrists! Use a computer when it’s an alternative or try to at least prop up your phone.
  2. 在过去的十年里,手机变得越来越重,实际上它们对你的手腕来说非常困难!如果可以的话,请使用计算机,或者至少尝试支撑您的手机。

Productivity 生产率

  1. Learn keyboard shortcuts. They’re easy to learn and you’ll get tasks done faster and easier.
    25.学习键盘快捷键。它们很容易学习,您将更快、更轻松地完成任务。
  2. Keep your desk and workspace bare. Treat every object as an imposition upon your attention, because it is. A workspace is not a place for storing things. It is a place for accomplishing things.
    26.保持你的办公桌和工作空间空旷。将每一个物体都视为对你注意力的强加,因为它确实如此。工作空间不是存放东西的地方。这是一个完成事情的地方。
  3. Reward yourself after completing challenges, even badly.
  4. 完成挑战后奖励自己,即使是很糟糕的奖励。

Rationality 理性

28. Noticing biases in others is easy, noticing biases in yourself is hard. However, it has a much higher pay-off.
28 .注意到别人的偏见很容易,注意到自己的偏见却很困难。然而,它的回报要高得多。

29. Explaining problems is good. Often in the process of laying out a problem, a solution will present itself.
29 .解释问题很好。通常,在提出问题的过程中,解决方案就会自然出现。

30. Selfish people should listen to advice to be more selfless, selfless people should listen to advice to be more selfish. This applies to many things. Whenever you receive advice, consider its opposite as well. You might be filtering out the advice you need most.
30 .自私的人应该听建议变得更无私,无私的人应该听建议变得更自私。这适用于很多事情。每当你收到建议时,也要考虑它的反面。您可能会过滤掉您最需要的建议。

Compassion 同情

31.Call your parents when you think of them, tell your friends when you love them.
31.想念父母时给他们打电话,爱朋友时告诉他们。

  1. Compliment people more. Many people have trouble thinking of themselves as smart, or pretty, or kind, unless told by someone else. You can help them out.
    32.多赞美别人。许多人很难认为自己聪明、漂亮或善良,除非是别人告诉的。你可以帮助他们。
  2. Don’t punish people for trying. You teach them to not try with you. Punishing includes whining that it took them so long, that they did it badly, or that others have done it better.
    33.不要因为人们的尝试而惩罚他们。你教他们不要和你一起尝试。惩罚包括抱怨他们花了这么长时间,他们做得不好,或者其他人做得更好。

34.Don't punish people for admitting they were wrong, you make it harder for them to improve.
34.不要惩罚那些承认自己错误的人,你会让他们更难进步。

  1. In general, you will look for excuses to not be kind to people. Resist these.
  2. 一般来说,你会寻找借口不善待他人。抵制这些。

Possessions 财产

36. Things you use for a significant fraction of your life (bed: 1/3rd, office-chair: 1/4th) are worth investing in.
36 .您一生中很大一部分时间使用的物品(床:1/3,办公椅:1/4)值得投资。

  1. “Where is the good knife?” If you’re looking for your good X, you have bad Xs. Throw those out.
    37.“好刀在哪里?”如果你正在寻找好的X,那么你就会有坏的X。把那些扔掉。
  2. If your work is done on a computer, get a second monitor. Less time navigating between windows means more time for thinking.
  3. 如果你的工作是在电脑上完成的,那就买第二台显示器。减少在窗口之间导航的时间意味着有更多的时间思考。
  4. Establish clear rules about when to throw out old junk. Once clear rules are established, junk will probably cease to be a problem. This is because any rule would be superior to our implicit rules (“keep this broken stereo for five years in case I learn how to fix it”).
  5. 制定关于何时扔掉旧垃圾的明确规则。一旦制定了明确的规则,垃圾可能将不再是一个问题。这是因为任何规则都优于我们的隐含规则(“将这个损坏的立体声音响保留五年,以防我学会如何修复它”)。
  6. When buying things, time and money trade-off against each other. If you’re low on money, take more time to find deals. If you’re low on time, stop looking for great deals and just buy things quickly online.
    40.买东西时,时间和金钱是相互权衡的。如果您缺钱,请花更多时间寻找优惠。如果您时间不够,请停止寻找超值优惠,只需在网上快速购买即可。

Self 自己

  1. Deficiencies do not make you special. The older you get, the more your inability to cook will be a red flag for people.
  2. 缺陷并不会让你变得特别。你年纪越大,你不会做饭对人们来说就越是一个危险信号。
  3. If you’re under 90, try things.
  4. 如果你还不到90岁,就尝试一些事情。

43. Things that aren’t your fault can still be your responsibility.
43 .不是你的错的事情仍然可能是你的责任。

44. Defining yourself by your suffering is an effective way to keep suffering forever (ex. incels, trauma).
44 .通过痛苦来定义自己是永远承受痛苦的有效方法(例如非自愿者、创伤)。

  1. Keep your identity small. “I’m not the kind of person who does things like that” is not an explanation, it’s a trap. It prevents nerds from working out and men from dancing.
    45.保持你的身份小。 “我不是那种会做那种事的人”不是解释,而是陷阱。它阻止书呆子锻炼和男人跳舞。
  2. Don’t confuse ‘doing a thing because I like it’ with ‘doing a thing because I want to be seen as the sort of person who does such things’.
  3. 不要将“因为喜欢而做一件事”与“因为我想被视为做这种事的人而做某事”混为一谈。

47. Remember that you are dying.
47 .记住你快要死了。

48. Personal epiphanies feel great, but they fade within weeks. Upon having an epiphany, make a plan and start actually changing behavior.
48 .个人的顿悟感觉很棒,但几周后就会消失。顿悟后,制定计划并开始实际改变行为。

Others 其他的

  1. In choosing between living with 0-1 people vs 2 or more people, remember that ascertaining responsibility will no longer be instantaneous with more than one roommate (“whose dishes are these?”).
  2. 在与 0-1 人同住还是与 2 人或更多人同住之间进行选择时,请记住,与超过一名室友一起居住时将不再能够立即确定责任(“这些是谁的菜?”)。
  3. When you ask people, “What’s your favorite book / movie / band?” and they stumble, ask them instead what book / movie / band they’re currently enjoying most. They’ll almost always have one and be able to talk about it.
    50.当你问别人“你最喜欢的书/电影/乐队是什么?”他们结结巴巴地问他们目前最喜欢什么书/电影/乐队。他们几乎总是有一个并且能够谈论它。

作者:shengjk1
来源:juejin.cn/post/7403288145196236840

收起阅读 »

Taro搭建支付宝小程序实战

web
1. 引言 在当今多端应用开发的趋势下,许多开发者面临着需要在不同平台(如微信、支付宝、百度等小程序,以及H5和React Native应用)上编写和维护多套代码的挑战。为了解决这一问题,市场上涌现了多种跨端开发框架,旨在帮助开发者实现“一次编写,多端运行”的...
继续阅读 »

1. 引言


在当今多端应用开发的趋势下,许多开发者面临着需要在不同平台(如微信、支付宝、百度等小程序,以及H5和React Native应用)上编写和维护多套代码的挑战。为了解决这一问题,市场上涌现了多种跨端开发框架,旨在帮助开发者实现“一次编写,多端运行”的目标。Taro 是其中最为流行的框架之一,但在选择开发工具时,了解其他同类框架的优缺点非常重要。


1.1 类似框架的对比


以下是 Taro、Uniapp、WePY 和 MPX 四种多端开发框架的优缺点对比表:


框架优点缺点
Taro- 支持使用 React 语法,符合许多前端开发者的使用习惯。
- 广泛支持多端,包括微信小程序、支付宝小程序、H5、React Native 等。
- 活跃的社区和丰富的插件生态系统。
- 提供了完善的跨平台 API 兼容性,减少了平台差异的处理工作。
- 构建时间相对较长,尤其是在多端同时输出时。
- 部分高级特性在某些平台上可能不完全兼容。
Uniapp- 使用 Vue 语法,适合 Vue 开发者。
- 支持广泛的多端输出,包括小程序、H5、App(通过原生渲染)。
- 简单易上手,适合中小型项目。
- 对复杂业务场景的支持有限,灵活性不如 Taro。
- 生态系统相对 Taro 较弱,插件丰富度不及 Taro。
WePY- 针对微信小程序的开发框架,支持类 Vue 语法。
- 轻量级,专注于微信小程序的开发,简单直接。
- 多端支持较弱,主要针对微信小程序,跨平台能力不足。
- 社区相对较小,更新频率较慢。
MPX- 提供了增强的组件化编程能力,适合大型复杂小程序项目。
- 拥有更好的编译性能,构建速度较快。
- 使用自定义语法,学习成本较高。
- 社区资源较少,生态系统不如 Taro 和 Uniapp 丰富。

通过这个对比表,我们可以根据项目需求和团队的技术栈选择最适合的多端开发框架。每个框架都有其独特的优势和局限性,因此选择时需要权衡各方面的因素。


1.2 为什么选择 Taro


Taro 作为一个基于 React 的多端开发框架,有以下几大优势使得它在众多选择中脱颖而出:



  1. 跨平台支持广泛:Taro 支持微信小程序、支付宝小程序、H5、React Native、快应用等多个平台,能够极大地提升开发效率,减少代码重复编写的成本。

  2. React 生态支持:Taro 使用 React 语法,这使得许多已有的 React 组件和库可以直接复用,开发者不需要学习新的开发模式,便能快速上手。

  3. 成熟的生态系统:Taro 拥有丰富的社区插件和第三方支持,提供了大量开箱即用的功能模块,如状态管理、路由管理等,这些都能帮助开发者更快地构建应用。

  4. 持续的更新与支持:Taro 由京东维护,得到了持续的更新与支持,具有较强的社区活力,能够及时响应开发者的需求和问题。


Taro 作为一个成熟且功能强大的多端开发框架,特别适合那些希望一次开发、多平台运行的项目需求。它不仅简化了跨平台开发的复杂性,还提供了丰富的功能支持,使得开发过程更加高效和愉悦。因此,我们选择 Taro 作为开发支付宝小程序的首选工具。


2. 环境搭建


2.1 安装 Taro CLI


首先,你需要安装 Taro 的 CLI 工具。确保你已经安装了 Node.js 环境,然后运行以下命令来全局安装 Taro CLI:


npm install -g @tarojs/cli

2.2 创建项目


安装完成后,可以通过以下命令来创建一个新的 Taro 项目:


taro init myApp

在创建过程中,Taro 会询问你一些选项,如选择框架(默认 React)、CSS 预处理器(如 Sass、Less)等。选择合适的选项后,Taro 会自动生成项目的目录结构和配置文件。


2.3 配置支付宝小程序


在 Taro 项目中,配置支付宝小程序的输出涉及多个方面,包括基本的项目配置、支付宝小程序特有的扩展配置、以及一些针对支付宝平台的优化设置。以下是详细的配置步骤和相关说明。


2.3.1 基本配置

首先,需要在项目的 config/index.js 文件中进行基础配置,确保 Taro 能够正确编译并输出支付宝小程序。


const config = {
projectName: 'myApp',
designWidth: 750,
deviceRatio: {
'640': 2.34 / 2,
'750': 1,
'828': 1.81 / 2
},
sourceRoot: 'src',
outputRoot: 'dist',
plugins: [],
defineConstants: {},
copy: {
patterns: [],
options: {}
},
framework: 'react',
compiler: 'webpack5',
cache: {
enable: true
},
mini: {
postcss: {
autoprefixer: {
enable: true,
config: {}
},
pxtransform: {
enable: true,
config: {}
},
url: {
enable: true,
config: {
limit: 10240 // 设置转换尺寸限制
}
}
}
},
h5: {
publicPath: '/',
staticDirectory: 'static',
esnextModules: [],
postcss: {
autoprefixer: {
enable: true,
config: {}
},
cssModules: {
enable: false, // 默认是 false,若需要支持 CSS Modules,设置为 true
config: {
namingPattern: 'module', // 转换模式,支持 global/module
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
}
}
}

2.3.2 支付宝小程序特有的配置

config/index.js 文件中,需要在 mini 配置块中进一步设置支付宝小程序的特有配置。Taro 会自动生成支付宝小程序所需的 app.jsonproject.config.json 等配置文件,但你可以根据需求自定义一些额外的配置。


mini: {
compile: {
exclude: ['src/lib/alipay-lib.js'] // 示例:排除不需要编译的文件
},
postcss: {
autoprefixer: {
enable: true,
config: {
browsers: ['last 3 versions', 'Android >= 4.1', 'ios >= 8']
}
}
},
// 支付宝小程序特有的配置
alipay: {
component2: true, // 启用支付宝小程序的基础组件规范 v2
axmlStrictCheck: true, // 严格的 axml 校验,帮助发现潜在问题
renderShareComponent: true, // 启用动态组件渲染
usingComponents: { // 注册全局自定义组件
'custom-button': '/components/custom-button/index'
},
// 支付宝扩展配置
plugins: {
myPlugin: {
version: '1.0.0',
provider: 'wx1234567890' // 插件提供者的AppID
}
},
window: {
defaultTitle: '支付宝小程序', // 设置小程序默认的标题
pullRefresh: true, // 支持下拉刷新
allowsBounceVertical: 'YES' // 支持竖向弹性滚动
},
pages: [
'pages/index/index',
'pages/detail/detail'
],
tabBar: {
color: '#000000',
selectedColor: '#1c1c1c',
backgroundColor: '#ffffff',
borderStyle: 'black',
list: [{
pagePath: 'pages/index/index',
text: '首页',
iconPath: 'assets/tabbar/home.png',
selectedIconPath: 'assets/tabbar/home-active.png'
}, {
pagePath: 'pages/detail/detail',
text: '详情',
iconPath: 'assets/tabbar/detail.png',
selectedIconPath: 'assets/tabbar/detail-active.png'
}]
}
}
}

2.3.3 支付宝小程序页面配置

每个页面的配置都在 pages.json 文件中进行定义,Taro 会自动处理这些配置。但你可以根据需要进一步自定义每个页面的表现,如是否启用下拉刷新、页面背景颜色等。


{
"pages": [
"pages/index/index",
"pages/detail/detail"
],
"window": {
"defaultTitle": "我的小程序",
"titleBarColor": "#ffffff",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white",
"backgroundColor": "#ffffff",
"enablePullDownRefresh": true // 启用下拉刷新
}
}

2.3.4 引入支付宝小程序扩展组件

支付宝小程序支持扩展组件和插件,这些可以直接在 usingComponents 中进行引入。例如,如果你需要使用支付宝小程序特有的扩展组件如 RichTextInput,可以在 alipay 配置中进行设置:


alipay: {
usingComponents: {
'rich-text': 'plugin://myPlugin/rich-text', // 引用插件中的组件
'custom-button': '/components/custom-button/index' // 引入自定义组件
}
}

2.3.5 设置支付宝小程序的分包加载

对于较大或复杂的支付宝小程序,你可能希望启用分包加载功能,以减少主包大小并提升加载速度。Taro 支持在配置文件中设置分包:


subPackages: [
{
root: 'packageA',
pages: [
'pages/logs/logs'
]
},
{
root: 'packageB',
pages: [
'pages/index/index'
]
}
],

2.3.6 环境变量配置

在开发和生产环境下,可能需要不同的配置。Taro 支持通过环境变量进行配置管理。你可以在项目根目录下创建 .env 文件,并定义不同环境下的变量:


// .env.development
TARO_ENV = 'alipay'
API_BASE_URL = 'https://api-dev.example.com'

// .env.production
TARO_ENV = 'alipay'
API_BASE_URL = 'https://api.example.com'

然后在代码中通过 process.env 访问这些变量:


const apiBaseUrl = process.env.API_BASE_URL

2.3.7 自定义 Webpack 配置

如果你需要更复杂的配置或优化,可以通过 config/index.js 中的 webpackChain 属性来自定义 Webpack 配置。例如,添加自定义的插件或优化构建过程:


mini: {
webpackChain (chain) {
chain.plugin('analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
}
}

2.3.8 部署和调试

在配置完成后,可以通过以下命令生成支付宝小程序的构建文件:


taro build --type alipay

生成的代码会存放在 dist 目录下,然后可以通过支付宝开发者工具打开并进行调试。


2.3.9 常见问题及解决方法


  1. 自定义组件路径错误:确保 usingComponents 中的路径正确,特别是相对于 dist 目录的相对路径。

  2. 跨域请求问题:在支付宝小程序中进行网络请求时,确保在 config/index.js 中配置了正确的 sourceRootoutputRoot

  3. 调试模式不一致:在开发和生产环境中使用不同的 API 端点时,确保环境变量配置正确并在代码中正确使用。


以上是使用 Taro 搭建支付宝小程序的详细配置步骤,涵盖了从基础配置到扩展功能的方方面面。通过这些配置,你可以更加灵活地控制支付宝小程序的表现和功能,满足项目的多样化需求。


3. 重要的 API 和组件


3.1 基础组件


Taro 提供了许多常用的基础组件,如 ViewTextButton 等,这些组件与 React 组件类似,但 Taro 的组件具有跨平台能力。


import { View, Text, Button } from '@tarojs/components'

const MyComponent = () => (
<View>
<Text>Hello, Taro!</Text>
<Button onClick={() => alert('Clicked!')}>Click me</Button>
</View>

)

3.2 事件处理


Taro 事件处理机制与 React 类似,可以直接使用 onClickonChange 等事件属性。此外,Taro 支持跨平台的事件兼容处理,不需要担心事件名称的差异。


3.3 路由跳转


Taro 提供了 Taro.navigateToTaro.redirectTo 等 API 用于在小程序中进行页面跳转。可以根据具体需求选择合适的跳转方式。


Taro.navigateTo({
url: '/pages/detail/index?id=123'
})

3.4 使用 Hooks


在 Taro 中可以使用 React 的 Hooks,如 useStateuseEffect 等,来管理组件的状态和生命周期。


import { useState, useEffect } from 'react'
import { View } from '@tarojs/components'

const Counter = () => {
const [count, setCount] = useState(0)

useEffect(() => {
Taro.setNavigationBarTitle({ title: `Count: ${count}` })
}, [count])

return (
<View>
<Button onClick={() => setCount(count + 1)}>Increment</Button>
<Text>{count}</Text>
</View>

)
}

4. 常见问题及坑点


4.1 样式问题


支付宝小程序的 CSS 支持度与微信小程序有所不同,某些高级的 CSS 特性可能无法生效。建议使用基础的 CSS 进行布局,并在项目中引入适当的 polyfill。


4.2 API 差异


在使用 Taro 开发支付宝小程序时,虽然 Taro 提供了跨平台的 API 兼容性,但是由于各个小程序平台的底层架构和能力不同,仍然存在一些需要特别注意的 API 差异。以下将详细说明一些与原生微信小程序不同的、并且在支付宝小程序中经常会用到的特殊 API。


4.2.1 支付宝特有的 API


1. my.request 与 wx.request

尽管 Taro 封装了 Taro.request 来统一请求接口,但在某些特殊情况下,开发者可能需要直接使用原生 API。支付宝小程序的 my.request 与微信的 wx.request 基本相同,但支持一些额外的配置选项,如 timeout 等。


示例:


// 支付宝小程序
my.request({
url: 'https://api.example.com/data',
method: 'GET',
timeout: 5000, // 设置请求超时时间
success: (res) => {
console.log(res.data);
},
fail: (err) => {
console.error(err);
}
});

在微信小程序中,wx.request 不支持 timeout 配置。


2. my.alert 和 wx.showModal

my.alert 是支付宝小程序中用来展示提示框的 API,而微信小程序使用 wx.showModal 来实现类似功能。my.alert 仅支持一个按钮,而 wx.showModal 可以支持两个按钮(确定和取消)。


示例:


// 支付宝小程序
my.alert({
title: '提示',
content: '这是一个提示框',
buttonText: '我知道了',
success: () => {
console.log('用户点击了确定按钮');
}
});

// 微信小程序
wx.showModal({
title: '提示',
content: '这是一个提示框',
showCancel: false, // 不显示取消按钮
success: (res) => {
if (res.confirm) {
console.log('用户点击了确定按钮');
}
}
});

3. my.getAuthCode 与 wx.login

支付宝小程序的 my.getAuthCode 用于获取用户的授权码,通常用于登录验证或支付场景。而微信小程序使用 wx.login 获取用户的登录凭证(code),这两者在使用上有所不同。


示例:


// 支付宝小程序
my.getAuthCode({
scopes: 'auth_user',
success: (res) => {
console.log('用户授权码:', res.authCode);
},
fail: (err) => {
console.error('获取授权码失败:', err);
}
});

// 微信小程序
wx.login({
success: (res) => {
console.log('用户登录凭证:', res.code);
},
fail: (err) => {
console.error('获取登录凭证失败:', err);
}
});

4. my.navigateToMiniProgram 与 wx.navigateToMiniProgram

这两个 API 都用于跳转到其他小程序。虽然功能类似,但在支付宝小程序中,my.navigateToMiniProgram 有一些额外的参数,比如 extraData,用于在跳转时传递数据。


示例:


// 支付宝小程序
my.navigateToMiniProgram({
appId: '2021000000000000',
path: 'pages/index/index',
extraData: {
foo: 'bar'
},
success: () => {
console.log('跳转成功');
},
fail: (err) => {
console.error('跳转失败:', err);
}
});

// 微信小程序
wx.navigateToMiniProgram({
appId: 'wx1234567890',
path: 'pages/index/index',
extraData: {
foo: 'bar'
},
success: () => {
console.log('跳转成功');
},
fail: (err) => {
console.error('跳转失败:', err);
}
});

5. my.tradePay 与 wx.requestPayment

my.tradePay 是支付宝小程序用于发起支付的 API,而微信小程序使用 wx.requestPayment 实现同样的功能。两者的参数配置有所不同,尤其是在支付方式和返回结果处理上。


示例:


// 支付宝小程序
my.tradePay({
tradeNO: '202408280000000000001',
success: (res) => {
if (res.resultCode === '9000') {
console.log('支付成功');
} else {
console.log('支付失败', res.resultCode);
}
},
fail: (err) => {
console.error('支付失败:', err);
}
});

// 微信小程序
wx.requestPayment({
timeStamp: '1609459200',
nonceStr: '5K8264ILTKCH16CQ2502SI8ZNMTM67VS',
package: 'prepay_id=wx20170101abc1234567890',
signType: 'MD5',
paySign: 'ABCD1234',
success: () => {
console.log('支付成功');
},
fail: (err) => {
console.error('支付失败:', err);
}
});

6. my.chooseCity 与 wx.chooseLocation

支付宝小程序提供了 my.chooseCity API 用于选择城市,而微信小程序没有直接对应的 API,但 wx.chooseLocation 可以选择位置,且在选址过程中包含了城市信息。


示例:


// 支付宝小程序
my.chooseCity({
showLocatedCity: true, // 显示当前所在城市
success: (res) => {
console.log('选择的城市:', res.city);
}
});

// 微信小程序
wx.chooseLocation({
success: (res) => {
console.log('选择的位置:', res.name);
console.log('所在城市:', res.address);
}
});

4.2.2 差异化 API 使用的注意事项



  1. 功能测试:由于 API 存在差异,所以我们需要在不同平台上进行充分测试,确保应用逻辑在所有平台上都能正常运行。

  2. 代码隔离:对于平台特有的 API,建议通过 Taro.getEnv() 判断运行环境,并使用条件判断来分别调用不同平台的 API,从而实现代码的隔离与复用。

  3. 兼容性处理:某些 API 在不同平台上可能有不同的参数或返回值格式,因此需要根据平台特性进行兼容性处理。


通过对以上 API 差异的详细了解,希望我们可以更好地在 Taro 项目中处理支付宝小程序与微信小程序的不同需求,提升应用的跨平台兼容性和用户体验。


4.3 路由管理


在 Taro 中,路由管理是跨平台开发中非常重要的一环。尽管 Taro 封装了微信小程序和支付宝小程序的路由操作,但在使用过程中仍然存在一些差异。以下详细说明 Taro 在支付宝和微信小程序中的路由管理方式,包括不同的跳转方式及参数的获取。


4.3.1 路由跳转方式


Taro 中提供了多种路由跳转方式,主要包括 Taro.navigateToTaro.redirectToTaro.switchTabTaro.reLaunchTaro.navigateBack。这些方法封装了微信和支付宝小程序的原生跳转方式,适用于不同的使用场景。


1. Taro.navigateTo

Taro.navigateTo 用于跳转到应用内的指定页面,新的页面会被加入到页面栈中。


示例:


Taro.navigateTo({
url: '/pages/detail/index?id=123&name=abc'
});


  • 微信小程序: 页面栈最大深度为10,超过后会自动释放栈顶页面。

  • 支付宝小程序: 页面栈最大深度为10,同样超过后会自动释放栈顶页面。


2. Taro.redirectTo

Taro.redirectTo 用于关闭当前页面并跳转到指定页面,跳转后无法返回到原页面。


示例:


Taro.redirectTo({
url: '/pages/home/index'
});


  • 微信小程序支付宝小程序 都会将当前页面从栈中移除,不允许用户回退。


3. Taro.switchTab

Taro.switchTab 用于跳转到指定的 tabBar 页面,并关闭其他所有非 tabBar 页面。


示例:


Taro.switchTab({
url: '/pages/home/index'
});


  • 微信小程序: url 必须是 tabBar 页面,否则会报错。

  • 支付宝小程序: 同样必须是 tabBar 页面,但支付宝小程序支持使用 extraData 传递额外数据。


4. Taro.reLaunch


Taro.reLaunch 用于关闭所有页面并跳转到指定页面,适用于需要重置应用状态的场景。


示例:


Taro.reLaunch({
url: '/pages/home/index'
});


  • 微信小程序支付宝小程序 行为一致,都会关闭所有页面并创建一个新的页面栈。


5. Taro.navigateBack


Taro.navigateBack 用于关闭当前页面并返回到上一级页面或多级页面。


示例:


Taro.navigateBack({
delta: 1 // 返回上一级页面
});


  • 微信小程序支付宝小程序 都支持通过 delta 指定返回的页面层级。


4.3.2 获取路由参数

无论是通过哪种方式跳转,页面之间通常需要传递参数。在 Taro 中,参数的传递和获取可以通过 this.$router.params 实现。以下是如何在页面中获取路由参数的详细说明。


1. URL 参数传递

当使用 Taro.navigateTo 或其他跳转方法时,可以在 url 中通过 query 传递参数。


示例:


// 页面跳转
Taro.navigateTo({
url: '/pages/detail/index?id=123&name=abc'
});

// 在目标页面获取参数
componentDidMount() {
const { id, name } = this.$router.params;
console.log('ID:', id); // 输出:ID: 123
console.log('Name:', name); // 输出:Name: abc
}


  • 微信小程序: 参数会自动编码并附加到 URL 后。

  • 支付宝小程序: 行为类似微信小程序,参数通过 URL query 传递。


2. extraData 参数传递

支付宝小程序允许通过 extraData 传递复杂对象,这在某些复杂场景下非常有用。


示例:


// 页面跳转
my.navigateTo({
url: '/pages/detail/index',
extraData: {
info: {
id: 123,
name: 'abc'
}
}
});

// 在目标页面获取参数
componentDidMount() {
const { info } = this.$router.params;
console.log('Info:', info); // 输出:Info: { id: 123, name: 'abc' }
}


  • 微信小程序: 目前不支持 extraData 参数传递,但可以通过 globalData 或其他全局状态管理工具如 Redux 实现类似效果。


3. 场景值与 scene 参数


在小程序的入口页面,通常会涉及到场景值(scene)的获取。Taro 提供了 this.$router.params.scene 来获取微信小程序中的 scene 值,这在处理分享或扫码进入时非常重要。


示例:


componentDidMount() {
const scene = this.$router.params.scene;
console.log('Scene:', scene); // 输出对应的场景值
}


  • 微信小程序: 支持 scene 参数传递,主要用于扫码进入或分享。

  • 支付宝小程序: 不直接支持 scene,但可以通过其他方式获取进入场景(如 my.getLaunchOptionsSync)。


4.3.3 注意事项



  1. 页面栈限制:无论是微信还是支付宝小程序,都有页面栈深度限制(通常为10层)。在开发复杂应用时,合理控制页面跳转的深度,避免栈溢出。

  2. 参数编码问题:确保传递的 URL 参数已经过适当的编码,避免特殊字符引发问题。

  3. 页面返回的数据传递:Taro 并未封装类似 onActivityResult 的机制,但可以通过全局状态管理或 eventBus 模式来实现页面返回的数据传递。


4.3.4 其他事项


通过上面的一些介绍,就可以更加灵活地使用 Taro 进行路由管理,充分利用不同平台的特性,提升应用的导航体验和用户体验。在使用 Taro 开发多端应用时,除了路由管理和 API 差异之外,还有一些关键点和常见的坑需要注意,以确保应用的稳定性、性能和可维护性。以下是一些使用 Taro 过程中需要特别注意的事项:


1. 跨平台兼容性

尽管 Taro 旨在提供跨平台的开发体验,但不同平台在渲染引擎、组件行为和 API 支持上仍有差异。开发者需要在每个目标平台上进行充分测试,确保功能和表现一致。


注意事项:


  • 组件兼容性:某些 Taro 组件在不同平台上的表现可能有所不同,如 scroll-view 的行为在微信和支付宝小程序中略有差异。

  • 样式兼容性:不同平台的样式支持不尽相同,如支付宝小程序对部分 CSS 属性的支持较弱,需要进行适配。

  • API 兼容性:Taro 提供的统一 API 在不同平台上可能会有细微的差异,建议使用 Taro.getEnv() 进行环境判断,以便针对特定平台编写适配代码。


2. 状态管理

Taro 支持使用多种状态管理工具,如 Redux、MobX、Recoil 等。根据项目的复杂度和团队的技术栈选择合适的状态管理方案。


注意事项:


  • 全局状态管理:对于跨页面的数据共享,使用全局状态管理工具能有效避免组件之间直接传递数据的问题。

  • 性能优化:在使用状态管理工具时,注意避免不必要的状态更新,尤其是在大规模应用中,应当进行性能调优以减少重渲染。


3. 性能优化

Taro 封装了小程序的框架,尽管提供了便捷性,但这也带来了一些性能开销。性能优化是确保 Taro 应用顺畅运行的关键。


注意事项:


  • 懒加载:对不常用的组件或页面使用懒加载技术,减少初次渲染的压力。

  • 分包加载:对于较大的应用,可以使用分包加载(特别是在微信小程序中)来优化启动速度。

  • 减少组件嵌套:过多的组件嵌套会增加渲染负担,尽量保持组件结构的扁平化。

  • 长列表优化:对长列表(如商品列表、评论列表)使用虚拟列表技术,避免一次性加载大量数据。


4. 开发工具与调试

Taro 提供了开发者工具来简化开发和调试过程,但在实际项目中,调试复杂问题时可能会遇到一些挑战。


注意事项:


  • 使用 Taro CLI:Taro CLI 提供了丰富的命令行工具,帮助你快速生成项目、构建应用和调试代码。

  • 跨平台调试:确保在每个平台的开发者工具中进行调试,并使用平台特有的工具,如微信小程序开发者工具和支付宝 IDE。

  • 源代码映射:使用源代码映射(Source Map)功能来调试编译后的代码,方便追踪错误。


5. 小程序的限制

各个小程序平台都有其独特的限制,如包大小限制、API 速率限制、页面栈限制等。在开发过程中,必须遵守这些限制,以免在发布或运行时遇到问题。


注意事项:



  • 包大小限制:微信和支付宝小程序对主包和分包的大小都有严格限制,尽量减少不必要的资源,压缩图片和代码。

  • 页面栈限制:小程序的页面栈深度通常为 10 层,超出后可能会引发崩溃,需要合理设计页面的跳转逻辑。

  • 请求速率限制:各平台对网络请求的速率和并发量都有要求,应当合并请求或使用请求队列来控制频率。


6. 国际化支持

如果应用需要支持多语言,Taro 提供了基础的国际化支持,但由于不同平台的特性,可能需要额外的配置和适配。


注意事项:


  • 文本管理:使用统一的国际化管理工具,如 i18next 或自定义的国际化方案。

  • 格式化问题:不同平台对日期、货币等格式化方式支持不同,使用第三方库(如 moment.js)来统一格式化操作。

  • 右到左(RTL)布局:如果应用需要支持 RTL 语言,确保在每个平台上都正确实现 RTL 布局。


7. 版本管理与更新

在多端开发中,版本管理和应用更新也是需要特别注意的地方。不同平台对更新机制的支持不尽相同,需要有针对性的处理策略。


注意事项:


  • 小程序版本控制:在不同平台上发布新版本时,注意同步版本号,并在应用内做好版本控制。

  • 热更新:Taro 目前不直接支持热更新,但可以通过后台配置管理、版本检测等方式实现相似的效果。

  • 数据迁移:在更新过程中,可能需要进行数据迁移(如数据库结构变更),确保用户数据的完整性。


8. 社区与文档支持

Taro 社区活跃,文档也在不断完善,但在实际开发中遇到问题时,了解如何有效利用社区资源和官方文档也很重要。


注意事项:


  • 官方文档:Taro 官方文档非常详细,建议在遇到问题时首先查阅文档,以获取官方推荐的解决方案。

  • 社区支持:遇到文档未覆盖的问题,可以到 GitHub Issues、Gitter 或其他开发者社区寻求帮助。

  • 示例项目:参考官方或社区提供的示例项目,可以帮助你快速上手并解决常见问题。


通过注意以上关键点,开发者可以更好地利用 Taro 的跨平台能力,同时避免常见的坑和问题,提升开发效率和应用质量。
官方文档地址: docs.taro.zone/docs


5. 结语


结语


Taro 的出现不仅解决了多端开发的复杂性问题,还大大提升了开发效率和代码的可维护性。通过统一的 API 和组件库,Taro 让开发者无需深入了解每个小程序平台的细节,即可快速构建和部署功能丰富的应用。


然而,正如任何工具或框架一样,Taro 并非完美无缺。跨平台开发固有的挑战仍然存在,包括平台间的差异、性能优化需求、状态管理复杂性,以及不同平台特有的限制。这些挑战提醒我们,尽管 Taro 能够极大地简化开发流程,但在开发过程中依然需要细致地进行测试、调优和适配工作。


从选择 Taro 作为开发框架,到深入了解其核心功能和最佳实践,再到避开潜在的坑和问题,需要全方位地掌握 Taro 的使用技巧。通过合理使用 Taro 的能力,结合自身项目的实际需求,开发者可以实现跨平台的一致用户体验,并保持代码库的可扩展性和维护性。


展望未来,随着小程序生态的不断发展和 Taro 框架的持续更新,开发者将会有更多的机会去探索和创新。Taro 的灵活性和强大的跨平台能力为我们提供了无限的可能,无论是在支付宝小程序、微信小程序,还是在更多的平台上,都能为用户带来一致、流畅的体验。


在使用 Taro 进行多端开发的过程中,我们不仅仅是编写代码,更是在打造一款能够适应多平台需求的高质量应用。通过不断学习和实践,我们能够充分发挥 Taro 的潜力,让每一个用户无论在哪个平台上使用我们的应用,都能感受到同样的便捷与愉悦。


最终,无论是初次使用 Taro 的新手,还是已经熟练掌握的老手,持续学习和优化始终是提升开发能力的关键。Taro 为我们提供了强大的工具,剩下的就是如何用好这些工具,创造出色的产品。相信随着更多开发者的加入和贡献,Taro 生态将会更加繁荣,为跨平台开发带来更多的可能性和惊喜。


作者:洞窝-海林


作者:洞窝技术
来源:juejin.cn/post/7408138735798616102
收起阅读 »

优雅实现任意形状的水球图,领导看了都说好

web
前言 翌日 我吃着早餐,划着水。 不一会,领导走了过来。 领导:小伙子,你去XX项目实现一个设备能源图,要求能根据剩余能量显示水波高低。 我: 啊?我?这个项目我没看过代码。 领导:任务有点急,你自己安排时间吧,好好搞,给你争取机会。 我:好吧。(谁叫咱只是一...
继续阅读 »

前言


翌日


我吃着早餐,划着水。


不一会,领导走了过来。


领导:小伙子,你去XX项目实现一个设备能源图,要求能根据剩余能量显示水波高低。


我: 啊?我?这个项目我没看过代码。


领导:任务有点急,你自己安排时间吧,好好搞,给你争取机会。


我:好吧。(谁叫咱只是一个卑微的打工人,只能干咯😎👌😭。)


你去把唐僧师徒除掉表情有哪些-你去把唐僧师徒除掉表情包大全


分析


看到图,类似要实现这样一个立方体形状的东西,然后需要根据剩余电量显示波浪高低。


image-20240828223431928


我心想,这不简单吗,这不就是一个水球图,恰好之前看过echarts中有一个水球图的插件。


想到这,我立马三下五除二,从echarts官网上下载了这个插件,心想下载好了就算搞定了。


波折


哪知,这个需求没有想象中的那么简单,UI设计的图其实是一个伪3D立方体,通过俯视实现立体效果。并且A面和B面都要有波浪。


image-20240828222621246


这就让我犯了难,因为官方提供的demo没有这样的形状,最相近也就是最后一个图案。


image-20240829010253125


那把两个最后一个图案拼接起来,组成A、B面,不就可以达到我们的效果了吗,然后最后顶上再放一个四边形C面,不就可以完美解决了。


想法是好的,但是具体思考实践方案起来就感觉麻烦了。根据我平时的解决问题的经验:如果方案实践起来,发现很麻烦就说明方法错了,需要换个方向思考。


于是我开始翻阅官方文档,找到了关于形状的定义shape属性。


救世主shape


它支持三种方式定义形状



  1. 最基础的是,直接编写属性内置的值,支持一些内置的常见形状如:


    'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow'


  2. 其次,它还支持根据容器形状填充container,具体来说就是可以填充满你的渲染容器。比如一个300X300的div,设置完shape:'container'后,他的渲染区域就会覆盖这个div大小。此时,你可以调整div的形状实现想要的图案,比如


    我们用两个div演示,我们将第二个div样式设置为border-radius: 100%;第一个图形就为方形,第二个就成为了经典圆形水球图。我们可以根据需要自行让div变成我们想要的形状。


    image-20240829013340696


  3. 最后,也就是我们这次要说的重点,他支持SVGpath://路径。


我们可以看到第二种方式实现复杂的图形有局促性,第三种方式告诉我们他支持svg的path路径时,这就给了我们非常多的可能性,我们可以通过路径绘制任意想要的图形。


看到这个属性后,岂不是只需要将UI切的svg文件中的path传入进去就可以实现这个效果了?随后开始了分析。


我们的图形可以由三个四边形构成,每个四边形四个顶点,合计12个顶点。


image-20240826115244773


从svg文件我们可以得到如下内容


    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="362.74609375"
   height="513.7080078125" viewBox="0 0 362.74609375 513.7080078125" fill="none">

   <path d="M128.239 177.367L128.239 512.015L361.056 397.17L361.056 76.6519L128.239 177.367Z" stroke="rgba(0, 0, 0, 1)"
       stroke-width="3.3802816901408472" stroke-linejoin="round" fill="#FFFFFF">

   </path>
   <path d="M1.69043 107.409L128.231 177.364L361.048 76.6482L229.656 1.69043L1.69043 107.409Z"
       stroke="rgba(0, 0, 0, 1)" stroke-width="3.3802816901408472" stroke-linejoin="round" fill="#FFFFFF">

   </path>
   <path d="M1.69043 107.413L1.69043 442.06L128.231 512.015L128.231 177.368L1.69043 107.413Z" stroke="rgba(0, 0, 0, 1)"
       stroke-width="3.3802816901408472" stroke-linejoin="round" fill="#FFFFFF">

   </path>
</svg>

我们可以发现,它是由三个path路径构成,而我们的水球图只支持一个path://开头的path路径字符串。解决这个问题也很简单,我们只需要将三个路径给他合并在一起就可以了,我们就可以实现这种伪3D效果了。


如此,我们便得到了路径。


path://M128.239 177.367L128.239 512.015L361.056 397.17L361.056 76.6519L128.239 177.367Z M1.69043 107.409L128.231 177.364L361.048 76.6482L229.656 1.69043L1.69043 107.409Z M1.69043 107.413L1.69043 442.06L128.231 512.015L128.231 177.368L1.69043 107.413Z

效果如图:


image-20240829021808241


哇瑟,真不赖,感觉已经实现百分之七八十了,内心已经在幻想,领导看到实现后大悦,说不愧是你呀,然后给我升职加薪,推举升任CTO,赢取白富美,翘着腿坐在库里南里的场景了。


等等!我的线条呢?整个水球图(也不能叫球了,水立方?)只有外轮廓,看不到线条棱角了,其实我觉得现在这种情况还蛮好看的,但是为了忠于UI设计的还原,还是得另寻办法,可不能让人家说菜,这么简单都实现不了。


好在,解决这个问题也很简单,官方提供了边框的配置项。(真是及时雨啊)


 backgroundStyle: {
borderColor: "#000",// 边框的颜色
borderWidth: 2,     // 边框线条的粗细
color: "#fff",      // 背景色
},

配置完边框线的粗细后,啊哈!这不就是我们想要的效果吗?


image-20240829022825173


最后还差一点,再将百分比显示出来,如下图,完美!


image-20240829023831415


拓展


然后我们类比别的图案也是类似,也是只需要将多个path组合在一起就可以了。


悟空


image-20240826113244828


某支


image-20240829014215329


钢铁侠


image-20240829014710481


是不是看起来非常的炫酷,实现方式也是一样,我们只需要将这些图案的path路径传入这个shape属性就行了,然后调整适当的颜色。


注意点:



  • 如果图形中包含填充的区域,可以让UI小姐姐,把填充改成线条模拟,用多个线条组成一个面模拟,类似微积分。

  • 图形的样式取决于path路径,水球图只支持路径,因此路径上的颜色不能单独设置了,只能通过配置项配置一个整体的颜色。

  • 关于svg矢量图标来源,可以上素材网站寻找,如我比较喜欢用的字节图标库阿里图标库等等


思考


上面实现的水球图有一点让我十分在意,就是图案的是怎么做到根据波浪是否遮挡文字,改变文字的颜色,它做到了即使水球图的波浪漫过了文字,文字会呈现白色,不会因为水漫过后,文字颜色与水球图颜色一致,导致文字不可见。


image-20240829024748518


这个特性也太酷了吧,对于用户体验来说大大增强。


由于强烈的好奇心,我开始研究源码这个是怎么实现的。


看了源码后,恍然大悟,原来是这样的



  1. 绘制背景

  2. 绘制内层的文本

  3. 绘制波浪

  4. 设置裁剪区域,将波浪覆盖的内层文本部分裁剪掉,留下没有被裁减的地方。(上半部分绿字)

  5. 绘制外层文本,由于设置了裁剪区域,之后的绘图被限制在裁剪区域。(裁剪区域的下半红字部分)


image-20240829025845365


这样,我们就完成了这个神奇的效果。


下面我提供了一个demo,大家可以通过注释draw中的函数,就能很快明白是怎么一回事了。


值得注意的是



  • 内层文本与外层文本的位置需要在同一个位置。

  • 裁剪区域位置、大小和波浪的位置、大小重合。


总结


完成这个需求后,领导果然非常高兴给我升了职、加了薪,就在我得意洋洋幻想当上CTO的时候,中午闹钟响了,原来只是中午做了个梦,想到下午还有任务,就继续搬砖去了🤣。


作者:是柠新呀
来源:juejin.cn/post/7407995254077767707
收起阅读 »

这可能是见过的最好用的弹幕库 🔥🔥

web
最近把五年前写的一个弹幕库给重构了一下,本来两年前就想着做这件事,但是其中有一段工作时间压力很大,所以就搁置了,导致没有时间来做这件事情,最近周末 + 晚上花一些时间重构了下,并把文档好好写了一下。言归正传,这篇文章会介绍部分这个弹幕库有的能力,如果正好符合你...
继续阅读 »

最近把五年前写的一个弹幕库给重构了一下,本来两年前就想着做这件事,但是其中有一段工作时间压力很大,所以就搁置了,导致没有时间来做这件事情,最近周末 + 晚上花一些时间重构了下,并把文档好好写了一下。言归正传,这篇文章会介绍部分这个弹幕库有的能力,如果正好符合你的需求或者感兴趣,可以帮忙点点 star 来支持一下。


我们有哪些能力


我们提供了灵活调整轨道自定义弹幕和容器样式弹幕运动算法等能力,还在提供非常丰富的钩子来让用户处理自定义的行为,只要你想要的,都能做到,本文档会简单介绍一些能力和一些功能的实现。





快速开始


对于一个开箱即用的 demo,可以非常简单的接入,如下所示:


import { create } from 'danmu';

const manager = create();

manager.mount('#root');
manager.startPlaying();

// 发送弹幕
manager.push('弹幕内容')

对轨道进行调整


我们对支持类似 CSS calc 表达式的能力,一些位置/宽高等信息都可以用表达式来计算。所以对于轨道来说可以很方便的进行调整。



  1. number:默认单位为 px

  2. string:表达式计算。支持(+-*/)数学计算,只支持 % 和 px 两种单位。


// 例如,这里的 100% 是指容器宽度(如果是高度相关的配置 100%  就是容器的高度)
manager.setGap('(100% - 10px) / 5');

限制为顶部 3 条弹幕


// 如果我们希望轨道高度为 50px
manager.setTrackHeight('100% / 3');

// 如果不设置渲染区域,轨道的高度会根据默认的 container.height / 3 得到,
// 这可能导致轨道高度不是你想要的
manager.setArea({
y: {
start: 0,
// 3 条轨道的总高度为 150px
end: 150,
},
});

限制为中间 3 条弹幕


manager.setTrackHeight('100% / 3');

manager.setArea({
y: {
start: `50%`,
end: `50% + 150`,
},
});

限制为几条不连续的轨道


限制为几条不连续的轨道,除了要做和连续轨道的操作之外,还需要借助 willRender 这个钩子来实现。


// 如果我们希望轨道高度为 50px,并渲染 0,2,4 这几条轨道
manager.setTrackHeight('100% / 6');

// 设置容器的渲染区域
manager.setArea({
y: {
start: 0,
// 6 条轨道的总高度为 300px
end: 300,
},
});

manager.use({
willRender(ref) {
// 高级弹幕和轨道不强相关,没有 trackIndex 这个属性
if (ref.trackIndex === null) return ref;

// 如果为 1,3,5 这几条轨道就阻止渲染,并重新添加等待下次渲染
if (ref.trackIndex % 2 === 1) {
ref.prevent = true;
manager.unshift(ref.danmaku);
}
return ref;
},
});

自定义渲染


弹幕和容器都允许自定义的渲染样式,你可以很方便的做到。


自定义弹幕的样式


1. 通过 manager.setStyle 来设置


import { create } from 'danmu';

// 需要添加的样式
const styles = {
color: 'red',
fontSize: '15px',
// .
};

const manager = create();

// 后续渲染的弹幕和当前已经渲染的弹幕会设置上这些样式。
for (const key in styles) {
manager.setStyle(key, styles[key]);
}

2. 通过 danamaku.setStyle 来设置


import { create } from 'danmu';

// 需要添加的样式
const styles = {
color: 'red',
fontSize: '15px',
// .
};

// 初始化的时候添加钩子处理,这样当有新的弹幕渲染时会自动添加上这些样式
const manager = create({
plugin: {
$moveStart(danmaku) {
for (const key in styles) {
danmaku.setStyle(key, styles[key]);
}
// 你也可以在这里给弹幕 DOM 添加 className
danmaku.node.classList.add('className');
},
},
});

// 对当前正在渲染的弹幕添加样式
manager.asyncEach((danmaku) => {
for (const key in styles) {
danmaku.setStyle(key, styles[key]);
}
});

自定义容器样式


import { create } from 'danmu';

// 需要添加的样式
const styles = {
background: 'red',
// .
};

const manager = create({
plugin: {
// 你可以在初始化的时候添加钩子处理
init(manager) {
for (const key in styles) {
manager.container.setStyle(key, styles[key]);
}
// 你也可以在这里给容器 DOM 添加 className
manager.container.node.classList.add('className');
},
},
});

// 或者直接调用 api
for (const key in styles) {
manager.container.setStyle(key, styles[key]);
}

高级弹幕的示例


本章节将介绍如何将弹幕固定在某一位置,以 top 和 left 这两个位置举例。由于我们需要自定义位置,所以我们需要使用高级弹幕的能力。


将弹幕固定在顶部


// 这条弹幕将会居中距离顶部 10px 的位置悬停 5s
manager.pushFlexibleDanmaku('弹幕内容', {
duration: 5000,
direction: 'none',
position(danmaku, container) {
return {
x: `50% - ${danmaku.getWidth() / 2}`,
y: 10, // 具体容器顶部的距离为 10px
};
},
});

固定在顶部第 2 条轨道上


// 这条弹幕将会在第二条轨道居中的位置悬停 5s
manager.pushFlexibleDanmaku('弹幕内容', {
duration: 5000,
direction: 'none',
position(danmaku, container) {
// 渲染在第 3 条轨道中
const { middle } = manager.getTrackLocation(2);
return {
x: `50% - ${danmaku.getWidth() / 2}`,
y: middle - danmaku.getHeight() / 2,
};
},
});

将弹幕固定在左边


// 这条弹幕将会在容器中间距离左边 10px 的地方停留 5s
manager.pushFlexibleDanmaku('弹幕内容', {
duration: 5000,
direction: 'none',
position(danmaku, container) {
// 渲染在第 3 条轨道中
const { middle } = manager.getTrackLocation(2);
return {
x: 10,
y: `50% - ${danmaku.getHeight() / 2}`,
};
},
});

发送带图片的弹幕


要让弹幕里面能够携带图片,要在弹幕的节点内部添加自定义的内容,实际上不止图片,你可以往弹幕的节点里面添加任何的内容。



本章节的组件以 React 来实现演示。



开发弹幕组件


export function Danmaku({ danmaku }) {
return (
<div>
<img src="https://abc.jpg" />
{danmaku.data}
</div>

);
}

渲染弹幕


import ReactDOM from 'react-dom/client';
import { create } from 'danmu';
import { Danmaku } from './Danmaku';

const manager = create<string>({
plugin: {
// 将组件渲染到弹幕的内置节点上
$createNode(danmaku) {
ReactDOM.createRoot(danmaku.node).render(<Danmaku danmaku={danmaku} />);
},
},
});

编写一个插件


编写一个插件是很简单的,但是借助内核暴露出来的钩子和 API,你可以很轻松的实现强大且定制化的需求。由于内核没有暴露出来根据条件来实现过滤弹幕的功能,原因在于内核不知道弹幕内容的数据结构,这和业务的诉求强相关,所以我们在此通过插件来实现精简弹幕的功能用来演示。


编写一个插件



  • 你编写的插件应当取一个 name,以便于调试定位问题(注意不要和其他插件冲突了)。

  • 插件可以选择性的声明一个 version,这在你的插件作为独立包发到 npm 上时很有用。


export function filter({ userIds, keywords }) {
return (manager) => {
return {
name: 'filter-keywords-or-user',
version: '1.0.0', // version 字段不是必须的
willRender(ref) {
const { userId, content } = ref.danmaku.data.value;
console.log(ref.type); // 可以根据此字段来区分是普通弹幕还是高级弹幕

if (userIds && userIds.includes(userId)) {
ref.prevent = true;
} else if (keywords) {
for (const word of keywords) {
if (content.includes(word)) {
ref.prevent = true;
break;
}
}
}
return ref;
},
};
};
}

注册插件


你需要通过 mananger.use() 来注册插件。


import { create } from 'danmu';

const manager = create<{
userId: number;
content: string;
}>();

manager.use(
filter({
userIds: [1],
keywords: ['菜'],
}),
);

发送弹幕



  • ❌ 被插件阻止渲染


manager.push({
userId: 1,
content: '',
});


  • ❌ 被插件阻止渲染


manager.push({
userId: 2,
content: '你真菜',
});


  • ✔️ 不会被插件阻止渲染


manager.push({
userId: 2,
content: '',
});


  • ✔️ 不会被插件阻止渲染


manager.push({
userId: 2,
content: '你真棒',
});

总结


本文档只是简单介绍了下现在的部分能力,更详细的文档在官网可以查看,如果对你的业务或者学习有帮助的,给个 star 支持一下作者,也欢迎大家评论探讨(不止弹幕,哈哈)。


作者:imtt
来源:juejin.cn/post/7408364808607957002
收起阅读 »

别再用模板语法和'+'来拼接url了

web
在前端开发中,我们经常需要处理URL,例如在发起HTTP请求时构建API端点,或在页面导航时构建动态链接、拼接动态参数。 过去,我们习惯于使用模板语法和字符串拼接来构建这些URL,现在在代码中依然可以看到新的代码还在使用这种方法。 但这种方法不仅容易出错,而且...
继续阅读 »

在前端开发中,我们经常需要处理URL,例如在发起HTTP请求时构建API端点,或在页面导航时构建动态链接、拼接动态参数。
过去,我们习惯于使用模板语法和字符串拼接来构建这些URL,现在在代码中依然可以看到新的代码还在使用这种方法。
但这种方法不仅容易出错,而且在维护和阅读代码时也不够直观。本文将介绍更现代和更安全的URL构建方法,并展示如何在实际项目中应用它们。


传统上,我们常使用字符串拼接或模板语法来构建URL。例如:


const baseUrl = "https://api.example.com";
const userId = 12345;
const endpoint = baseUrl + "/users/" + userId + "/details";
console.log(endpoint); // "https://api.example.com/users/12345/details"

import { TYPE_EDIT } from '@/constants/type.ts'
const type = TYPE_EDIT
const url = 'https://api.example.com/userInfo'
const newUrl = url + '?type=' + type + '&model=1&share=1&fromModule=wechat'
console.log(urlUrl) // https://api.example.com/userInfo?type=TYPE_EDIT&model=1&share=1&fromModule=wechat

或使用ES6模板字符串:


const baseUrl = "https://api.example.com";
const userId = 12345;
const endpoint = `${baseUrl}/users/${userId}/details`;
console.log(endpoint); // "https://api.example.com/users/12345/details"

import { TYPE_EDIT } from '@/constants/type.ts'
const type = TYPE_EDIT
const url = 'https://api.example.com/userInfo'
const newUrl = url + `?type=${type}&model=1&share=1&fromModule=wechat`
console.log(urlUrl) // https://api.example.com/userInfo?type=TYPE_EDIT&model=1&share=1&fromModule=wechat

虽然模板字符串在一定程度上提高了可读性,但这种方法仍存在几个问题:



  1. 易读性差:当URL变得复杂时,拼接和模板字符串会变得难以阅读和维护(现阶段已经难以阅读和维护了)。

  2. 错误处理麻烦:拼接过程中如果有任何错误(例如漏掉斜杠),可能会导致难以排查的BUG。

  3. 缺乏类型安全:拼接字符串无法提供编译时的类型检查,容易引入错误。


使用URL构造器


为了解决这些问题,现代JavaScript引入了URL构造器,可以更优雅和安全地处理URL。URL构造器提供了一种更结构化和直观的方法来构建和操作URL。


基本用法


const baseUrl = "https://api.example.com";
const userId = 12345;
const url = new URL(`/users/${userId}/details`, baseUrl);
console.log(url.href); // "https://api.example.com/users/12345/details"


添加查询参数


URL构造器还提供了一种简便的方法来添加和操作查询参数:


const baseUrl = "https://api.example.com";
const userId = 12345;

const url = new URL(`/users/${userId}/details`, baseUrl);
url.searchParams.append('type', 'EDIT');
url.searchParams.append('module', 'wechat');
console.log(url.href); // "https://api.example.com/users/12345/details?type=EDIT&module=wechat"


拼接数组参数


假设我们有一个URL,需要将一个数组作为查询参数添加到URL中。


const baseUrl = 'https://example.com';
const url = new URL(baseUrl);

const arrayParam = ['value1', 'value2', 'value3'];
// 将数组转换为逗号分隔的字符串
url.searchParams.set('array', arrayParam.join(','));

console.log(url.toString()); // https://example.com/?array=value1,value2,value3


解析数组参数


当我们获取URL并需要解析其中的数组参数时,可以使用URLSearchParams对象进行解析。


const urlString = 'https://example.com/?array=value1,value2,value3';
const url = new URL(urlString);

const arrayParamString = url.searchParams.get('array');
// 将逗号分隔的字符串转换回数组
const arrayParam = arrayParamString ? arrayParamString.split(',') : [];

console.log(arrayParam); // ['value1', 'value2', 'value3']


以下是一个完整示例,包括拼接和解析数组参数的操作:


// 拼接数组参数到URL
const baseUrl = 'https://example.com';
const url = new URL(baseUrl);

const arrayParam = ['value1', 'value2', 'value3'];
url.searchParams.set('array', arrayParam.join(','));

console.log(url.toString()); // https://example.com/?array=value1,value2,value3

// 解析数组参数从URL
const urlString = url.toString();
const parsedUrl = new URL(urlString);

const arrayParamString = parsedUrl.searchParams.get('array');
const parsedArrayParam = arrayParamString ? arrayParamString.split(',') : [];

console.log(parsedArrayParam); // ['value1', 'value2', 'value3']


处理多个同名参数


有时我们可能会遇到需要处理多个同名参数的情况,例如?array=value1&array=value2&array=value3。可以使用URLSearchParamsgetAll方法:


// 拼接多个同名参数到URL
const url = new URL(baseUrl);

const arrayParam = ['value1', 'value2', 'value3'];
arrayParam.forEach(value => url.searchParams.append('array', value));

console.log(url.toString()); // https://example.com/?array=value1&array=value2&array=value3

// 解析多个同名参数从URL
const urlString = url.toString();
const parsedUrl = new URL(urlString);

const parsedArrayParam = parsedUrl.searchParams.getAll('array');

console.log(parsedArrayParam); // ['value1', 'value2', 'value3']


通过这些方法,可以更加优雅和简便地处理URL中的数组参数,提升代码的可读性和可维护性。


但实际情况往往比上面的示例更复杂,比如参数是一个对象、根据实际情况来设置参数的值、要处理undefined'undefined'0'0'Boolean'true'NaN等不同类型和异常的值,每次使用时都去处理显然是不合理的,这时候就可以将拼接和移除参数的函数封装成方法来使用。


/**
* 获取URL查询参数并返回一个对象,支持数组
* @param {string} urlString - 需要解析的URL字符串
* @returns {Object} - 包含查询参数的对象
*/

function getURLParams(urlString) {
const url = new URL(urlString);
const params = new URLSearchParams(url.search);
const result = {};

for (const [key, value] of params.entries()) {
if (result[key]) {
if (Array.isArray(result[key])) {
result[key].push(value);
} else {
result[key] = [result[key], value];
}
} else {
result[key] = value;
}
}

return result;
}

/**
* 设置URL的查询参数,支持对象和数组
* @param {string} urlString - 基础URL字符串
* @param {Object} params - 需要设置的查询参数对象
* @returns {string} - 带有查询参数的URL字符串
*/

function setURLParams(urlString, params) {
const url = new URL(urlString);
const searchParams = new URLSearchParams();

for (const key in params) {
if (params.hasOwnProperty(key)) {
const value = params[key];
if (Array.isArray(value)) {
value.forEach(val => {
if (val !== undefined && !Number.isNaN(val)) {
searchParams.append(key, val);
} else {
console.warn(`Warning: The value of "${key}" is ${val}, which is invalid and will be ignored.`);
}
});
} else if (value !== undefined && !Number.isNaN(value)) {
searchParams.append(key, value);
} else {
console.warn(`Warning: The value of "${key}" is ${value}, which is invalid and will be ignored.`);
}
}
}

url.search = searchParams.toString();
return url.toString();
}

// 测试用例
const baseUrl = 'https://example.com';

// 测试 getURLParams 方法
const testUrl = 'https://example.com/?param1=value1&param2=value2&param2=value3';
const parsedParams = getURLParams(testUrl);
console.log(parsedParams); // { param1: 'value1', param2: ['value2', 'value3'] }

// 测试 setURLParams 方法
const params = {
param1: 'value1',
param2: ['value2', 'value3'],
param3: undefined,
param4: NaN,
param5: 'value5',
param6: 0,
};

const newUrl = setURLParams(baseUrl, params);
console.log(newUrl); // 'https://example.com/?param1=value1&param2=value2&param2=value3&param5=value5'

以上代码是根据掌握的知识编写的基本使用示例,像这种工作完全不用自己来写,现在已经有非常成熟的库可以直接使用。


qs


npmjs http://www.npmjs.com/package/qs


它是开源免费项目,每周下载量将近7千万,支持任意字符,对象进行解析和拼接,支持@types/qs,导入后11.3k,建议打包编译时排除在打包文件外用cdn替代。


image.png


query-string


npmjs http://www.npmjs.com/package/que…


image.png


它是开源免费项目,每周下载量达千万,支持任意字符、对象进行解析和拼接,支持ts,导入后仅2.5k字节。


image.png


PC和H5如果使用了微前端,建议一开始打包时就将依赖排除在打包文件外,用cdn链接来替代,仅加载一次就可以缓存下来,可以加速页面加载、减小打包文件大小。


当然更多时候我们在编写h5、小程序项目的时候并不希望为了一个url解析参数和拼接参数的功能而引入一整个依赖。
这时候一个简单的解析和拼接的函数就可以搞定。


方法有多种实现方式,下面还有一种通过正则来实现的,但下面拼接的时候会忽略数字0,所以参数一定要用字符串。


/**
* 合并查询参数到 URL 的函数
* 将给定的查询对象 Query 合并到指定的 URL 中
*
* @param {Object} query - 要合并到 URL 中的查询对象
* @param {string} url - 作为基础的 URL,默认为当前页面的 URL
* @returns {string} 生成的合并查询参数后的新 URL
*/

export function getUrlMergeQuery(query = {}, url) {
url = url || window.location.href
const _orgQuery = getQueryObject(url)
const _query = {..._orgQuery,...query }
let _arr = []
for (let key in _query) {
const value = _query[key]
if (value) _arr.push(`${key}=${encodeURIComponent(_query[key])}`)
}
return `${url.split('?')[0]}${_arr.length > 0? `?${_arr.join('&')}` : ''}`
}

/**
* 从 URL 中提取查询参数对象
*
* @param {string} [url=window.location.href] - 要解析的 URL 字符串。如果未提供,则使用当前页面的 URL
* @returns {Object} - 包含提取的查询参数的对象
*/

export function getQueryObject(url = window.location.href) {
const search = url.substring(url.lastIndexOf('?') + 1);
const obj = {};
const reg = /([^?&=]+)=([^?&=]*)/g;
search.replace(reg, (rs, $1, $2) => {
const name = decodeURIComponent($1);
let val = decodeURIComponent($2);
val = String(val);
obj[name] = val;
return rs;
});
return obj;
}


你的项目中一定提供了合适的方法,不要在用字符串拼接的方法来拼接参数了。


作者:pe7er
来源:juejin.cn/post/7392788843097931802
收起阅读 »

职场进阶:从研发到一线主管

前言 背景是把近来看的管理相关书籍(书籍附文末)和个人思考进行了梳理。在这里顺手把个人关键角色转型的核心分享给大家,希望对大家有所帮助。 至今经历过的三次关键角色转型: 从新手到团队骨干 从团队骨干到虚线组长 从虚线组长到一线主管 本着大道至简的初衷,以下...
继续阅读 »



前言


背景是把近来看的管理相关书籍(书籍附文末)和个人思考进行了梳理。在这里顺手把个人关键角色转型的核心分享给大家,希望对大家有所帮助。


至今经历过的三次关键角色转型:



  • 从新手到团队骨干

  • 从团队骨干到虚线组长

  • 从虚线组长到一线主管


本着大道至简的初衷,以下是角色转型的3个关键点:



  1. 核心:思维转变(重中之重)

  2. 能力:其次是技能补充(如果已知技能可以提前储备)

  3. 时间:基于角色的时间管理


关键角色


成为团队骨干


思维转变(重中之重):



  • 主人翁意识:具备主人翁意识,不要只着眼于自己的一亩三分地。

  • 结果导向:任何事情以结果为导向,适当的“吃亏”。

  • 超预期交付:不仅限于完成明确的任务。


补充技能:



  • 项目管理能力:协作、沟通、排期和任务管理、风险把控。


时间管理:



  • 做好基于任务管理的TODO list

  • 项目管理时间


其他:



  • 养成思考的习惯。比如,做完一个项目后进行必要的总结。

  • 提升技术广度和深度。比如,学习未来可能会用到的技术栈,逐步尝试读源码。


详细总结如下:


图片


成为虚线组长


思维转变(重中之重):



  • 具备体系化建设能力,成为某个领域的专家。


补充技能:



  • 领导能力。比如能指导其他人完成工作,并且承担培养组员的责任。


时间管理:



  • 体系建设时间:调研业界实现,归纳和梳理内部问题,产出基于当前的团队的最佳实践。

  • 团队管理时间:组员日常任务管理,负责组员个人成长。


详细总结如下:


图片


成为一线主管


思维转变(重中之重):



  • 尽早意识到被衡量成功的方式变了:从你个人的成功转变为团队的成功

  • 尽早意识你的时间必然变得零碎:招聘时间、业务和跨团队沟通时间、团队规划时间、项目管理时间、汇报文档时间、预算时间、组员1V1时间、流程和标准建立时间,这就是你的工作内容


补充技能:



  • 面试技巧:基于预算范围内招聘高素质、高度自律、高绩效的员工

  • 组织和梯队建设能力:基于业务发展动态调整组织,识别团队现状设计梯队成长目标

  • 预算管理


时间管理:基于角色的时间管理



  • 团队管理者角色:

    • 招聘时间

    • 流程和标准建立时间

    • 业务沟通时间

    • 1V1时间:提前准备问题,比如请你想想,我常做哪些浪费你的时间又不产生效果的事情?

    • 培训时间:核心让每个人知道公司是如何运作的



  • 项目管理者角色:

    • 项目管理时间



  • 个人角色:

    • 个人事项时间




详细总结如下:


图片


总结



  • 从新手到团队骨干

    • 核心思维转变:主人翁意识和超预期交付



  • 从团队骨干到虚线组长

    • 核心思维转变:从交付单个项目到交付好用系统



  • 从虚线组长到一线主管

    • 核心思维转变:团队的成功才是你的成功,接受时间碎片化的转变,做好基于角色的时间管理




接下来面临的关键角色转型:


从技术人到具备经营者意识的技术人转变。


作者:施展TIGERB
来源:juejin.cn/post/7401812292210737162
收起阅读 »

昨天晚上,RPC 线程池被打满了,原因哭笑不得

大家好,我是五阳。 1. 故障背景 昨天晚上,我刚到家里打开公司群,就看见群里有人讨论:线上环境出现大量RPC请求报错,异常原因:被线程池拒绝。虽然异常量很大,但是异常服务非核心服务,属于系统旁路,服务于数据核对任务,即使有大量异常,也没有实际的影响。 原来有...
继续阅读 »

大家好,我是五阳。


1. 故障背景


昨天晚上,我刚到家里打开公司群,就看见群里有人讨论:线上环境出现大量RPC请求报错,异常原因:被线程池拒绝。虽然异常量很大,但是异常服务非核心服务,属于系统旁路,服务于数据核对任务,即使有大量异常,也没有实际的影响。


原来有人在线上刷数据,产生了大量 binlog,数据核对任务的请求量大幅上涨,导致线程池被打满。因为并非我负责的工作内容,也不熟悉这部分业务,所以没有特别留意。


第二天我仔细思考了一下,觉得疑点很多,推导过程过于简单,证据链不足,最终结论不扎实,问题根源也许另有原因。


1.1 疑点



  1. 请求量大幅上涨, 上涨前后请求量是多少?

  2. 线程池被打满, 线程池初始值和最大值是多少,线程池队列长度是多少?

  3. 线程池拒绝策略是什么

  4. 影响了哪些接口,这些接口的耗时波动情况?

  5. 服务的 CPU 负载和 GC情况如何?

  6. 线程池被打满的原因仅仅是请求量大幅上涨吗?


带着以上的几点疑问,第二天一到公司,我就迫不及待地打开各种监控大盘,开始排查问题,最后还真叫我揪出问题根源了。


因为公司的监控系统有水印,所以我只能陈述结论,不能截图了。


2. 排查过程


2.1 请求量的波动情况



  • 单机 RPC的 QPS从 300/s 涨到了 450/s。

  • Kafka 消息 QPS 50/s 无 明显波动。

  • 无其他请求入口和 无定时任务。


这也能叫请求量大幅上涨,请求量增加 150/s 能打爆线程池?就这么糊弄老板…… ,由此我坚定了判断:故障另有根因


2.2 RPC 线程池配置和监控


线上的端口并没有全部被打爆,仅有 1 个 RPC 端口 8001 被打爆。所以我特地查看了8001 的线程池配置。



  • 初始线程数 10

  • 最大线程数 1024(数量过大,配置的有点随意了)

  • 队列长度 0

  • 拒绝策略是抛出异常立即拒绝。

  • 在 20:11到 20:13 分,线程从初始线程数10,直线涨到了1024 。


2.3 思考


QPS 450次/秒 需要 1024 个线程处理吗?按照我的经验来看,只要接口的耗时在 100ms 以内,不可能需要如此多的线程,太蹊跷了。


2.4 接口耗时波动情况



  • 接口 平均耗时从 5.7 ms,增加到 17000毫秒。


接口耗时大幅增加。后来和他们沟通,他们当时也看了接口耗时监控。他们认为之所以平均耗时这么高,是因为RPC 请求在排队,增加了处理耗时,所以监控平均耗时大幅增长。


这是他们的误区,错误的地方有两个。



  1. 此RPC接口线程池的队列长度为 0,拒绝策略是抛出异常。当没有可用线程,请求会即被拒绝,请求不会排队,所以无排队等待时间。

  2. 公司的监控系统分服务端监控和调用端监控,服务端的耗时监控不包含 处理连接的时间,不包含 RPC线程池排队的时间。仅仅是 RPC 线程池实际处理请求的耗时。 RPC 调用端的监控包含 RPC 网络耗时、连接耗时、排队耗时、处理业务逻辑耗时、服务端GC 耗时等等。


他们误认为耗时大幅增加是因为请求在排队,因此忽略了至关重要的这条线索:接口实际处理阶段的性能严重恶化,吞吐量大幅降低,所以线程池大幅增长,直至被打满。


接下来我开始分析,接口性能恶化的根本原因是什么?



  • CPU 被打满?导致请求接口性能恶化?

  • 频繁GC ,导致接口性能差?

  • 调用下游 RPC 接口耗时大幅增加 ?

  • 调用 SQL,耗时大幅增加?

  • 调用 Redis,耗时大幅增加

  • 其他外部调用耗时大幅增加?


2.5 其他耗时监控情况


我快速的排查了所有可能的外部调用耗时均没有明显波动。也查看了机器的负载情况,cpu和网络负载 均不高,显然故障的根源不在以上方向



  • CPU 负载极低。在故障期间,cpu.busy 负载在 15%,还不到午高峰,显然根源不是CPU 负载高。

  • gc 情况良好。无 FullGC,youngGC 1 分钟 2 次(younggc 频繁,会导致 cpu 负载高,会使接口性能恶化)

  • 下游 RPC 接口耗时无明显波动。我查看了服务调用 RPC 接口的耗时监控,所有的接口耗时无明显波动。

  • SQL 调用耗时无明显波动。

  • 调用 Redis 耗时无明显波动。

  • 其他下游系统调用无明显波动。(如 Tair、ES 等)


2.6 开始研究代码


为什么我一开始不看代码,因为这块内容不是我负责的内容,我不熟悉代码。


直至打开代码看了一眼,恶心死我了。代码非常复杂,分支非常多,嵌套层次非常深,方法又臭又长,堪称代码屎山的珠穆朗玛峰,多看一眼就能吐。接口的内部分支将近 10 个,每个分支方法都是一大坨代码。


这个接口是上游 BCP 核对系统定义的 SPI接口,属于聚合接口,并非单一职责的接口。看了 10 分钟以后,还是找不到问题根源。因此我换了问题排查方向,我开始排查异常 Trace。


2.7 从异常 Trace 发现了关键线索


我所在公司的基建能力还是很强大的。系统的异常 Trace 中标注了各个阶段的处理耗时,包括所有外部接口的耗时。如SQL、 RPC、 Redis等。


我发现确实是内部代码处理的问题,因为 trace 显示,在两个 SQL 请求中间,系统停顿长达 1 秒多。不知道系统在这 1 秒执行哪些内容。我查看了这两个接口的耗时,监控显示:SQL 执行很快,应该不是SQL 的问题


机器也没有发生 FullGC,到底是什么原因呢?


前面提到,故障接口是一个聚合接口,我不清楚具体哪个分支出现了问题,但是异常 Trace 中指明了具体的分支。


我开始排查具体的分支方法……, 然而捏着鼻子扒拉了半天,也没有找到原因……


2.8 山穷水复疑无路,柳暗花明又一村


这一坨屎山代码看得我实在恶心,我静静地冥想了 1 分钟才缓过劲。



  • 没有外部调用的情况下,阻塞线程的可能性有哪些?

  • 有没有加锁? Synchiozed 关键字?


于是我按着关键字搜索Synchiozed关键词,一无所获,代码中基本没有加锁的地方。


马上中午了,肚子很饿,就当我要放弃的时候。随手扒拉了一下,在类的属性声明里,看到了 Guava限流器。


激动的心,颤抖的手


private static final RateLimiter RATE_LIMITER = RateLimiter.create(10, 20, TimeUnit.SECONDS);

限流器:1 分钟 10次调用。


于是立即查看限流器的使用场景,和异常 Trace 阻塞的地方完全一致。


嘴角出现一丝很容易察觉到的微笑。


image.png


破案了,真相永远只有一个。


3. 问题结论


Guava 限流器的阈值过低,每秒最大请求量只有10次。当并发量超过这个阈值时,大量线程被阻塞,RPC线程池不断增加新线程来处理新的请求,直到达到最大线程数。线程池达到最大容量后,无法再接收新的请求,导致大量的后续请求被线程池拒绝。


于是我开始建群、摇人。把相关的同学,还有老板们,拉进了群里。把相关截图和结论发到了群里。


由于不是紧急问题,所以我开开心心的去吃午饭了。后面的事就是他们优化代码了。


4. 思考总结


4.1 八股文不是完全无用,有些八股文是有用的


有人质疑面试为什么要问八股文? 也许大部分情况下,大部分八股文是没有用的,然而在排查问题时,多知道一些八股文是有助于排查问题的。


例如要明白线程池的原理、要明白 RPC 的请求处理过程、要知道影响接口耗时的可能性有哪些,这样才能带着疑问去追踪线索。


4.2 可靠好用的监控系统,能提高排查效率


这个问题排查花了我 1.5 小时,大部分时间是在扒拉代码,实际查看监控只用了半小时。如果没有全面、好用的监控,线上出了问题真的很难快速定位根源。


此外应该熟悉公司的监控系统。他们因为不清楚公司监控系统,误认为接口监控耗时包含了线程池排队时间,忽略了 接口性能恶化 这个关键结论,所以得出错误结论。


4.2 排查问题时,要像侦探一样,不放过任何线索,确保证据链完整,逻辑通顺。


故障发生后第一时间应该是止损,其次才是排查问题。


出现故障后,故障制造者的心理往往处于慌乱状态,逻辑性较差,会倾向于为自己开脱责任。这次故障后,他们给定的结论就是:因为请求量大幅上涨,所以线程池被打满。 这个结论逻辑不通。没有推导过程,跳过了很多推导环节,所以得出错误结论,掩盖了问题的根源~


其他人作为局外人,逻辑性会更强,可以保持冷静状态,这是老板们的价值所在,他们可以不断地提出问题,在回答问题、质疑、继续排查的循环中,不断逼近事情的真相。


最终一定要重视证据链条的完整。别放过任何线索。


出现问题不要慌张,也不要吃瓜嗑瓜子。行动起来,此时是专属你的柯南时刻


我是五阳,关注我,追踪更多我在大厂的工作经历和大型翻车现场。


image.png


作者:五阳
来源:juejin.cn/post/7409181068597313573
收起阅读 »

别再混淆了!一文带你搞懂@Valid和@Validated的区别

上篇文章我们简单介绍和使用了一下Springboot的参数校验,同时也用到了 @Valid 注解和 @Validated 注解,那它们之间有什么不同呢?区别先总结一下它们的区别:来源@Validated :是S...
继续阅读 »

上篇文章我们简单介绍和使用了一下Springboot的参数校验,同时也用到了 @Valid 注解和 @Validated 注解,那它们之间有什么不同呢?

区别

先总结一下它们的区别:

  1. 来源

    • @Validated :是Spring框架特有的注解,属于Spring的一部分,也是JSR 303的一个变种。它提供了一些 @Valid 所没有的额外功能,比如分组验证。
    • @Valid:Java EE提供的标准注解,它是JSR 303规范的一部分,主要用于Hibernate Validation等场景。
  2. 注解位置

    • @Validated : 用在类、方法和方法参数上,但不能用于成员属性。
    • @Valid:可以用在方法、构造函数、方法参数和成员属性上。
  3. 分组

    • @Validated :支持分组验证,可以更细致地控制验证过程。此外,由于它是Spring专有的,因此可以更好地与Spring的其他功能(如Spring的依赖注入)集成。
    • @Valid:主要支持标准的Bean验证功能,不支持分组验证。
  4. 嵌套验证

    • @Validated :不支持嵌套验证。
    • @Valid:支持嵌套验证,可以嵌套验证对象内部的属性。

这些理论性的东西没什么好说的,记住就行。我们主要看分组嵌套验证是什么,它们怎么用。

实操阶段

话不多说,通过代码来看一下分组嵌套验证

为了提示友好,修改一下全局异常处理类:

@RestControllerAdvice
public class GlobalExceptionHandler {

/**
* 参数校检异常
* @param e
* @return
*/

@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseResult handle(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();

StringJoiner joiner = new StringJoiner(";");

for (ObjectError error : bindingResult.getAllErrors()) {
String code = error.getCode();
String[] codes = error.getCodes();

String property = codes[1];
property = property.replace(code ,"").replaceFirst(".","");

String defaultMessage = error.getDefaultMessage();
joiner.add(property+defaultMessage);
}
return handleException(joiner.toString());
}

private ResponseResult handleException(String msg) {
ResponseResult result = new ResponseResult<>();
result.setMessage(msg);
result.setCode(500);
return result;
}
}

分组校验

分组验证是为了在不同的验证场景下能够对对象的属性进行灵活地验证,从而提高验证的精细度和适用性。一般我们在对同一个对象进行保存或修改时,会使用同一个类作为入参。那么在创建时,就不需要校验id,更新时则需要校验用户id,这个时候就需要用到分组校验了。

对于定义分组有两点要特别注意

  1. 定义分组必须使用接口。
  2. 要校验字段上必须加上分组,分组只对指定分组生效,不加分组不校验。

有这样一个需求,在创建用户时校验用户名,修改用户时校验用户id。下面对我们对这个需求进行一个简单的实现。

  1. 创建分组

CreationGr0up 用于创建时指定的分组:

public interface CreationGr0up {
}

UpdateGr0up 用于更新时指定的分组:

public interface UpdateGr0up {
}
  1. 创建用户类

创建一个UserBean用户类,分别校验 username 字段不能为空和id字段必须大于0,然后加上CreationGr0up和 UpdateGr0up 分组。

/**
* @author 公众号-索码理(suncodernote)
*/

@Data
public class UserBean {

@NotEmpty( groups = {CreationGr0up.class})
private String username;

@Min(value = 18)
private Integer age;

@Email(message = "邮箱格式不正确")
private String email;

@Min(value = 1 ,groups = {UpdateGr0up.class})
private Long id;
}
  1. 创建接口

ValidationController 中新建两个接口 updateUser 和 createUser

@RestController
@RequestMapping("validation")
public class ValidationController {

@GetMapping("updateUser")
public UserBean updateUser(@Validated({UpdateGr0up.class}) UserBean userBean){
return userBean;
}

@GetMapping("createUser")
public UserBean createUser(@Validated({CreationGr0up.class}) UserBean userBean){
return userBean;
}
}
  1. 测试

先对 createUser接口进行测试,我们将id的值设置为0,也就是不满足id必须大于0的条件,同样 username 不传值,即不满足 username 不能为空的条件。  通过测试结果我们可以看到,虽然id没有满足条件,但是并没有提示,只提示了username不能为空。

再对 updateUser接口进行测试,条件和测试 createUser接口的条件一样,再看测试结果,和 createUser接口测试结果完全相反,只提示了id最小不能小于1。 

至此,分组功能就演示完毕了。

嵌套校验

介绍嵌套校验之前先看一下两个概念:

  1. 嵌套校验(Nested Validation) 指的是在验证对象时,对对象内部包含的其他对象进行递归验证的过程。当一个对象中包含另一个对象作为属性,并且需要对这个被包含的对象也进行验证时,就需要进行嵌套校验。
  2. 嵌套属性指的是在一个对象中包含另一个对象作为其属性的情况。换句话说,当一个对象的属性本身又是一个对象,那么这些被包含的对象就可以称为嵌套属性

有这样一个需求,在保存用户时,用户地址必须要填写。下面来简单看下示例:

  1. 创建地址类 AddressBean

AddressBean 设置 countrycity两个属性为必填项。

@Data
public class AddressBean {

@NotBlank
private String country;

@NotBlank
private String city;
}
  1. 修改用户类,将AddressBean作为用户类的一个嵌套属性

特别提示:想要嵌套校验生效,必须在嵌套属性上加 @Valid 注解。

@Data
public class UserBean {

@NotEmpty(groups = {CreationGr0up.class})
private String username;

@Min(value = 18)
private Integer age;

private String email;

@Min(value = 1 ,groups = {UpdateGr0up.class})
private Long id;

//嵌套验证必须要加上@Valid
@Valid
@NotNull
private AddressBean address;
}
  1. 创建一个嵌套校验测试接口
@PostMapping("nestValid")
public UserBean nestValid(@Validated @RequestBody UserBean userBean){
System.out.println(userBean);
return userBean;
}
  1. 测试

我们在传参时,只传 country字段,通过响应结果可以看到提示了city 字段不能为空。 响应结果

可以看到使用了 @Valid 注解来对 Address 对象进行验证,这会触发对其中的 Address 对象的验证。通过这种方式,可以确保嵌套属性内部的对象也能够参与到整体对象的验证过程中,从而提高验证的完整性和准确性。

总结

本文介绍了@Valid注解和@Validated注解的不同,同时也进一步介绍了Springboot 参数校验的使用。不管是 JSR-303JSR-380又或是 Hibernate Validator ,它们提供的参数校验注解都是有限的,实际工作中这些注解可能是不够用的,这个时候就需要我们自定义参数校验了。下篇文章将介绍一下如何自定义一个参数校验器。


作者:索码理
来源:juejin.cn/post/7344958089429434406
收起阅读 »