普通视图

发现新文章,点击刷新页面。
今天 — 2025年5月17日技术

从this丢失到精准绑定:用一个例子彻底搞懂bind的「救命」场景

作者 海底火旺
2025年5月17日 12:03

前几天写代码时遇到个怪事:用setTimeout调用对象方法,结果this.name打印出undefined。折腾半小时才发现是this指向问题——直到用bind修复后,代码才正常运行。今天就用这个真实案例,结合callapplybind的核心差异,带大家彻底搞懂「绑定this」的底层逻辑。

先看一个「翻车现场」:setTimeout里的this丢失

先看用户提供的代码(稍作修改更直观):

var obj = {
    name: 'cherry',
    func1: function() {
        console.log("当前this指向:", this); 
        console.log("打印name:", this.name); 
    },
    func2: function() {
        setTimeout(function() {
            this.func1(); // 目标:调用obj的func1
        }, 1000);
    }
};

obj.func2(); 
// 1秒后输出:
// 当前this指向: Window {...}(浏览器环境)
// 打印name: undefined

为什么会翻车?

setTimeout的回调函数是独立调用的。在JavaScript中,函数的this在调用时动态绑定,规则是:

  • 如果函数作为对象的方法调用(如obj.func2()),this指向该对象;
  • 如果函数独立调用(如function() {}),this默认指向全局对象(浏览器是window,严格模式是undefined)。

上面的代码中,setTimeout的回调函数是独立调用的,所以this指向window。而window没有func1方法(除非你全局定义过),自然会报错this.func1 is not a function(或打印undefined)。

用bind「拯救」this:用户代码的正确写法

用户提供的修复方案是用bind(obj)绑定this

var obj = {
    name: 'cherry',
    func1: function() {
        console.log("当前this指向:", this); 
        console.log("打印name:", this.name); 
    },
    func2: function() {
        setTimeout(function() {
            this.func1(); 
        }.bind(obj), 1000); // 关键:用bind绑定obj作为this
    }
};

obj.func2(); 
// 1秒后输出:
// 当前this指向: {name: 'cherry', func1: ƒ, func2: ƒ}
// 打印name: cherry

为什么bind能解决问题?

bind(obj)做了两件事:

  1. 返回一个新函数:这个新函数的this被永久绑定为obj,无论它在哪里调用;
  2. 不立即执行bind不会像call/apply那样立即执行原函数,而是等待setTimeout触发时执行新函数。

经过1秒后再出现: 屏幕录制 2025-05-17 113037.gif

对比call和apply:为什么不能用它们?

如果尝试用callapply

// 错误示范:用call
setTimeout(function() {
    this.func1(); 
}.call(obj), 1000); 
// 结果:立即执行(不会等1秒),且setTimeout的第一个参数变成了undefined(因为call立即执行函数,返回值是undefined)

callapply立即执行函数,导致两个问题:

  1. 函数会在setTimeout注册时立即执行,而不是延迟1秒;
  2. setTimeout的第一个参数需要是函数(或字符串),但call执行后的返回值是func1的执行结果(这里是undefined),导致定时器无效。

使用call和apply都是以下效果,cherry都是立即执行: 屏幕录制 2025-05-17 113854.gif

深入理解bind:它到底做了什么?

1. 绑定this,忽略后续调用的this

bind绑定this后,无论新函数如何调用,this都会指向绑定的值:

const boundFunc = function() {
    console.log(this.name);
}.bind({ name: '绑定对象' });

boundFunc(); // 输出:绑定对象(即使独立调用)
boundFunc.call({ name: 'call的this' }); // 输出:绑定对象(call无法覆盖bind的绑定)

2. 预填充参数,创建「偏函数」

bind可以提前传递部分参数,后续调用时只需传剩余参数:

function add(a, b) {
    return a + b;
}

const add5 = add.bind(null, 5); // 预填充第一个参数为5
console.log(add5(3)); // 8(相当于add(5,3))

3. 被new操作符覆盖的绑定

如果用bind绑定的函数作为构造函数(用new调用),bindthis会被new创建的新对象覆盖:

const Person = function(name) {
    this.name = name;
};

const BoundPerson = Person.bind({}); // 绑定this为{}
const p = new BoundPerson('张三'); 
console.log(p.name); // 张三(this指向新创建的p对象)

开发中最常用的5种场景(含用户案例)

1. 解决定时器/回调函数的this丢失(用户案例)

setTimeoutsetInterval的回调函数常因独立调用导致this丢失,bind是最直接的解决方案:

// 用户代码解析:
// 1. obj.func2调用时,this指向obj;
// 2. setTimeout的回调函数通过.bind(obj)绑定this为obj;
// 3. 1秒后回调执行时,this指向obj,成功调用func1。

2. 继承父类构造函数(经典OOP)

用构造函数实现继承时,call可以调用父类构造函数,绑定子类实例的this

function Animal(name) {
  this.name = name;
}

function Dog(name, age) {
  // 用call调用父类,绑定Dog实例的this
  Animal.call(this, name); 
  this.age = age;
}

const dog = new Dog('小黑', 3);
console.log(dog); // { name: '小黑', age: 3 }

3. 数组方法应用于类数组(apply经典用法)

类数组对象(如arguments、DOM节点集合)可以用apply调用数组方法:

function sum() {
  // arguments是类数组对象(有length但无数组方法)
  // 用apply调用Array.prototype.reduce,计算总和
  return Array.prototype.reduce.apply(arguments, [function(acc, cur) {
    return acc + cur;
  }, 0]);
}

console.log(sum(1, 2, 3)); // 6

5. 求数组的最大值/最小值(apply简化代码)

Math.max需要多个参数,apply可以将数组展开为参数列表:

const scores = [95, 88, 92, 100, 85];
const maxScore = Math.max.apply(null, scores); // 100(等价于Math.max(95,88,92,100,85))

总结:记住这3个关键点

  1. call和apply:立即执行函数,call传参数列表,apply传数组;
  2. bind:返回绑定后的新函数,不立即执行,适合延迟调用(如setTimeout、事件绑定);
  3. 核心价值:解决this指向问题,让函数在指定上下文中执行。

回到用户的例子,bind(obj)就像给setTimeout的回调函数装了一个「定位器」,无论它在哪里执行,this都会精准指向obj。这就是bind最核心的作用——固定this,让函数的执行上下文可控

下次遇到this丢失的问题,先想想:是需要立即执行(用call/apply),还是延迟执行(用bind)?想清楚这一点,就能在开发中灵活选择啦!

计算机图形学中的齐次坐标:从基础到应用

作者 Mintopia
2025年5月17日 11:34

一、齐次坐标的基本概念

在计算机图形学中,齐次坐标是一种用 N+1 维向量表示 N 维空间点的方法。在二维空间中,我们通常用 (x, y) 表示一个点,但在齐次坐标中,这个点被表示为 (x, y, w)。其中,w 是一个额外的坐标分量,当 w ≠ 0 时,对应的二维笛卡尔坐标为 (x/w, y/w)。

齐次坐标的优势在于它能够统一处理点和向量,并且能够简洁地表示平移、旋转、缩放等变换。

为什么需要齐次坐标?

在二维空间中,我们可以用矩阵乘法来表示旋转和缩放变换,但平移变换无法直接用 2×2 矩阵表示。齐次坐标通过增加一个维度,让我们能够用 3×3 矩阵统一表示所有的仿射变换(平移、旋转、缩放、剪切等)。

二、齐次坐标的表示

在齐次坐标中:

  • 二维点表示为 (x, y, 1)
  • 二维向量表示为 (x, y, 0)
  • 当 w ≠ 1 时,点 (x, y, w) 对应的笛卡尔坐标为 (x/w, y/w)

向量的 w 分量为 0,这意味着向量在平移变换下不会改变,而点会受到平移的影响。

三、齐次坐标的变换矩阵

在齐次坐标系统中,二维变换可以用 3×3 矩阵表示。下面我们用 JavaScript 实现一个简单的齐次坐标和变换矩阵库。

JavaScript 实现

下面是一个基于齐次坐标的二维变换库的实现,包含了点和向量的表示以及基本变换矩阵的创建和应用。

class Vector3 {
  constructor(x, y, w) {
    this.x = x;
    this.y = y;
    this.w = w !== undefined ? w : 1; // 默认 w 为 1,表示点
  }
  // 转换为笛卡尔坐标
  toCartesian() {
    if (this.w === 0) {
      return new Vector3(this.x, this.y, 0); // 向量保持不变
    }
    return new Vector3(this.x / this.w, this.y / this.w, 1);
  }
  // 应用变换矩阵
  applyMatrix(matrix) {
    const { x, y, w } = this;
    const m = matrix.values;
    return new Vector3(
      m[0][0] * x + m[0][1] * y + m[0][2] * w,
      m[1][0] * x + m[1][1] * y + m[1][2] * w,
      m[2][0] * x + m[2][1] * y + m[2][2] * w
    );
  }
  // 向量加法(仅用于 w=0 的向量)
  add(v) {
    if (this.w !== 0 || v.w !== 0) {
      throw new Error('Vector addition is only defined for vectors (w=0)');
    }
    return new Vector3(this.x + v.x, this.y + v.y, 0);
  }
  // 向量点积(仅用于 w=0 的向量)
  dot(v) {
    if (this.w !== 0 || v.w !== 0) {
      throw new Error('Dot product is only defined for vectors (w=0)');
    }
    return this.x * v.x + this.y * v.y;
  }
  // 向量长度(仅用于 w=0 的向量)
  length() {
    if (this.w !== 0) {
      throw new Error('Length is only defined for vectors (w=0)');
    }
    return Math.sqrt(this.dot(this));
  }
}
class Matrix3 {
  constructor() {
    // 初始化为单位矩阵
    this.values = [
      [1, 0, 0],
      [0, 1, 0],
      [0, 0, 1]
    ];
  }
  // 设置为平移矩阵
  setTranslation(tx, ty) {
    this.values = [
      [1, 0, tx],
      [0, 1, ty],
      [0, 0, 1]
    ];
    return this;
  }
  // 设置为旋转变换矩阵
  setRotation(angleInRadians) {
    const c = Math.cos(angleInRadians);
    const s = Math.sin(angleInRadians);
    this.values = [
      [c, -s, 0],
      [s, c, 0],
      [0, 0, 1]
    ];
    return this;
  }
  // 设置为缩放变换矩阵
  setScaling(sx, sy) {
    this.values = [
      [sx, 0, 0],
      [0, sy, 0],
      [0, 0, 1]
    ];
    return this;
  }
  // 矩阵乘法
  multiply(matrix) {
    const result = new Matrix3();
    const a = this.values;
    const b = matrix.values;
    const c = result.values;
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
      }
    }
    return result;
  }
  // 应用于点或向量
  transform(point) {
    return point.applyMatrix(this);
  }
}

四、基本变换的实现

1. 平移变换

平移变换将点 (x, y) 移动到 (x + tx, y + ty)。在齐次坐标中,平移矩阵为:

// 创建平移矩阵
const translationMatrix = new Matrix3();
translationMatrix.setTranslation(50, 100); // 沿 x 轴平移 50,沿 y 轴平移 100
// 创建点 (10, 20)
const point = new Vector3(10, 20);
// 应用平移变换
const translatedPoint = translationMatrix.transform(point);
console.log(translatedPoint.toCartesian()); // 输出: (60, 120, 1)

2. 旋转变换

旋转变换将点绕原点旋转一定角度。在齐次坐标中,旋转矩阵为:

// 创建旋转变换矩阵(绕原点旋转 90 度)
const rotationMatrix = new Matrix3();
rotationMatrix.setRotation(Math.PI / 2); // 90 度 = π/2 弧度
// 创建点 (10, 0)
const point = new Vector3(10, 0);
// 应用旋转变换
const rotatedPoint = rotationMatrix.transform(point);
console.log(rotatedPoint.toCartesian()); // 输出: (0, 10, 1)

3. 缩放变换

缩放变换改变点的大小。在齐次坐标中,缩放矩阵为:

// 创建缩放变换矩阵(x 方向缩放 2 倍,y 方向缩放 0.5 倍)
const scalingMatrix = new Matrix3();
scalingMatrix.setScaling(2, 0.5);
// 创建点 (10, 20)
const point = new Vector3(10, 20);
// 应用缩放变换
const scaledPoint = scalingMatrix.transform(point);
console.log(scaledPoint.toCartesian()); // 输出: (20, 10, 1)

五、变换的组合

在计算机图形学中,我们经常需要组合多个变换。例如,先旋转,再平移,最后缩放。变换的组合顺序非常重要,因为矩阵乘法不满足交换律。

变换顺序示例

下面的例子展示了不同变换顺序的效果:

// 创建一个点
const point = new Vector3(10, 0);
// 创建变换矩阵
const translationMatrix = new Matrix3().setTranslation(50, 0);
const rotationMatrix = new Matrix3().setRotation(Math.PI / 2);
// 顺序 1: 先旋转后平移
const transform1 = translationMatrix.multiply(rotationMatrix);
const result1 = transform1.transform(point);
console.log("先旋转后平移:", result1.toCartesian()); // 输出: (50, 10, 1)
// 顺序 2: 先平移后旋转
const transform2 = rotationMatrix.multiply(translationMatrix);
const result2 = transform2.transform(point);
console.log("先平移后旋转:", result2.toCartesian()); // 输出: (-10, 60, 1)

六、齐次坐标在透视投影中的应用

齐次坐标在透视投影中也有重要应用。透视投影是模拟人眼视觉的一种投影方式,远处的物体看起来比近处的小。

在透视投影中,我们可以通过修改齐次坐标的 w 分量来实现这种效果。例如,将点 (x, y, z) 投影到 z=0 的平面上:

// 透视投影矩阵
function createPerspectiveMatrix(d) {
  const matrix = new Matrix3();
  matrix.values = [    [1, 0, 0],
    [0, 1, 0],
    [0, 0, 1/d]
  ];
  return matrix;
}
// 创建透视投影矩阵(d 是投影平面到视点的距离)
const perspectiveMatrix = createPerspectiveMatrix(100);
// 创建三维点 (50, 0, 50) 在齐次坐标中表示为 (50, 0, 50, 1)
// 注意:我们的 Vector3 类可以处理这种情况,w 分量默认为 1
const point3D = new Vector3(50, 0, 50);
// 应用透视投影
const projectedPoint = perspectiveMatrix.transform(point3D);
console.log("投影后的点:", projectedPoint.toCartesian()); // 输出: (100, 0, 1)

七、应用实例:实现一个简单的图形变换工具

下面我们用 HTML5 Canvas 和上面实现的齐次坐标库来创建一个简单的图形变换工具,支持平移、旋转和缩放。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>齐次坐标图形变换示例</title>
  <style>
    canvas {
      border: 1px solid #ccc;
    }
    .controls {
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <canvas id="canvas" width="500" height="400"></canvas>
  <div class="controls">
    <button id="translateBtn">平移</button>
    <button id="rotateBtn">旋转</button>
    <button id="scaleBtn">缩放</button>
    <button id="resetBtn">重置</button>
  </div>
  <script>
    // 前面定义的 Vector3 和 Matrix3 类的代码放在这里
    
    // 获取 canvas 和绘图上下文
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    
    // 创建一个简单的图形(三角形)
    const originalPoints = [
      new Vector3(100, 50),
      new Vector3(200, 150),
      new Vector3(50, 150)
    ];
    
    // 当前变换矩阵
    let transformMatrix = new Matrix3();
    
    // 绘制图形
    function draw() {
      // 清空画布
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      
      // 应用变换
      const transformedPoints = originalPoints.map(point => 
        transformMatrix.transform(point).toCartesian()
      );
      
      // 绘制坐标系
      ctx.strokeStyle = '#ccc';
      ctx.beginPath();
      ctx.moveTo(0, canvas.height / 2);
      ctx.lineTo(canvas.width, canvas.height / 2);
      ctx.moveTo(canvas.width / 2, 0);
      ctx.lineTo(canvas.width / 2, canvas.height);
      ctx.stroke();
      
      // 绘制变换后的图形
      ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
      ctx.beginPath();
      ctx.moveTo(transformedPoints[0].x, transformedPoints[0].y);
      for (let i = 1; i < transformedPoints.length; i++) {
        ctx.lineTo(transformedPoints[i].x, transformedPoints[i].y);
      }
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
      
      // 绘制变换后的点
      ctx.fillStyle = 'blue';
      transformedPoints.forEach(point => {
        ctx.beginPath();
        ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
        ctx.fill();
      });
    }
    
    // 初始化绘制
    draw();
    
    // 添加按钮事件
    document.getElementById('translateBtn').addEventListener('click', () => {
      // 创建平移矩阵并与当前矩阵相乘
      const translateMatrix = new Matrix3().setTranslation(50, 30);
      transformMatrix = translateMatrix.multiply(transformMatrix);
      draw();
    });
    
    document.getElementById('rotateBtn').addEventListener('click', () => {
      // 创建旋转变换矩阵并与当前矩阵相乘
      const rotateMatrix = new Matrix3().setRotation(Math.PI / 12); // 15度
      transformMatrix = rotateMatrix.multiply(transformMatrix);
      draw();
    });
    
    document.getElementById('scaleBtn').addEventListener('click', () => {
      // 创建缩放变换矩阵并与当前矩阵相乘
      const scaleMatrix = new Matrix3().setScaling(1.2, 1.2);
      transformMatrix = scaleMatrix.multiply(transformMatrix);
      draw();
    });
    
    document.getElementById('resetBtn').addEventListener('click', () => {
      // 重置变换矩阵
      transformMatrix = new Matrix3();
      draw();
    });
  </script>
</body>
</html>

八、总结

齐次坐标是计算机图形学中一个非常重要的概念,它通过增加一个维度,统一了点和向量的表示,并且能够用矩阵乘法简洁地表示各种变换。掌握齐次坐标和变换矩阵是理解和实现更复杂图形算法的基础。

通过本文的介绍和示例代码,你应该对齐次坐标有了基本的理解,并且能够实现简单的图形变换。在实际应用中,齐次坐标还广泛应用于 3D 图形、计算机视觉和机器人学等领域。

81.爬楼梯

2025年5月17日 11:33

题目链接

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

解法1 暴力递归和记忆化递归

思路

这本质上就是一个斐波那契数列,所以当 n 走到只有 1 个台阶的时候,只有一种解法。

然后递归去处理。到达 n 这个台阶,有两种方法,要么是走 1 步,要么是走 2 步,所以返回结果是 (n - 1) + (n - 2)

但是这个方法存在大量的计算,所以无法通过题解。如果这个解法要想通过题解,必须要保存已经计算过的步数,下面给出两种方法。

代码

朴素计算

function climbStairs(n: number): number {
    if (n <= 1) return 1;
    return climbStairs(n - 1) + climbStairs(n - 2);
};

memo缓存

function climbStairs(n: number): number {
    const memo = new Array(n + 1).fill(-1); // -1 代表未计算
    memo[0] = 1;
    memo[1] = 1;

    function dfs(i: number): number {
        if (memo[i] !== -1) return memo[i];
        memo[i] = dfs(i - 1) + dfs(i - 2);
        return memo[i];
    }

    return dfs(n);
}

时空复杂度

时间复杂度:朴素 O(2^n),记忆化O(n)

空间复杂度:朴素 O(n),记忆化O(n)

解法2 动态规划

思路

其实上面已经将问题分解成了子问题,到达这个台阶只有两种方法,要么 n - 1,要么 n - 2

所以其实只需要两个变量就可计算完毕当前台阶,即 prevcur 。当前台阶就是 prev + cur ,然后再更新 prevcur

代码

function climbStairs(n: number): number {
    if (n <= 1) return 1;
    let prev = 1;
    let cur = 1;
    for (let i = 2; i <= n; i++) {
        const temp = prev + cur;
        prev = cur;
        cur = temp;
    }

    return cur;
}

时空复杂度

时间复杂度:O(n)

空间复杂度:O(1)

three.js 字体使用全解析

作者 Mintopia
2025年5月17日 11:28

在 3D 可视化项目中,文字是传递信息的重要元素。Three.js 作为强大的 3D 库,提供了多种添加和处理文字的方式。本文将深入探讨 Three.js 中字体的使用方法,从基础文字渲染到高级文字动画,帮助你在 3D 场景中完美呈现文字内容。

一、Three.js 中字体的基本概念

在 Three.js 中使用字体,主要有两种方式:

  1. 基于 Canvas 的 2D 文字渲染:使用 Canvas 绘制文字,然后将其作为纹理应用到 3D 平面上。
  1. 3D 文字几何体:使用 Three.js 提供的 TextGeometry 创建具有厚度和深度的 3D 文字模型。

这两种方式各有优缺点,适用于不同的场景。下面我们将详细介绍这两种方式的实现方法。

二、使用 Canvas 生成文字纹理

基础实现方法

通过 Canvas 生成文字纹理是一种简单且灵活的方法,特别适合需要动态更新文字内容的场景。

// 创建Canvas文字纹理函数
function createTextTexture(text, parameters = {}) {
  parameters = Object.assign({
    fontface: "Arial",
    fontsize: 72,
    backgroundColor: { r: 0, g: 0, b: 0, a: 0 },
    textColor: { r: 255, g: 255, b: 255, a: 255 }
  }, parameters);
  // 创建Canvas元素
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');
  
  // 设置Canvas尺寸
  context.font = `${parameters.fontsize}px ${parameters.fontface}`;
  const textWidth = context.measureText(text).width;
  
  // 设置Canvas尺寸,考虑文字宽度和高度
  canvas.width = textWidth + parameters.fontsize;
  canvas.height = parameters.fontsize * 2;
  
  // 重置字体设置,因为Canvas尺寸改变后可能会重置
  context.font = `${parameters.fontsize}px ${parameters.fontface}`;
  context.textBaseline = 'middle';
  context.textAlign = 'center';
  
  // 绘制背景
  context.fillStyle = `rgba(${parameters.backgroundColor.r}, ${parameters.backgroundColor.g}, ${parameters.backgroundColor.b}, ${parameters.backgroundColor.a})`;
  context.fillRect(0, 0, canvas.width, canvas.height);
  
  // 绘制文字
  context.fillStyle = `rgba(${parameters.textColor.r}, ${parameters.textColor.g}, ${parameters.textColor.b}, ${parameters.textColor.a})`;
  context.fillText(text, canvas.width / 2, canvas.height / 2);
  
  // 创建纹理
  const texture = new THREE.CanvasTexture(canvas);
  
  return texture;
}
// 在Three.js场景中使用Canvas生成的文字纹理
function addTextToScene() {
  // 创建文字纹理
  const texture = createTextTexture("Hello Three.js", {
    fontface: "Arial",
    fontsize: 48,
    textColor: { r: 255, g: 255, b: 255 },
    backgroundColor: { r: 0, g: 0, b: 0, a: 0 }
  });
  
  // 创建材质和平面几何体
  const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true });
  const geometry = new THREE.PlaneGeometry(2, 1);
  const textMesh = new THREE.Mesh(geometry, material);
  
  // 添加到场景
  scene.add(textMesh);
}

高级应用:动态更新文字内容

Canvas 纹理的一个重要优势是可以动态更新文字内容。以下是一个实现动态更新文字的示例:

// 动态更新文字内容
function updateText(text) {
  // 重新生成纹理
  const texture = createTextTexture(text);
  
  // 更新材质的纹理
  textMesh.material.map.dispose();
  textMesh.material.map = texture;
  textMesh.material.needsUpdate = true;
}
// 在动画循环中更新文字
let counter = 0;
function animate() {
  requestAnimationFrame(animate);
  
  // 每100帧更新一次文字
  if (counter % 100 === 0) {
    updateText(`Frame: ${counter}`);
  }
  
  counter++;
  renderer.render(scene, camera);
}

三、使用 TextGeometry 创建 3D 文字

准备字体文件

使用 TextGeometry 需要先加载字体文件。Three.js 使用 JSON 格式的字体文件,这些文件可以通过 Three.js 提供的字体工具生成。

以下是加载字体并创建 3D 文字的示例:

// 加载字体并创建3D文字
function loadFontAndCreateText() {
  const loader = new THREE.FontLoader();
  
  // 加载字体文件
  loader.load('fonts/helvetiker_regular.typeface.json', function(font) {
    // 创建文字几何体
    const geometry = new THREE.TextGeometry('Three.js 3D Text', {
      font: font,
      size: 0.5,
      height: 0.1,  // 文字厚度
      curveSegments: 12,
      bevelEnabled: true,
      bevelThickness: 0.03,
      bevelSize: 0.02,
      bevelOffset: 0,
      bevelSegments: 5
    });
    
    // 计算边界框以居中文字
    geometry.computeBoundingBox();
    const centerOffset = -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x);
    
    // 创建材质
    const material = new THREE.MeshPhongMaterial({ 
      color: 0x44aa88, 
      specular: 0x111111 
    });
    
    // 创建网格
    const textMesh = new THREE.Mesh(geometry, material);
    
    // 设置位置
    textMesh.position.x = centerOffset;
    textMesh.position.y = 0.2;
    textMesh.position.z = 0;
    
    // 添加到场景
    scene.add(textMesh);
  });
}

优化 3D 文字性能

对于复杂场景中的大量文字,性能可能成为问题。以下是一些优化建议:

  1. 合并几何体:如果有多个静态文字,可以使用BufferGeometryUtils.mergeBufferGeometries合并它们以减少绘制调用。
  1. 降低细节:减少curveSegments和bevelSegments的值可以显著提高性能。
  1. 使用实例化:对于重复的文字,可以使用THREE.InstancedMesh。

四、文字的高级效果与动画

文字材质与光照效果

使用不同的材质可以为文字带来不同的视觉效果:

// 使用不同材质的文字示例
function createTextWithMaterials() {
  // 加载字体
  const loader = new THREE.FontLoader();
  loader.load('fonts/helvetiker_regular.typeface.json', function(font) {
    // 创建基础文字几何体
    const geometry = new THREE.TextGeometry('Materials', {
      font: font,
      size: 0.4,
      height: 0.1
    });
    
    // 不同材质的文字
    const materials = [
      new THREE.MeshBasicMaterial({ color: 0xff0000 }),
      new THREE.MeshLambertMaterial({ color: 0x00ff00 }),
      new THREE.MeshPhongMaterial({ color: 0x0000ff, shininess: 100 }),
      new THREE.MeshStandardMaterial({ color: 0xffff00, metalness: 0.5, roughness: 0.5 })
    ];
    
    // 为每种材质创建一个文字实例
    materials.forEach((material, index) => {
      const textMesh = new THREE.Mesh(geometry.clone(), material);
      textMesh.position.set(-2 + index * 1.5, 0, 0);
      scene.add(textMesh);
    });
  });
}

文字动画效果

以下是一个文字波浪动画的实现:

// 文字波浪动画
function createWavyText() {
  const loader = new THREE.FontLoader();
  loader.load('fonts/helvetiker_regular.typeface.json', function(font) {
    const geometry = new THREE.TextGeometry('Wave Animation', {
      font: font,
      size: 0.5,
      height: 0.1
    });
    
    // 创建顶点材质
    const material = new THREE.ShaderMaterial({
      vertexShader: `
        uniform float time;
        varying vec3 vPosition;
        
        void main() {
          vPosition = position;
          // 添加波浪效果
          float offset = sin(position.x * 5.0 + time) * 0.05;
          vec3 newPosition = position + normal * offset;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
        }
      `,
      fragmentShader: `
        varying vec3 vPosition;
        
        void main() {
          // 基于位置创建颜色渐变
          vec3 color = mix(vec3(0.2, 0.2, 1.0), vec3(1.0, 0.2, 0.2), vPosition.y + 0.5);
          gl_FragColor = vec4(color, 1.0);
        }
      `,
      uniforms: {
        time: { value: 0.0 }
      },
      side: THREE.DoubleSide,
      transparent: false
    });
    
    const textMesh = new THREE.Mesh(geometry, material);
    scene.add(textMesh);
    
    // 动画循环
    function animate() {
      requestAnimationFrame(animate);
      material.uniforms.time.value += 0.05;
      renderer.render(scene, camera);
    }
    
    animate();
  });
}

五、性能优化与最佳实践

字体文件管理

  • 对于小型项目,可以直接使用 Three.js 提供的内置字体。
  • 对于大型项目,考虑创建自定义字体以减少文件大小。
  • 使用字体压缩工具减小字体文件体积。

动态文字的性能考量

  • 频繁更新 Canvas 纹理会影响性能,尽量减少更新频率。
  • 对于实时数据显示,考虑使用数字精灵而非完整文字。

渲染性能优化

  • 对于远距离可见的文字,考虑使用低精度几何体。
  • 使用THREE.LOD(Level of Detail)根据距离动态切换文字精度。

六、实际应用案例

游戏中的 3D 文字

在游戏中,3D 文字可用于显示玩家名称、分数或游戏状态:

// 游戏中的玩家名称显示
function createPlayerNameTag(player) {
  const loader = new THREE.FontLoader();
  loader.load('fonts/helvetiker_regular.typeface.json', function(font) {
    const geometry = new THREE.TextGeometry(player.name, {
      font: font,
      size: 0.2,
      height: 0.02
    });
    
    const material = new THREE.MeshPhongMaterial({ color: player.color });
    const nameTag = new THREE.Mesh(geometry, material);
    
    // 设置文字位置,使其始终面向相机
    nameTag.position.set(0, 2, 0);
    player.add(nameTag);
    
    // 确保文字始终面向相机
    scene.add(new THREE.PointLight(0xffffff, 1, 100));
    scene.add(new THREE.PointLight(0xffffff, 0.5, 100));
  });
}

数据可视化中的文字标签

在数据可视化中,文字标签用于标识数据点:

// 数据可视化中的标签
function addDataLabels(data) {
  data.forEach((item, index) => {
    const texture = createTextTexture(item.label, {
      fontsize: 36,
      textColor: { r: 255, g: 255, b: 255 }
    });
    
    const material = new THREE.SpriteMaterial({ map: texture });
    const sprite = new THREE.Sprite(material);
    
    // 设置位置
    sprite.position.set(item.x, item.y, item.z);
    sprite.scale.set(1, 0.5, 1);
    
    scene.add(sprite);
  });
}

七、常见问题与解决方案

文字模糊问题

  • 原因:Canvas 尺寸过小或纹理过滤设置不当。
  • 解决方案:增加 Canvas 尺寸,或设置纹理的minFilter和magFilter为THREE.NearestFilter。

3D 文字渲染不完整

  • 原因:相机的near值设置过大,或文字超出了视锥体。
  • 解决方案:减小相机的near值,或调整文字位置使其在视锥体内。

性能问题

  • 原因:过多的文字对象或复杂的文字几何体。
  • 解决方案:合并几何体、使用实例化、降低细节级别或使用 Canvas 纹理替代 3D 文字。

八、总结

Three.js 提供了丰富的字体使用方式,从简单的 2D 文字纹理到复杂的 3D 文字几何体,能够满足各种场景的需求。在实际应用中,应根据具体需求选择合适的方法,并注意性能优化。通过合理使用材质、动画和交互效果,可以为 3D 场景增添丰富的信息和生动的视觉体验。

希望本文能帮助你掌握 Three.js 中字体的使用技巧,创造出更加精彩的 3D 项目!

父子都有点击事件,阻止冒泡event.stopPropagation()

2025年5月17日 11:12

为什么需要阻止冒泡?

假设结构是这样包裹的,两层都有点击事件,

    <button @click="handleContinue">继续学习</button> 
   </div>

在点击内层的按钮时,会导致向父元素冒泡外层元素的点击事件同时也被触发,从而可能导致多个 $router.push() 被调用,导致两次路由跳转 → 而第一次被取消,控制台就会报错。

阻止冒泡的正确使用:

1、结构中事件中携带event:

      @click="handleContinue($event)" > 继续学习 </el-button>

2、 阻止冒泡event.stopPropagation()

methods: {
  handleContinue(event) {
    event.stopPropagation(); // 阻止冒泡
    console.log('跳转继续学习 /my-courses');
    this.$router.push('/my-courses');
  },
  bannerClick() {
    this.$router.push('/my-content');
  }
}

这样就能避免因事件冒泡导致的重复导航和控制台警告了。

event.stopPropagation()方法用于阻止事件继续传播‌,适用于需要精确控制事件影响范围的场景,如防止点击一个元素时触发其父元素的特定行为。 event.preventDefault()方法用于阻止事件的默认行为‌,适用于需要自定义处理某些操作的场景,如表单提交、链接导航等。

用 CSS Houdini 打造像素风组件的边框魔法 —— 解构 pixel-ui 的 PixelBox

作者 猫闷台817
2025年5月17日 10:42

用 CSS Houdini 打造像素风组件的边框魔法 —— 解构 pixel-uiPixelBox

本文是继上一篇介绍像素风组件库 pixel-ui 的延续,这次我们深入聊聊其中的魔法工具 —— PixelBox 背后的 CSS Houdini 技术,看看是如何利用 paintWorklet 实现灵活可配置的像素风边框、圆角与阴影效果的。欢迎点赞、收藏、评论支持,给组件库来个 Star!⭐️


🔙 回顾一下:什么是 pixel-ui

QQ_1747449535102.png

@mmt817/pixel-ui 是一个采用像素风格美学打造的 Vue 3 组件库,核心理念是“复古 ✖️ 现代”,结合 NES 时代的像素图形与现代前端技术,包括:

  • 使用 Vue 3 + TypeScript 构建
  • 采用 UnoCSS 作为原子化 CSS 引擎
  • 利用 CSS Houdini 构建自定义图形渲染
  • 集成 Playground / Storybook / VitePress 演示和文档
  • 支持像素风 按钮 / 卡片 / 折叠面板 / 动画精灵 等组件

其中 PxCardPxButtonGroupPxCollapse 等组件都依赖一个核心能力:像素风的边框绘制 —— 这就是我们今天的主角 PixelBox 背后的技术实现。


🎨 什么是 CSS Houdini?

CSS Houdini 是浏览器暴露的一套底层 API,允许开发者 扩展 CSS 渲染机制。它的核心模块之一是:

paintWorklet:通过自定义 JavaScript 绘图函数,实现 CSS 背景绘制,支持响应 CSS 属性变化。

简单理解就是 JS in CSS, 在 CSS 代码中执行类似 canvas 绘制逻辑的 js 脚本

相比传统的 CSS 背景图片或伪元素绘制,Houdini 提供了:

  • 动态可控:可以通过 CSS 变量实时传值
  • 高性能:在浏览器的渲染管线中执行,无需 DOM 操作
  • 可组合性强:可以组合多个 Paint Worklet、继承边框等机制

📦 PixelBox 背后的 pixelbox.worklet.ts 做了什么?

这段 Worklet 脚本是整个 PxCard / PxButton 等组件视觉表现的核心,它负责绘制出那种 NES 风格的边框与阴影。

👇 下面我们分块分析这段核心逻辑:

1️⃣ 注册 Paint Worklet 和输入属性

registerPaint('pixel-box', class {
  static get inputProperties(): string[] {
    return [
      '--px-border',
      '--px-border-radius',
      '--px-bg-color',
      '--px-bg-shadow-border',
      // 更多……
    ];
  }
})

这部分定义了这个 Worklet 会监听哪些 CSS 属性。当这些属性发生变化时,paint(ctx, geom, props) 会被调用重新绘制。

属性设计非常灵活,比如:

  • --px-border: 控制边框像素宽度
  • --px-border-radius: 圆角半径
  • --px-bg-shadow-border: 像素阴影宽度
  • --px-bg-shadow-position: 像素阴影显示位置

2️⃣ 核心绘制逻辑 paint(ctx, size, props)

  paint(
    ctx: PaintRenderingContext2D,
    size: { width: number; height: number },
    props: StylePropertyMap
  ): void {
      const { width, height } = size
      const pbBorder = getInt(props, '--px-border') * 2
      let pbBorderRadius = getInt(props, '--px-border-radius')
      // ...其他属性的取值/预处理

      ctx.fillStyle = pbBackgroundColor
      const startY = pbBorder / 2
      const contentHeight = size.height - pbBorder

      // button 整体背景区域
      let startX
      let contentWidth
      if (buttonGroupFlag || buttonGroupLast) {
        startX = 0
        contentWidth = size.width - pbBorder / 2
      } else {
        startX = pbBorder / 2
        contentWidth = size.width - pbBorder
      }
      ctx.fillRect(startX, startY, contentWidth, contentHeight)


      // TODO: 侧边阴影/圆角侧边阴影/圆角边框/多余色块清理/边框绘制
}

在这个函数中,我们可以通过 JavaScript 绘图 API(Canvas)对每一个像素层进行精细控制,从而实现像素感极强的边框结构,还可以根据传入的属性决定阴影位置、按钮组合是否共享边框等。


🧪 实际应用效果

如下图中所示,我们的 PxCardPxButtonGroup 等组件都调用了:

background: paint(pixel-box);

通过设定对应 CSS 变量即可完全定制组件风格:

  --px-border: 3px;
  --px-border-t: 3px;
  --px-border-r: 3px;
  --px-border-b: 3px;
  --px-border-l: 3px;
  --px-border-radius: 0px;
  --px-border-color: var(--px-color-base);
  --px-bg-color: transparent;
  --px-bg-shadow-border: 3px;
  --px-bg-shadow-color: var(--px-button-bg-shadow-color);
  --px-bg-shadow-position: bottom-right;

QQ_1747448856024.png

这样每个组件的样式变成了动态渲染的画布,而不是死板的类名或背景图!


✨ 为什么选用 Paint Worklet 而不是别的方法?

方法 灵活性 动态能力 性能 适合像素风?
CSS 背景图 ❌ 固定图 一般
Canvas 元素 ❌(脱离 DOM)
Paint Worklet ✅✅ ✅✅(响应 CSS 变量) ✅✅✅

CSS Houdini 的 paintWorklet 是实现像素边框最优雅 + 高效的方式之一!


📦 如何接入 pixelbox.worklet.ts

首先, 出于测试需要, 我在 xxx.worklet.ts文件中统一暴露注册函数并立即执行

export function registerPixelBox() {
  if (typeof registerPaint !== 'undefined') {
    registerPaint('pixelbox', PixelBox)
  }
}

registerPixelBox()

然后在对应 vue 组件处将 Worklet 注册逻辑封装为:

// CSS Houdini Paint Worklet
const paint = () => {
  try {
    if ('paintWorklet' in CSS) {
      ;(CSS as any).paintWorklet.addModule(workletURL)
    } else {
      debugWarn(
        COMP_NAME,
        'CSS Houdini Paint Worklet API is not supported in this browser.'
      )
    }
    // (CSS as any).paintWorklet.addModule(workletURL)
  } catch (error) {
    console.error('Error loading Paint Worklet:', error)
  }
}

onMounted(async () => {
  paint()
})

并在组件库打包时,将这段 Worklet 单独构建成浏览器能识别的 JS 文件。

这里注意, pixel-ui使用的是 *.worklet.ts 但是CSS.paintWorklet.addModule('pixelbox.worklet.js') 会在 独立作用域 中执行 worklet 文件,相当于 Web Worker 环境, 需要预先编译 ts 文件才能正常使用

开发者只需引入组件,即可自动加载绘制逻辑,无需额外配置。


在这期间, pixel-ui 调研过程中发现常规像素圆角的处理方式大致分为三种

770f967d3740384f0e227ef2c9c2f59e.png

最初仅支持圆角 radius 以斜向像素点形式呈现, 结果在 --px-border-radius 值偏大时会出现表现力较差的问题, 于是限定圆角形式如图三种, 并更新绘制逻辑, 例如 PxButton 由此引入新属性 chubby, 一个更圆的圆角??

QQ_1747449313375.png

不过当前绘制逻辑仍有缺陷, chubby 对各项属性要求, 适配性不高, 请合理使用

🔚 小结:未来计划 & 欢迎支持!

通过本文我们深入了解了 pixel-uiPixelBox 背后的原理 —— CSS Houdini 的 paintWorklet,它赋予了组件边框渲染的无限可能。未来我们还会支持:

  • ✅ 像素边框动画
  • ✅ 多层投影
  • ✅ 像素风边角图标装饰
  • ✅ 更多定制化像素风格背景(科幻/自然...)

🧱 项目地址: github.com/maomentai81…
🙌 欢迎大家来个 Star ⭐️,提提 Issue 🐛,一起来打造复古又现代的 UI 库!


📢 关注我,持续分享 Vue 3 + CSS Houdini 等前端高级玩法!

如果你对:

  • 像素风组件库
  • CSS Houdini / Canvas / Web Animation API
  • 自定义渲染系统 / 动画组件开发

感兴趣,欢迎关注我,评论交流,让我们把前端玩出花!🌸

【CSS问题】margin塌陷

作者 咔咔库奇
2025年5月17日 10:34

目录

一、什么是css margin塌陷

二、margin塌陷的原因

三、塌陷的深入机制

四、解决margin塌陷的方法

1、避免同级元素margin重叠:

2、解决父子元素粘连:

五、注意事项


一、什么是css margin塌陷

        CSS中的margin塌陷(也称为margin collapsing)是一个常见的布局问题,主要发生在垂直方向上。当两个或多个元素的垂直margin相遇时,它们不会按照预期叠加,而是会发生重叠,导致最终的外边距值比单独设置时小。

二、margin塌陷的原因

  1. 同级元素:两个同级的元素,垂直排列,上面的盒子给margin-bottom,下面的盒子给margin-top,那么他们两个间距就会重叠,以大的那个盒子的外边距计算。
  2. 父子元素:子元素给一个margin-top,其父级元素也会受到影响,同时产生上边距,父子元素会进行粘连。
  3. 示例:设想页面上有一个蓝色的矩形(.parent),它内部有一个粉色的矩形(.child)。由于margin塌陷,.child元素的margin-top实际上导致了整个.parent元素向下移动,使得.parent的顶部与页面顶部之间的间距增加了,而不是.child元素内部增加了间距。     
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Margin Collapse Example</title>
    <style>
        .parent {
            width: 200px;
            height: 200px;
            background-color: blue;
            margin-top: 50px;
        }
        .child {
            width: 100px;
            height: 100px;
            background-color: pink;
            margin-top: 30px; /* 这个边距将与父级边距一起消失 */
        }
    </style>
</head>
<body>
    <div class="parent">
        <div class="child"></div>
    </div>
</body>
</html>

在上述示例中,.child元素的margin-top与.parent元素的margin-top发生了塌陷,导致整个.parent元素相对于页面顶部移动了50px,而不是80px。

三、塌陷的深入机制

  • 合并规则‌:当两个垂直方向上的外边距相遇时,它们会按照特定的规则合并。对于同级元素,取两者中较大的值作为合并后的外边距;对于父子元素,子元素的margin-top会与父元素的margin-top合并,导致整个父元素向下移动。
  • BFC(块级格式化上下文) ‌:BFC是一个独立的渲染区域,只有属于同一个BFC的元素才会发生外边距合并。触发BFC的方法包括设置overflow属性为hiddenautoscroll,将display属性设置为table-cellinline-blockflex等,或者将float属性设置为leftright等。
  • 包含块‌:每个元素都有一个包含块,它决定了元素的定位和大小。在margin塌陷的情况下,子元素的margin-top实际上是与父元素的包含块顶部对齐,而不是与父元素的内部内容对齐。

四、解决margin塌陷的方法

1、避免同级元素margin重叠

可以使两个外边距不同时出现,即要么只设置上面的margin-bottom,要么只设置下面的margin-top

示例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Avoid Margin Overlap Example</title>
<style>
  .sibling1 {
    background-color: lightcoral;
    margin-bottom: 20px; /* 只设置下面的margin */
  }
  .sibling2 {
    background-color: lightseagreen;
    /* 不设置margin-top,避免与上面的margin重叠 */
  }
</style>
</head>
<body>
  <div class="sibling1">Sibling 1</div>
  <div class="sibling2">Sibling 2</div>
</body>
</html>

2、解决父子元素粘连

为父盒子设置border:为外层添加border后父子盒子就不是真正意义上的贴合,可以设置透明边框(border:1px solid transparent;)。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solve Parent-Child Adhesion Example</title>
<style>
  .parent {
    background-color: lightblue;
    border: 1px solid transparent; /* 设置透明边框以避免粘连 */
  }
  .child {
    background-color: coral;
    margin-top: 30px;
  }
</style>
</head>
<body>
  <div class="parent">
    <div class="child">Child Element</div>
  </div>
</body>
</html>

为父盒子添加overflow:hidden:这样可以触发父盒子的块级格式化上下文(BFC),从而避免margin塌陷。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Margin Collapse Example</title>
<style>
  .parent {
    background-color: lightblue;
    display: flex; /* 改变display属性以触发BFC */
  }
  .child {
    background-color: coral;
    margin-top: 30px;
  }
</style>
</head>
<body>
  <div class="parent">
    <div class="child">Child Element</div>
  </div>
</body>
</html>

为父盒子设置padding值:通过给父元素添加内边距,可以使得子元素的margin不再与父元素的顶部粘连。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Margin Collapse Example</title>
<style>
  .parent {
    background-color: lightblue;
    padding-top: 20px; /* 添加内边距以避免margin塌陷 */
  }
  .child {
    background-color: coral;
    margin-top: 30px;
  }
</style>
</head>
<body>
  <div class="parent">
    <div class="child">Child Element</div>
  </div>
</body>
</html>

改变父盒子的display属性:如设置为display:table;display:flex;等,都可以触发BFC,从而解决margin塌陷问题。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Margin Collapse Example</title>
<style>
  .parent {
    background-color: lightblue;
    display: flex; /* 改变display属性以触发BFC */
  }
  .child {
    background-color: coral;
    margin-top: 30px;
  }
</style>
</head>
<body>
  <div class="parent">
    <div class="child">Child Element</div>
  </div>
</body>
</html>

利用伪元素:给父元素的前面添加一个空元素,并设置该伪元素的样式以避免margin塌陷。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Margin Collapse Example</title>
<style>
  .parent::before {
    content: "";
    display: block;
    height: 0; /* 创建一个不可见的块级元素 */
  }
  .parent {
    background-color: lightblue;
  }
  .child {
    background-color: coral;
    margin-top: 30px;
  }
</style>
</head>
<body>
  <div class="parent">
    <div class="child">Child Element</div>
  </div>
</body>
</html>

改变子元素的定位:如设置为position:absolute;,这样子元素就不再相对于父元素定位,而是相对于最近的已定位祖先元素定位(如果没有,则相对于初始包含块定位)。

示例:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Margin Collapse Example</title>
<style>
  .parent {
    background-color: lightblue;
  }
  .child {
    background-color: coral;
    margin-top: 30px;
    position: absolute; /* 改变定位方式 */
    top: 20px; /* 调整位置 */
  }
</style>
</head>
<body>
  <div class="parent">
    <div class="child">Child Element</div>
  </div>
</body>
</html>

3、触发BFC的其他方法

  • 将元素的display属性设置为inline-blocktable-celltable-captionflow-rootflexinline-flexgridinline-grid等。
  • 将元素的float属性设置为leftright等(非none)。
  • 将元素的position属性设置为absolutefixed

五、注意事项

1、在解决margin塌陷问题时,需要根据具体的布局需求和元素关系来选择合适的方法。

2、触发BFC是解决margin塌陷的一种有效手段,但需要注意BFC对布局的其他可能影响。

码字不易,觉得有用的话,各位大佬就点点赞呗

CSS基础知识05(弹性盒子、布局详解,动画,3D转换,calc)

作者 咔咔库奇
2025年5月17日 10:32

0、弹性盒子、布局

弹性盒子(Flex Box)是CSS3引入的一种新的布局模式,旨在提供一种更有效的方式来布局、对齐和分配在容器中项目的空间,即使它们的大小未知或是动态变化的。以下是对弹性盒子的超级详解:

0.1.弹性盒子的基本概念

弹性容器(Flex Container) :通过设置display属性的值为flexinline-flex,将一个元素定义为弹性容器。弹性容器内包含了一个或多个弹性子元素。

弹性子元素(Flex Items) :弹性容器的子元素。弹性子元素在弹性容器内按照弹性盒子的规则进行布局。

0.2.弹性盒子的主轴和交叉轴

主轴(Main Axis) :弹性元素排列的主要方向,可以是水平方向或垂直方向。该轴的开始和结束被称为main start和main end。

交叉轴(Cross Axis) :垂直于主轴的方向。该轴的开始和结束被称为cross start和cross end。

0.3.弹性盒子的属性

没有添加弹性盒子的属性

编辑

<html>
<head>
<style>
.box {
width: 1000px;
height: 1000px;

}

.b1 {
width: 100px;
height: 100px;
background-color: red;
}

.b2 {
width: 100px;
height: 100px;
background-color: blue;
}

.b3 {
width: 100px;
height: 100px;
background-color: green;
}

.b4 {
width: 100px;
height: 100px;
background-color: yellow;
}
</style>
</head>
<body>
<body>
<div class="box">
<div class="b1">1</div>
<div class="b2">2</div>
<div class="b3">3</div>
<div class="b4">4</div>
</div>
</body>

</body>
</html>

flex-direction

设置弹性子元素的排列方向。

row

row:从左到右水平排列(默认值)。

编辑

.box {
width: 1000px;
    height: 1000px;
display: flex;
    flex-direction: row;
}

row-reverse

row-reverse:从右到左水平排列。

编辑

.box {
width: 1000px;
    height: 1000px;
display: flex;
    flex-direction: row-reverse;
}

column

column:从上到下垂直排列。

编辑

        .box{
            width: 1000px;
            height: 1000px;
            display: flex;
            flex-direction: column;
        }

column-reverse

column-reverse:从下到上垂直排列。

编辑

        .box{
            width: 1000px;
            height: 1000px;
            display: flex;
            flex-direction: column-reverse;
        }

flex-wrap

设置弹性子元素是否换行。

nowrap

nowrap:不换行(默认值)。

如果所有子元素的宽/高总值大于父元素的宽/高,那么为了子元素不溢出,会把内容挤压变形到自适应的宽高。

编辑

.box {
    width: 300px;
    height: 200px;
    display: flex;
    flex-wrap: nowrap;
}

wrap

wrap:换行。

编辑

.box {
    width: 300px;
    height: 200px;
    display: flex;
    flex-wrap: nowrap;
}

wrap-reverse

wrap-reverse:反向换行。

编辑

flex-dirction和flex-wrap的组合简写模式
.box{
    width: 300px;
    height: 200px;
    display: flex;
    flex-flow: row wrap;
}

编辑

justify-content

定义弹性子元素在主轴上的对齐方式。

flex-start

flex-start:靠主轴起点对齐。

编辑

.box{
    width: 500px;
    height: 200px;
    border: 1px solid black;

    display: flex;
    justify-content: flex-start;
    
}

flex-end

flex-end:靠主轴终点对齐。

编辑

.box{
    width: 500px;
    height: 200px;
    display: flex;
    justify-content: flex-end;
    border: 1px solid black;
}

center

center:居中对齐。

编辑

.box{
width: 500px;
height: 200px;
display: flex;
justify-content: center;
border: 1px solid black;
}

space-between

space-between:两端对齐,元素之间的间隔相等。

编辑

.box{
width: 500px;
height: 200px;
display: flex;
justify-content: space-between;
border: 1px solid black;
}

space-around

space-around:元素两侧的间距相同,元素之间的间距比两侧的间距大一倍。

编辑

.box{
width: 500px;
height: 200px;
display: flex;
justify-content: space-around;
border: 1px solid black;
}

space-evenly

space-evenly:元素间距离平均分配。

编辑

.box{
width: 500px;
height: 200px;
display: flex;
justify-content: space-evenly;
border: 1px solid black;
}

align-items(单行)

定义弹性子元素在交叉轴上的对齐方式(适用于单行)。

flex-start

flex-start:交叉轴起点对齐。

编辑

.box{
width: 500px;
height: 200px;
display: flex;
align-items: flex-start;
border: 1px solid black;
}

flex-end

flex-end:交叉轴终点对齐。

编辑

.box{
width: 500px;
height: 200px;
display: flex;
align-items: flex-end;
border: 1px solid black;
}

center

center:交叉轴中点对齐。

编辑

.box{
width: 500px;
height: 200px;
display: flex;
align-items: center;
border: 1px solid black;
}

baseline

baseline:元素的第一行文字的基线对齐。

改变了每个盒子字体的大小这样看基线比较直观

编辑

.box{
width: 500px;
height: 200px;
display: flex;
align-items: baseline;
border: 1px solid black;
}

stretch

stretch:默认值,如果元素未设置高度或者为auto,将占满整个容器的高度。

红盒子不设置高度,容器的填充方向是按照侧轴方向填充的

编辑

.box{
width: 500px;
height: 200px;
display: flex;
align-items: stretch;
border: 1px solid black;
}

align-content(多行)

定义多行弹性子元素在交叉轴上的对齐方式(适用于多行)。

flex-start

flex-start:交叉轴的起点对齐。

编辑

.box{
width: 500px;
height: 600px;
display: flex;
align-content: flex-start;
flex-wrap: wrap;
border: 1px solid black;
}

flex-end

flex-end:交叉轴的终点对齐。

编辑

.box{
width: 500px;
height: 300px;
display: flex;
align-content: flex-end;
flex-wrap: wrap;
border: 1px solid black;
}

center

center:交叉轴的中点对齐。

编辑

.box{
width: 500px;
height: 300px;
display: flex;
align-content: center;
flex-wrap: wrap;
border: 1px solid black;
}

space-between

space-between:交叉轴两端对齐之间间隔平分。

编辑

.box{
width: 500px;
height: 300px;
display: flex;
align-content: space-between;
flex-wrap: wrap;
border: 1px solid black;
}

space-around

space-around:元素两侧的间距相同,元素之间的间距比两侧的间距大一倍。

编辑

.box{
width: 500px;
height: 300px;
display: flex;
align-content: space-around;
flex-wrap: wrap;
border: 1px solid black;
}

stretch

stretch:默认值,轴线占满整个交叉轴。

编辑

.box{
width: 500px;
height: 300px;
display: flex;
align-content: stretch;
flex-wrap: wrap;
border: 1px solid black;
}

space-evenly

space-evenly:在交叉轴上平均分配空间。

编辑

.box{
width: 500px;
height: 300px;
display: flex;
align-content: space-evenly;
flex-wrap: wrap;
border: 1px solid black;
}

align-self

允许单个弹性子元素有与其他子元素不同的对齐方式,可覆盖align-items属性。

auto

auto:默认值,表示继承父容器的align-items属性。如果没有父元素,则等同于stretch

其他值(如flex-startflex-endcenterbaselinestretch)与align-items相同。

编辑

.box{
width: 500px;
height: 300px;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
border: 1px solid black;
}
/*6号盒子靠底部*/
.b6 {
    align-self: flex-end;
    width: 100px;
    height: 100px;
    background-color: aquamarine;
}
/*7号盒子单行居中*/
.b7 {
    align-self: center;
    width: 100px;
    height: 100px;
    background-color: #9317ff;
}

order

order:设置弹性子元素的排列顺序(数值越小,排列越靠前;默认为0)。

编辑

.box{
width: 500px;
height: 300px;
display: flex;
align-content: stretch;
flex-wrap: wrap;
border: 1px solid black;
}

.b1 {
width: 100px;
height: 100px;
background-color: red;
order: 3;
}

.b2 {
width: 100px;
height: 100px;
background-color: blue;
order: 1;
}

.b3 {
width: 100px;
height: 100px;
background-color: green;
order: 0;
}
.b4 {
width: 100px;
height: 100px;
background-color: yellow;
order: 2;
}

flex-grow

flex-grow:定义弹性子元素的伸展系数(默认值为0,即如果存在剩余空间,也不伸展)。

编辑

.box{
width: 500px;
height: 300px;
display: flex;
align-content: stretch;
flex-wrap: wrap;
border: 1px solid black;
}

.b1 {
width: 100px;
height: 100px;
background-color: red;
flex-grow: 3;
}

.b2 {
width: 100px;
height: 100px;
background-color: blue;
flex-grow: 1;
}

.b3 {
width: 100px;
height: 100px;
background-color: green;
}
.b4 {
width: 100px;
height: 100px;
background-color: yellow;
}

flex-shrink

flex-shrink:定义了弹性子元素的收缩系数(默认值为1,即如果空间不足,该项目将等比例缩小)。

flex-basis

flex-basis:定义弹性子元素在主轴上的基础长度(默认值auto,即项目的本来大小)。

该属性可以单独使用,也可以与flex-growflex-shrink一起使用,简写为flex属性(如flex: 1 1 auto)。

0.4.弹性盒子的使用场景

响应式布局:弹性盒子可以根据容器的大小和内容的变化自动调整布局,使得页面在不同的屏幕尺寸和设备上都能够适应。

复杂的布局:弹性盒子提供了多种对齐和分布元素的方式,可以方便地实现复杂的布局。

简化代码:与浮动布局相比,弹性布局减少了代码量和复杂度,提高了可读性和可维护性。

一、动画

1.1.动画序列与关键帧

1.@keyframes:

定义动画的关键帧,通过指定0%到100%之间的百分比来定义动画的各个阶段

在每个关键帧中,可以设置元素的样式属性,如位置、大小、颜色等。

/* 创建关键帧动画 动画名字叫one */
@keyframes one {
  /* 从什么到什么 */
  /* 0% */
  from {}
  /* 100% */
  to {}
}

1.2.动画属性详解

1.animation-name:

指定要应用于元素的动画名称,该名称必须与@keyframes中定义的动画名称相匹配

2.animation-duration

定义动画的持续时间,即动画从开始到结束所需要的时间。

单位可以是秒或毫秒(ms)

3.animation-timing-function:

控制动画的缓冲行为,即动画的速度曲线

属性值:ease(逐渐慢下来)、linear(匀速)、ease-in(加速)、ease-out(减速)等,还可以使用cubic-bezier函数指定以速度曲线

4.animation-delay:

指定动画开始前的延迟时间

单位:m或者ms

5.animation-iteration-count:
指定动画应该播放的次数

属性值:数字表示播放次数,infinite表示无限次播放

6.animation-direction

指定动画的播放方向。

属性值:normal(正常)、reverse(反向)、alternate(奇数次正向播放,偶数次反向播放)等。

7.animation-fill-mode

定义动画在开始或结束时元素的状态。

属性值:none(不改变元素状态)、forwards(动画结束时保持最后一帧的状态)、backwards(动画开始前保持第一帧的状态)等。

8.animation-play-state

控制动画的播放状态。

属性值:running(播放)和paused(暂停)。

1.3.动画简写

上述动画属性可简写为一个属性

animation: name duration timing-function delay iteration-count direction fill-mode;

1.4.使用动画库

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
<link rel="stylesheet" href="./iconfont.css">
<style>
  h1 {
    animation-iteration-count: infinite;
  }
</style>
<h1 class="animate__animated animate_bounce">我是动画库</h1>
<!-- animate_ _animated基础类名-->
<!-- animate_bounce动画效果-->

1.5.动画事件

使用JavaScript可以监听动画的开始(animationstart)、结束(animationend)、迭代(animationiteration)等事件。

例:

html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Animation Events</title>
    <style>
        .animated-box {
            width: 100px;
            height: 100px;
            background-color: red;
            position: relative;
            animation: moveBox 2s infinite;
        }

        @keyframes moveBox {
            0% { left: 0; }
            50% { left: 200px; }
            100% { left: 0; }
        }
    </style>
</head>
<body>
    <div class="animated-box" id="box"></div>

    <script src="script.js"></script>
</body>
</html>

js文件

// 获取动画元素
const box = document.getElementById('box');

// 监听动画开始事件
box.addEventListener('animationstart', function(event) {
    console.log('Animation started:', event.animationName);
    // 可以在这里添加额外的逻辑,比如改变背景颜色
    box.style.backgroundColor = 'blue';
});

// 监听动画结束事件
box.addEventListener('animationend', function(event) {
    console.log('Animation ended:', event.animationName);
    // 可以在这里添加额外的逻辑,比如重置背景颜色
    box.style.backgroundColor = 'red';
});

// 监听动画迭代事件
box.addEventListener('animationiteration', function(event) {
    console.log('Animation iterated:', event.animationName, 'Iteration count:', event.iterationCount);
    // 可以在这里添加额外的逻辑,比如改变透明度
    box.style.opacity = '0.5';
    // 可以设置一个延时来恢复透明度,避免一直半透明
    setTimeout(() => {
        box.style.opacity = '1';
    }, 100); // 延时100毫秒
});

二、CSS3 3D转换

CSS3的3D转换属性允许开发者对网页元素进行3D空间中的移动、旋转、缩放等操作。

2.1.  3D变换函数

translate3d(tx, ty, tz) :在3D空间中移动元素。

rotate3d(x, y, z, angle) :围绕3D空间中的某个轴旋转元素。

scale3d(sx, sy, sz) :在3D空间中缩放元素。

perspective(d) :为3D元素添加透视效果,使元素看起来更加立体。通常应用于父元素。

transform-style:控制子元素是否开启三维立体环境,如flat(默认,不开启3D立体空间)、preserve-3d(开启3D立体空间)。

2.2. 3D变换与动画结合

可以将3D变换与动画属性结合使用,创建复杂的3D动画效果。

例如,使用rotate3danimation属性创建一个旋转的3D立方体。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Cube Animation</title>
<style>
  body {
    margin: 0;
    perspective: 1000px; /* 为整个场景添加透视效果 */
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f0f0f0;
  }
  .scene {
    width: 200px;
    height: 200px;
    position: relative;
    transform-style: preserve-3d; /* 开启3D立体空间 */
    animation: rotateCube 5s infinite linear; /* 应用旋转动画 */
  }
  .cube {
    width: 100%;
    height: 100%;
    position: absolute;
    transform-style: preserve-3d; /* 开启3D立体空间 */
  }
  .face {
    position: absolute;
    width: 200px;
    height: 200px;
    background: rgba(255, 255, 255, 0.9);
    border: 1px solid #ccc;
    box-sizing: border-box;
    font-size: 20px;
    font-weight: bold;
    color: #333;
    text-align: center;
    line-height: 200px;
    user-select: none; /* 禁止用户选择文本 */
  }
  .front  { transform: translateZ(100px); } /* 前面 */
  .back   { transform: rotateY(180deg) translateZ(100px); } /* 后面 */
  .right  { transform: rotateY(90deg) translateZ(100px); } /* 右面 */
  .left   { transform: rotateY(-90deg) translateZ(100px); } /* 左面 */
  .top    { transform: rotateX(90deg) translateZ(100px); } /* 上面 */
  .bottom { transform: rotateX(-90deg) translateZ(100px); } /* 下面 */
  
  @keyframes rotateCube {
    from { transform: rotateX(0deg) rotateY(0deg); } /* 动画开始时的状态 */
    to   { transform: rotateX(360deg) rotateY(360deg); } /* 动画结束时的状态 */
  }
</style>
</head>
<body>
<div class="scene">
  <div class="cube">
    <div class="face front">Front</div>
    <div class="face back">Back</div>
    <div class="face right">Right</div>
    <div class="face left">Left</div>
    <div class="face top">Top</div>
    <div class="face bottom">Bottom</div>
  </div>
</div>
</body>
</html>

编辑

2.3. 3D变换的浏览器兼容性

        大多数现代浏览器都支持CSS3的3D变换,但为了确保兼容性,最好检查并测试在不同浏览器上的表现。

解决方案

1.使用css前缀

        可以针对不同浏览器添加相应的CSS前缀(例如-moz-、-ms-、-webkit-等)。

2.使用JavaScript库

        可以使用一些JavaScript库(如Three.js、Babylon.js等)来实现跨浏览器的3D变换效果。

这些库封装了底层的浏览器兼容性处理,简化了开发过程,并提供了丰富的3D图形功能。

3.检测浏览器支持

        通过JavaScript代码检测浏览器是否支持CSS3的3D变换属性。

        如果浏览器不支持3D变换,则提供替代方案或降级处理,以确保用户体验的连续性。

4.考虑使用其他技术

        如果3D变换效果在特定浏览器中无法实现,开发者可以考虑使用其他技术来实现相似的效果。

        例如,可以使用SVG、Canvas等技术来绘制和渲染3D图形。

三、calc函数在动画中的应用

calc()函数允许开发者在CSS中执行一些计算,以动态地设置元素的属性。在动画中,calc()函数可以用于计算元素的位移、大小等属性,从而创建更复杂的动画效果。

1、基本用法

calc()函数支持四则运算和混合单位,如百分比、px、em等。

在使用calc()函数时,需要注意运算符两侧的空格要求。

2、在动画中的应用

可以使用calc()函数来计算元素的位移量,以实现更平滑的动画过渡效果。

例如,可以使用calc(50% - 50px)来设置一个元素相对于其父元素宽度的一半再减去50px的位置。

注意:- + * / 四种运算的运算符两边必须有空格,不然不执行

感恩家人们点点赞

Vue.js项目中实现中英文国际化的完整指南

2025年5月17日 10:31

前言

国际化(Internationalization,简称i18n)是现代web应用不可或缺的功能,尤其对于面向全球用户的产品。本文将详细介绍如何在Vue项目中实现中英文切换功能,从基础配置到进阶实践,包括动态内容、日期格式化等方面的国际化处理。

技术栈选择

在Vue项目中实现国际化,最常用的库是vue-i18n。该库提供了完善的国际化解决方案,可以轻松处理以下国际化需求:

  • 文本翻译
  • 数字格式化
  • 日期时间本地化
  • 复数处理
  • 消息格式化

基础配置实现

第一步:安装依赖

image.png

第二步:创建语言包文件

首先,我们需要创建语言包文件,通常放在src/locales目录下:

image.png zh-cn.json示例:

image.png en.json示例:

image.png

第三步:配置i18n实例

在src/i18n/index.js中配置i18n实例:

image.png

第四步:在主应用中集成i18n

在main.js中引入i18n配置:

image.png

使用方法

模板中使用翻译

image.png

组件中使用国际化 (Composition API)

image.png

进阶实践

处理动态内容和响应性

在地图组件中,我们使用了computed属性来确保翻译内容能响应语言变化:

image.png

处理页面标题和元数据

javascript

image.png

处理日期和数字格式

image.png

处理动态加载的语言包

对于大型应用,我们可以按需加载语言包以提高性能:

image.png

实现ECharts组件的国际化

ECharts组件的国际化需要特别处理,我们可以在语言切换时更新图表数据:

image.png

image.png

处理后端API数据的国际化

有时,我们需要处理来自后端的多语言数据。有两种常见的方式:

1. 客户端处理翻译 (推荐)

image.png

2. 服务器返回所有语言版本,客户端选择

image.png

实现语言切换UI组件

最后,我们来实现一个语言切换组件:

image.png

性能优化建议

  1. 按需加载语言包:对于大型应用,按路由或功能模块加载语言包
  1. 缓存翻译结果:对于复杂的翻译处理,可以缓存结果
  1. 避免过度使用computed:只有真正需要响应语言变化的地方才使用computed

完整项目结构示例

image.png

总结

通过使用vue-i18n,我们可以轻松地在Vue项目中实现中英文切换功能。关键点包括:

  1. 创建语言包文件并组织翻译内容
  1. 配置i18n实例并在应用中注册
  1. 使用t函数或$t指令进行翻译
  1. 对于动态内容,使用computed属性确保响应性
  1. 处理特殊场景如日期、数字格式化和动态加载
  1. 实现友好的语言切换UI

通过这些步骤,可以构建一个完善的国际化应用,为全球用户提供良好的本地化体验。

KPrinter之 USB 接口指南

作者 lvha
2025年5月17日 10:25

欢迎使用 KPrinter UTS插件, 此插件适用于蓝牙热敏打印机,以下是说明及使用教程:

  • 适用厂商:佳博启锐汉印 等主流打印机
  • 支持的指令类型:TSPL/TSCCPCLESC
  • 支持平台:安卓iOS鸿蒙-开发中
  • 单位换算:200dip:1mm = 8dot300dip:1mm = 12dot
  • 插件下载地址

USBDevice 类型

// USB设备信息
export type USBDevice = {
   deviceName : string   // USB设备名称
   deviceId : string     // USB设备ID
   productName : string  // 产品名称
   productId : string  // 产品ID
   vendorId : string  // 供应商ID
   manufacturerName : string  // 生产商名称
   isConnect : boolean   // 是否连接
}

导入

import * as KPrinter from '@/uni_modules/kaka-KPrinter';

USB 接口说明


获取连接状态 - isConnect

const isConnect = KPrinter.isConnect();
uni.showToast({
    title: `连接状态: ${isConnect ? '已连接' : '未连接'}`
})

获取设备 - getUsbDeviceList

KPrinter.getUsbDeviceList((device) => {
    // device为usb设备对象(USBDevice)
});

连接USB设备- connectUSB

KPrinter.connectUSB('111'); // 参数为设备名称, device.deviceName

断开USB设备- connectUSB

KPrinter.disConnectUSB();

写入数据 - writeDataUSB

/*
    需要先构造指令,写入才有效
      如:
        KPrinter.cleanCmd()
        KPrinter.tscSelfTest()
        KPrinter.writeData();
*/ 
KPrinter.writeDataUSB();

USB 事件监听


USB连接状态监听
// usb连接监听
KPrinter.onUSBConnectStateChange({
    usbDeviceAttached: (device) => {
        uni.showToast({
            title: `插入USB设备`
        });
    },
    onSuccess: (device) => {
        uni.showToast({
            title: `USB 连接成功`
        });
    },
    onDisconnect: () => {
        uni.showToast({
            title: `USB 断开连接`
        });
    },
    onFail: (msg) => {
        uni.showToast({
            title: `USB连接失败: ${msg}`
        });
    }
});
USB数据回传监听
KPrinter.onDataReceive((data) => {
    let result = String.fromCharCode(...byte);
    console.log("收到USB数据: ", result, data);
});
USB数据写入完成监听
// 数据写入是否完成监听
KPrinter.onWriteComplete((isComplete) => {
    console.log("写入 " + (isComplete ? "成功" : "失败"));
});

指令使用说明


TSPL指令

CPCL指令

ESC指令

用AI做了个图片上传/下发应用

2025年5月17日 10:13

微信群的二维码每周都要更新一次,比较麻烦。于是搞了个简单的上传/下发的 Web 应用。

下面是优化前后流程,虽然看似步骤少了一步,但大大节省了时间。

主要功能

  • 常见类型图片上传,支持删除,提供外链访问
  • 支持上传前修改图片名,同名自动覆盖
  • 秘钥登录,配置更简单

Github: github.com/ATQQ/image-…

体验地址:imageupload.test.sugarat.top (秘钥testpwd)

imageupload.test.sugarat.top/images/user…

AI做了啥

Web站点生成

bolt.new/~/sb1-58wfa…

使用 Bolt(bolt.new) 生成

Prompt 如下

实现一个Vue3 SSR的应用,通过填写一个指定的秘钥(服务器上可以配置多个秘钥)
就可以上传图片到服务器上,支持用户单选或者多选图片,上传后给用户返回图片链接,
链接构成 domain/images/秘钥对应账号名/图片名称

其中图片名称可以由用户可选指定,不指定就动态生成一个不重复的图片名

同时集成图片的自动压缩

哐哐的一顿输出,分分钟就好了。

图片压缩功能生成得有问题,代码上就先给移除了😄

项目最终就是 Vue Nuxt 技术栈。

镜像脚本生成

使用 Cursor 的 chat 功能,也是 kuakua 的就生成了!

我做了什么

  1. 代码逻辑的微调
  2. 镜像脚本的微调,构建镜像上传
  3. 部署服务器

上面的工作理论上AI都能搞定,复杂点的可以结合一下 MCP,但个人觉得重要的部分还是需要人工 Review 改造一下。

细微的地方修改,Prompt 效率还是没有直接改 code 来得快。

如何部署

Docker

最简单的方式使用 Docker 镜像(当然也是AI生成的)

docker run -d \
  --name image-uploader \
  -p 3000:3000 \
  -v $(pwd)/data:/app/data \
  -e NODE_ENV=production \
  -e HOST=0.0.0.0 \
  -e PORT=3000 \
  -e SECRET_ACCOUNT_USER1=your-secret-key-here \
  --restart unless-stopped \
  sugarjl/image-uploader

通过修改SECRET_ACCOUNT_XXX的值来设置秘钥 比如

  -e SECRET_ACCOUNT_HELLO=a123456 \

PM2

# 拉代码
git clone https://github.com/ATQQ/image-uploader.git

# Gitee 地址(Github 访问受阻)
git clone https://gitee.com/sugarjl/image-uploader.git

cd image-uploader

# 装依赖
npm install
# 构建
npm run build

# 启动
# 在 ecosystem.config.cjs env中添加或修改秘钥
pm2 start ecosystem.config.cjs
# 或者 启动时通过环境变量指定秘钥
SECRET_ACCOUNT_USER1=test pm2 start ecosystem.config.cjs

最后

有 AI 后,能快速验证的各种想法,分分钟就生成 demo ,效率杠杠的!

语言不再是开发的障碍。

使用 UniApp 实现车牌号选择组件(含键盘和新能源支持)

作者 一清三白
2025年5月17日 09:57

使用 UniApp 实现车牌号选择组件(含键盘和新能源支持)

在移动应用开发中,车牌号输入是一个常见需求,尤其是在与交通、停车、违章查询等相关的应用中。与普通文本输入不同,车牌号输入有着特定的规则和格式,需要专门的键盘和交互设计。本文将详细介绍如何使用 UniApp 框架实现一个功能完善的车牌号选择组件,支持常规车牌和新能源车牌。

效果预览

车牌号选择组件效果图车牌号选择组件效果图车牌号选择组件效果图

功能特点

  • 支持普通车牌和新能源车牌输入
  • 自定义键盘,分为省份简称键盘和字母数字键盘
  • 智能切换键盘类型
  • 支持车牌号规则限制(如第二位只能输入字母或数字等)
  • 集成删除和关闭功能
  • 支持初始化已有车牌号
  • 优雅的样式和流畅的交互体验

实现思路

  1. 创建车牌号输入框组件
  2. 实现省份简称键盘
  3. 实现字母数字键盘
  4. 添加车牌号输入规则和限制
  5. 实现键盘切换和交互逻辑

代码实现

完整代码

  • 在components下建立car-number-input文件 复制以下代码
<template>
<view>
<view class="car-input-container">
<view class="car-input-box" 
v-for="(item,index) in inputList" :key="index" @click="plateInput(index)">
<view class="car-input-item" 
:class="[curInput == index?'sel-item':'',(maxNum-1) == index?'last-item':'']" >
<view :class="curInput == index?'sel-item-line':''"></view>
<img :src="xnyImgBase64" class="new-item-img" v-if="(maxNum-1) == index"/>
{{item}}
</view>
</view>
</view>

<view class="car-number-container" v-if="showKeyPop1">
<view class="plate-close" @click="closeKeyboard"><text class="plate-close-btn">关闭</text></view>
<view class="plate-popup-list">
    <view class="plate-popup-item province-item" v-for="(item,index) in keyProvince1" :key="index" @click="tapKeyboard(item)">{{item}}</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item province-item" v-for="(item,index) in keyProvince2" :key="index" @click="tapKeyboard(item)">{{item}}</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item province-item" v-for="(item,index) in keyProvince3" :key="index" @click="tapKeyboard(item)">{{item}}</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item province-item" v-for="(item,index) in keyProvince4" :key="index" @click="tapKeyboard(item)">{{item}}</view>
<!-- 删除 -->
<view class="plate-popup-item province-item del" @click="onPlateDelTap">
    <image :src="deleteImgBase64" />
</view>
</view>
</view>

<view class="car-number-container" v-if="showKeyPop2">
<view class="plate-close" @click="closeKeyboard"><text class="plate-close-btn">关闭</text></view>
<view class="plate-popup-list">
    <view class="plate-popup-item" :class="lockInput.includes(item)?'lock-item':''" 
v-for="(item,index) in keyEnInput1" :key="index" @click="tapKeyboard(item)">
{{item}}
</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item" :class="lockInput.includes(item)?'lock-item':''" 
v-for="(item,index) in keyEnInput2" :key="index" @click="tapKeyboard(item)">
{{item}}
</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item" :class="lockInput.includes(item)?'lock-item':''"  
v-for="(item,index) in keyEnInput3" :key="index" @click="tapKeyboard(item)">
{{item}}
</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item" :class="lockInput.includes(item)?'lock-item':''" 
v-for="(item,index) in keyEnInput4" :key="index" @click="tapKeyboard(item)">
{{item}}
</view>
<!-- 删除 -->
<view class="plate-popup-item del" @click="onPlateDelTap">
    <image :src="deleteImgBase64" />
</view>
</view>
</view>
</view>
</template>

<script>
export default {
name: 'car-number-input',
emits: ['numberInputResult'],
props: {
defaultStr:{
type: String,
default: ''
},
plateNum: {
type: String,
default: ''
},
// maxNum: {
// type: Number,
// default: 8
// },
},
data() {
return {
inputList:[" "," "," "," "," "," "," "," "],
curInput:-1,
maxNum:8,
showKeyPop1:false,
showKeyPop2:false,
keyProvince1: ['京', '津', '晋', '冀', '蒙', '辽', '吉', '黑', '沪'],
keyProvince2: ['苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘'],
keyProvince3: ['粤', '桂', '琼', '渝', '川', '贵', '云', '藏'],
keyProvince4: ['陕', '甘', '青', '宁', '新', 'W' ],
keyEnInput1: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
keyEnInput2: ["Q", "W", "E", "R", "T", "Y", "U", "P", "学", "军"],
keyEnInput3: ["A", "S", "D", "F", "G", "H", "J", "K", "L", "警"],
keyEnInput4: ["Z", "X", "C", "V", "B", "N", "M", "港", "澳"],
lockInput: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
xnyImgBase64:"data:image/png;base64,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PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMCAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjItMDktMTlUMTU6MDQ6NDQrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjItMDktMTlUMTU6MDQ6NDQrMDg6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIyLTA5LTE5VDE1OjA0OjQ0KzA4OjAwIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmNjMmVkZmYwLTk0NjktNGJhNC1hODc2LWVkYmJlZjRhYjk0MyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOmJkYTBhZTk3LWFmZWYtOWI0Ni1iOGFhLWZjOTMyMmM5OTk0NCIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjM1N2JlZGY2LTI5YWUtNGJmNS04YjQwLTVlMWQ5YjNlMmJmZiIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9IkRpc3BsYXkiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDozNTdiZWRmNi0yOWFlLTRiZjUtOGI0MC01ZTFkOWIzZTJiZmYiIHN0RXZ0OndoZW49IjIwMjItMDktMTlUMTU6MDQ6NDQrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4wIChNYWNpbnRvc2gpIi8+IDxyZGY6bGkgc3RFdnQ6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",
deleteImgBase64:"data:image/png;base64,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"
}
},
watch: {
defaultStr(val) {
if(val != "" && val != null){
const valList = val.split("")
for (let i in valList) {
this.inputList[i] = valList[i]
}
}
},
curInput(val){
this.showOrHidePop(val)

this.keyEnInput2 = ["Q", "W", "E", "R", "T", "Y", "U", "O", "P", "军"]
switch(val){
    case 1:
        this.lockInput = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "学", "军", "警", "港", "澳"]
break
    case 2:
        this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
case 3:
    this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
case 4:
    this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
case 5:
    this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
case 6:
    this.lockInput = ["O"]
this.keyEnInput2 = ["Q", "W", "E", "R", "T", "Y", "U", "P", "学", "军"]
break
case 7:
    this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
    default:
        this.lockInput = []
break
}
}
},
created() {
if(this.defaultStr != "" && this.defaultStr != null){
const valList = this.defaultStr.split("")
for (let i in valList) {
this.inputList[i] = valList[i]
}
}
},
methods: {
plateInput(e){
this.curInput = e
this.showOrHidePop(e)
},
showOrHidePop(val){
if(val == -1){
this.showKeyPop1 = false
this.showKeyPop2 = false
}else if(val == 0){
this.showKeyPop1 = true
this.showKeyPop2 = false
}else{
this.showKeyPop1 = false
this.showKeyPop2 = true
}

},
tapKeyboard(e){
if(this.lockInput.includes(e)){
return
}

this.inputList[this.curInput] = e
if(this.curInput < this.maxNum-2){
this.curInput++
}else{
this.curInput = -1
}

this.emitResult()
},
closeKeyboard(){
this.curInput = -1
},
onPlateDelTap(){
if(this.inputList[this.curInput] == " "){
this.curInput--
}
this.inputList[this.curInput] = " "

this.emitResult()
},
emitResult(){
const returnResult = this.inputList.join("")
this.$emit('numberInputResult', returnResult);
}
}
};
</script>

<style scoped lang="scss">
.car-input-container{
position: relative;
padding: 0 5px;
height: 44px;
.car-input-box{
display: inline-block;
width: 12.5%;
height: 44px;
vertical-align: middle;
.car-input-item{
position: relative;
border: 1px solid #E2E2E2;
border-radius: 10rpx;
height: 40px;
line-height: 40px;
width: 80%;
margin-left: 10%;
text-align: center;
font-size: 17px;
.sel-item-line{
position: absolute;
bottom: 3px;
left: 15%;
height: 2px;
background-color: #2979ff;
width: 70%;
}
.new-item-img{
position: absolute;
top: -6px;
left: 50%;
margin-left: -15px;
height: 13px;
width: 30px;
z-index: 9;
}
}
.sel-item{
color: #2979ff;
}
.last-item{
border: 1px solid #18bc37;
}
}
}
.car-number-container{
position: fixed;
z-index: 999;
bottom: 0;
left: 0;
width: 100%;
height: 254px;
background-color: #E3E2E7;
-webkit-box-shadow: 0 0 30upx rgba(0, 0, 0, 0.1);
box-shadow: 0 0 30upx rgba(0, 0, 0, 0.1);
overflow: hidden;
text-align: center;
.plate-close{
height: 40px;
line-height: 40px;
text-align: right;
background-color: #FFF;
.plate-close-btn{
font-size: 13.5px;
color: #555;
margin-right: 15px;
}
}
//键盘主体内容-单行
.plate-popup-list {
    margin: 0 auto;
    overflow: hidden;
    display: inline-block;
    display: table;

    &:last-child {
        margin-bottom: 2px;
    }
}
//键盘主体内容-单个
.plate-popup-item {
    float: left;
    font-size: 16px;
    width: 8vw;
    margin: 0 1vw;
    margin-top: 8px;
    height: 40px;
    line-height: 40px;
    background: #FFFFFF;
    border-radius: 5px;
    color: #4A4A4A;
image {
width: 16px;
height: 16px;
margin: 12px auto;
}
}
.plate-popup-item:active{
background-color: #EAEAEA;
}
.province-item{
width: 8.8vw;
}
.lock-item{
color: #AAA;
}
}
</style>

使用方法

在父组件中引入并使用该车牌号选择组件:

<template>
    <view>
        <car-number-input @numberInputResult="getPlateNumber" :defaultStr="plateNumber"></car-number-input>
        <view class="result">
            当前车牌号:{{plateNumber}}
        </view>
    </view>
</template>

<script>
    import carNumberInput from '@/components/car-number-input/car-number-input.vue'
    
    export default {
        components: {
            carNumberInput
        },
        data() {
            return {
                plateNumber: '京A12345'
            }
        },
        methods: {
            getPlateNumber(val) {
                this.plateNumber = val.trim();
                console.log('当前车牌号:', this.plateNumber);
            }
        }
    }
</script>

组件结构

组件主要分为两部分:车牌号输入框和自定义键盘。

template 部分
<template>
<view>
<view class="car-input-container">
<view class="car-input-box" 
v-for="(item,index) in inputList" :key="index" @click="plateInput(index)">
<view class="car-input-item" 
:class="[curInput == index?'sel-item':'',(maxNum-1) == index?'last-item':'']" >
<view :class="curInput == index?'sel-item-line':''"></view>
<img :src="xnyImgBase64" class="new-item-img" v-if="(maxNum-1) == index"/>
{{item}}
</view>
</view>
</view>

<view class="car-number-container" v-if="showKeyPop1">
<view class="plate-close" @click="closeKeyboard"><text class="plate-close-btn">关闭</text></view>
<view class="plate-popup-list">
    <view class="plate-popup-item province-item" v-for="(item,index) in keyProvince1" :key="index" @click="tapKeyboard(item)">{{item}}</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item province-item" v-for="(item,index) in keyProvince2" :key="index" @click="tapKeyboard(item)">{{item}}</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item province-item" v-for="(item,index) in keyProvince3" :key="index" @click="tapKeyboard(item)">{{item}}</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item province-item" v-for="(item,index) in keyProvince4" :key="index" @click="tapKeyboard(item)">{{item}}</view>
<!-- 删除 -->
<view class="plate-popup-item province-item del" @click="onPlateDelTap">
    <image :src="deleteImgBase64" />
</view>
</view>
</view>

<view class="car-number-container" v-if="showKeyPop2">
<view class="plate-close" @click="closeKeyboard"><text class="plate-close-btn">关闭</text></view>
<view class="plate-popup-list">
    <view class="plate-popup-item" :class="lockInput.includes(item)?'lock-item':''" 
v-for="(item,index) in keyEnInput1" :key="index" @click="tapKeyboard(item)">
{{item}}
</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item" :class="lockInput.includes(item)?'lock-item':''" 
v-for="(item,index) in keyEnInput2" :key="index" @click="tapKeyboard(item)">
{{item}}
</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item" :class="lockInput.includes(item)?'lock-item':''"  
v-for="(item,index) in keyEnInput3" :key="index" @click="tapKeyboard(item)">
{{item}}
</view>
</view>
<view class="plate-popup-list">
    <view class="plate-popup-item" :class="lockInput.includes(item)?'lock-item':''" 
v-for="(item,index) in keyEnInput4" :key="index" @click="tapKeyboard(item)">
{{item}}
</view>
<!-- 删除 -->
<view class="plate-popup-item del" @click="onPlateDelTap">
    <image :src="deleteImgBase64" />
</view>
</view>
</view>
</view>
</template>
script 部分
<script>
export default {
name: 'car-number-input',
emits: ['numberInputResult'],
props: {
defaultStr:{
type: String,
default: ''
},
plateNum: {
type: String,
default: ''
},
// maxNum: {
// type: Number,
// default: 8
// },
},
data() {
return {
inputList:[" "," "," "," "," "," "," "," "],
curInput:-1,
maxNum:8,
showKeyPop1:false,
showKeyPop2:false,
keyProvince1: ['京', '津', '晋', '冀', '蒙', '辽', '吉', '黑', '沪'],
keyProvince2: ['苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘'],
keyProvince3: ['粤', '桂', '琼', '渝', '川', '贵', '云', '藏'],
keyProvince4: ['陕', '甘', '青', '宁', '新', 'W' ],
keyEnInput1: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
keyEnInput2: ["Q", "W", "E", "R", "T", "Y", "U", "P", "学", "军"],
keyEnInput3: ["A", "S", "D", "F", "G", "H", "J", "K", "L", "警"],
keyEnInput4: ["Z", "X", "C", "V", "B", "N", "M", "港", "澳"],
lockInput: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
xnyImgBase64:"data:image/png;base64,...", // 新能源标识图片base64编码(已省略)
deleteImgBase64:"data:image/png;base64,..." // 删除按钮图片base64编码(已省略)
}
},
watch: {
defaultStr(val) {
if(val != "" && val != null){
const valList = val.split("")
for (let i in valList) {
this.inputList[i] = valList[i]
}
}
},
curInput(val){
this.showOrHidePop(val)

this.keyEnInput2 = ["Q", "W", "E", "R", "T", "Y", "U", "O", "P", "军"]
switch(val){
    case 1:
        this.lockInput = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "学", "军", "警", "港", "澳"]
break
    case 2:
        this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
case 3:
    this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
case 4:
    this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
case 5:
    this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
case 6:
    this.lockInput = ["O"]
this.keyEnInput2 = ["Q", "W", "E", "R", "T", "Y", "U", "P", "学", "军"]
break
case 7:
    this.lockInput = ["O", "学", "军", "警", "港", "澳"]
break
    default:
        this.lockInput = []
break
}
}
},
created() {
if(this.defaultStr != "" && this.defaultStr != null){
const valList = this.defaultStr.split("")
for (let i in valList) {
this.inputList[i] = valList[i]
}
}
},
methods: {
plateInput(e){
this.curInput = e
this.showOrHidePop(e)
},
showOrHidePop(val){
if(val == -1){
this.showKeyPop1 = false
this.showKeyPop2 = false
}else if(val == 0){
this.showKeyPop1 = true
this.showKeyPop2 = false
}else{
this.showKeyPop1 = false
this.showKeyPop2 = true
}

},
tapKeyboard(e){
if(this.lockInput.includes(e)){
return
}

this.inputList[this.curInput] = e
if(this.curInput < this.maxNum-2){
this.curInput++
}else{
this.curInput = -1
}

this.emitResult()
},
closeKeyboard(){
this.curInput = -1
},
onPlateDelTap(){
if(this.inputList[this.curInput] == " "){
this.curInput--
}
this.inputList[this.curInput] = " "

this.emitResult()
},
emitResult(){
const returnResult = this.inputList.join("")
this.$emit('numberInputResult', returnResult);
}
}
};
</script>
style 部分
<style scoped lang="scss">
.car-input-container{
position: relative;
padding: 0 5px;
height: 44px;
.car-input-box{
display: inline-block;
width: 12.5%;
height: 44px;
vertical-align: middle;
.car-input-item{
position: relative;
border: 1px solid #E2E2E2;
border-radius: 10rpx;
height: 40px;
line-height: 40px;
width: 80%;
margin-left: 10%;
text-align: center;
font-size: 17px;
.sel-item-line{
position: absolute;
bottom: 3px;
left: 15%;
height: 2px;
background-color: #2979ff;
width: 70%;
}
.new-item-img{
position: absolute;
top: -6px;
left: 50%;
margin-left: -15px;
height: 13px;
width: 30px;
z-index: 9;
}
}
.sel-item{
color: #2979ff;
}
.last-item{
border: 1px solid #18bc37;
}
}
}
.car-number-container{
position: fixed;
z-index: 999;
bottom: 0;
left: 0;
width: 100%;
height: 254px;
background-color: #E3E2E7;
-webkit-box-shadow: 0 0 30upx rgba(0, 0, 0, 0.1);
box-shadow: 0 0 30upx rgba(0, 0, 0, 0.1);
overflow: hidden;
text-align: center;
.plate-close{
height: 40px;
line-height: 40px;
text-align: right;
background-color: #FFF;
.plate-close-btn{
font-size: 13.5px;
color: #555;
margin-right: 15px;
}
}
//键盘主体内容-单行
.plate-popup-list {
    margin: 0 auto;
    overflow: hidden;
    display: inline-block;
    display: table;

    &:last-child {
        margin-bottom: 2px;
    }
}
//键盘主体内容-单个
.plate-popup-item {
    float: left;
    font-size: 16px;
    width: 8vw;
    margin: 0 1vw;
    margin-top: 8px;
    height: 40px;
    line-height: 40px;
    background: #FFFFFF;
    border-radius: 5px;
    color: #4A4A4A;
image {
width: 16px;
height: 16px;
margin: 12px auto;
}
}
.plate-popup-item:active{
background-color: #EAEAEA;
}
.province-item{
width: 8.8vw;
}
.lock-item{
color: #AAA;
}
}
</style>

功能解析

1. 车牌格式与输入框

车牌号的输入格式为:

  • 普通车牌:7位字符(1位省份简称 + 1位字母 + 5位字母或数字)
  • 新能源车牌:8位字符(1位省份简称 + 1位字母 + 6位字母或数字,最后一位用绿色框标识)

在组件中,使用了一个长度为8的数组来存储车牌号各位的字符:

inputList:[" "," "," "," "," "," "," "," "]

并使用 v-for 指令遍历数组,创建对应数量的输入框:

<view class="car-input-box" 
    v-for="(item,index) in inputList" :key="index" @click="plateInput(index)">
    <!-- 内容省略 -->
</view>

2. 自定义键盘设计

组件实现了两种不同的键盘:

  1. 省份简称键盘:用于输入车牌的第一位,包含全国各省市自治区的简称。
  2. 字母数字键盘:用于输入车牌的其他位置,包含字母、数字以及特殊标识(如"学"、"警"等)。

键盘布局采用了多行设计,使用者可以快速找到并点击所需的字符:

<view class="plate-popup-list">
    <view class="plate-popup-item province-item" v-for="(item,index) in keyProvince1" :key="index" @click="tapKeyboard(item)">{{item}}</view>
</view>

3. 输入规则与限制

车牌号的不同位置有不同的输入规则。例如:

  • 第一位只能是省份简称
  • 第二位通常只能是字母
  • 其他位置可以是字母或数字

组件通过 watch 监听当前输入位置 curInput 的变化,动态设置当前位置可用和禁用的字符:

watch: {
    curInput(val){
        this.showOrHidePop(val)
        
        this.keyEnInput2 = ["Q", "W", "E", "R", "T", "Y", "U", "O", "P", "军"]
        switch(val){
            case 1:
                this.lockInput = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "学", "军", "警", "港", "澳"]
                break
            // 其他位置的规则...
        }
    }
}

禁用的字符在键盘上以灰色显示,并且不响应点击事件:

<view class="plate-popup-item" :class="lockInput.includes(item)?'lock-item':''" 
    v-for="(item,index) in keyEnInput1" :key="index" @click="tapKeyboard(item)">
    {{item}}
</view>

4. 新能源车牌支持

新能源车牌比普通车牌多一位,且最后一位通常用绿色边框标识。组件通过条件渲染和特殊样式来实现这一特性:

<view class="car-input-item" 
    :class="[curInput == index?'sel-item':'',(maxNum-1) == index?'last-item':'']" >
    <view :class="curInput == index?'sel-item-line':''"></view>
    <img :src="xnyImgBase64" class="new-item-img" v-if="(maxNum-1) == index"/>
    {{item}}
</view>

其中 last-item 类应用了绿色边框,而 new-item-img 则显示了新能源车标识。

5. 事件处理与交互

组件实现了多种交互方式:

  • 点击输入框:激活对应位置的输入并弹出相应键盘
  • 点击键盘字符:输入字符并自动跳转到下一位
  • 点击删除按钮:删除当前位置的字符
  • 点击关闭按钮:收起键盘

当输入完成或变更时,组件会通过事件向父组件传递当前的车牌号:

emitResult(){
    const returnResult = this.inputList.join("")
    this.$emit('numberInputResult', returnResult);
}

组件优化方向

  1. 支持自定义车牌长度:目前组件固定为8位,可以通过props动态设置长度以支持更多车牌类型。
  2. 增加输入验证:可以添加车牌格式的实时验证功能,提示用户输入是否符合规范。
  3. 改进键盘布局:可根据用户习惯优化键盘布局,提高输入效率。
  4. 增加动画效果:为键盘弹出/收起和输入框切换添加平滑过渡动画。

总结

通过本文,我们实现了一个功能完善的车牌号选择组件,支持普通车牌和新能源车牌的输入。该组件封装了复杂的交互逻辑和输入规则,为用户提供了便捷的车牌号输入体验。在实际项目中,可以根据具体需求对组件进行进一步定制和优化。

希望这个组件能够帮助到各位开发者,如有任何问题或建议,欢迎在评论区留言交流!

Java开发者的AI工具箱提升10倍开发效率

2025年5月17日 09:40

想获取更多高质量的Java技术文章?欢迎访问 技术小馆官网,持续更新优质内容,助力技术成长!

曾几何时,无数个深夜,我们对着屏幕苦苦调试那个顽固的NullPointerException,或者为了写一个高效的算法而绞尽脑汁。如今,AI工具正在重塑Java开发的方式。

想象一下,一个虚拟助手能帮你重构臃肿的代码,自动生成单元测试,甚至在你犯错前提醒你可能的坑。这不是科幻电影,而是当下已经实现的技术现实。近期,我在项目中引入AI辅助工具后,团队的bug率下降了40%,代码审查时间缩短了一半,开发速度提升了惊人的300%。这篇文章,我将分享那些真正改变我工作方式的AI工具和技巧,告诉你如何像"10倍工程师"一样高效编码。

一、AI编码助手

1. GitHub Copilot

Copilot就像你肩膀上的编程小精灵,时刻准备帮你写出优质代码。只要输入注释或方法签名,它就能智能推断你的意图。

// 创建一个处理HTTP请求的工具类,支持GET和POST方法

仅凭这一行注释,Copilot就能生成完整实现:

public class HttpUtils {
    private static final OkHttpClient client = new OkHttpClient();
    
    public static String get(String url) throws IOException {
        Request request = new Request.Builder()
            .url(url)
            .build();
            
        try (Response response = client.newCall(request).execute()) {
            return response.body().string();
        }
    }
    
    public static String post(String url, Map<String, String> params) throws IOException {
        FormBody.Builder formBuilder = new FormBody.Builder();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            formBuilder.add(entry.getKey(), entry.getValue());
        }
        
        Request request = new Request.Builder()
            .url(url)
            .post(formBuilder.build())
            .build();
            
        try (Response response = client.newCall(request).execute()) {
            return response.body().string();
        }
    }
}

使用精确的注释引导Copilot,如"使用线程安全的单例模式实现缓存管理器",而不是模糊的"创建缓存类"。

2. Amazon CodeWhisperer

作为Copilot的免费替代品,CodeWhisperer在AWS相关代码生成上特别出色。

// 上传文件到S3存储桶并设置公开访问权限

CodeWhisperer会生成:

public void uploadToS3(String bucketName, String keyName, File file) {
    try {
        AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
                             .withRegion(Regions.US_EAST_1)
                             .build();
        
        // 上传文件
        s3Client.putObject(bucketName, keyName, file);
        
        // 设置公开访问权限
        s3Client.setObjectAcl(bucketName, keyName, CannedAccessControlList.PublicRead);
        
    } catch (AmazonServiceException e) {
        System.err.println("上传失败: " + e.getErrorMessage());
    }
}

与Copilot不同,它还会标记潜在的安全问题,如公开设置可能导致的数据泄露风险。

3. Tabnine

Tabnine的独特之处在于提供本地运行选项,适合对数据安全有高要求的团队。它还会随着你编码风格的积累变得越来越智能。

二、代码质量的AI工具

1. SonarLint的AI增强版

不仅能发现问题,还能提供智能修复。例如检测到:

if (user.getName().equals("admin")) { // 可能的NPE
    grantAdminAccess();
}

AI修复建议:

if ("admin".equals(user.getName())) { // 避免NPE的最佳实践
    grantAdminAccess();
}

2. DeepCode

通过学习百万级开源项目,DeepCode能发现复杂的逻辑缺陷。比如这段看似正常的代码:

public void process(List<Transaction> transactions) {
    for (Transaction t : transactions) {
        if (t.getAmount() > 1000) {
            sendAlert(t);
            return; // 这里有逻辑问题
        }
    }
}

DeepCode会警告你:return语句导致发现第一笔大额交易后就退出方法,可能漏检后续交易。

3. Codota

专注于帮你写出地道的Java代码。当你开始输入:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream().

Codota会智能提示最佳实践:

names.stream()
     .filter(name -> name.startsWith("A"))
     .sorted()
     .collect(Collectors.toList());

并解释每个操作的作用。

4. Jabel

自动将旧版Java代码升级到新语法特性:

// Java 7 风格
Map<String, List<Order>> ordersByCustomer = new HashMap<>();
for (Order order : orders) {
    String customerId = order.getCustomerId();
    if (!ordersByCustomer.containsKey(customerId)) {
        ordersByCustomer.put(customerId, new ArrayList<>());
    }
    ordersByCustomer.get(customerId).add(order);
}

转换为:

// Java 10+ 风格
var ordersByCustomer = orders.stream()
    .collect(Collectors.groupingBy(Order::getCustomerId));

三、AI辅助测试

1. Diffblue Cover

自动生成单元测试,覆盖各种边界情况:

public class StringUtils {
    public static String reverse(String input) {
        if (input == null) return null;
        return new StringBuilder(input).reverse().toString();
    }
}

Diffblue生成测试:

@Test
public void testReverseWithNormal() {
    assertEquals("cba", StringUtils.reverse("abc"));
}

@Test
public void testReverseWithEmpty() {
    assertEquals("", StringUtils.reverse(""));
}

@Test
public void testReverseWithNull() {
    assertNull(StringUtils.reverse(null));
}

2. TestGPT

允许你用自然语言描述测试需求:

测试用户注册功能:验证用户名长度限制、邮箱格式正确性、密码强度要求

TestGPT生成:

@Test
public void testUserRegistrationWithValidData() {
    UserDto user = new UserDto("johndoe", "john@example.com", "Str0ng!Pass");
    RegistrationResult result = registrationService.register(user);
    
    assertTrue(result.isSuccess());
    assertNotNull(result.getUserId());
}

@Test
public void testUserRegistrationWithShortUsername() {
    UserDto user = new UserDto("jo", "john@example.com", "Str0ng!Pass");
    RegistrationResult result = registrationService.register(user);
    
    assertFalse(result.isSuccess());
    assertEquals("用户名长度必须大于3个字符", result.getErrorMessage());
}

// 更多测试...

3. MutantX

不是测试你的程序,而是测试你的测试!它通过修改程序代码(创建"变种")来检验测试的有效性:

// 原始代码
public double calculateDiscount(double amount, int customerYears) {
    if (amount > 1000 && customerYears > 5) {
        return amount * 0.2; // 20%折扣
    }
    return amount * 0.1; // 10%折扣
}

// MutantX创建的变种
public double calculateDiscount(double amount, int customerYears) {
    if (amount > 1000 || customerYears > 5) { // 将&& 改为||
        return amount * 0.2;
    }
    return amount * 0.1;
}

如果你的测试无法检测出这种变化,就说明测试覆盖不足。

四、AI加持的性能调优

1. JXRay与AI

热门AI插件包括:

Automation for Jira (带有AI功能的自动化工具)

Smart Checklist AI

Time Estimation AI

Deep Learning for Jira

分析堆转储文件,识别内存问题:

发现问题: EventListener对象泄漏
- 原因: 监听器注册后未移除
- 影响: 每用户操作泄漏约2KB内存
- 修复建议:

@Override
public void onDestroy() {
    eventBus.unregister(this); // 添加注销监听器
    super.onDestroy();
}

2. IBM AI Optimization

通过分析应用运行时特征,自动优化JVM参数:

应用特征: 大内存、低延迟需求、频繁小对象创建

推荐JVM配置:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:+UseStringDeduplication
-Xms4g -Xmx4g

同时解释每个参数的作用和预期效果。

3. PerformanceGPT

智能识别性能瓶颈,并提供优化方案:

检测到的性能问题:
- OrderService.processOrder() 方法平均执行时间: 2.3秒
- 主要瓶颈: 重复数据库查询 (72%执行时间)

优化建议:
```java
// 原代码
public OrderResult processOrder(Order order) {
    Customer customer = customerDao.findById(order.getCustomerId());
    // 处理订单...
    Payment payment = processPayment(order);
    // 更新客户数据
    customer = customerDao.findById(order.getCustomerId()); // 重复查询
    customer.setLastOrderDate(new Date());
    customerDao.update(customer);
    // ...
}

// 优化建议
public OrderResult processOrder(Order order) {
Customer customer = customerDao.findById(order.getCustomerId());
// 处理订单...
Payment payment = processPayment(order);
// 直接使用已查询的对象
customer.setLastOrderDate(new Date());
customerDao.update(customer);
// ...
}

五、项目管理与协作的AI工具

1. JIRA的AI插件

智能分解需求、估算工作量:

产品需求: "实现用户活动日志功能"

AI分解结果:
1. 设计活动日志数据模型 (3点)
2. 开发日志记录服务 (5点)
3. 创建日志查询API (3点)
4. 实现管理界面展示 (8点)

总计: 19点 (约4人天)
智能分配: 张三(后端), 李四(前端)

2. AI文档助手

IntelliJ IDEA插件:

AI Assistant - JetBrains官方AI工具

Codeium - 支持自动文档生成

Tabnine - 带有文档生成功能

根据代码自动生成API文档:

/**
 * 处理支付相关操作
 */
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
    /**
     * 创建新支付
     */
    @PostMapping
    public ResponseEntity<PaymentResponse> createPayment(@RequestBody PaymentRequest request) {
        // 实现...
    }
}

生成的Markdown文档:

# 支付API

## 创建支付
**POST** `/api/payments`

**请求体**:
```json
{
  "amount": 100.50,
  "currency": "CNY",
  "paymentMethod": "WECHAT_PAY",
  "description": "订单#12345的支付"
}

响应:

{
  "paymentId": "PAY-123456789",
  "status": "PROCESSING",
  "createdAt": "2023-10-15T14:30:45Z"
}

### 3. CodeReviewAI

代码审查助手,自动检查常见问题:

```java
// 提交审查的代码
public void processUsers(List<User> users) {
    for (int i = 0; i < users.size(); i++) {
        User user = users.get(i);
        updateUserStatus(user);
        notifyUser(user);
    }
}

AI反馈:

建议:
1. 使用forEach或增强for循环提高可读性
2. users参数应添加非空检查
3. 考虑批量处理通知以提高性能

优化代码:
```java
public void processUsers(List<User> users) {
    Objects.requireNonNull(users, "Users list cannot be null");
    
    users.forEach(user -> {
        updateUserStatus(user);
    });
    
    // 批量发送通知
    notifyUsers(users);
}

通过这些AI工具的组合使用,Java开发者可以显著提升工作效率,减少重复性工作,专注于更有创造性的任务。无论是代码生成、质量保证、测试自动化、性能优化还是项目管理,AI都能成为你的得力助手。关键是找到适合自己工作流程的工具组合,并善用它们的优势。

告别代码拼写灾难:这个VS Code插件让你的程序不再“漏洞百出”

作者 哈希茶馆
2025年5月17日 09:05

一个让程序员“社死”的深夜

凌晨两点,程序员小明颤抖着按下代码提交键——这是他熬了三个通宵的项目。第二天晨会演示时,用户页面突然崩溃。老板皱眉、同事憋笑,问题根源竟是一个拼写错误:recieve(正确应为receive)。小明盯着屏幕上的红色波浪线苦笑:“要是有人早点提醒我就好了……”

拯救“手残党”的神器:Code Spell Checker

它是什么?

一款轻量级VS Code插件,专为代码和文档设计的“智能纠错仪”。无论是变量名、注释还是Markdown文档,它都能用红色波浪线精准标记拼写错误,并给出修正建议。

为什么需要它?

  • 变量名拼错导致程序崩溃(比如lenght写成length)?直接拦截!
  • 技术文档里的JavaScript写成Javscript当场逮捕!
  • 支持30+编程语言50+种词典,连古希腊语和医学术语都不放过。

从“漏洞百出”到“滴水不漏”

1. 智能纠错实战

  • 快速修正:光标悬停 → 按下⌘+.(Mac)或Ctrl+.(Win) → 选择user一键替换!
  • 忽略规则:在代码中添加// cSpell:ignore usr永久白名单。

2. 个性化配置

  • 切换英式/美式英语:在设置中修改"cSpell.language": "en-GB"
  • 支持复合词// cSpell:enableCompoundWords允许errormessage(而非error message
  • 屏蔽技术术语:用正则表达式// cSpell:ignoreRegExp 0x[0-9a-f]+忽略16进制数值

高级技巧:让拼写检查“指哪打哪”

  • 按文件类型精准打击:在状态栏点击🔍图标,一键屏蔽LaTeX中的数学公式或SQL里的表名。
  • 多语言混编也不怕
  • 自定义词典扩展:安装医学词典(Medical Terms)或Python专用词库,连NumPyDataFrame都认得!

为什么它值得信赖?

  • 零隐私泄露:所有检查在本地完成,绝不联网传输数据
  • 低误报率:智能拆分camelCasesnake_case,把HTMLInput识别为HTML+Input
  • 开源免费:GitHub超3.4K星,可赞助支持开发者

结尾行动号召

别再让undefined变成undefindconsole.log写成consle.log立即安装Code Spell Checker,让你的代码和文档从此告别“小学生级”错误。 (偷偷告诉你:写这篇文章时,它帮我抓到了6个拼写bug……)🔍✨

🔥 关注我的公众号「哈希茶馆」一起交流更多开发技巧

手动实现一个微前端qiankun

作者 竹业
2025年5月17日 08:45

前言

作为前端开发者,微前端架构已经成为解决大型应用复杂性的重要手段。本文将带你简单了解下qiankun的原理,并尝试手写实现一个简化版的微前端框架。

微前端核心概念

微前端是一种将多个独立交付的前端应用组合成一个更大整体的架构风格。主要解决:

  • 巨石应用拆解
  • 技术栈无关
  • 独立开发部署
  • 增量升级

qiankun核心原理

qiankun的核心原理可以概括为以下几个部分:

  • 应用加载:动态加载子应用的HTML、JS、CSS资源
  • 应用隔离:实现JS沙箱和CSS样式隔离
  • 应用通信:提供父子应用、子应用间的通信机制
  • 生命周期:管理子应用的挂载、卸载等生命周期

手动实现简单版qiankun

qiankun的使用

  • 主应用中下载qiankun框架,注册子应用,运行
  • 子应用中对外暴露3个生命周期函数:bootstrap、mount、unmount

主应用修改

子应用修改

  • main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

let app = null

function render(props = {}) {
  const { container } = props
  console.log('container', container)
  app = new Vue({
    render: h => h(App),
  })

  // 使用提供的容器或默认容器
  const mountEl = container ? container.querySelector('#app') : '#app'
  console.log('mountEl', mountEl)

  app.$mount(mountEl)
}

// 独立运行时直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

// qiankun 生命周期钩子
export async function bootstrap() {
  console.log('[vue3] app bootstrap')
}

export async function mount(props) {
  console.log('[vue3] mount props', props)
  render(props)
}

export async function unmount() {
  console.log('[vue2] unmount')
  app?.$unmount?.()
  app = null
}
  • vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  // 配置打包输出格式
  configureWebpack: {
    output: {
      library: `vue2-sub-app`,
      libraryTarget: 'umd',
    }
  },
  // 开发环境跨域配置
  devServer: {
    port: 3001,
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  },
  // 生产环境publicPath配置
  publicPath: process.env.NODE_ENV === 'production' ? '/vue2-sub-app/' : '/'
})

主应用修改

  • main.js
import { registerMicroApps, start } from 'qiankun'
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'

const app = createApp(App)

// 注册子应用
registerMicroApps([
  {
    name: 'vue-sub-app', // 子应用名称,与子应用的package.json中的name一致
    entry: '//localhost:3001', // 子应用入口
    container: '#subapp-container', // 子应用挂载节点
    activeRule: '/vue-sub-app', // 子应用路由前缀
    props: {} // 主应用传递给子应用的数据
  },
  {
    name: 'vue2-sub-app',
    entry: '//localhost:3002',
    container: '#subapp-container',
    activeRule: '/vue2-sub-app',
    props: {
      enableReact19Features: true
    }
  }
])

// 启动qiankun
start()
app.use(router)
app.mount('#app')
  • App.vue
<script setup>
import HelloWorld from "./components/HelloWorld.vue";

const goVue = () => {
  location.href = "/vue-sub-app";
};

const goVue2 = () => {
  location.href = "/vue2-sub-app";
};
</script>

<template>
  <div>
    <h2>微前端</h2>
    <!-- <button @click="goVue2">vue2</button>
    <button @click="goVue">vue3</button> -->
    <router-link to="/vue2-sub-app">vue2</router-link>
    <router-link to="/vue-sub-app">vue3</router-link>
    <!-- 子应用渲染 -->
    <div id="subapp-container"></div>
  </div>
</template>

手动实现qiankun

实现registerMicroApps方法

let _apps = [];
// 保存注册的路由
export const registerMicroApps = (apps) => {
  _apps = [...apps]
}

实现start方法

  • 监听路由变换,取出对应的app配置
  • 根据配置的路径,加载页面资源
  • 执行js方法,将dom节点挂载到父页面
let prev = '';
let next = '';
const start = () => {
    const rawPushState = window.history.pushState;
    window.history.pushState = function (...args) {
        prev = window.location.pathname; // 记录上次路由
        rawPushState.apply(window.history, args); // 执行原生方法
        next = window.location.pathname; // 记录当前路由
        handleRoute()
    }
}
const handleRoute = async () => {
   // 上一个子应用执行销毁
   const prevApp = _apps.find(item => item.activeRule === prev);
   if (prevApp) {
    await prevApp.unmount();
   }
  
  // 取出当前app
  const path = window.location.pathname;
  const app = _apps.find(item => item.activeRule === path);
  if (!app) return
  
  const { template, execScripts } = await importHTML(app.entry)
  const container = document.querySelector(app.container)
  container.appendChild(template)
  window.__POWERED_BY_QIANKUN__ = true // 设置变量,让子应用按要求渲染
  const appExports = await execScripts(); // 执行子应用的js
  
  // 设置子应用生命周期函数
  app.bootstrap = appExports.bootstrap;
  app.mount = appExports.mount;
  app.unmount = appExports.unmount;
  
  // 执行生命周期函数
  await app.bootstrap?.()
  await app.mount?.({
    container: document.querySelector(app.container),
  })
}

// 
const importHTML = async url => {
  // 加载子页面
  const html = await fetch(url).then(res => res.text())
  const template = document.createElement('div')
  template.innerHTML = html
    
  // 取出html中的js并加载
  const scripts = template.querySelectorAll('script')
  const getExternalScripts = async () => {
    return Promise.all([...scripts].map(async script => {
      const src = script.getAttribute('src')
      if (!src) {
        return script.innerHTML
      } else {
        return fetch(src.startsWith('http') ? src : `${url}${src}`).then(res => res.text())
      }

    })
    )
  }
  
  // 执行子页面js
  const execScripts = async () => {
    const scripts = await getExternalScripts();
    const module = { exports: {} }
    const exports = module.exports
    scripts.forEach(code => {
      eval(code)
    })
    return module.exports
  }
  return {
    template,
    getExternalScripts,
    execScripts
  }
}

最后

通过手动实现简化版qiankun,我们深入理解了微前端的核心原理;对于JS沙箱和样式隔离还要进一步了解。

JavaScript中的类型判断方法你知道几种?

作者 哆啦美玲
2025年5月17日 08:09

hello~好久不见,这里是哆啦美玲哈哈!最近觉得记住知识点最好的方法,还是回顾的时候写文章分享给大家最有用,虽然我写的一般,大家将就看吧。今天就是分享一个js知识点——如何判断数据类型?

image.png

数据类型

js中的数据类型分两种:基本类型和引用类型对象。

基本类型(原始类型) 包括:number、string、boolean、null、undefined、symbol、bigInt

引用类型包括 :Object、array、function、Data

类型判断的方法

1. typeof 判断原始类型

typeof 可以准确的判断除了null 之外的所有原始类型,不能准确判断引用类型,除了function

那typeof是怎么使用的呢?我们直接看下面的代码:

// 判断基本类型
console.log(typeof 'hello'); //string
console.log(typeof 123); // number
console.log(typeof true); // boolean
console.log(typeof null); // object
console.log(typeof undefined); // undefined
console.log(typeof Symbol(1)); // symbol
console.log(typeof 123n); // bigint

// 判断引用类型
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof new Date()); // object
console.log(typeof function foo(){}); // function

由此,我们可以发现,直接使用 typeof 关键词去判断基本类型时,只有null一种类型会被误判成为object,其他基本数据类型都能够成功判断。

其实在之前的文章中解释过: 了解JavaScript的底层——内存机制 - 掘金

这里我们再解释一次,因为JS在判断类型时,会把变量的值转换成二进制进行判断;在计算机中所有的引用类型的二进制前三位是0,而null转换成二进制全部都是0,所以被误判成是对象。此外,最特殊的函数使用typeof是可以准确的判断出是function。

2. instanceof 判断引用类型
2.1 原理

instanceof 关键字是通过原型链来判断类型相等,只能判断引用类型(原始类型没有隐式原型)

我们直接看代码举例:

console.log({} instanceof Object);  // instanceof关键字(隶属于) 输出true
console.log([] instanceof Array);  // true
console.log(new Date() instanceof Date);  // true
console.log(function(){} instanceof Function);  // true

console.log([] instanceof Object);  // true
console.log(new Date() instanceof Object);  // true
console.log(function(){} instanceof Object);  // true

// console.log(null instanceof null); // 报错,右边必须是对象 
console.log("hello" instanceof String);  // false
console.log(123 instanceof Number);  // false
console.log(true instanceof Boolean);  // false

从上面的代码结果来看,数组、函数等对象,不仅能够判断它们是否隶属于他们本身的构造函数,还能判断是否是一个对象。所以instanceof关键字是判断隶属于的关系,即判断某个变量是否隶属于某种类型,返回true或者false。而在第10行中我们如果执行它,会得到报错提示:Right-hand side of 'instanceof' is not an object,这告诉我们instanceof的右边必须放一个对象才能进行判断。

但是它到底是怎么判断的呢?我们看下面一段代码:

function Car(){
    this.run = 'running'
}

Bus.prototype = new Car()

function Bus(){
    this.name = 'BYD'
}

let bus = new Bus();

console.log(bus instanceof Bus); // true  bus.__proto__ == Bus.prototype
console.log(bus instanceof Car); // true  bus.__proto__.__proto__ == Car.prototype
console.log(bus instanceof Object); // true bus.__proto__.__proto__.__proto__ == Object.prototype

我们自己创造一个构造函数Bus,并且基于Bus构建一个实例对象bus,所以13行代码返回true很好理解。然后我们又创造了一个Car的构造函数,并且让Bus的对象原型是Car的实例对象后,我们打印14行的结果是true,为什么呢?

其实,根据代码的结果,我们应该可以猜到instanceof可能是根据原型链来进行判断,我们直接来看bus、Bus和Car之间有什么关联:

首先我们回顾new的原理:将构造函数的显示原型(Object.prototype) 赋值给 实例对象的对象原型(即隐式原型obj.__ proto__)。(不懂的可以去看这篇文章:搞懂this,如此简单 - 掘金

所以11行代码构造实例对象bus时,会实现 bus.__ proto__ == Bus.prototype。

而第五行代码Bus.prototype = new Car(),其中Bus.prototype也是一个对象,它也存在对象原型(隐式原型),所以我们人为的将构造函数Car的显示原型赋值给它的对象原型,即 Bus.prototype.__ proto__ == Car.prototype。所以我们就可以将Bus.prototype替换掉,得到 Bus.prototype._ proto_ == bus.__ proto__._ proto_ == Car.prototype

第15行代码也同理,顺着原型链进行追溯,相信你们也可以推理得到 bus.__ proto__.__ proto__.__ proto__ == Object.prototype。所以,instanceof就是根据原型链来判断数据是否为引用类型。

2.2 手写myInstanceof方法

前面我们已经搞清楚了instanceof的判断原理,所以我们也可以手戳一个instanceof方法,实现同理。这里我直接展示我使用的两种方法:循环和递归,代码如下:

// 方法一:while循环
function my_instanceof(L, R) {
    while (L !== null) {
        L = L.__proto__
        if (L === R.prototype) {
            return true
        }
    }
    return false
}
console.log(my_instanceof([], Object));

// 方法二:递归
function myInstanceof(L, R) {
    if (L !== null) {
        L = L.__proto__
        if (L !== R.prototype) {
            return myInstanceof(L, R); 
            //每次递归的判断结果 (true 或 false) 都需要返回给上一层应该是直接返回递归的结果,
            // 确保每层递归都能够返回正确的判断。
        }
        return true
    }
    return false

}
console.log(myInstanceof([],Array)); 
2.3 包装类
console.log(new String('hello') instanceof String); // true
console.log("hello" instanceof String); // false

我们总说let str = 'hello' 和 let str = new String('hello') 是一样的,但是为什么上面代码会出现两个结果呢?这就是我们要聊的包装类的问题。

在JavaScript中,并没有像Java中那样明确的“包装类”概念。JavaScript是一种动态类型语言,变量的类型是可以在运行时决定的,因此并不需要使用包装类来将原始数据类型(基本类型)封装成对象。然而,JavaScript仍然有一些类似的概念,尤其是与基本数据类型(例如 string、number、boolean)相关的对象封装

  1. 显式创建包装对象,即直接用对应的构造函数创建变量,会将原始数据类型包装为对象,并允许访问它们的属性和方法。例如:构建字符串对象 let str = new String('abc')。
  2. 基本数据类型在V8执行下会自动被封装为对象类型。当我们访问字符串、数字、布尔值等原始类型的属性或方法时,JavaScript会自动将它们包装成相应的包装类对象。即在V8的眼里let a = 1会被执行成 let a = new Number(1)
  3. 原始类型不能拥有属性和方法,属性和方法只能是引用类型的。
  4. 访问对象身上不存在的属性不会报错,会是undefined

方便理解,我举个例子:

let num = 123  // let num = new Number(123)
num.a = 1  // {a:1}
// delete num.a
console.log(num.a); // undefined
console.log(num); // 123

代码执行的逻辑如下:

  1. 第一行代码在V8眼里会执行成let num = new Number(123),所以第二行代码能够往num上添加a属性。
  2. 我们需要的num是原始类型,不是包装类,V8严格按照原始类型不能拥有属性和方法这一原理,会将num身上的a属性移除,即delete num.a
  3. 访问Number对象 num身上不存在的属性是不会报错的,会返回undefined。
  4. 读取值的时候会执行:读取[[PrimitiveValue]]的值。如下图,num内部其实有一个内置属性[[PrimitiveValue]],是只有V8能够访问的用来存取原始类型的值的属性。

f52ac2729f2399424b0e7aac7774797.png

上面例子是没有直接显示创建包装类的情况,如果是显示创建的情况如下: image.png

我们会发现,显示创建包装类的情况下,V8是直接当做一个对象进行处理的,所以可以往其身上添加属性和方法。

3. Object.prototype.toString().call(x)

Object.prototype.toString().call(x)是借助Object原型上的toString方法在执行过程中会读取 X 的内部属性[[Class]]这一机制来进行数据的类型判断。如图: f14dd0e23455a33df72e10eb9abc0a2.png

在官方文档中关于Object.prototype.toString()触发时会执行以下步骤描述如下::

  1. 如果 this 值为 undefined,返回 “[object Undefined]”。
  2. 如果 this 值为 null,返回 “[object Null]”。
  3. 设 O 为调用 ToObject(C打造的V8用的方法) 的结果,并将 this 值作为参数,即V8会执行ToObject(this)
  4. class 为 O 的 [[Class]] 内部属性的值,即class等于O的数据类型。
  5. 返回 String 值,该值是将三个字符串拼接—— "[object " 、 class 和 "]"

根据上面的步骤,我总结为:Object.prototype.toString()会读取this的值身上的内置属性[[Class]] ——对象的类型,以一个很特殊的状态返回 “[object ”、class 和 “]”

所以这个方法关键在于 this 指向谁,这就要使用call() 将this显示绑定在需要判断的对象上,就能够返回能够显示对象类型的字符串。(有不熟悉call的可以看这篇:搞懂this,如此简单 - 掘金

测试代码如下:

let a = 1 
let b = {}
console.log(Object.prototype.toString.call(a)); // [object Number] 
console.log(Object.prototype.toString.call(b)); // [object Object]
4. Array.isArray() 判断数组

Array.isArray()是专门用来判断一个对象是不是数组的方法,代码如下:

let arr = []
// 判断一个对象是不是数组
console.log(Array.isArray({}));

好了,我的分享到这里就结束啦~喜欢我的分享记得给我点个赞喔!

ღ( ´・ᴗ・ `)比心,我们下次再见!!

image.png

实现一个文本逐字输出效果

作者 JYeontu
2025年5月17日 02:57

说在前面

网页中文本逐字打印这个效果大家应该都不陌生吧,很多个人博客首页都喜欢用这个效果来做一个欢迎语或自我介绍,今天我们一起来看看这个效果是怎么实现的

在线预览

码上掘金

CodePen

codepen.io/yongtaozhen…

代码实现

html

<div id="text"></div>

准备一个div作为文本容器即可,当然,不准备直接在js中创建也可以😁

css

文本容器居中显示

这个看个人喜欢,我这里直接flex布局居中。

body {
  margin: 0;
  overflow: hidden;
  background: #000;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}
#text {
  margin: auto;
  color: white;
  font-size: clamp(1.2rem, 3vw, 1.8rem); /* 响应式字体 */
  font-family: "楷体", cursive;
  text-align: center;
  text-shadow: 0 0 10px rgba(255, 64, 129, 0.6);
}
  • clamp函数:clamp() 函数接收三个用逗号分隔的[表达式]作为参数,按最小值、首选值、最大值的顺序排列。

闪烁光标

使用伪元素在文本最后插入一个光标,动画循环修改光标透明度即可

#text::after {
  content: "|";
  margin-left: 2px;
  animation: blink 0.7s infinite;
}
.cursor {
  margin-left: 2px;
  animation: blink 0.7s infinite;
}
@keyframes blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}

JavaScrip

const text = ["hello😳你好呀", "欢迎来到我的空间✨"];
let lineIndex = 0;
let charIndex = 0;
const textDiv = document.getElementById("text");
let cursor;
const typeSpeed = 150; // 打字速度(毫秒)

function typeText() {
  if (lineIndex < text.length) {
    if (charIndex < text[lineIndex].length) {
      textDiv.innerHTML += text[lineIndex][charIndex++];
      setTimeout(typeText, typeSpeed);
    } else {
      if (lineIndex === text.length - 1) {
        return;
      }
      setTimeout(() => {
        textDiv.innerHTML += "<br>";
        lineIndex++;
        charIndex = 0;
        setTimeout(typeText, typeSpeed);
      }, typeSpeed / 2);
    }
  }
}
setTimeout(typeText, typeSpeed);
  • 数组存储文本:支持多行内容,通过 lineIndexcharIndex 实现行与字符的双重遍历。
  • 打印速度typeSpeed 控制每个字符显示的延迟,数值越小打字越快。
  • 递归调用:通过 setTimeout 递归调用 typeText,实现逐个字符的延迟显示。
  • 换行逻辑:非最后一行结束时,添加
    换行,并通过 lineIndex++ 切换到下一行,同时重置 charIndex0
  • 结束处理:所有内容显示完毕后,自动停止操作,保留最后一个光标(如需隐藏,可额外进行处理)。

源码

gitee

gitee.com/zheng_yongt…

github

github.com/yongtaozhen…


  • 🌟 觉得有帮助的可以点个 star~
  • 🖊 有什么问题或错误可以指出,欢迎 pr~
  • 📬 有什么想要实现的功能或想法可以联系我~

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

JS第二十七次笔记

2025年5月17日 00:52

1.目录

image.png

1.1 Node.js的模块化

模块:包括内置模块和自定义模块

image.png

image.png

utils.js
/**
 * 目标:基于 CommonJS 标准语法,封装属性和方法并导出
 */
const baseURL = 'http://hmajax.itheima.net'//基地址
const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)
//reduce是累加器(上一次结果,当前每一项)


//导出 基地址和求和函数,给其他模块使用
// CommonJS规范导出
module.exports = {
  baseURL: baseURL,
  arraySum: getArraySum
}
/**
 * 目标:基于 CommonJS 标准语法,导入工具属性和方法使用
 */
// 导入
// 使用CommonJS 标准语法,导出必须使用module.exports;导入必须使用require

const obj = require('./utils.js')
console.log(obj)
const result = obj.arraySum([5, 1, 2, 3])
console.log(result)

1.2 ECMAScript标准

1.2.1 ECMAScript标准——默认导出和导入

image.png

/**
 * 目标:基于 CommonJS 标准语法,封装属性和方法并导出
 */
const baseURL = 'http://hmajax.itheima.net'//基地址
const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)
//reduce是累加器(上一次结果,当前每一项)


//导出 基地址和求和函数,给其他模块使用
// ECMAScript规范导出
export default {
  //键:值
  url: baseURL,
  getArraySum
}

/**
 * 目标:基于 ECMAScript标准语法,导入工具属性和方法使用
 */
// 导入

// import 变量名 from '模块路径';
import obj from './utils.js'
console.log(obj);
// 创建package.json文件,内容如下:
// {
//   // 选module就表示可以使用ECMAScript 6的模块化语法
//   "type": "module"
// }

1.2.2 ECMAScript默认不支持CommonJS标准语法

注意:Node.js默认支持CommonJS标准语法,而ECMAScript默认不支持标准语法,所以需要创建一个package.json文件,并设置["type":"module"](module就表示ECMAScript语法)

{
  "type": "module"
}

结果:

image.png

1.2.3 ECMAScript标准——命名导出和导入

image.png命名导出
*ECMAScript标准语法
*export修饰模块


// 命名(按需导出):export表示导出对应模块
export const baseURL = 'http://hmajax.itheima.net'//基地址
export const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)
//reduce是累加器(上一次结果,当前每一项)

命名导入
*ECMAScript标准语法

// 按需导入又称为命名导入(可在大括号中指定多个或一个模块进行导入)
import { baseURL } from './utils.js'
console.log(baseURL);

结果: image.png

2 软件包

2.1 包的概念

image.png

2.2 npm——软件包管理器

image.png

2.3 npm——安装所有依赖

使用场景:当项目只有package.json没有node_modules时,使用 npm i 命令安装所有依赖软件包 image.png

image.png

2.4 npm——全局软件包nodemon

nodemon:热更新,检测到文件变化后,自动更新 安装全局命令:npm i -g image.png

2.5 Node.js总结

有很多模块,默认为common模块

2.5.1 Node.js模块化

image.png

2.5.2 Node.js包

image.png

2.5.3 常用命令

image.png

3 Express——框架

3.1 Express定义

image.png

3.2 Express使用

image.png

image.png

image.png

image.png

vite-plugin-uni-pages 控制页面是否打包

2025年5月16日 23:36

vite-plugin-uni-pages 有提供一个钩子方法onBeforeWriteFile,此方法在生成 pages.json 之前调用

pagesGlobConfig

image.png

我们可以在此方法里面访问 uni-pages 插件的上下文,可以看到, pages.json 是通过pagesGlobConfig 和 pageMetaData 和 subPageMetaData 合并生成

在我的 微信小程序 『轻便万物迹』中,分包里面有两个文件夹,现在我不想打包 storage/index 这个页面, 于是我在 route 定义中添加了一个字段 __enable, 用来表示我不想打包此文件, 可以看到, 打包的时候有输出此自定义字段。于是我们可以在读到有此字段的时候,将该对象进行移除,这样,打包后的pages.json 就不会出现此页面。

image.png

image.png

onBeforeWriteFile(ctx) {
  console.log(ctx.pageMetaData);
  console.log(ctx.subPageMetaData);
  ctx.subPageMetaData.forEach((item) = >{
    const newPages = item.pages.filter((route) = >route.__enable === false);
    item.pages = newPages;
  });
}

image.png

可以看到, 构建产物中不包含有__enable: false 字段的页面。

如何控制不同平台的打包

同理, 我们只需要在页面的route对象中声明哪些平台打包,哪些平台不打包,然后通过 UNI_PLATFORM 获取当前正在打包平台名,进行页面过滤。如下图,此时设置了storage/index 这个页面不进行打包(微信平台 mp-weixin),构建产物也不会包含 storage/index 这个页面

const { UNI_PLATFORM } = process.env;


console.log('UNI_PLATFORM -> ', UNI_PLATFORM); // 得到 mp-weixin, h5, app 等

image.png

image.png

如何直接生成完整的pages.json, 但是在pages.json 里面进行条件编译

可以通过json转js然后添加注释实现,具体实现后续更新

image.png

❌
❌