解决伪元素三角形box-shadow阴影不合理的问题

在CSS中,我们经常使用伪元素(::before, ::after)来创建各种形状,特别是三角形。然而,当我们需要为这些三角形添加box-shadow时,经常会遇到阴影显示不合理的问题。本文将探讨这个问题的原因及解决方案。

问题描述

当我们使用边框法创建三角形并尝试添加box-shadow时,阴影会出现在包含三角形的矩形框上,而不是三角形本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.triangle {
position: relative;
width: 100px;
height: 100px;
}

.triangle::after {
content: '';
position: absolute;
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 100px solid #3498db;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}

上面的代码会在整个伪元素的矩形区域上显示阴影,而不是只在下方的三角形上。

解决方案

方法1:使用filter: drop-shadow

filter: drop-shadow 可以正确识别元素的透明部分,只对有颜色的部分应用阴影:

1
2
3
4
.triangle::after {
/* 三角形代码同上 */
filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.5));
}

方法2:使用clip-path

结合clip-path裁剪出三角形形状,然后应用box-shadow:

1
2
3
4
5
6
7
8
9
.triangle::after {
content: '';
position: absolute;
width: 100px;
height: 100px;
background: #3498db;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}

方法3:使用SVG

使用SVG创建三角形可以更精确地控制阴影:

1
2
3
4
5
6
<div class="triangle">
<svg width="100" height="100" viewBox="0 0 100 100">
<polygon points="50,0 0,100 100,100" fill="#3498db"
filter="drop-shadow(0 0 10px rgba(0,0,0,0.5))"/>
</svg>
</div>

注意事项

  1. filter: drop-shadow 的性能消耗比 box-shadow 略高,在动画中大量使用可能导致性能问题
  2. clip-path的浏览器兼容性较好,但某些旧版本浏览器可能需要前缀
  3. SVG方案兼容性最好,但会增加DOM节点
  4. 如果必须使用边框法创建三角形,可以考虑在外部容器上应用阴影

总结

对于伪元素创建的三角形阴影问题,推荐优先使用filter: drop-shadow方案,它既能保持代码简洁,又能正确显示阴影。在需要更复杂形状或更好性能时,可以考虑clip-path或SVG方案。

解决HTML img元素底部幽灵空隙问题

在使用div包裹<img>元素时,可能会遇到元素底部出现异常空隙的问题。

问题原因

  1. 行内元素的基线对齐特性
  2. 父容器的默认行高设置
  3. 图片与文本混合时的垂直对齐问题

解决方案

方法一:设置图片为块级元素

1
<img src="/path/to/image.jpg" style="display: block;" />

或在 CSS 中全局设置:

1
2
3
img {
display: block;
}

方法二:调整垂直对齐方式

1
2
3
<img
src="/path/to/image.jpg"
style="vertical-align: middle; display: inline-block;" />

方法三:移除父元素的行高

1
2
3
.post-content img {
line-height: 0;
}

方法四:使用负边距(不推荐)

1
2
3
img {
margin-bottom: -4px;
}

商品图片放大镜效果

注意的点

  1. 取景框要设置csspointer-events: none;,否则鼠标移动到取景框上时,会触发鼠标移动事件,导致位置抽搐。
  2. 图片幽灵缝隙问题,可以考虑display: flex;或者其他的解决办法。
  3. 使用transform代替topleft,避免回流,优化性能。
  4. 预览图的定位问题。
  5. 考虑取景框边框对放大预览图片的影响。

直接贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<template>
<div class="container flex">
<div class="img" @mousemove="onMousemove">
<img src="../../public/logos/avatar1.png" alt="" ref="img">
<div ref="small" class="small"></div>
</div>
<div class="prev">
<img src="../../public/logos/avatar1.png" alt="" ref="prevImg">
</div>
</div>
</template>

<script setup>
import { onMounted, ref } from 'vue';

const scale = ref(1);
const img = ref(null);
const small = ref(null);
const prevImg = ref(null);
const offsetX = ref(0);
const offsetY = ref(0);

function checkElements() {
if (!img.value) throw new Error('找不到图片...');
if (!small.value) throw new Error('找不到取景框...');
if (!prevImg.value) throw new Error('找不到预览图...');
}

onMounted(() => {
checkElements();
const imgWidth = img.value.offsetWidth;
const smallWidth = small.value.offsetWidth;

scale.value = imgWidth / smallWidth;
prevImg.value.style.transform = `scale(${scale.value})`;
})

function onMousemove(e) {
checkElements();
offsetX.value = Math.max(0, e.offsetX);
offsetY.value = Math.max(0, e.offsetY);

const imgWidth = img.value.offsetWidth;
const imgHeight = img.value.offsetHeight;
const smallWidth = small.value.offsetWidth;
const smallHeight = small.value.offsetHeight;

updatePrev({ offsetX: offsetX.value, offsetY: offsetY.value, imgWidth, imgHeight, smallWidth, smallHeight });
}

function updatePrev({ offsetX, offsetY, imgWidth, imgHeight, smallWidth, smallHeight }) {
const position = {
x: Math.max(0, Math.min(offsetX - smallWidth / 2, imgWidth - smallWidth)),
y: Math.max(0, Math.min(offsetY - smallHeight / 2, imgHeight - smallHeight)),
}

small.value.style.transform = `translate(${position.x}px, ${position.y}px)`;
prevImg.value.style.transform = `translate(${-(position.x - smallWidth) * scale.value}px, ${-(position.y - smallHeight) * scale.value}px) scale(${scale.value})`;
}
</script>

<style lang='less' scoped>
.container {
width: 100%;
height: 700px;
display: flex;
justify-content: center;
border: 1px rgb(0, 0, 0) solid;
flex-direction: column;
align-items: center;
gap: 10px;
}

img {
width: 100%;
vertical-align: bottom;
}

.img {
width: 300px;
height: 300px;
border: 2px solid gray;
position: relative;
cursor: move;
display: flex;


.small {
width: 100px;
height: 100px;
border: 1.5px solid rgba(127, 255, 212, 1);
position: absolute;
top: 0;
left: 0;
background-color: rgba(127, 255, 212, 0.382);
z-index: 10;
pointer-events: none; // 防止鼠标移动到取景框上时,触发鼠标移动事件,导致位置抽搐。
box-sizing: border-box;
}
}

.prev {
width: 300px;
height: 300px;
border: 1.5px dashed gray;
overflow: hidden;
position: relative;
display: flex;

img {
position: absolute;
top: 0;
right: 0;
}
}
</style>

ReactPHP 异步操作封装

ReactPHP 异步操作封装

功能概述

封装一个异步操作方法,确保传入的函数不会阻塞后续代码执行。

依赖安装

1
composer require react/event-loop react/promise

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

use React\EventLoop\Loop;
use React\Promise\Deferred;

/**
* 异步执行回调函数,不阻塞事件循环
* @param callable $callback 需要异步执行的回调函数
* @return \React\Promise\Promise 返回 Promise 对象用于处理结果
*/
function asyncOperation(callable $callback): \React\Promise\Promise
{
$deferred = new Deferred();

// 获取当前事件循环实例
$loop = Loop::get();

// 在下一个事件循环 tick 中执行回调
$loop->futureTick(function () use ($callback, $deferred) {
try {
// 执行回调并获取结果
$result = $callback();
$deferred->resolve($result);
} catch (\Throwable $e) {
// 捕获异常并拒绝 promise
$deferred->reject($e);
}
});

return $deferred->promise();
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

require __DIR__ . '/vendor/autoload.php';

// 正确使用示例(非阻塞)
asyncOperation(function () {
// 模拟异步操作(不能使用同步阻塞代码)
return '异步操作完成';
})->then(function ($result) {
echo $result; // 输出:异步操作完成
})->otherwise(function (\Throwable $e) {
echo '出错:' . $e->getMessage();
});

// 后续代码会立即执行
echo "立即执行的内容\n";

// 错误使用示例(同步阻塞代码仍然会阻塞)
asyncOperation(function () {
sleep(2); // 同步阻塞操作
return '这仍然会阻塞2秒';
})->then(function ($result) {
echo $result;
});

注意事项

  1. 不要使用同步阻塞代码

    • 避免 sleep()file_get_contents() 等同步阻塞操作
    • 使用 ReactPHP 提供的异步替代方案
  2. 推荐异步操作方式

    1
    2
    3
    4
    // 正确:使用 ReactPHP 的异步 HTTP 客户端
    asyncOperation(function () use ($client) {
    return $client->request('GET', 'https://api.example.com');
    });
  3. CPU 密集型任务

    • 建议使用多进程方案处理
    • 可考虑 workerman 等扩展

常见问题

Q: 为什么我的异步操作仍然阻塞?
A: 检查回调函数中是否包含同步阻塞代码,如 sleep()file_get_contents()

Q: 如何处理 CPU 密集型任务?
A: 使用多进程方案,ReactPHP 不适合处理 CPU 密集型任务

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

require __DIR__ . '/vendor/autoload.php';

// 异步执行多个任务
$promises = [
asyncOperation(function () {
return "任务1完成";
}),
asyncOperation(function () {
return "任务2完成";
}),
asyncOperation(function () {
return "任务3完成";
})
];

// 等待所有任务完成
React\Promise\all($promises)->then(function ($results) {
print_r($results);
})->otherwise(function (\Throwable $e) {
echo '出错:' . $e->getMessage();
});

// 保持事件循环运行(实际应用中通常由 HTTP 服务器等保持)
Loop::get()->addTimer(1, function () {
Loop::get()->stop();
});

这个封装提供了简单易用的异步操作方式,适用于大多数 ReactPHP 异步编程场景。

TypeScript 映射类型与条件类型(附形象记忆法)

TypeScript 映射类型与条件类型(附形象记忆法)

一、映射类型(Mapped Types)详解

1.1 基础概念

映射类型允许我们基于现有类型的属性,创建一个新类型,通常用于批量修改属性。

核心语法

1
2
3
type MappedType<T> = {
[P in keyof T]: /* 对属性P的操作 */
};

1.2 常见内置映射类型

1.2.1 Readonly<T>

将所有属性设为只读:

1
2
3
4
5
6
7
8
9
10
11
interface User {
name: string;
age: number;
}

type ReadonlyUser = Readonly<User>;
// 等同于:
// {
// readonly name: string;
// readonly age: number;
// }

形象记忆:就像给所有门加上”禁止进入”的锁(readonly

1.2.2 Partial<T>

将所有属性变为可选:

1
2
3
4
5
6
type PartialUser = Partial<User>;
// 等同于:
// {
// name?: string;
// age?: number;
// }

形象记忆:就像把所有必填字段变成”选填”(?

1.2.3 Required<T>

将所有属性变为必填(与 Partial 相反)

1
2
type RequiredUser = Required<PartialUser>;
// 恢复所有属性为必填

1.2.4 Record<K, T>

创建一个对象类型,其键为 K,值为 T

1
2
type UserRoles = Record<string, "admin" | "user" | "guest">;
// 等同于 { [key: string]: "admin" | "user" | "guest" }

形象记忆:就像创建一个”键值对”的表格

1.2.5 Pick<T, K>

从 T 中选取指定的属性 K

1
2
type UserName = Pick<User, "name">;
// 等同于 { name: string }

形象记忆:就像从菜单中”点菜”(只选需要的)

1.2.6 Omit<T, K>

从 T 中排除指定的属性 K

1
2
type UserWithoutAge = Omit<User, "age">;
// 等同于 { name: string }

形象记忆:就像从套餐中”删除”不想要的项


1.3 自定义映射类型

1.3.1 添加前缀/后缀

1
2
3
4
5
6
type AddPrefix<T, Prefix extends string> = {
[P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};

type PrefixedUser = AddPrefix<User, "user_">;
// 等同于 { user_name: string; user_age: number }

形象记忆:就像给所有文件名加前缀

1.3.2 条件修改属性

1
2
3
4
5
6
type MakeNullable<T> = {
[P in keyof T]: T[P] | null;
};

type NullableUser = MakeNullable<User>;
// 等同于 { name: string | null; age: number | null }

形象记忆:就像给所有必填字段加上”可为空”的选项


二、条件类型(Conditional Types)详解

2.1 基础语法

1
T extends U ? X : Y

如果类型 T 可以赋值给 U,则结果为 X,否则为 Y

2.2 常见用法

2.2.1 类型判断

1
2
3
4
type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

形象记忆:就像做选择题(是/否)

2.2.2 提取数组元素类型

1
2
3
4
5
type ElementType<T> = T extends (infer U)[] ? U : never;

type Str = ElementType<string[]>; // string
type Num = ElementType<number[]>; // number
type NeverArr = ElementType<number>; // never

形象记忆:就像从容器中”提取”内容

2.2.3 分支处理

1
2
3
4
5
6
7
8
9
type ResponseType<T> = T extends { error: any }
? "error"
: T extends { data: any }
? "success"
: "unknown";

type Success = ResponseType<{ data: string }>; // "success"
type Error = ResponseType<{ error: string }>; // "error"
type Unknown = ResponseType<{ status: number }>; // "unknown"

形象记忆:就像交通信号灯(不同条件走不同分支)


2.3 高级用法

2.3.1 分布式条件类型

当 T 是联合类型时,条件类型会自动分发:

1
2
3
4
type ToArray<T> = T extends any ? T[] : never;

type StrOrNumArray = ToArray<string | number>;
// 等同于 string[] | number[]

形象记忆:就像批量处理邮件(每个收件人收到单独的邮件)

2.3.2 条件类型递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type DeepReadonly<T> = {
[P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

interface Nested {
a: number;
b: {
c: string;
d: {
e: boolean;
};
};
}

type ReadonlyNested = DeepReadonly<Nested>;
// 所有层级都变为readonly

形象记忆:就像俄罗斯套娃,一层层深入


三、形象记忆法总结

概念 记忆技巧
Readonly<T> 给所有属性加”禁止修改”标签
Partial<T> 把所有必填变成”选填”
Pick<T,K> 从菜单中”点菜”
Omit<T,K> 从套餐中”删除”不想要的
AddPrefix<T,P> 给文件名加前缀
MakeNullable<T> 给必填字段加”可为空”选项
IsString<T> 做选择题(是/否)
ElementType<T> 从容器中”提取”内容
分布式条件类型 批量处理邮件(每个收件人单独处理)
条件类型递归 俄罗斯套娃,一层层深入