阅读视图

发现新文章,点击刷新页面。

深拷贝与浅拷贝:代码世界里的永恒与瞬间

深夜的咖啡厅里,小美对着屏幕上的代码叹气。
“又出 bug 了?” 小帅递来一杯热拿铁。
“浅拷贝害的。” 小美指着屏幕上的数组:“我复制了一份数据,结果用户修改副本时,原数据也跟着变了……” 小帅凑近看了眼代码:“就像你给我发张照片,我修图后原图也被改了?”
“对!但深拷贝不会。” 小美敲下一行 JSON.parse(JSON.stringify(arr)),“它会彻底复制所有层级的数据,像重新捏一个一模一样的泥人,连指纹都独立。” 小帅若有所思:“所以浅拷贝是瞬间的镜像,深拷贝是永恒的守护?”
小美点头:“就像我们的初心 —— 代码会变,但数据的灵魂值得被温柔以待。” 以上纯属瞎编,如有雷同,纯属巧合。

在计算机中,不同的数据类型会在不同的内存空间进行存储,

image.png

基本数据类型

  • 存储位置:基本数据类型包括NumberStringBooleanNullUndefinedSymbol(ES6 新增)和BigInt(ES2020 新增),它们的值直接存储在栈内存中。

  • 特点:栈内存是一种自动分配和释放的内存空间,是一种线性的数据结构,具有先进后出的特点。基本数据类型的大小是固定的,所以在栈内存中可以直接存储它们的值,访问速度快。例如:

let num = 10;
let str = 'hello';
let bool = true;

引用数据类型

  • 存储位置:引用数据类型包括ObjectArrayFunction等。当创建一个引用数据类型的变量时,实际上是在堆内存中分配一块空间来存储对象的内容,然后在栈内存中存储该对象在堆内存中的地址。 即引用数据类型的实例存储在堆内存中,而在栈内存中存储的是指向堆内存中该实例的引用
  • 特点:堆内存是一种用于动态分配内存的区域,其大小不固定,可以根据需要动态地分配和释放。引用数据类型的大小是不固定的,因此需要在堆内存中分配空间来存储其内容
let obj = { name: 'John', age: 30 };
let arr = [1, 2, 3, 4, 5];
function add(a, b) { return a + b; }

image.png

浅拷贝

//浅拷贝
let a = 1;
let b = a;
a = 2;
console.log(a);
console.log(b);
  • 原理:在 JavaScript 中,基本数据类型(像 NumberStringBoolean 等)是按值传递的。当你执行 let b = a 时,实际上是把 a 的值复制给了 b。此后,a 和 b 是相互独立的,对 a 进行修改不会影响 b
  • 输出结果a 的值是 2b 的值依然是 1

深拷贝 - 对于一维数组

//深拷贝  一维数组
let x = [1, 2, 3];
let y = [];
//1、展开运算符
//y = []  堆内存中开辟新的空间
y = [...x];
2for循环
for(let i = 0;i < x.length;i++){
    y[i] = x[i];
}
x = [111, 222, 333];
console.log(x);
console.log(y);
  • 原理:此代码运用展开运算符 ... 对数组 x 进行了深拷贝。展开运算符会把数组 x 的元素逐一复制到新数组 y 里。这里的复制是对数组元素的复制,并存放进新的内存空间。当你重新给 x 赋值时,y 不会受到影响。
  • 输出结果x 的值是 [111, 222, 333]y 的值是 [1, 2, 3]

深拷贝 - 多层数组嵌套

//深拷贝  多层数组嵌套
let m = [1, 2, 3, [4, 5,]];
//在堆内存  [1,2,3,0x1234] 第二层还是地址
//直接展开深层次还是存放地址

//1、使用JOSON   但是不适用function
let n = JSON.parse(JSON.stringify(m));
m[3][1] = 1000;
console.log(m);
console.log(n);
  • 原理:这里借助 JSON.stringify() 把对象 m 转换为 JSON 字符串,再使用 JSON.parse() 把 JSON 字符串转换回对象 n。这样做的效果是递归地复制对象的所有属性,进而实现深拷贝。但要注意,JSON.stringify() 无法处理functionSymbol 等特殊类型。
  • 输出结果m 的值是 [1, 2, 3, [4, 1000]]n 的值是 [1, 2, 3, [4, 5]]

下面举例JSON拷贝引用数据类型中含有函数的方法

深拷贝 - 函数处理(JSON 方式)错误使用

//2、如果是函数
//由于JSON 不能拷贝函数
let j = {
    name: 'zhangsan',
    age: 18,
    fn() {
        console.log(this.name);
    }
}
let k = JSON.parse(JSON.stringify(j));
console.log(j);
console.log(k);
  • 原理:当使用 JSON.stringify() 处理包含函数的对象时,函数会被忽略。所以,对象 k 里不会有 fn 方法。
  • 输出结果j 包含 nameage 和 fn 方法,而 k 只有 name 和 age 属性。

浅拷贝 - 函数(一层)

//2、 函数  一层 无嵌套
let o = {
    name: 'zhangsan',
    age: 18,
    fn() {
        console.log(this.name);
    }
}
let p = { ...o };
o.fn = function () {
    console.log(this.age);
}
console.log(o);
console.log(p);
o.fn();
p.fn();
  • 原理:这里我们回归最初使用展开运算符 ... 对对象 o 进行深拷贝。浅拷贝只复制对象的一层属性,如果属性是数组或者对象,复制的只是地址,所以会有影响。但是如果是函数的话,由于修改方式不同,函数的修啊给i相当于重写了这个函数,所以在堆内存中又会重新开辟一块内存,当修改 o 的 fn 方法时,p 的 fn 方法不受影响。
  • 输出结果o 的 fn 方法会输出 agep 的 fn 方法会输出 name

通过上面的诸多铺垫,接下来介绍手写深拷贝函数。

手写深拷贝函数


function deepClone(data) {
    //函数直接返回,因为函数返回后 会再内存中开辟一个空间
    //object————> 三种情况    Three sThree scenarios对象 数组 null
    if(typeof data === 'object' && data !== null){
        let res = Array.isArray(data)? [] : {};
        for(let j in data){
            if(data.hasOwnProperty(j)){
                res[j]  = deepClone(data[j]);
            }
        }
        return res;
    }else{
        return data;
    }
}
//示例
let object = {
    name: 'zhangsan',
    age: 18,
    fn() {
        console.log(this.name);
    },
    fn1: function () {
        console.log(this.age);
    },
}

let object2 = deepClone(object);
object.fn1 = function () {
    console.log(this.age + 10);
}
console.log(object);
console.log(object2);
object.fn1();
object2.fn1();
  • 原理deepClone 函数是一个递归函数,用于实现深拷贝。如果传入的数据是对象或数组,就会创建一个新的对象或数组,然后递归地复制每个属性。如果传入的数据是基本数据类型或函数,就直接返回。
  • 输出结果:修改 object 的 fn1 方法不会影响 object2 的 fn1 方法,object 的 fn1 方法会输出 age + 10object2 的 fn1 方法会输出 age

代码的世界里,深拷贝是永恒的承诺,而浅拷贝是短暂的相遇

代码的世界里,没有真正的永恒。但我们依然执着于深拷贝 —— 就像明知人生无常,却依然相信爱情。愿你在每一次复制中,都能守护住最珍贵的初心,就像deepClone函数里的递归,在层层剥离的温柔里,找到属于自己的永恒。

如果您觉得这篇文章对您有帮助,欢迎点赞和收藏,大家的支持是我继续创作优质内容的动力🌹🌹🌹也希望您能在😉😉😉我的主页 😉😉😉找到更多对您有帮助的内容。

  • 致敬每一位赶路人

B站首页的 Banner 这么好看,我让你直接用到你的项目!

写在前面

我最开始是用 Angular 去实现了B站的 Banner ,那时候还没有人做这东西,可以看到第一个图是好几年以前的了。然后随着逐步完善,在这几年偶尔也看到有人发过这东西的实现方法。

但我为什么要写这篇文章?因为我打算用原生 JS 和三大框架都去实现一遍,以满足所有人的需求。而且我作为几乎每一期都 copy 的玩家,存货多,也知道最简单的 copy 方法。

本文的原生 JS 代码我直接从我已经完善的 Angular 代码基础上提取出来,主要讲讲我大致的方法原理,里面的具体实现细节就不讲了,毕竟讲起来费劲,主要就是一些根据鼠标移动计算图片位置,自己认真读起来不会很难。

代码与相关静态资源已提交至 gitee ,需要的直接去看完整代码即可。

gitee.com/CrimsonHu/b…

B站是怎么实现 Banner 的

我们打开控制台,首先找到它的 DOM 结构,可以看到 Banner 里面有一个个 class 为 layer 的 div:

点开 div,会看到一个 img 与控制这个 img 位置与大小的样式:

Banner 的本质就是一层层的图片,按照特定的宽度、高度、位置( translate 的 X 与 Y 属性)、以及角度、缩放、透明度这些来进行排列的。

在 Banner 上移动鼠标,我们会发现 transform 属性的各项值会随着鼠标的移动而更新

所以实现思路就有了:把这些图片按照指定的样式进行排列,然后做一个鼠标移动的监听事件即可

那么如何知道鼠标移动了一定的距离,每个图片各自的移动量是多少呢?

这个也简单。因为它的移动量基本都是线性的。目前我只看到有两期是在线性移动的基础上加了点额外的效果,这个在这里就把它忽略。把它的线性动画的移动量拿过来后已经显示的很不错了。

于是我发现了一个很好用的方法:

以某一张图片为基准,以它的移动量为一个基准单位,去看其它每个图片移动了多少。

例如,我以图片 A 移动了 5px 为基准,那么我就看图片 A 移动了 5px 的时候,其它的各个图片分别移动了多少。所以,通过这个取基准的方法,我们可以得出,根据鼠标的移动,带动图片 A 的位置更新,从而得出其它各个图片的位置的更新值

使用原生代码实现

1. 资源获取

现在理清了原理,那么我们就先将每张图片的静态资源获取下来,这一步就省略不写了。

2. 定义基准移动量

现在我们以第一张图片为基准,去定义移动量:

{ 
    type: 'image', 
    file: '477fb7a2f5e2cc1b2aa00c679c8ab168b47ff1b9.png@1c.webp', 
    width: 1728, 
    height: 162, 
    x: 0,  // 原始 translateX 值
    y: 0,  // 原始 translateY 值
    r: 0,  // 原始 rotate 值
    s: 1,  // 原始 scale 值 
    o: 1,  // 原始 opacity 值 
    newX: -1.17573, // 更新后的 translateX 值
    newY: 0,        // 更新后的 translateY 值
    newRotate: 0,   // 更新后的 rotate 值
    bench: -1.17573 
},

其实没更新的值可以省略不写,比如这段定义里面的 newOpacity 我省略了,而且 newYnewRotate 也可以省略,因为后续代码我会处理省略的字段不进行计算更新。

这段定义代码需要注意的地方就是 xnewXbench 这三个,这是我们定义的一个基准移动量。

鼠标在 Banner 上移动,这个图片的 translateX 值从 0 更新到 -1.17573,以此为基准,基准字段记为 bench

于是,需要你花亿点点时间,去看第一个图片的 translateX 在 -1.17573 这个值的时候,其它的图片的位置值各自是多少:

因为每次重新打开页面重新打开控制台时,这种细微操作的移动,更新的值都不会完全一致,图中的值会与我记录的稍微不一致,所以在完成所有图片的数据采集之前不要更改窗口尺寸与开关控制台。

[
    { type: 'image', file: '477fb7a2f5e2cc1b2aa00c679c8ab168b47ff1b9.png@1c.webp', width: 1728, height: 162, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -1.17573, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: 'dd4aa5af4898a15dde074fe6b833bdfbc045dd47.png@1c.webp', width: 1632, height: 153, x: -42.5, y: 0, r: 0, s: 1, o: 1, newX: -46.2014, newY: 1.48055, newRotate: -2.17727, bench: -1.17573 },
    { type: 'image', file: 'ae64c474ba2748b4fea1a1433b9b373598d3e686.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: 0, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: 'd41b0292d9d1b0fce37a35f6efacac579093cdb3.png@1c.webp', width: 1728, height: 162, x: -63, y: -18, r: 0, s: 1, o: 1, newX: -68.8786, newY: -18, newRotate: -2.17727, bench: -1.17573 },
    { type: 'image', file: 'a12cbab877db2b6d3b96ec012735cfe3998de399.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: 3.70136, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: '6f27fe80f495e83546921c2c58b2107c2ee89706.png@1c.webp', width: 1824, height: 171, x: 9.5, y: 0, r: 0, s: 1, o: 1, newX: -0.842045, newY: -2.06841, newRotate: 0, newScale: 0.978227, bench: -1.17573 },
    { type: 'image', file: 'ac580ac636595e27b5d5c8b1f9d802751823f02f.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: 14.8055, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: '987a407da2e02a12be549ad7f011dc11f73216e9.png@1c.webp', width: 1632, height: 153, x: 0, y: 8.5, r: 0, s: 1, o: 1, newX: 5.55205, newY: 7.38959, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: '843ee84b16bab6274b98b317b3adaf1351b4be76.png@1c.webp', width: 1632, height: 153, x: 8.5, y: 0, r: 0, s: 1, o: 1, newX: -2.60409, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: 'a5607001ac06b4780106e2a10aa835657e553f07.png@1c.webp', width: 1632, height: 153, x: 8.5, y: 0, r: 0, s: 1, o: 1, newX: 15.9027, newY: -12.9548, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: 'b14cad3c76e957705810c26f4c6acc236f280721.png@1c.webp', width: 1632, height: 153, x: 17, y: 0, r: 0, s: 1, o: 1, newX: 11.448, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: 'f569be6834a5968c038bcc0fb0403f8f77e24c18.png@1c.webp', width: 1632, height: 153, x: 8.5, y: 0, r: 0, s: 1, o: 1, newX: 1.09727, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: 'eb98a42561de7d45f8856407c4531d2724ac6fc6.png@1c.webp', width: 1344, height: 126, x: 21, y: 21, r: 0, s: 1, o: 1, newX: -9.48182, newY: 17.9518, newRotate: 2.17727, newScale: 1.13064, bench: -1.17573 },
    { type: 'image', file: '480a5c02dbcd3afd5f790cf621efbd1b2d39efcb.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -18.5068, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: 'cc1405d4d805baf9c459f67c03f095f70ec864c6.png@1c.webp', width: 1632, height: 153, x: 0, y: 17, r: 0, s: 1, o: 1, newX: -5.55205, newY: 18.8507, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: '1696ffeacb26bcce7ad50037952620ee43614a4a.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -7.40273, newY: -2.96109, newRotate: 0, newScale: 1.08709, bench: -1.17573 },
    { type: 'image', file: 'b0747b7f64e0e07e379a4bd416a0891b3e52c7d9.png@1c.webp', width: 1651.2, height: 154.8, x: 34.4, y: 0, r: 0, s: 1, o: 1, newX: 11.9305, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: 'ec9f99bf71be9408a359d7cd48dfb47e48ebce2c.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -9.25341, newY: 6.29232, newRotate: -8.70909, newScale: 1.13064, bench: -1.17573 },
    { type: 'image', file: 'c4cef2b063a853521c7f413d8e7782ff653e4126.png@1c.webp', width: 1632, height: 153, x: 17, y: 0, r: 0, s: 1, o: 1, newX: -12.6109, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: '567e0221b3bd89e34d85c604549d915c95ca17d1.png@1c.webp', width: 1920, height: 180, x: 30, y: 0, r: 0, s: 1, o: 1, newX: 73.5455, newY: 0, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: '23c05e4d9e0ffbc16671e14c406fbe785b06ff9f.png@1c.webp', width: 1632, height: 153, x: 15.3, y: 17, r: 0, s: 1, o: 1, newX: -18.0123, newY: 17, newRotate: 0, bench: -1.17573 },
    { type: 'image', file: 'fbfeb5de7e41fd1e888fca7ac76cbf2f8a42f3d3.png@1c.webp', width: 1920, height: 180, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -43.5455, newY: 0, newRotate: 0, bench: -1.17573 },
]

3. 展示静态效果

有了这些数据后,先来把每个图层的图片按照这些位置数据显示出来,用原生 JS 做一个静态展示:

<!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>
        .bili-banner {
            position: relative;
            width: 100%;
            height: 100%;
            background-color: #f9f9f9;
            display: flex;
            justify-content: center;
            background-repeat: no-repeat;
            background-position: center 0;
            background-size: cover;
            filter: brightness(var(--img-filter-brightness));
        }

        .animated-banner {
            position: absolute;
            width: 100%;
            height: 100%;
            top: 0;
            left: 0;
            overflow: hidden;
        }

        .layer {
            position: absolute;
            height: 100%;
            width: 100%;
            left: 0;
            top: 0;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .logo {
            position: absolute;
            width: 220px;
            height: 105px;
            left: 0;
            top: 0;
            transform: scale(0.4) translate(-60%, -60%);
        }

        .logo>img {
            width: 100%;
            height: 100%;
        }

        .banner-container {
            position: relative;
            width: 800px;
            height: 150px;
        }
    </style>
</head>

<body>
    <div class="banner-container" id="winter-5"> </div>
</body>

<script>
    const baseSrc = './assets/winter-5/';

    const imgData = [...];

    const container = document.getElementById('winter-5');

    const biliBanner = document.createElement('div');
    biliBanner.className = 'bili-banner';

    const animatedBanner = document.createElement('div');
    animatedBanner.className = 'animated-banner';

    for (let i = 0; i < imgData.length; i++) {
        const layer = document.createElement('div');
        layer.className = 'layer';
        const img = document.createElement('img');
        img.src = baseSrc + imgData[i].file;
        img.style.width = imgData[i].width + 'px';
        img.style.height = imgData[i].height + 'px';
        img.style.transform = '' +
            'translate(' +
            imgData[i].x + 'px, ' +
            imgData[i].y + 'px' +
            ')' + ' ' +
            'rotate(' + imgData[i].r + 'deg)' + ' ' +
            'scale(' + imgData[i].s + ')';
        img.style.opacity = imgData[i].o;
        layer.appendChild(img);
        animatedBanner.appendChild(layer);
    }

    biliBanner.appendChild(animatedBanner);
    container.appendChild(biliBanner);

</script>

</html>

上述代码的效果如图所示:

4. 实现完整功能

现在能够看到每张图片能够按照正确的位置显示出来了,接下来的主要工作就是添加鼠标移动事件,去计算每个图片的移动量。具体就不详细讲了。

可以看到,已经基本达到了B站首页 Banner 的效果。

使用三大框架各自实现的代码直接在 gitee 上看,文中就不贴出来了。

gitee.com/CrimsonHu/b…

下面是原生 JS 实现的完整代码,贴在这里供大家学习参考。这里我使用了面向对象的方式将其进行封装,方便初始化多个实例与销毁。同时我也建议前端人应该同时掌握面向对象与函数式,在合适的地方用合适的方法,而不是一味地吹捧函数式编程,去拒绝面向对象的思维。

<!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>
        .bili-banner {
            position: relative;
            width: 100%;
            height: 100%;
            background-color: #f9f9f9;
            display: flex;
            justify-content: center;
            background-repeat: no-repeat;
            background-position: center 0;
            background-size: cover;
            filter: brightness(var(--img-filter-brightness));
        }

        .animated-banner {
            position: absolute;
            width: 100%;
            height: 100%;
            top: 0;
            left: 0;
            overflow: hidden;
        }

        .layer {
            position: absolute;
            height: 100%;
            width: 100%;
            left: 0;
            top: 0;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .logo {
            position: absolute;
            width: 220px;
            height: 105px;
            left: 0;
            top: 0;
            transform: scale(0.4) translate(-60%, -60%);
        }

        .logo>img {
            width: 100%;
            height: 100%;
        }

        .banner-container {
            position: relative;
            width: 800px;
            height: 150px;
        }
    </style>
</head>

<body>
    <div class="banner-container" id="winter-5"></div>
</body>

<script>

    class ResizeObserverWrap {
        observer;
        dom;
        constructor(dom, callback) {
            this.dom = dom;
            this.observer = new ResizeObserver((entries) => {
                if (!Array.isArray(entries) || !entries.length) {
                    return;
                }
                for (let entry of entries) {
                    callback(entry.target);
                }
            });
            this.observer.observe(this.dom);
        }
        unmount() {
            this.observer.unobserve(this.dom);
        }
    }

    class BilibiliBannerBase {

        baseSrc = './assets/winter-5/';

        imgDomList = [];
        imgData = [];
        imgNewData = [];

        resizeObserver;
        container;
        containerWidth = 0;

        // 声明自定义参数
        marginLeft = 0;
        moveRate = 300;
        maxMove = null;
        maxLeftPosition = null;
        maxRightPosition = null;

        // 声明状态信息
        isReseting = false;
        isInResetingEnter = false;
        startPoint = { x: 0, y: 0 }
        transition = 0.75;
        moveX = 0;

        // 声明loop与render方法更新界面
        isDestroyed = false;
        frameTime = 0;
        fps = 60;

        // 监听事件
        mouseenter = (e) => {
            if (this.isReseting) {
                this.isInResetingEnter = true;
            } else {
                this.bilibiliStart(e.clientX, e.clientY);
            }
        }
        mousemove = (e) => {
            if (!this.isReseting) {
                if (this.isInResetingEnter) {
                    this.bilibiliStart(e.clientX, e.clientY);
                    this.isInResetingEnter = false;
                } else {
                    this.bilibiliMove(e.clientX, e.clientY);
                }
            }
        }
        mouseleave = (e) => {
            this.bilibiliEnd();
            this.reset();
        }

        constructor(param = {
            container,
            imgData,
            marginLeft,
            moveRate,
            maxMove: { left, right },
            maxLeftPosition: { index, cut },
            maxRightPosition: { index, cut },
        }) {
            window.requestAnimationFrameFunc = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
            this.container = param.container;
            this.imgData = param.imgData;
            this.init();
        }

        init() {
            const biliBanner = document.createElement('div');
            biliBanner.className = 'bili-banner';
            const animatedBanner = document.createElement('div');
            animatedBanner.className = 'animated-banner';
            for (let i = 0; i < this.imgData.length; i++) {
                const layer = document.createElement('div');
                layer.className = 'layer';
                const img = document.createElement('img');
                img.src = this.baseSrc + this.imgData[i].file;
                img.style.width = this.imgData[i].width + 'px';
                img.style.height = this.imgData[i].height + 'px';
                img.style.transform = '' +
                    'translate(' +
                    this.imgData[i].x + 'px, ' +
                    this.imgData[i].y + 'px' +
                    ')' + ' ' +
                    'rotate(' + this.imgData[i].r + 'deg)' + ' ' +
                    'scale(' + this.imgData[i].s + ')';
                img.style.opacity = this.imgData[i].o;
                img.style.filter = this.imgData[i].filter == undefined ? 'none' : this.imgData[i].filter;
                this.imgDomList.push(img);
                layer.appendChild(img);
                animatedBanner.appendChild(layer);
            }
            biliBanner.appendChild(animatedBanner);
            this.container.appendChild(biliBanner);
            this.resizeObserver = new ResizeObserverWrap(this.container, (e) => {
                this.containerWidth = e.clientWidth;
            });
            this.imgData.forEach((each, i) => {
                each.x += this.marginLeft;
                each.newX += this.marginLeft;
                this.imgNewData.push({
                    currentX: each.x,
                    currentY: each.y == undefined ? 0 : each.y,
                    currentRotate: each.r == undefined ? 0 : each.r,
                    currentScale: each.s == undefined ? 0 : each.s,
                    currentOpacity: each.o == undefined ? 1 : each.o,
                });
            });
            // 添加鼠标移动事件
            this.container.addEventListener('mouseenter', this.mouseenter);
            this.container.addEventListener('mousemove', this.mousemove);
            this.container.addEventListener('mouseleave', this.mouseleave);
            this.loop(0);
        }

        destroy() {
            this.isDestroyed = true;
            this.resizeObserver.unmount();
            this.container.removeEventListener('mouseenter', this.mouseenter);
            this.container.removeEventListener('mousemove', this.mousemove);
            this.container.removeEventListener('mouseleave', this.mouseleave);
            this.container.innerHTML = '';
        }

        render() {
            this.imgDomList.forEach((img, i) => {
                img.style.transform = '' +
                    'translate(' +
                    this.imgNewData[i].currentX + 'px, ' +
                    this.imgNewData[i].currentY + 'px' +
                    ')' + ' ' +
                    'rotate(' + this.imgNewData[i].currentRotate + 'deg)' + ' ' +
                    'scale(' + this.imgNewData[i].currentScale + ')';
                img.style.opacity = this.imgNewData[i].currentOpacity;
                img.style.filter = this.imgNewData[i].filter == undefined ? 'none' : this.imgNewData[i].filter;
                img.style.transition = this.transition + 's';
            })
        }

        loop(e) {
            if (e - this.frameTime >= 1000 / this.fps) {
                this.frameTime = e;
                this.render();
            }
            window.requestAnimationFrameFunc((e1) => {
                if (!this.isDestroyed) {
                    this.loop(e1);
                }
            });
        }

        bilibiliStart(x, y) {
            this.startPoint.x = x;
            this.startPoint.y = y;
            this.transition = 0;
        }

        bilibiliMove(x, y) {
            let moveX = (x - this.startPoint.x) / this.moveRate;
            this.startPoint.x = x;
            this.moveX += moveX;
            if (this.maxMove) {
                let v = this.moveX * this.moveRate;
                if (moveX < 0 && v * -1 > this.maxMove.left) {
                    return;
                } else if (moveX > 0 && v > this.maxMove.right) {
                    return;
                }
            }
            if (this.moveX > 0) {
                if (Math.abs(this.moveX) > this.getLeftWidth()) {
                    this.moveX -= moveX;
                    return;
                }
            } else {
                if (Math.abs(this.moveX) > this.getRightWidth()) {
                    this.moveX -= moveX;
                    return;
                }
            }
            for (let i = 0; i < this.imgData.length; i++) {
                this.moveFunction(i);
            }
        }

        bilibiliEnd() {
            this.startPoint.x = 0;
            this.startPoint.y = 0;
        }

        getLeftWidth() {
            let leftWidth;
            if (this.maxLeftPosition) {
                let i = this.maxLeftPosition.index;
                let r = (this.imgData[i].newX - this.imgData[i].x) / this.imgData[i].bench;
                leftWidth = (this.imgData[i].width - this.containerWidth) / 2 - this.imgData[i].x;
                leftWidth = leftWidth - this.maxLeftPosition.cut;
                leftWidth = leftWidth / r;
                console.log(leftWidth)
            } else {
                leftWidth = this.containerWidth / 2 - this.marginLeft;
            }
            return leftWidth;
        }

        getRightWidth() {
            let rightWidth;
            if (this.maxRightPosition) {
                let i = this.maxRightPosition.index;
                let r = (this.imgData[i].newX - this.imgData[i].x) / this.imgData[i].bench;
                rightWidth = (this.imgData[i].width - this.containerWidth) / 2 + this.imgData[i].x;
                rightWidth = rightWidth - this.maxRightPosition.cut;
                rightWidth = rightWidth / r;
            } else {
                rightWidth = this.containerWidth / 2 + this.marginLeft;
            }
            return rightWidth;
        }

        moveFunction(i) {
            let bench = this.imgData[i].bench;
            // 移动量 - X
            let x = this.imgData[i].x;
            let newX = this.imgData[i].newX;
            if (x != null && newX != null && x != newX) {
                let x1 = (newX - x) / bench;
                this.imgNewData[i].currentX = x1 * this.moveX + x;
            }
            // 移动量 - Y
            let y = this.imgData[i].y;
            let newY = this.imgData[i].newY;
            if (y != null && newY != null && y != newY) {
                let y1 = (newY - y) / bench;
                this.imgNewData[i].currentY = y1 * this.moveX + y;
            }
            // 移动量 - Rotate
            let r = this.imgData[i].r;
            let newRotate = this.imgData[i].newRotate;
            if (r != null && newRotate != null && r != newRotate) {
                let r1 = (newRotate - r) / bench;
                this.imgNewData[i].currentRotate = r1 * this.moveX + r;
            }
            // 移动量 - Scale
            let s = this.imgData[i].s;
            let newScale = this.imgData[i].newScale;
            if (s != null && newScale != null && s != newScale) {
                let s1 = (newScale - s) / bench;
                this.imgNewData[i].currentScale = s1 * this.moveX + s;
            }
            // 移动量 - Opacity
            let o = this.imgData[i].o;
            let newOpacity = this.imgData[i].newOpacity;
            if (o != null && newOpacity != null && o != newOpacity) {
                let o1 = (newOpacity - o) / bench;
                this.imgNewData[i].currentOpacity = o1 * this.moveX + o;
            }
            if (this.imgNewData[i].currentOpacity < 0) { // 透明度检测,在0~1范围内
                this.imgNewData[i].currentOpacity = 0;
            } else if (this.imgNewData[i].currentOpacity > 1) {
                this.imgNewData[i].currentOpacity = 1;
            }
        }

        reset() {
            this.transition = 0.75;
            this.moveX = 0;
            for (let i = 0; i < this.imgData.length; i++) {
                let data = this.imgData[i];
                this.imgNewData[i].currentX = data.x;
                this.imgNewData[i].currentY = data.y == undefined ? 0 : data.y;
                this.imgNewData[i].currentRotate = data.r == undefined ? 0 : data.r;
                this.imgNewData[i].currentScale = data.s == undefined ? 0 : data.s;
                this.imgNewData[i].currentOpacity = data.o == undefined ? 0 : data.o;
            }
            this.isReseting = true;
            setTimeout(() => {
                this.isReseting = false;
            }, 750);
        }

    }

    let banner = new BilibiliBannerBase({
        container: document.getElementById('winter-5'),
        imgData: [
            { type: 'image', file: '477fb7a2f5e2cc1b2aa00c679c8ab168b47ff1b9.png@1c.webp', width: 1728, height: 162, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -1.17573, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: 'dd4aa5af4898a15dde074fe6b833bdfbc045dd47.png@1c.webp', width: 1632, height: 153, x: -42.5, y: 0, r: 0, s: 1, o: 1, newX: -46.2014, newY: 1.48055, newRotate: -2.17727, bench: -1.17573 },
            { type: 'image', file: 'ae64c474ba2748b4fea1a1433b9b373598d3e686.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: 0, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: 'd41b0292d9d1b0fce37a35f6efacac579093cdb3.png@1c.webp', width: 1728, height: 162, x: -63, y: -18, r: 0, s: 1, o: 1, newX: -68.8786, newY: -18, newRotate: -2.17727, bench: -1.17573 },
            { type: 'image', file: 'a12cbab877db2b6d3b96ec012735cfe3998de399.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: 3.70136, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: '6f27fe80f495e83546921c2c58b2107c2ee89706.png@1c.webp', width: 1824, height: 171, x: 9.5, y: 0, r: 0, s: 1, o: 1, newX: -0.842045, newY: -2.06841, newRotate: 0, newScale: 0.978227, bench: -1.17573 },
            { type: 'image', file: 'ac580ac636595e27b5d5c8b1f9d802751823f02f.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: 14.8055, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: '987a407da2e02a12be549ad7f011dc11f73216e9.png@1c.webp', width: 1632, height: 153, x: 0, y: 8.5, r: 0, s: 1, o: 1, newX: 5.55205, newY: 7.38959, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: '843ee84b16bab6274b98b317b3adaf1351b4be76.png@1c.webp', width: 1632, height: 153, x: 8.5, y: 0, r: 0, s: 1, o: 1, newX: -2.60409, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: 'a5607001ac06b4780106e2a10aa835657e553f07.png@1c.webp', width: 1632, height: 153, x: 8.5, y: 0, r: 0, s: 1, o: 1, newX: 15.9027, newY: -12.9548, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: 'b14cad3c76e957705810c26f4c6acc236f280721.png@1c.webp', width: 1632, height: 153, x: 17, y: 0, r: 0, s: 1, o: 1, newX: 11.448, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: 'f569be6834a5968c038bcc0fb0403f8f77e24c18.png@1c.webp', width: 1632, height: 153, x: 8.5, y: 0, r: 0, s: 1, o: 1, newX: 1.09727, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: 'eb98a42561de7d45f8856407c4531d2724ac6fc6.png@1c.webp', width: 1344, height: 126, x: 21, y: 21, r: 0, s: 1, o: 1, newX: -9.48182, newY: 17.9518, newRotate: 2.17727, newScale: 1.13064, bench: -1.17573 },
            { type: 'image', file: '480a5c02dbcd3afd5f790cf621efbd1b2d39efcb.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -18.5068, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: 'cc1405d4d805baf9c459f67c03f095f70ec864c6.png@1c.webp', width: 1632, height: 153, x: 0, y: 17, r: 0, s: 1, o: 1, newX: -5.55205, newY: 18.8507, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: '1696ffeacb26bcce7ad50037952620ee43614a4a.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -7.40273, newY: -2.96109, newRotate: 0, newScale: 1.08709, bench: -1.17573 },
            { type: 'image', file: 'b0747b7f64e0e07e379a4bd416a0891b3e52c7d9.png@1c.webp', width: 1651.2, height: 154.8, x: 34.4, y: 0, r: 0, s: 1, o: 1, newX: 11.9305, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: 'ec9f99bf71be9408a359d7cd48dfb47e48ebce2c.png@1c.webp', width: 1632, height: 153, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -9.25341, newY: 6.29232, newRotate: -8.70909, newScale: 1.13064, bench: -1.17573 },
            { type: 'image', file: 'c4cef2b063a853521c7f413d8e7782ff653e4126.png@1c.webp', width: 1632, height: 153, x: 17, y: 0, r: 0, s: 1, o: 1, newX: -12.6109, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: '567e0221b3bd89e34d85c604549d915c95ca17d1.png@1c.webp', width: 1920, height: 180, x: 30, y: 0, r: 0, s: 1, o: 1, newX: 73.5455, newY: 0, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: '23c05e4d9e0ffbc16671e14c406fbe785b06ff9f.png@1c.webp', width: 1632, height: 153, x: 15.3, y: 17, r: 0, s: 1, o: 1, newX: -18.0123, newY: 17, newRotate: 0, bench: -1.17573 },
            { type: 'image', file: 'fbfeb5de7e41fd1e888fca7ac76cbf2f8a42f3d3.png@1c.webp', width: 1920, height: 180, x: 0, y: 0, r: 0, s: 1, o: 1, newX: -43.5455, newY: 0, newRotate: 0, bench: -1.17573 },
        ],
        maxMove: { left: 2000, right: 2000 },
    });
</script>

</html>

几种比较实用的指令举例

1. 如何实现一个自定义指令,控制元素的权限(如按钮权限)?

问题解析

  • 核心需求‌:根据用户权限动态显示/隐藏元素(如按钮)。
  • 难点‌:权限逻辑复用性、响应式更新权限状态。

解决方案

// 权限指令 v-permission
Vue.directive('permission', {
  inserted(el, binding, vnode) {
    const { value: requiredPermission } = binding;
    const userPermissions = vnode.context.$store.getters.userPermissions;

    if (!requiredPermission || !userPermissions.includes(requiredPermission)) {
      el.parentNode?.removeChild(el); // 直接移除元素
    }
  }
});

// 使用
<button v-permission="'edit'">编辑</button>

优化点

  • 响应式更新‌:在 update 钩子中处理权限变化,重新渲染。
  • 服务端权限验证‌:可通过 binding.value 传递异步权限码。

2. 如何用指令实现全局防抖(v-debounce)?

问题解析

  • 核心需求‌:防止按钮重复点击或输入框频繁触发事件。
  • 难点‌:通用性(支持多种事件)、参数传递(防抖时间)。

解决方案

Vue.directive('debounce', {
  inserted(el, binding) {
    const { value: handler, arg: event = 'click', modifiers } = binding;
    const delay = modifiers.delay || 300;

    let timer = null;
    el.addEventListener(event, (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => handler.apply(this, args), delay);
    });
  }
});

// 使用:防抖点击事件,延迟500ms
<button v-debounce:click.delay="submitForm">提交</button>

优化点

  • 支持修饰符‌:通过 modifiers 配置不同防抖时间。
  • 内存泄漏处理‌:在 unbind 钩子中移除事件监听。

3. 如何实现一个拖拽指令(v-draggable)?

问题解析

  • 核心需求‌:让元素可拖拽,支持边界限制。
  • 难点‌:DOM 操作、事件解绑、性能优化。

解决方案

Vue.directive('draggable', {
  inserted(el) {
    let isDragging = false;
    let initialX = 0, initialY = 0;

    const onMouseDown = (e) => {
      isDragging = true;
      initialX = e.clientX - el.offsetLeft;
      initialY = e.clientY - el.offsetTop;
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('mouseup', onMouseUp);
    };

    const onMouseMove = (e) => {
      if (!isDragging) return;
      const x = e.clientX - initialX;
      const y = e.clientY - initialY;
      el.style.left = `${x}px`;
      el.style.top = `${y}px`;
    };

    const onMouseUp = () => {
      isDragging = false;
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
    };

    el.addEventListener('mousedown', onMouseDown);
  },
  unbind(el) {
    // 清理事件防止内存泄漏
    el.removeEventListener('mousedown', onMouseDown);
  }
});

// 使用
<div v-draggable style="position: absolute;">拖拽我</div>

优化点

  • 边界限制‌:在 onMouseMove 中计算元素位置时添加边界判断。
  • 性能优化‌:使用 transform 代替 left/top 减少回流。

4. 如何通过指令实现图片懒加载(v-lazy)?

问题解析

  • 核心需求‌:图片进入视口时再加载资源。
  • 难点‌:交叉观察器(IntersectionObserver)的使用、占位符设计。

解决方案

javascriptCopy Code
Vue.directive('lazy', {
  inserted(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = new Image();
          img.src = binding.value;
          img.onload = () => el.src = binding.value;
          observer.unobserve(el); // 加载后停止观察
        }
      });
    });
    observer.observe(el);
  }
});

// 使用
<img v-lazy="'https://example.com/large-image.jpg'" src="placeholder.jpg">

优化点

  • 兼容性‌:降级方案(如 scroll 事件监听)。
  • 错误处理‌:添加 onerror 回调显示默认图片。

5. 如何设计一个支持动态内容的指令(如 Tooltip)?

问题解析

  • 核心需求‌:鼠标悬停时显示动态内容提示。
  • 难点‌:动态内容渲染、位置计算、组件化与指令的协作。

解决方案

Vue.directive('tooltip', {
  bind(el, binding) {
    const tooltip = document.createElement('div');
    tooltip.className = 'custom-tooltip';
    document.body.appendChild(tooltip);

    el.addEventListener('mouseenter', () => {
      tooltip.textContent = binding.value;
      const rect = el.getBoundingClientRect();
      tooltip.style.left = `${rect.left + rect.width / 2}px`;
      tooltip.style.top = `${rect.top - 30}px`;
      tooltip.style.display = 'block';
    });

    el.addEventListener('mouseleave', () => {
      tooltip.style.display = 'none';
    });
  },
  unbind(el) {
    // 清理 Tooltip 元素
    const tooltip = document.querySelector('.custom-tooltip');
    tooltip?.remove();
  }
});

// 使用
<button v-tooltip="'这是提示内容'">悬停查看提示</button>

优化点

  • 内容动态更新‌:在 update 钩子中更新 tooltip.textContent
  • 动画效果‌:通过 CSS 过渡或第三方动画库增强交互。

复杂场景设计原则

  1. 解耦与复用‌:将指令逻辑拆分为独立函数,方便复用。
  2. 性能优化‌:避免在指令中频繁操作 DOM,优先使用 CSS 或 requestAnimationFrame
  3. 响应式处理‌:通过 binding.value 监听数据变化,更新指令行为。
  4. 内存管理‌:在 unbind 或 beforeUnmount 中清理事件和对象。

后端程序员写TypeScript,被言中...

写在前面

作为一个写php和go的后端程序员,虽然也写过js、微信小程序、uniapp,但是对于ts还是比较陌生的,曾经就有一个同事,嘲讽我写不来ts,最终还是被他言中了。甚至有同事说 周围写ts的,没有一个不骂的

图片

**
**

接触ts

最近公司的项目使用vue-element-plus-admin,ts技术栈,前面的简单功能,写写表格啥的,复制一下demo和同事的代码,其实还好,有个功能要用Echart搞拆线图。本来想着这个功能也不难,以前又不是没写过,对着官网一顿ctrl c ctrl v, 然后 npm run dev运行得好好的,图表显示完全正常。最后想git commit死活过不了npm run ts:check,报错像小作文一样长,一个文件,90多个error,给我整懵了。。。

src/views/Monitor/Realtime.vue:47:78 - error TS18046: 'value' is of type'unknown'.

47

src/views/Monitor/Realtime.vue:51:31 - error TS2339: Property 'energy' does not exist on type'{}'.

51

src/views/Monitor/Realtime.vue:54:78 - error TS18046: 'value' is of type'unknown'.

54

src/views/Monitor/Realtime.vue:58:24 - error TS18046: 'value' is of type'unknown'.

58

src/views/Monitor/Realtime.vue:65:7 - error TS6133: 'getLatestDataList' is declared but its value is never read.

65

src/views/Monitor/Realtime.vue:18:11 - error TS2322: Type '{ [x: string]: unknown; dataset?: { mainType?: "dataset" | undefined; seriesLayoutBy?: SeriesLayoutBy | undefined; sourceHeader?: OptionSourceHeader | undefined; ... 8 more ...; dimensions?: (string | ... 1 more ... | undefined)[] | undefined; } | { ...; }[] | undefined; ... 41 more ...; colorLayer?: (string | ... 3...' is not assignable to type'EChartsOption'.

18

最痛苦的是,npm run ts:check一次要好久,简直浪费时间。

与AI的失败尝试

看到这一堆报错,咱立马将它复制到ds里面,然后ds就一顿输出给了答案,其中像error TS6133这样的我自己都能解决,对于error TS18046这样的ds解决起来也很简单,主要是error TS2322不管是ds还是gpt,都不行,只会在错误的路上越来越远。。。

ds和gpt给的错误答案,如下:

// 尝试1
const option = {} as EChartsOption;

// 尝试2
const option = ref<EChartsOption>({});

// 尝试3
const option = reactive<EChartsOption>({});

// 尝试4
option.value = {
      ...option.value,
      legend: {
        data: res.data.legend
      },
      xAxis: [
        {
          type: 'category',
          boundaryGap: false,
          data: res.data.category
        }
      ],
      series: res.data.series.map((item: any) => ({
        name: item.title,
        type: 'line',
        data: item.data
      })) as SeriesOption[]
    }

// 尝试5
option.value = Object.assign({}, option.value, {
      legend: {
        data: res.data.legend,
      },
      xAxis: [
        {
          type: "category",
          boundaryGap: false,
          data: res.data.category,
        },
      ],
      series: res.data.series.map((item: any) => ({
        name: item.title,
        type: "line",
        data: item.data,
      })) as SeriesOption[],
    });
  });

// 尝试6
const seriesData: SeriesOption[] = res.data.series.map((item: any) => ({
  name: item.title,
  type: "line",
  data: item.data
}));

// 尝试7
option.value = {
  ...option.value,
  series: seriesData
};

// 尝试8
function isEChartsOption(obj: any): obj is EChartsOption {
  return true; // 这里应该写实际判断逻辑
}

查源码

各种调试都不行之后,就开始看源码,通过对比,发现的一点规律,用官方demo, 就可以通过npm run ts:check检查,但是只要想动态赋值,就会报错。但不可能不用后端接口数据。

图片转存失败,建议直接上传图片文件

问同事

查了源码也不行,只剩下最后一个办法,就是问前端同事,结果他看了一眼,轻描淡写地说: "用any吧。"

const option = ref<any>({
  backgroundColor: '#ffffff',
  title: {
    text: ''
  },
  tooltip: {
    trigger: 'axis'
  },
  legend: {
    data: []
  },
  grid: {
    left: '3%',
    right: '3%',
    bottom: '6%',
    containLabel: false
  },
  xAxis: [
    {
      type: 'category',
      boundaryGap: false,
      data: []
    }
  ],
  yAxis: [
    {
      type: 'value',
      name: '(数量)',
      nameLocation: 'end'
    }
  ],
  series: [] as SeriesOption[]
})

简直不可思议,就如此简单,困扰我几天的问题一下子解决了。any就像ts的"免检通行证",让ts:check直接过了。

后续

为此我特意查了一下any, 结果如下

类型安全的"逃生舱"

在ts的世界里,any类型就像一个特殊的逃生舱门。当你在严格的类型检查中感到窒息时,它为你提供了一条"生路"。这个看似简单的类型,实际上是TypeScript类型系统中最为复杂和争议的存在。

any的起源可以追溯到TypeScript最早期的设计阶段。当时团队面临一个关键决策:如何让JavaScript开发者平滑过渡到TypeScript?答案是any——它允许开发者逐步采用类型检查,而不是全有或全无的选择。这个设计决策虽然看似简单,却极大地降低了TypeScript的采用门槛,成为它成功的关键因素之一。

any的典型使用场景

在实际开发中,any有几个合理的应用场景:

  1. 1. 渐进式迁移:将JavaScript项目迁移到TypeScript时,any可以作为临时解决方案
  2. 2. 处理动态内容:如第三方API响应、用户输入等无法预知结构的数据
  3. 3. 快速原型开发:在验证概念阶段,避免被类型系统拖慢速度
  4. 4. 与无类型库交互:当使用纯JavaScript编写的库时

以ECharts为例,这个强大的图表库在TypeScript中使用时经常会遇到any的身影。考虑以下场景:

typescript

复制

const chart = echarts.init(document.getElementById('chart'));
const option: any = {
  // 复杂的配置项
  series: [{
    type: 'pie',
    data: [
      { value: 1048, name: 'Search Engine' },
      { value: 735, name: 'Direct' }
    ]
  }]
};
chart.setOption(option);

在这个例子中,ECharts的配置对象极为复杂且嵌套深,完整定义其类型可能得不偿失。此时使用any可以快速实现功能,但同时也埋下了隐患。

真相:EChartsOption 的类型设计问题

深入查了一下,发现 ECharts 的 EChartsOption 类型定义得非常严格,但 ECharts 运行时却非常宽松。

  1. 1. ECharts 的 option 允许很多动态字段,但 EChartsOption 没有完全涵盖这些可能性。
  2. 2. TS 类型定义基于文档,而 ECharts 运行时可以接受更多参数
  3. 3. 部分字段使用了 unknown 类型,导致 TS 无法自动推导兼容性

换句话说,ECharts 本身能正常跑,但 TypeScript 认为它“不够安全” 。这就是 npm run dev 没问题,npm run ts:check 却报错的原因。

综上

image.png

大家都说any大法好,能用就用,千万不要自己卷自己。

-- END --

如何设计灵活可扩展的前端日志解决方案:提升应用稳定性与可观测性

设计灵活可扩展的前端日志解决方案:提升应用稳定性与可观测性

在前端开发的世界里,日志记录是一项至关重要的任务。它不仅能够帮助开发者快速定位和解决问题,还能为应用的健康监控和性能优化提供有力支持。然而,随着项目的不断发展和业务需求的日益复杂,传统的日志记录方式往往难以满足灵活扩展的需求。本文将介绍一种灵活可扩展的前端日志解决方案——awesome-logger,并详细阐述其设计思路、架构特点以及使用方法。

一、设计目标与需求分析

在设计前端日志解决方案时,我们通常会面临以下几个关键需求:

  1. 快速定位用户问题:能够自动收集完整的上下文信息,如操作系统、设备型号、用户代理等,以便在用户反馈异常时,能够快速定位问题根源。
  2. 应用健康度监控:支持多等级日志管理,如 infowarnerror 级别,帮助开发者构建稳定性大盘,实时监控应用的健康状态。
  3. 多日志系统同构上报:能够同时接入多个日志服务,如阿里云 SLS、腾讯云 CLS 等,实现一次上报同步多平台。
  4. 自定义日志扩展:允许开发者根据业务需求自定义日志上报逻辑,如接入私有日志系统、进行数据加密等。
  5. 高效开发与标准化:提供开箱即用的主流插件,减少基础设施的重复开发,提高开发效率。

二、awesome-logger 架构设计

awesome-logger 采用分层架构,确保功能解耦与扩展性,主要分为以下三层:

  1. 核心层(@awesome-logger/core
    • Logger 类:管理日志生成、等级控制及插件注册。提供 infowarnerror 等方法,支持基础字段配置。
    • LogPlugin 抽象类:定义插件开发规范。所有插件需实现 sendLog 方法,将日志数据发送到目标服务。
  2. 插件层(@awesome-logger/plugin-* 提供具体日志服务的实现。例如,@awesome-logger/plugin-sls 对接阿里云 SLS 日志服务。开发者可根据规范自定义插件,扩展日志能力。
  3. 使用层(@awesome-logger/client
    • Client 类:封装核心功能,简化用户接入。支持通过 usePlugin 注册插件,并提供统一的日志接口。

三、awesome-logger 核心优势

  1. 内置标准化日志字段,助力高效排查 预定义了如 os(操作系统)、device(设备型号)、ua(用户代理)等关键环境字段,无需额外开发即可收集全面的上下文信息,帮助开发者快速定位问题根源。
  2. 基础字段灵活配置,支持实时查询分析 允许用户自定义基础字段(如 uidenv 等),这些字段会自动附加到每条日志中。结合阿里云 SLS 等服务,可实现日志的实时过滤与查询,精准定位用户反馈场景。
  3. 多等级日志管理,构建应用健康监控 支持 infowarnerror 等多种日志等级,帮助开发者建立稳定性大盘,实时监控应用健康状态。通过不同等级的日志分类,可快速识别潜在风险与异常。
  4. 插件化架构设计,轻松对接任意日志服务 采用插件机制,开发者可自由扩展日志能力。内置支持阿里云 SLS、腾讯云 CLS 等主流日志服务,同时允许自定义插件,适配私有日志系统或其他第三方服务。

四、快速开始

1. 安装依赖

npm install @awesome-logger/client @awesome-logger/core @awesome-logger/plugin-sls

2. 初始化与配置

import Client from '@awesome-logger/client';
import { Logger } from '@awesome-logger/core';
import { SLSLogPlugin } from '@awesome-logger/plugin-sls';

// 创建 Logger 实例
const logger = new Logger({
  uid: 'test_user_1',
  release: '1.0.0',
  env: 'production',
});

// 也可以使用 logger.setBaseField 方法处理异步场景
logger.setBaseField({ uid: 'user_001' });

// 配置基础字段
const client = new Client(logger);

// 注册阿里云 SLS 插件
const slsPlugin = new SLSLogPlugin({
  host: 'your-sls-endpoint', // 公网域名
  project: 'your-project',
  logstore: 'your-logstore',
  count: 20, // 发送条数阈值
  time: 3, // 发送时间阈值
});
client.usePlugin(slsPlugin);

3. 日志上报

// 上报信息日志
client.info('enter_home_page', { page: 'home' });

// 上报警告日志
client.warn('api_timeout', { latency: 500 });

// 上报错误日志
client.error('api_fail', { errorCode: 500, endpoint: '/api/data' });

4. 日志上报到阿里云 SLS

效果如下图:

sls_sql.gif

注意事项

  1. 在使用 plugin-sls 之前,您需要创建一个阿里云账户。
  2. 开通 SLS 日志服务,支持免费试用一个月,50GB 容量。
  3. 创建一个日志项目(log project)。
  4. 然后创建一个日志库(logstore,前端上报时记得开启 Web Tracking 选项)。
  5. 上报一些日志后,方可创建索引。
  6. 索引支持设置、追加和覆盖。

五、自定义插件开发

awesome-logger 支持开发者自定义插件,轻松对接私有日志系统或其他服务:

1. 创建插件类

import { LogPlugin } from '@awesome-logger/core';

class CustomLogPlugin extends LogPlugin {
  sendLog(logData: Record<string, any>) {
    // 自定义日志上报逻辑
    console.log('自定义日志服务上报:', logData);
    // 示例:发送到自研日志系统
    fetch('https://your-log-service.com', {
      method: 'POST',
      body: JSON.stringify(logData)
    });
  }
}

export default CustomLogPlugin;

2. 使用自定义插件

import Client from '@awesome-logger/client';
import CustomLogPlugin from './CustomLogPlugin';

const client = new Client();
const customPlugin = new CustomLogPlugin({ /* 自定义配置 */ });
client.usePlugin(customPlugin);

client.info('click', { message: 'click button' });

3. 效果展示

iShot_2024-11-23_16.37.32.gif

六、日志内置字段

awesome-logger 的日志包含了丰富的内置字段,这些字段可以帮助开发者更好地了解日志的上下文信息,具体如下:

字段 类型 说明
uid string | number 用户 UID
release string 前端应用版本号
env string 环境:local、pre、prod
type string 类型,如日志等级:info、warn、error
key string 日志 key,用以标识一条日志记录
data Record<string, any> | string 日志 key 对应的数据
ua string 浏览器 navigator.userAgent 信息
url string 当前页面的 URL 信息
os string 当前设备的操作系统信息
osVersion string 当前设备的操作系统版本
traceId string 前后端约定的 UUID,用以追踪问题
sessionId string 会话 ID,用以区分同一会话范围内的日志
browser string 浏览器:Chrome、Safari、iOS Safari 等
browserVersion string 浏览器版本信息
container string 页面运行所在容器信息,如:钉钉、浏览器
device string 设备类型,如:手机、桌面端
clientTime string | number 客户端时间戳

七、贡献与反馈

我们欢迎社区贡献!如果您有功能建议、Bug 反馈或想参与开发,请提交 GitHub Issue 或 Pull Request。如果您觉得这个项目有用,不妨给它一个 Star 支持一下吧!

通过 awesome-logger,您可以轻松实现前端日志的标准化、可观测性与灵活上报,让日志成为您应用稳定性的强大助力!快来体验吧!

🔥 纯CSS黑科技!仅用1个DIV实现3D立体选项卡

🔥 纯CSS黑科技!仅用1个DIV实现3D立体选项卡


🌟 先睹为快:效果展示

3D立体选项卡效果图
(此处为效果示意图,实际运行可见:)
三⼤视觉亮点
✅ 真3D透视效果
✅ 自适应缺口圆角
✅ 无JavaScript纯CSS实现


🛠 核心实现原理拆解

1️⃣ 3D空间构建

css

复制

.tab {
    transform: perspective(40px) rotateX(30deg);
    transform-origin: center bottom;
}
  • perspective(40px):创建纵深40px的3D观察空间(值越小透视感越强)
  • rotateX(30deg):沿X轴旋转30度(模拟仰视视角)
  • transform-origin:设置变形基准点为底部中心点

2️⃣ 魔法缺口实现

利⽤径向渐变创造视觉缺口:

css

复制

.tab:before {
    background: radial-gradient(circle at 0 0, 
        transparent 10px, 
        #ed6a5e 10px
    );
}

参数解析

  • circle at 0 0:以左上角为圆心
  • transparent 10px:0-10px区域透明
  • #ed6a5e 10px:10px后显示主色

🎨 完整代码逐行解析

html

复制

<!DOCTYPE html>
<html>
<head>
    <style>
        .tab {
            /* 基础样式 */
            width: 150px;      /* 选项卡宽度 */
            height: 40px;      /* 选项卡高度 */
            background: #ed6a5e; /* 主题色 */
            border-radius: 10px 10px 0 0; /* 顶部圆角 */
            
            /* 3D变形 */
            position: relative;
            transform-origin: center bottom;
            transform: perspective(40px) rotateX(30deg);
        }
        
        /* 伪元素创造缺口 */
        .tab::before,
        .tab::after {
            content: '';
            position: absolute;
            width: 10px;       /* 缺口宽度 */
            height: 10px;      /* 缺口高度 */
            bottom: 0;         /* 定位到底部 */
        }
        
        /* 左侧缺口 */
        .tab:before {
            left: -10px;
            background: radial-gradient(...);
        }
        
        /* 右侧缺口 */
        .tab:after {
            right: -10px;
            background: radial-gradient(...);
        }
    </style>
</head>
<body>
    <div class="tab"></div>
</body>
</html>

运行 HTML


💡 关键技术点深度剖析

1. 伪元素定位黑科技

属性 作用说明 关键值解析
position: absolute 绝对定位 相对于.tab容器定位
bottom: 0 底部对齐 使缺口出现在底部边缘
left/right: -10px 负向定位 让元素超出容器边界

2. 径向渐变精准控制

径向渐变示意图

  • 透明到实色的硬切换:通过设置相同断点值(10px)实现锋利边缘
  • 圆心位置控制circle at 0 0circle at 100% 0形成对称缺口

🚀 扩展应用指南

方案1:动态交互增强

css

复制

/* 悬停放大效果 */
.tab:hover {
    transform: perspective(60px) rotateX(15deg);
    transition: all 0.3s ease;
}

/* 激活状态指示 */
.tab.active {
    background: #ff6b6b;
    box-shadow: 0 0 15px rgba(255,107,107,0.5);
}

方案2:多选项卡系统

html

复制

<div class="tabs">
    <div class="tab active">Tab1</div>
    <div class="tab">Tab2</div>
</div>

<style>
    .tabs {
        display: flex;
        gap: 20px;
    }
</style>

运行 HTML


⚠️ 浏览器兼容性提示

特性 兼容性 降级方案
CSS Transform 3D IE10+ 移除perspective属性
radial-gradient IE10+ 改用实色背景+边框
CSS伪元素 全浏览器 无需处理

🔧 调试技巧速查

  1. 透视强度调试:增大perspective值观察景深变化
  2. 旋转角度调试:修改rotateX值感受视角变化
  3. 缺口尺寸调试:同时调整伪元素的宽高和渐变断点值

🚨 创作不易,如果本文对你有帮助,欢迎点赞收藏!关于CSS黑科技,你还想了解哪些实现技巧?欢迎在评论区留言讨论!

理解浏览器视口:为什么你的屏幕分辨率不直接决定网页的显示区域?

前言

作为前端开发者,我们在学习前端知识时通常会默认一件事:px 像素单位并不适合直接设置给宽高、边距、定位等跟布局有关的属性,如果你实际写过一些 demo 就会发现,相同的像素值设置在同样的盒子上,在不同的显示器上显示的效果总是不一样

例如:给一个盒子设置 width: 1500px; height: ...px; 可能在一些小显示器上会横向溢出,导致出现横向滚动条,在一些大显示器上甚至填不满横向宽度

再深入学习时,我们会学到一个概念:视口 viewport

视口代表当前可见的计算机图形区域。在 Web 浏览器术语中,通常与浏览器窗口相同,但不包括浏览器的 UI,菜单栏等——即指你正在浏览的文档的那一部分。

如图,红色方框的位置便是我们常说的默认的视口

1.png

此时你便会有这样的疑惑:

  • 不同的显示器或浏览器窗口中,网页的显示区域(视口)大小到底是多少?
  • 当你将浏览器窗口最大化时,不同分辨率的显示器展现的可用空间为什么不符合直觉?

本文将带你捋清楚这个问题

视口的核心概念

在浏览器中,视口(Viewport)并不是一个简单的“窗口大小”,而是多层概念的叠加:

1. 布局视口(Layout Viewport)

  • 作用:CSS布局的基准,决定元素如何排列(如百分比宽度、vw/vh单位)。

    • PC端:等于浏览器内容区域(去掉工具栏、滚动条)。(如图中的红色区域
    • 移动端:默认较大(如980px),确保桌面网页在手机上不“挤爆”。

1.png

2. 视觉视口(Visual Viewport)

  • 作用:用户当前实际看到的区域,随缩放、滚动动态变化。(如图中的蓝色区域
  • 示例:手机竖屏转横屏时,视觉视口宽度从375px变为812px(iPhone 13)。

2.png

3. 理想视口(Ideal Viewport)

  • 目标:让布局视口与设备逻辑宽度一致,需通过 <meta name="viewport"> 手动启用。
  • 为什么需要让布局视口与设备逻辑宽度一致?后文会说
<meta name="viewport" content="width=device-width, initial-scale=1">

缩放比例

在讲解如何计算出视口宽高的具体步骤之前,需要先知道一个前置知识:缩放比例

操作系统缩放比例(Scaling):操作系统会根据屏幕尺寸和分辨率自动调整缩放比例,以平衡物理尺寸与显示内容的可读性

操作系统会根据显示器的分辨率和尺寸自动设置一个缩放比例,具体的缩放比例可以自行前往系统设置查看

  • Windows:设置 > 系统 > 显示 > 缩放比例
  • macOS:系统设置 > 显示器 > 分辨率 > 默认缩放

3.png

还有一个概念是设备像素比(DPR),它的定义是物理像素与逻辑像素的比值(如Retina屏DPR=2),在浏览器中,设备像素比(DPR)= 物理像素 / 逻辑像素(由操作系统缩放比例决定),但DPR只会影响图像清晰度,不改变布局视口的CSS像素值,此处便不展开解释了

如何计算视口宽高

前文说过:操作系统会根据显示器的分辨率和尺寸自动设置一个缩放比例,这也是导致不同分辨率的显示器展现的可用空间为什么不符合直觉的原因

所以,要计算出视口宽高,我们需要这几个数值:

  • 显示器的分辨率
  • 操作系统给当前显示器设置的缩放比例

理论上来说,显示器的分辨率 / 操作系统缩放比例 就是视口的大小

下面通过两个实际例子给出计算过程

显示器 A

显示器参数:16寸显示器,分辨率2560×1600,假设操作系统给的缩放是150%,默认计算最大化浏览器后的视口大小。

  • 操作系统缩放比例,即浏览器的 DPR:DPR = 1.5
  • 浏览器视口大小:2560×1600 / 1.5 ≈ 1707×1067

减去浏览器的导航栏,滚动条等占用的大小

  • 视口宽度:1707px - 滚动条 8px ≈ 1699px
  • 视口高度:1067px - 导航栏 160px ≈ 907px

所以最终计算结果,视口大小为:1699×907

显示器 B

显示器参数:24寸显示器,分辨率2560×1440,假设操作系统给的缩放是125%,默认计算最大化浏览器后的视口大小。2040×992

  • 操作系统缩放比例,即浏览器的 DPR:DPR = 1.25
  • 浏览器视口大小:2560×1440 / 1.25 = 2048×1152

减去浏览器的导航栏,滚动条等占用的大小

  • 视口宽度:2048px - 滚动条 8px ≈ 2040px
  • 视口高度:1152px - 导航栏 160px ≈ 992px

所以最终计算结果,视口大小为:1699×907

但浏览器还有导航栏,滚动条等,这些也会占用大小,所以需要减去(此处的计算过程只给出大致大小,不同浏览器的导航栏和滚动条大小不一定相同,所以最终结果只是一个参考,后文会给出如何获取具体的视口宽高的方法

原理理解

为什么 显示器的分辨率 / 操作系统缩放比例 就是视口的大小?

如果操作系统不进行缩放,即缩放比例为 100% ,理论上的视口大小确实就是显示器的分辨率大小,但对于大尺寸高分辨率的显示器来说,不进行缩放的显示效果会比较奇怪。

(具体是怎么个奇怪法可以自行去系统设置里调整一下缩放比例,但记得不要改太大了,改太大的话你自己可能是改不回来的,参考前段时间网上盛行的赛博灯泡与赛博华佗

此处用放大来讨论,缩小也是同理的。由于操作系统对显示器的显示内容进行了放大,导致具体显示到显示器上的内容的具体像素大小就不能跟显示器原本的分辨率一样了,否则会导致太大,屏幕放不下

放大后,操作系统渲染到屏幕上的内容会变大,显示器能显示的内容就变少了,此处给出一幅图可以参考

4.png

看懂这幅图后,你便能理解视口大小的计算公式为什么是除以 /

如何获取视口宽高

获取视口尺寸

  • 视觉视口宽度 = window.innerWidth(包含滚动条,但实测中部分浏览器可能会减去滚动条)。
  • 布局视口宽度 = document.documentElement.clientWidth(不包含滚动条)。
// 视觉视口(含滚动条)
console.log("Visual Viewport:", window.innerWidth, window.innerHeight);

// 布局视口(不含滚动条)
console.log("Layout Viewport:", document.documentElement.clientWidth, document.documentElement.clientHeight);

5.png

获取设备像素比(缩放比例)

  • 设备像素比(缩放比例) = window.devicePixelRatio
console.log("DPR:", window.devicePixelRatio);

6.png

移动端的视口处理

移动端设备的屏幕尺寸、交互方式(如触摸、旋转)和浏览习惯与桌面端存在本质差异,这导致浏览器在移动端对视口的处理逻辑更加复杂,在开发时需要特殊对待。本节将深入解析移动端的视口机制,并回答以下关键问题:

  • 为什么手机浏览器默认会缩小网页?
  • 如何让网页真正适配手机屏幕?
  • 为什么用户缩放会破坏你的布局?

移动端视口的默认行为

移动浏览器会有一些默认行为:

  • 布局视口默认值较大(如 iOS Safari 默认 980px),确保未适配移动端的桌面网页能完整显示(尽管会被缩小)。

  • 视觉视口初始缩放:浏览器会自动计算缩放比例,使网页宽度匹配屏幕宽度。

    • 例如:一个宽度 980px 的桌面网页在 iPhone 13(逻辑宽度 390px)上显示时,初始缩放比例 ≈ 390/980 ≈ 0.4(即缩小到 40%)。

结果:用户看到的是缩小后的整体页面,需要手动放大才能阅读内容——体验极差!

解决方法

给网页加上<meta name="viewport"> 标签

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  • width=device-width:将布局视口宽度设为设备逻辑宽度(如 iPhone 13 的 390px)。
  • initial-scale=1:禁用初始缩放,1 CSS 像素 = 1 逻辑像素。

标签的隐藏规则:

  • 如果未设置 width,移动浏览器仍会使用默认布局视口(如 980px)。
  • width=device-width 和 initial-scale=1 同时存在时,浏览器会取两者计算结果的较大值,避免冲突。

屏幕旋转时

  • 竖屏转横屏时,布局视口宽度从 390px(iPhone 13 竖屏)变为 844px(横屏)。
  • 应对方案:使用 CSS 媒体查询动态调整布局:
@media (orientation: portrait) { /* 竖屏样式 */ }  
@media (orientation: landscape) { /* 横屏样式 */ }  

键盘弹出时

  • 当用户点击输入框时,虚拟键盘可能占据 50% 的屏幕高度,视觉视口高度急剧缩小。

  • 开发者陷阱100vh 在移动端可能包含被键盘遮挡的区域,导致底部内容不可见。

  • 解决方案

    • 使用 window.innerHeight 动态计算高度。
    • CSS 新特性:height: 100dvh(实验性属性,动态适配视口高度)。

用户主动缩放

  • 移动端允许用户双指缩放页面,这会改变视觉视口尺寸(如放大后,视觉视口宽度从 390px 变为 195px)。
  • 禁止缩放(谨慎使用)
<meta name="viewport" content="user-scalable=no">
  • 副作用:可能影响无障碍访问,建议仅在特定场景(如全屏游戏)使用。

高 DPI 屏幕下的“1px 边框问题”

由于移动端设备像素比(DPR)通常 ≥ 2,直接设置 border: 1px 会显示为 2 物理像素宽,显得过粗

解决方案

方案1:利用 viewport 缩放

<meta name="viewport" content="width=device-width, initial-scale=0.5">  
  • 设置初始缩放为 1/DPR(如 DPR=2 时缩放 0.5),此时 1 CSS 像素 = 1 物理像素。
  • 缺点:需用 JavaScript 动态计算 DPR,且可能影响布局单位。

方案2:CSS 媒体查询 + 伪元素

@media (-webkit-min-device-pixel-ratio: 2) {  
  .thin-border {  
    position: relative;  
    &::after {  
      content: "";  
      position: absolute;  
      left: 0;  
      top: 0;  
      width: 200%;  
      height: 200%;  
      border: 1px solid #000;  
      transform: scale(0.5);  
      transform-origin: 0 0;  
    }  
  }  
}  
  • 优点:纯 CSS 实现,无副作用。

实际开发

在理解了视口的概念和如何计算视口大小后,此处列举一些在实际开发中可能会用到的例子

开发时

移动端必加 <meta viewport> 标签

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

避免固定视口假设

  • 错误做法:在进行网页页面布局时大量使用 px 作为单位
  • 正确做法:使用弹性布局(Flexbox/Grid)和相对单位(%vw)。

处理高DPI屏幕

  • 使用 srcset 提供高清图片:

    <img src="image.jpg" 
         srcset="image@2x.jpg 2x, image@3x.jpg 3x">
    

调试时

  1. Chrome DevTools 设备模式

    • 模拟不同设备尺寸、DPR、屏幕旋转。
    • 支持触摸模拟、限制网络速度。
  2. 真机调试

    • iOS:通过 Safari 的 Web Inspector 连接 iPhone。
    • Android:Chrome 远程调试。
  3. 在线工具

理解这些机制后,你将能:

  • 更精准地设计响应式布局。
  • 避免“为什么我的页面在这台显示器上显示不全?”的困惑。
  • 为用户提供真正跨设备一致的体验。

最终建议:打开浏览器的开发者工具,亲自调整缩放比例、旋转设备方向,观察视口变化——这是掌握这一知识的最佳方式!

一些拓展小知识

  • @viewport 规则
    允许开发者在CSS中直接定义视口的宽度、高度、缩放比例等属性。例如:
@viewport {
  width: device-width;   /* 视口宽度等于设备逻辑宽度 */
  zoom: 1.0;             /* 初始缩放比例为1 */
}

该规则与移动端常用的 <meta name="viewport"> 标签功能一致,但通过CSS语法实现。

那在 <meta> 标签里写的会怎么处理呢,实际上会被转换为等效的 @viewport 规则:

也就是说

<meta name="viewport" content="width=device-width, initial-scale=1.0">
@viewport {
  width: device-width;   /* 视口宽度等于设备逻辑宽度 */
  zoom: 1.0;             /* 初始缩放比例为1 */
}

这两段代码其实是等价的

REFERENCED

微信小程序常见问题记录合集

一、开放能力web-view

  • 定义:承载网页的容器。会自动铺满整个小程序页面,个人类型的小程序暂不支持使用
  • 接口:web-view网页中可使用 JSSDK 提供的部分接口
  • 问题:微信官方有说明web-view现存的Bug,同时也给出了对应的Tip,具体见下图
  • 文档:官方文档传送门

截屏2025-04-06 15.30.26.png

二、原生组件层级最高

  • 微信小程序原生组件的层级特性‌:在微信小程序中,原生组件如canvas、video、textarea等是由客户端直接创建和渲染的,因此它们的层级在页面中是最高的,不受z-index属性的控制。这意味着无论其他组件的z-index设置得多么高,都无法覆盖在原生组件之上‌。

  • 原生组件的层级特性‌:原生组件的层级是独立的,完全独立于webview,因此无法通过z-index来控制它们与其他组件的层级关系。这种设计使得原生组件在性能和稳定性上具有优势,但同时也带来了布局上的挑战‌。

  • 解决原生组件层级问题的方法‌:

    1. 使用cover-view和cover-image‌:这两个组件也是原生组件,但它们的层级比canvas更高,可以在某些情况下覆盖canvas。然而,它们的使用有一定的局限性,仅支持嵌套cover-view和cover-image,以及在其中使用button‌。
    2. 将canvas转换为图片‌:在canvas绘制完成后,使用canvasToTempFilePath方法将其转换为图片,然后隐藏canvas,显示图片。这种方法适用于静态的canvas绘图,但对于动画可能会影响性能‌。
    3. 调整布局‌:通过调整页面布局,尽量避免canvas与其他需要交互的组件重叠。例如,可以将canvas放置在页面的固定位置,其他组件则布局在canvas遮挡不到的区域‌。
  • 原生组件支持同层渲染‌:为了解决原生组件的层级问题,微信小程序引入了同层渲染机制。通过同层渲染,原生组件可以与其他组件随意叠加,解决了层级限制的问题。但需要注意的是,组件内部仍由原生渲染,样式一般对原生组件内部无效‌。

三、slot插槽节点渲染机制

  • 在浏览器中,slot 节点内容会被渲染到子节点里面,小程序中会被渲染为使用 slot 的兄弟节点

    image.png

  • 小程序中每个页面和组件都会有对应的 shadow-root 存放其真实节点

    image.png

四、ios系统使用new Date()解析时间出现NaN

  • 现象:ios系统使用new date("yyyy-MM-dd")解析时间时会显示NaN,这个不光是小程序,移动端iOS大部分机型都会出现这个问题

  • 原因:由于ios无法识别yyyy-MM-dd的格式

  • 解决:替换连接符-为可识别的/

let val = "2025-04-03 17:59:01"
let time = val.replace(/-/g, "/")
let dateTime = new Date(time)

五、全局样式变量定义(app.wxss文件)

  • page 标签方式

    /* 标签定义全局变量 */
    page {
    }
    
  • :root 伪类方式

    /* 伪类定义全局变量 */
    :root {
    }
    
  • 两者的区别和选择建议

    • 兼容性 :从兼容性方面来看, page 是小程序特有的选择器,兼容性良好。而 :root 是标准的 CSS 伪类,在支持 CSS 变量的环境中都能使用。
    • 语义 : page 更明确地表示这是小程序的页面,语义上更贴合小程序的开发环境。 :root 则更偏向于标准 CSS 的使用习惯。
    • 开发建议:小程序中更推荐使用原生的 page 标签选择器。

六、原生<text>组件空白行渲染问题

  • 小程序中使用原生<text>,若文本前后有空白行,或者是文本内容单独写一行,在模拟器中渲染出来会出现莫名其妙的空白行 image.pngimage.png
  • 经过验证后发现,只有特定微信开发者工具版本(我用的是Stable 1.06.2412050)才会出现,可能其他版本也会有类似问题
  • 解决方案:去除文本内容前后的空行,使得文本内容和<text>组件在同一行即可解决 image.pngimage.png

超燃!手把手教你打造赛博朋克风流光按钮🤩🤩🤩

超燃!手把手教你打造赛博朋克风流光按钮

一、效果抢先看

这款按钮拥有令人惊艳的赛博朋克风格:当鼠标悬停时,橙色流光从四角向中央汇聚,伴随三层光晕扩散效果;点击时按钮产生下沉微交互,同时光效变为暖金色。整个过程丝滑流畅,仿佛来自未来科技的操控界面。

动画.gif

二、核心原理解析

1. 四重边框魔法

按钮通过四个<div>元素构建动态边框:

<div class="top"></div>  <!-- 上边框 -->
<div class="bottom"></div> <!-- 下边框 -->
<div class="left"></div>  <!-- 左边框 -->
<div class="right"></div> <!-- 右边框 -->

运行 HTML

初始状态下:

  • 上下边框:宽15px,高2px
  • 左右边框:宽2px,高15px
    通过position: absolute精准定位到按钮四边

2. 流光展开动画

button:hover .top,
button:hover .bottom {
    width: 100%; /* 宽度扩展到100% */
}
button:hover .left,
button:hover .right {
    height: 100%; /* 高度扩展到100% */
}

搭配transition: 0.5s all实现0.5秒的平滑过渡

三、Transition属性详解

1. 动画引擎

transition: [属性] [时长] [缓动函数] [延迟];
  • 属性:指定要过渡的CSS属性(本例用all表示所有属性)
  • 时长:动画持续时间(0.5s=500毫秒)
  • 缓动函数:控制动画速度曲线(默认ease)
  • 延迟:动画开始前的等待时间

2. 实战应用对比

/* 悬停时边框0.5秒渐变 */
button div {
    transition: 0.5s all;
}

/* 点击时快速响应 */
button:active {
    transition: 0.2s all; /* 更短的0.2秒动画 */
}

不同状态设置不同过渡时长,营造层次分明的交互体验

四、光影黑科技

1. 三重光晕

box-shadow: 
    0 0 15px #ff7700,
    0 0 30px #ff7700,
    0 0 50px #ff7700;

通过叠加三层阴影,创建出极具深度的发光效果

2. 动态光渗

button:hover {
    box-shadow: inset 0 0 25px #ff7700;
}

内阴影实现按钮表面流光溢彩的效果

五、代码结构解析

元素 定位点 初始尺寸 悬停动画
.top 左上角 15x2 宽度扩展到100%
.bottom 右下角 15x2 宽度扩展到100%
.left 左上角 2x15 高度扩展到100%
.right 右下角 2x15 高度扩展到100%

六、延伸思考

  1. 颜色改造:将#ff7700改为#00ff88可瞬间切换为赛博绿光风格
  2. 速度实验:将transition时长改为1s感受慢动作科技感
  3. 边框进阶:修改div的height/width值创建不同厚度的光刃效果

七、浏览器支持提醒

本效果基于现代CSS3特性,推荐在Chrome/Firefox/Edge等现代浏览器中使用。如需兼容IE11,需增加-ms-transition前缀。

通过掌握transition的妙用,我们成功打造出这个极具未来感的交互元素。这种技术可延伸应用于菜单栏、进度条等各类组件的动效设计,让网页充满生命力。现在就去CodePen复现这个效果,开启你的前端动效之旅吧!

最受欢迎的十个 JavaScript 动画库 -2025

好文翻译:dev.to/hadil/top-1…

大家好,我是 luckySnail,动画是现代前端开发中必备的技能,作为一名 Web开发者,你一定深知动画效果对于用户体验至关重要,甚至能决定其成败。但说实话,从零开始手写自定义动画,那可真是个苦差事。因此 JavaScript 动画库应运而生!

到了 2025 年,Web 动画的世界简直精彩纷呈!无论你是要构建一个简洁的个人作品集、一个动态的 Web 应用程序,还是仅仅想为你的项目增添一些亮点,这些动画库都能满足你的需求。让我们一起深入了解 2025 年最佳 JavaScript 动画库 Top 10,看看它们如何提升你的开发技能。

1) GSAP (GreenSock Animation Platform) 🟢

GSAP(GreenSock 动画平台)是当之无愧的 JavaScript 动画库之王。它速度快、灵活性强,并且可以在所有浏览器上无缝运行。借助 GSAP,你可以实现从简单过渡到复杂时间轴动画的多种效果。

GSAP

特性:

  • 闪电般的速度,让其他库望尘莫及,瞬间秒杀!
  • 背后有超大规模的社区支持,教程多到学不完,助你轻松精通。
  • 无论你是初学者还是资深开发者,都能轻松驾驭。

👌🏻 最佳应用场景: 复杂和高性能的动画。

查看 GSAP 🔥

##2) Three.js 🎲

如果你对 3D 动画感兴趣,那么 Three.js 就是你的不二之选。它是创建令人惊叹的 3D 视觉效果和交互式体验的强大工具,所有这些都可以在浏览器中完成。

Three.js

特性:

  • 即使你不是 3D 大神,也能轻松玩转 3D 图形!
  • 令人难以置信的文档和丰富的示例。
  • 非常适合游戏、数据可视化和沉浸式网站。

👌🏻 最佳应用场景: 3D 动画和 WebGL 项目。查看 Three.js 🔥

3) Anime.js 🤹🏻‍♂️

Anime.js 是一个轻量级且直观的库,可以使用最少的代码轻松创建流畅、复杂的动画。

Anime.js

特性:

  • 语法简洁到爆,几分钟就能上手,瞬间起飞!
  • 轻松支持 CSS、SVG、DOM 和 JavaScript 动画。
  • 非常适合为你的用户界面 (UI) 添加微妙而优雅的动画。

👌🏻 最佳应用场景: 轻量级和优雅的动画。

查看Anime.js 🔥

4) Framer Motion 🔳

Framer Motion 专为 React 开发者而构建,是一个可用于生产环境的动画库,可以轻松创建流畅、声明式的动画。

Framer Motion

特性:

  • 与 React 无缝集成,实现流畅的工作流程。
  • 声明式 API,让动画效果像魔法一样神奇!
  • 非常适合交互式和基于手势的动画。

👌🏻 最佳应用场景: 基于 React 的项目。

查看 Framer Motion 🔥

##5) ScrollMagic 🎩

ScrollMagic 是一款专注于滚动触发动画效果的强大利器。它可以让你创建惊艳的效果,这些效果会在用户滚动页面时触发。

ScrollMagic

特性:

  • 讲故事和打造惊艳视差效果的绝佳利器!
  • 与GSAP 配合使用,实现高级动画。
  • 易于集成到现有项目中。

👌🏻 最佳应用场景: 滚动触发的动画。

查看 ScrollMagic 🔥

6) Mo.js 🔺

Mo.js 是一个运动图形库,专门用于创建美观、可定制的动画。它非常适合为你的网站添加有趣、动态的元素。

Mo.js

特性:

  • 高度定制化和模块化,让你拥有无限创意空间!
  • 非常适合 UI 动画和令人愉悦的微交互。* 独特的、有趣的动画风格,脱颖而出。

👌🏻 最佳应用场景: 创意和有趣的动画。

查看 Mo.js 🔥

7) Theatre.js 🎭

Theatre.js 是一个现代动画库,旨在通过可视化编辑器创建富有表现力的高性能动画。它非常适合希望无缝协作的开发人员和设计师。

Theatre.js

特性:

  • 可视化编辑器,让动画创作变得直观又有趣!
  • 非常适合创建富有表现力的高性能动画。
  • 非常适合设计师和开发人员之间的协作工作流程。

👌🏻 最佳应用场景: 高性能和视觉驱动的动画。

查看 Theatre.js 🔥

8) Popmotion 🕺

Popmotion 是一个函数式、响应式动画库,非常适合创建交互式、基于物理的动画。

Popmotion### 特性:

  • 函数式编程的强大力量与趣味性完美结合!
  • 非常适合交互式和手势驱动的动画。
  • 轻量级且灵活,适用于任何项目。

👌🏻 最佳应用场景: 交互式和基于物理的动画。查看 Popmotion 🔥

9) Lottie by Airbnb 🎬

Lottie 可以轻松地将高质量的、基于矢量的动画添加到你的 Web 项目中。它非常适合集成在 After Effects 中创建的动画。

Lottie by Airbnb

特性:

*轻量级且可扩展,适用于任何项目。

  • 与 After Effects 导出的 JSON 文件无缝衔接,简直天作之合!
  • 非常适合添加精致、专业的动画。

👌🏻 最佳应用场景: 基于矢量的动画和 After Effects 集成。查看 Lottie 🔥

10) Barba.js 🔄

Barba.js 是一个轻量级的库,用于创建流畅、无缝的页面过渡效果。它非常适合单页应用程序 (SPA),并可以改善用户体验。

Barba.js### 特性:

  • 易于设置和使用,即使对于初学者也是如此。
  • 如黄油般丝滑的过渡效果,瞬间提升用户体验!
  • 与其他动画库完美协作。

👌🏻 最佳应用场景: 页面过渡和 SPA。

查看Barba.js 🔥

🎉 为什么动画在 2025 年如此重要

在 2025 年,动画不再仅仅是装饰,它们已成为用户体验的关键组成部分。从引导用户浏览你的应用程序到让你的网站充满活力,动画能让你的项目 C 位出道!借助这些库,你无需成为动画专家即可创造出令人惊叹的作品。


翻译到这里就结束了,但是我想补充一些动画库,这里没有提到我们常使用的:

  • React Spring:基于物理的动画库,专为 React 设计
  • VueUse/Motion:专为Vue应用设计的动画解决方案,提供简单易用的动画API
  • AutoAnimate - 一个零配置的动画工具,自动为DOM变化添加平滑动画效果
  • Three.js - 用于创建3D动画和WebGL内容的JavaScript库

对了 animejs 发布了最新 4 版本,非常炫酷,大家快去看看吧!

参考

  1. 前 100万个网站使用动画库情况: trends.builtwith.com/javascript/…
  2. @vueuse/motion: motion.vueuse.org/
  3. AutoAnimate: github.com/formkit/aut…
  4. Three.js: threejs.org/

MCP 模型上下文协议

一、MCP 概述与背景 MCP 由 Anthropic 于 2024 年 11 ⽉推出,是⼀种开放协议,旨在标准化⼤语⾔模型(LLMs)应⽤程序与外部数 据源和⼯具之间的交互⽅式。 MCP的核⼼在于建

模块化

模块化 模块化就是将功能进行拆分成不同的模块(子功能),最后组合起来。 能够方便代码服务,增强代码可维护性,实现代码解耦。

第六章 :介绍全局状态管理库

目前为止,我们已经学习了几个共享状态的模式了。这本书剩下的内容,会介绍使用了这些模式的各种库。在深入这些库之前,我们会回顾全局状态会遇到的挑战,以及两大主题:状态应该存在哪里 和 如何控制 重新 渲染
❌