阅读视图

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

npm link本地测试React组件库报错“Invalid hook call”?从多实例到pnpm依赖的完整排查指南

最近开发了一个React拖拽排序组件库,计划通过npm link到本地测试时,遇到了报错:Warning: Invalid hook call. Hooks can only be called inside of the body of a function component.经过多轮排查,最终发现到React多实例冲突pnpm对peerDependencies的隐式安装是核心原因,以下是完整解决流程,供同类问题参考

一、问题初判:React版本兼容性?

测试项目使用Next 15(App Router)报错,初步怀疑版本冲突:

  • Next 15 App Router默认依赖React 19,但组件库用了React 18.3.1
  • 尝试放宽版本限制:将组件库package.jsonreact的版本范围改为"^18.3.1 || ^19.0.0",但测试仍报错
  • 结论:版本兼容性非主因(组件库并没有使用与react18强相关的特性,后来使用了一个react18的项目也不行)

二、排查方向:组件库依赖配置问题

查阅React官方文档(Invalid Hook Call警告)和关键Issue(github.com/facebook/re… ,提示可能是React多实例Hooks未在函数组件内调用导致。因为组件代码没有问题所以排除后者,重点排查多实例:

1. 验证多实例的关键方法

  • 现象辅助判断:同时出现Invalid hook callCannot read properties of null (reading 'useState'),是典型多实例特征(不同React实例的Hooks上下文不共享)

  • 打包产物检查

    • 构建后搜索组件库代码,确认仅在入口有import React from 'react',其他位置无重复引入或者明显的function useState(){}定义等
    • 使用rollup-plugin-visualizer分析打包依赖,确认React未被打包进组件库

2. 修复尝试:配置peerDependencies与外部化

  • peerDependencies声明:在组件库package.json中,将reactreact-dom标记为peerDependencies(告知用户需自行安装)
  • Rollup外部化配置:在vite.config.ts中,通过build.rollupOptions.external将React相关模块排除在打包外:["react", "react-dom", "react/jsx-runtime"] 关键:包括react/jsx-runtime
  • 结果:仍报错

三、关键突破:React路径指向异常

尝试暴力验证:将组件库打包后的import React from 'react'改为测试项目中React的绝对路径(如import React from '/path/to/test-project/node_modules/react'),测试项目运行正常

  • 结论:实锤组件库与测试项目引用了不同路径的React实例

四、终极原因:pnpm对peerDependencies的隐式安装

进一步排查依赖管理工具pnpm的特性:

  • pnpm默认行为:pnpm 10默认开启autoInstallPeers官方文档)(pnpm9也是),会自动安装peerDependencies到当前项目的node_modules中(这篇文章stackoverflow.com/questions/7… 有误导性)
  • 问题触发场景:组件库依赖了motion,而motionpeerDependencies中声明了react,pnpm因autoInstallPeers=true,会隐式为组件库安装一个独立的React实例
  • 验证方法:执行pnpm why react(显示哪个包依赖了react导致的下载),输出显示React由motion的peer依赖触发安装

五、最终解决方案

通过配置pnpm禁用自动安装peer依赖,确保组件库与测试项目共享同一React实例:

  1. 在组件库根目录创建.npmrc文件,添加:auto-install-peers = false
  2. 删除lock文件,重新安装依赖(pnpm i),可以看到pnpm-lock.yaml中开头有一行autoInstallPeers: false
  3. 重新build组件库

总结与避坑指南

  • 核心原则:本地测试组件库时,确保组件库与测试项目共享同一React实例(路径、版本完全一致)

  • 关键配置

    • 组件库必须声明reactpeerDependencies,避免打包时包含React
    • Rollup/Vite需外部化reactreact-domreact/jsx-runtime(避免打包)
    • pnpm用户需检查auto-install-peers配置(默认开启,可能导致隐式安装独立实例)

tips:npm link和pnpm link的区别

  1. npm link:先npm link将组件库注册到全局,再在测试项目npm link 包名引用全局链接,即通过全局node_modules建立软链接
  2. pnpm link:直接将组件库路径硬链接到测试项目的node_modules(需显式指定路径pnpm link 组件库路径),无需经过全局node_modules

希望这篇记录能帮到遇到类似问题的开发者

常用DOM

目录 获取DOM节点 通过ID查找节点 通过标签名查找节点 通过类名查找节点 通过CSS选择器查找单个节点 通过CSS选择器查找所有节点 通过关系获取节点 获取下一个兄弟节点 获取上一个兄弟节点

JavaScript原型链

在JavaScript中,原型链是一个非常重要的概念。它不仅决定了对象的继承机制,还影响了对象属性的查找过程。本文将详细介绍JavaScript中的原型链,包括它的基本概念、工作原理以及实际应用。

语法规范/错误/运算符/判断分支/注释

JavaScript 语法涵盖多方面。语法规范上,严格区分大小写,语句建议以分号结尾,代码会忽略多余空格与换行。常见错误有引号、括号不匹配,逗号、分号缺失等。运算符丰富,包括算术、比较、逻辑等。判断分

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

前几天写代码时遇到个怪事:用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)?想清楚这一点,就能在开发中灵活选择啦!

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

一、齐次坐标的基本概念

在计算机图形学中,齐次坐标是一种用 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 图形、计算机视觉和机器人学等领域。

three.js 字体使用全解析

在 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 项目!

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

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

凌晨两点,程序员小明颤抖着按下代码提交键——这是他熬了三个通宵的项目。第二天晨会演示时,用户页面突然崩溃。老板皱眉、同事憋笑,问题根源竟是一个拼写错误: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……)🔍✨

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

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

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

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

说在前面

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

在线预览

码上掘金

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第二十七次笔记

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

js中是否存在真正的异步?

JavaScript 本身没有“异步执行”的能力,所谓异步,其实都是“通过回调 + 事件循环机制实现的异步行为”。


🔹 1. JS 是单线程的语言

JavaScript 的运行环境(如浏览器、Node.js)中,JS 本身只在主线程中执行,不能同时干两件事。比如:

console.log(1) 
setTimeout(() => console.log(2), 1000) 
console.log(3)`

虽然 setTimeout 看起来是“异步的”,但本质上:

  • setTimeout 把回调函数登记给浏览器或 Node 的 “定时器模块”

  • JS 主线程继续执行;

  • 等 1 秒后,事件循环机制(Event Loop) 将回调函数“推”到 JS 主线程队列中执行。

所以 JS 本身没有“异步线程”,异步操作是环境提供的能力(浏览器/Node 的 Web APIs)。


🔹 2. 所有异步行为,归根结底都是「延迟执行的回调函数」

举例:PromisesetTimeoutfetchasync/await,都可以还原为回调逻辑。

setTimeout(() => console.log('A'), 1000)

==就是注册一个回调函数==,延迟执行。


🔹 3. Promise 也只是对“回调”的封装

new Promise((resolve) => {   setTimeout(() => {     resolve('done')   }, 1000) }).then(result => {   console.log(result) })

核心仍然是:注册回调,只不过 .then 把回调收敛到一个更好管理的语法结构中。

前端主题色小案例

以下是一个基于Vue3 + Sass的完整主题色解决方案,包含动态切换、状态管理、样式组织等核心模块: 一、项目结构设计 styles/_variables.scss 该文件定义可扩展的色板系统,支持

uniapp实现在线pdf预览以及下载

后端返回一个url地址,我需要将在在页面中渲染出来。因为在浏览器栏上我输入url地址就可以直接预览pdf文件,因此直接的想法是通过web-view组件直接渲染。那么有什么问题呢?
❌