普通视图

发现新文章,点击刷新页面。
今天 — 2025年6月10日首页

bignumber.js深度解析:驾驭任意精度计算的终极武器

作者 烛阴
2025年6月9日 23:33

一、核心痛点:为什么需要bignumber.js?

1.1 JavaScript的数字精度灾难

// 经典精度问题
console.log(0.1 + 0.2 === 0.3); // false

// 大数溢出问题
const MAX = Number.MAX_SAFE_INTEGER; // 9007199254740991
console.log(MAX + 1 === MAX + 2); // true!

二、快速入门:基础运算四步法

2.1 安装与引入

npm install bignumber.js
import BigNumber from 'bignumber.js';

2.2 创建BigNumber对象

// 从字符串创建(推荐,避免精度损失)
const num1 = new BigNumber('0.1');

// 从数字创建(不推荐)
const num2 = new BigNumber(0.2); 

// 从其他BigNumber实例创建
const num3 = new BigNumber(num1);

2.3 基本运算

const sum = num1.plus(num2); // 0.3
const diff = num1.minus(0.05); // 0.05
const product = num1.times(10); // 1
const quotient = num1.dividedBy(3); // 0.333333...

2.4 结果输出

// 转换为字符串
sum.toString(); // '0.3'

// 保留小数位
quotient.toFixed(4); // '0.3333'

// 转换为原始数字(注意精度风险)
product.toNumber(); // 1

三、高级运算:金融级精度控制

3.1 精度与舍入模式

// 全局配置精度
BigNumber.set({ DECIMAL_PLACES: 10 });

// 单个运算控制
const pi = new BigNumber('3.1415926535');
pi.dividedBy(2).toFixed(4); // 默认四舍五入:'3.1416'

//指定特定舍入法
pi.toFixed(4, BigNumber.ROUND_HALF_EVEN); // '3.1416' 

3.2 比较与判断

const a = new BigNumber('0.0000000001');
const b = new BigNumber('0.0000000000000000001');

// 精度比较
a.isGreaterThan(b); // true
a.isLessThanOrEqualTo(b); // false

// 特殊值检测
const nan = new BigNumber(NaN);
nan.isNaN(); // true

const inf = new BigNumber(Infinity);
inf.isFinite(); // false

3.3 数学函数

// 开平方
const sq = new BigNumber(256).sqrt(); // 16

// 指数运算
const exp = new BigNumber(2).exponentiatedBy(10); // 1024

// 模运算
new BigNumber(15).mod(4); // 3

四、最佳实践指南

4.1 安全操作规范

// ✅ 始终使用字符串初始化
new BigNumber('0.00000001');

// ❌ 避免浮点数初始化
new BigNumber(0.00000001); // 可能不精确

// ✅ 重要计算指定舍入模式
value.decimalPlaces(8, BigNumber.ROUND_HALF_UP);

// ✅ 使用自定义错误处理
try {
  const result = new BigNumber('abc');
} catch (e) {
  console.error('Invalid number:', e.message);
}

4.2 类型定义(TypeScript)

import { BigNumber } from 'bignumber.js';

interface FinancialResult {
  total: BigNumber;
  tax: BigNumber;
}

function calculateInvoice(amount: string): FinancialResult {
  const amountBN = new BigNumber(amount);
  const tax = amountBN.times(0.1);
  
  return {
    total: amountBN.plus(tax),
    tax
  };
}

结语

官网地址mikemcl.github.io/bignumber.j…
GitHub仓库github.com/MikeMcl/big…


如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript开发干货。

昨天 — 2025年6月9日首页

安卓和ios小程序开发中的兼容性问题举例

2025年6月9日 20:10

一、布局渲染差异

1.1 时间格式解析不一致

具体表现
iOS 系统的 JavaScript 引擎只能解析YYYY/MM/DD格式的日期字符串,而安卓可以解析YYYY-MM-DD。若直接使用连字符格式,iOS 会返回Invalid Date
解决方案

javascript

// 安全的日期解析函数
function parseDate(dateStr) {
  if (!dateStr) return null;
  // 替换连字符为斜杠
  const formattedStr = dateStr.replace(/-/g, '/');
  const date = new Date(formattedStr);
  // 验证日期有效性
  return isNaN(date.getTime()) ? null : date;
}

// 使用示例
const iosSafeDate = parseDate('2024-01-31'); // iOS和安卓均能正确解析

1.2 安全区域适配

具体表现
iOS 全面屏设备(如 iPhone X 系列)底部存在安全区域,而安卓设备刘海屏 / 挖孔屏位置不一。
解决方案

css

/* 通用安全区域适配方案 */
.page-container {
  padding-bottom: env(safe-area-inset-bottom);
  padding-top: env(safe-area-inset-top);
}

/* 动态计算状态栏高度 */
.status-bar {
  height: var(--status-bar-height);
}

javascript

// 在app.js中全局设置状态栏高度
App({
  onLaunch() {
    wx.getSystemInfo({
      success: ({ statusBarHeight }) => {
        wx.setStorageSync('statusBarHeight', statusBarHeight);
      }
    });
  }
});

1.3 字体渲染差异

具体表现
iOS 默认使用 San Francisco 字体,安卓默认使用 Roboto 字体,导致相同文本显示高度不同。
解决方案

css

/* 统一字体族 */
.page-content {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
  line-height: 1.5; /* 强制统一行高 */
}

二、事件处理差异

2.1 触摸事件传递机制

具体表现
iOS 的catchtouchmove会完全阻止滚动,安卓则允许部分滚动穿透。
解决方案

html

预览

<!-- 滚动遮罩层 -->
<view class="mask" catchtouchmove="preventDefault">
  <!-- 内容 -->
</view>

javascript

// 阻止默认滚动行为
Page({
  preventDefault(e) {
    // iOS需要返回false,安卓返回true或false均可
    return false;
  }
});

2.2 长按事件差异

具体表现
iOS 的长按触发时间约为 800ms,安卓约为 500ms。
解决方案

javascript

// 自定义长按事件
Page({
  data: {
    longPressTimer: null
  },
  
  touchStart(e) {
    this.data.longPressTimer = setTimeout(() => {
      this.handleLongPress();
    }, 600); // 取中间值
  },
  
  touchEnd() {
    clearTimeout(this.data.longPressTimer);
  },
  
  handleLongPress() {
    // 长按处理逻辑
  }
});

三、API 接口差异

3.1 文件路径处理

具体表现
iOS 的文件路径区分大小写,安卓不区分。
解决方案

javascript

// 规范化文件路径
function normalizePath(path) {
  return path.toLowerCase(); // 统一转换为小写
}

// 使用示例
const filePath = normalizePath('/User/Images/IMG_001.JPG'); // iOS和安卓均能正确识别

3.2 录音格式兼容性

具体表现
iOS 默认支持 AAC 格式,安卓默认支持 AMR 格式。
解决方案

javascript

// 自适应录音格式
wx.getSystemInfo({
  success: ({ platform }) => {
    const format = platform === 'ios' ? 'aac' : 'amr';
    wx.startRecord({
      format,
      success: (res) => {
        // 处理录音文件
      }
    });
  }
});

四、系统特性差异

4.1 键盘弹起行为

具体表现
iOS 键盘弹起时页面整体上移,安卓键盘可能覆盖输入框。
解决方案

javascript

// 监听键盘事件调整布局
Page({
  onLoad() {
    wx.onKeyboardHeightChange(res => {
      this.setData({
        keyboardHeight: res.height
      });
    });
  }
});

css

/* 动态调整输入框位置 */
.input-container {
  bottom: {{keyboardHeight}}px;
  position: fixed;
}

4.2 后台运行限制

具体表现
iOS 小程序进入后台 5 分钟后会被终止,安卓可运行更长时间。
解决方案

javascript

// 缓存关键数据
App({
  onShow() {
    // 恢复数据
    const cacheData = wx.getStorageSync('backgroundData');
    if (cacheData) this.restoreState(cacheData);
  },
  
  onHide() {
    // 保存关键数据
    const state = this.collectAppState();
    wx.setStorage({
      key: 'backgroundData',
      data: state
    });
  }
});

五、缓存与存储差异

5.1 本地存储限制

具体表现
iOS 本地存储上限约 10MB,安卓约 20MB。
解决方案

javascript

// 智能存储管理
class StorageManager {
  constructor(limit = 8 * 1024 * 1024) { // 预留2MB空间
    this.limit = limit;
  }
  
  async set(key, value) {
    const data = JSON.stringify(value);
    const size = this.calculateSize(data);
    
    if (size > this.limit) {
      await this.clearOldestData(size);
    }
    
    wx.setStorageSync(key, data);
  }
  
  // 其他方法...
}

5.2 数据持久化差异

具体表现
iOS 会自动清理长时间未使用的小程序数据,安卓则不会。
解决方案

javascript

// 定期同步重要数据到服务器
function syncDataToServer() {
  const localData = wx.getStorageSync('importantData');
  wx.request({
    url: 'https://api.example.com/sync',
    method: 'POST',
    data: localData
  });
}

// 启动时检查数据完整性
App({
  onLaunch() {
    this.checkDataIntegrity();
    setInterval(syncDataToServer, 86400000); // 每天同步一次
  }
});

六、性能优化差异

6.1 列表渲染性能

具体表现
iOS 对长列表渲染更流畅,安卓在数据量大时易卡顿。
解决方案

javascript

// 使用虚拟列表组件
import VirtualList from '@vant/weapp/virtual-list';

Page({
  data: {
    list: [],
    itemHeight: 80, // 单项高度固定
    visibleRange: { start: 0, end: 20 }
  },
  
  onLoad() {
    this.initVirtualList();
  },
  
  initVirtualList() {
    // 初始化大数据列表
    const list = Array(1000).fill(0).map((_, i) => ({ id: i, text: `Item ${i}` }));
    this.setData({ list });
  }
});

6.2 图片加载策略

具体表现
iOS 支持渐进式图片加载,安卓默认不支持。
解决方案

html

预览

<!-- 使用占位图+懒加载 -->
<image 
  class="lazy-image" 
  src="{{item.placeholderUrl}}" 
  data-src="{{item.realUrl}}" 
  bindload="onImageLoad"
/>

javascript

Page({
  onImageLoad(e) {
    const { src, dataset } = e.currentTarget;
    if (src !== dataset.src) {
      this.setData({
        [`list[${dataset.index}].src`]: dataset.src
      });
    }
  }
});

Three.js 光影魔法:如何单独点亮你的3D模型

2025年6月9日 18:21

在 Three.js 的世界里,光与材质的交相互动创造了我们所见的视觉效果。一个常见的场景是:你已经精心布置了全局的环境光和方向光,整个场景看起来很和谐,但你发现其中某个特定的模型显得有些暗淡。你希望单独把它调亮,又不想影响场景中的其他物体。

这可能是一个需要强调的主角模型、一个交互式 UI 元素,或者一个需要模拟发光效果的物体。

这篇文章将带你深入了解如何实现这一目标,从最直接的方法到其背后的核心渲染原理,让你彻底掌握控制物体亮度的“魔法”。

我们的基础场景

在开始之前,让我们先搭建一个简单的基础场景作为参照。这个场景包含两个完全相同的立方体和两种基本光源:环境光(AmbientLight)和方向光(DirectionalLight)。

import * as THREE from 'three';

// 场景、相机、渲染器等基础设置...
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
// ...

// 1. 全局光照
const ambientLight = new THREE.AmbientLight(0x404040, 1.0); // 提供基础环境亮度
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); // 模拟太阳光
directionalLight.position.set(3, 5, 4);
scene.add(directionalLight);

// 2. 两个对比物体
const geometry = new THREE.BoxGeometry(1, 1, 1);

// 物体A:标准蓝色立方体
const standardMaterial = new THREE.MeshStandardMaterial({ color: 0x0077ff });
const standardCube = new THREE.Mesh(geometry, standardMaterial);
standardCube.position.x = -1.5;
scene.add(standardCube);

// 物体B:我们希望调亮的目标
const targetMaterial = new THREE.MeshStandardMaterial({ color: 0x0077ff });
const targetCube = new THREE.Mesh(geometry, targetMaterial);
targetCube.position.x = 1.5;
scene.add(targetCube);

// 渲染循环...

在这个场景中,两个蓝色立方体看起来一模一样,它们的亮度完全由全局光照决定。现在,我们的任务是让右侧的 targetCube 单独变亮。

核心武器:material.emissive 自发光属性

要单独控制一个物体的亮度,最直接、最高效的方法就是利用材质的 自发光(Emissive) 属性。

你可以把它想象成给物体内部装了一个灯泡。这个“灯泡”发出的光会直接叠加到物体最终的颜色上,并且它不受场景中任何光源的影响

MeshStandardMaterialMeshBasicMaterial 等多种材质都支持以下两个关键属性:

  • material.emissive: 一个 THREE.Color 对象,定义了物体自身发出的光的颜色
  • material.emissiveIntensity: 一个浮点数,定义了自发光的强度,默认为 1.0

实践:点亮我们的目标

现在,我们来修改 targetMaterial

// ...接上文...

// 物体B:我们希望调亮的目标
const targetMaterial = new THREE.MeshStandardMaterial({
    color: 0x0077ff, // 物体本身的基础颜色依然是蓝色
    emissive: 0xffffff, // 关键!设置一个白色的自发光
    emissiveIntensity: 0.6 // 控制自发光的强度,可以随意调整
});

const targetCube = new THREE.Mesh(geometry, targetMaterial);
targetCube.position.x = 1.5;
scene.add(targetCube);

效果立竿见 victimes! 右侧的立方体在保持其蓝色色调的同时,整体亮度得到了显著提升。即使你关闭所有场景光源,它依然会显示为一个有亮度的形状。

深入剖析:自发光颜色如何选择?

一个常见的问题是:emissive 的颜色应该怎么设置?设置成红色会怎么样?

1. 白色自发光 (0xffffff):纯粹的亮度提升

当你只想让物体变亮而不改变其原有色调时,应该使用白色自发光。

  • 原理:白色 (rgb(1,1,1)) 包含了所有颜色通道。当它与物体的基础色叠加时,会等比例地提升基础色的R、G、B分量,效果就是纯粹的亮度增加。
  • 比喻:就像用一个白色手电筒去照一个有色的物体,物体只会变亮,不会变色。

2. 彩色自发光 (如 0xff0000):创造发光体与色彩混合

当你希望物体本身发出某种特定颜色的光(如霓虹灯、岩浆),或者想创造特殊的艺术效果时,可以使用彩色自发光。

  • 原理:彩色自发光会与物体的基础色发生颜色混合
  • 比喻:用一个红色手电筒去照一个蓝色物体,物体最终会呈现出紫色的色调,因为蓝色表面反射的光与手电筒发出的红光混合了。

原理揭秘:为何光照是乘法,自发光是加法?

你可能会好奇,最终的颜色是如何计算出来的。一个简化的模型可以帮助我们理解:

最终颜色 = (基础颜色 * 场景光照) + (自发光颜色 * 自发光强度)

这里的 乘法加法 是关键,它们分别代表了两种完全不同的物理过程。

光照的本质:调制(Modulation),因此是乘法

一个不发光的物体,它的颜色来自于它对入射光线的反射吸收。材质的 color 属性定义了它对不同颜色光的“反射率”。

  • 过程:入射光颜色 * 材质反射率 = 反射光颜色
  • 示例
    • 白光 (1,1,1) 照射到红色材质 (1,0,0) 上。
    • 反射光 = (1,1,1) * (1,0,0) = (1*1, 1*0, 1*0) = (1,0,0),结果是红色。
    • 材质的颜色像一个滤光片,调制了入射光,所以这是一个乘法过程。

自发光的本质:叠加(Addition),因此是加法

自发光是物体自身产生的能量,它与外部光线无关。它的贡献是直接叠加在物体反射的光之上的。

  • 过程:物体反射的光 + 物体自身发出的光 = 最终看到的光
  • 示例:黑暗房间里,一张桌子反射了微弱的月光,同时桌上的手机屏幕自身在发光。你眼中看到的手机屏幕亮度,就是月光反射的亮度与屏幕自发光亮度的总和。这是一个纯粹的叠加过程,所以是加法。

理解了这一点,你就掌握了控制 Three.js 中颜色与亮度的核心逻辑。

其他方案与对比

除了 emissive,还有一些其他方法可以影响单个物体的亮度,它们适用于不同的场景。

方法 原理 优点 缺点 适用场景
自发光 (Emissive) 材质自身发光,与光源无关 最直接、简单、常用,效果可控 可能看起来有点“假”,像霓虹灯,缺乏阴影细节 UI元素、发光体、魔法效果,或简单粗暴地提亮某个模型
调整基础色 (Color) 改变材质对光的反射率,调成更亮的颜色 简单,符合物理直觉 完全依赖现有光照,亮度上限受光源限制 当你只想让一个物体在同样光照下显得比其他物体更“白”或更“亮”时
调整PBR属性 改变 roughness (粗糙度) 和 metalness (金属度) 物理效果真实,能创造出质感差异 效果比较微妙,更侧重于质感而非单纯的亮度 调整金属、塑料、玻璃等不同材质的视觉表现,低粗糙度会产生更亮的高光
专用光源 + 图层 添加一个只影响特定物体的光源(如PointLight 效果真实,有光影和方向感 设置复杂,增加性能开销 精确的光照控制,如台灯只照亮书本,或给主角添加“天使光”

总结

要在 Three.js 中单独调亮一个模型,使用 material.emissive 是最推荐的首选方案

  • 若要保持原色仅提升亮度,请设置 emissive: 0xffffff (白色),并用 emissiveIntensity 精确调节。
  • 若要创造自发光物体或混合色彩,请将 emissive 设置为目标颜色。

理解“光照是乘法,自发光是加法”这一核心原理,将帮助你更自由地创造出丰富而逼真的三维视觉效果。现在,去施展你的光影魔法吧!

tauri项目,如何在rust端读取电脑环境变量

作者 1024小神
2025年6月9日 18:15

如果想在前端通过调用来获取环境变量的值,可以通过标准的依赖:

std::env::var(name).ok()

想在前端通过调用来获取,可以写一个command函数:

#[tauri::command]
pub fn get_env_var(name: String) -> Result<String, String> {
    println!("get_env_var: {}", name);
    std::env::var(name).map_err(|e| e.to_string())
}

注意如果拿不到可以获取所有的环境变量的值来看一下:

#[tauri::command]
pub fn get_env_var(name: String) -> Result<String, String> {
    println!("get_env_var: {}", name);
    println!("All environment variables:");
    for (key, value) in env::vars() {
        println!("{}: {}", key, value);
    }
    std::env::var(name).map_err(|e| e.to_string())
}

如果当你设置完,还是拿不到的话,建议重启电脑或者重新你的编辑器,因为:

当前运行的程序(包括你当前的命令行窗口或 Tauri 应用)不会自动获得这些新变量,因为它们的环境是进程启动时就固定下来的。

环境变量在操作系统中是每个进程独立拷贝的。

一句话解释JS链式调用

作者 PasserbyX
2025年6月9日 18:09

结论

实现链式调用的核心:每个方法执行完毕后返回对象本身(this)。

注意事项

  1. 链式调用适合不需要返回值的方法
  2. 过度使用链式可能会降低代码可读性
  3. 调试链式调用可能比普通调用更难

链式调用非常优雅,但是要注意不要滥用!

案例

题目1:基础实现题

题目:请实现一个简单的 StringBuilder 类,支持链式调用方法 append() 和 toString(),示例用法如下:

const sb = new StringBuilder();
const result = sb.append('Hello').append(' ').append('World').toString();
console.log(result); // 输出 "Hello World"
export class StringBuilder {
    constructor() {
        this.str = ''
    }
    append(str) {
        this.str += str
        return this
    }
    toString() {
        return this.str
    }
}

题目2:实现计算器

实现一个 Calculator 类,支持链式调用加减乘除运算,最后通过 value 属性获取结果。

export class Calculator {
    constructor() {
        this.value = 0;
    }
    add(number) {
        this.value += number
        return this
    }
    subtract(number) {
        this.value -= number
        return this
    }
    multiply(number) {
        this.value *= number
        return this
    }
    divide(number) {
        this.value /= number
        return this
    }
}

题目3:原理分析

  1. 为什么能实现链式调用?

因为调用某个具体的方法之后,最终返回了 this 本身。

  1. 如果要去掉链式调用特性,最少需要修改哪些地方?

只要去掉 return this

  1. 这种模式有什么优缺点?

优点:

  • 函数使用起来更加直观(可读性更强)
  • 更加简介、流畅

缺点

  • 调试相对困难,需要增加一些日志来简易调试。
  • 错误处理变得复杂
class Query {
  constructor(selector) {
    this.element = document.querySelector(selector);
  }

  css(property, value) {
    if (this.element) {
      this.element.style[property] = value;
    }
    return this;
  }

  addClass(className) {
    if (this.element) {
      this.element.classList.add(className);
    }
    return this;
  }

  on(event, callback) {
    if (this.element) {
      this.element.addEventListener(event, callback);
    }
    return this;
  }
}

// 使用示例
new Query('#myBtn')
  .css('color', 'red')
  .addClass('active')
  .on('click', () => console.log('Clicked!'));

JS 模块化

作者 古夕
2025年6月8日 00:32

一、JS 中的模块化规范

要明白我们的打包工具究竟做了什么,首先必须明白的一点就是 JS 中的模块化。
在 ES6 规范之前,我们有 CommonJS、AMD 等主流的模块化规范。

1、CommonJS(node.js 原生 & 同步加载 & 多次加载一次执行)

Node.js 是一个基于 V8 引擎,事件驱动 I/O 的服务端 JS 运行环境。
在 2009 年刚推出时,它就实现了一套名为 CommonJS 的模块化规范。

在 CommonJS 规范里:

  • 每个 JS 文件都是一个模块(module),每个模块内部都可以使用 require 函数和 module.exports 对象,来对模块进行导入和导出。

示例代码

// index.js
require('./moduleA');
const str = require('./moduleB');
console.log('index', str);

// moduleA.js
const timestamp = require('./moduleB');
setTimeout(() => console.log('moduleA', timestamp), 1000);

// moduleB.js
module.exports = new Date().getTime();

执行说明

  • index.js 代表的模块通过执行 require 函数,分别加载了相对路径为 ./moduleA./moduleB 的两个模块,同时输出 moduleB 模块的结果。
  • moduleA.js 文件内也通过 require 函数加载了 moduleB.js 模块,在 1s 后也输出了加载进来的结果。
  • moduleB.js 文件内部定义了一个时间戳,使用 module.exports 对象导出。

运行结果(执行 node index.js

index 1738743973675
moduleA 1738743973675

模块特性

  • 每个模块都是单例的,当一个模块被多次加载的时候,最终执行实际上只会被执行一次。

2、AMD(浏览器端 & 异步 & 多次加载一次执行)

另一个为 WEB 开发者所熟知的 JS 运行环境就是浏览器了。浏览器并没有提供像 Node.js 里一样的 require 方法。
不过,受到 CommonJS 模块化规范的启发,WEB 端还是逐渐发展起来了 AMD、SystemJS 规范等适合浏览器端运行的 JS 模块化开发规范。

AMD 全称 Asynchronous module definition,意为异步的模块定义,不同于 CommonJS 规范的同步加载,AMD 正如其名所有模块默认都是异步加载,这也是早期为了满足 web 开发的需要,因为如果在 web 端也使用同步加载,那么页面在解析脚本文件的过程中可能使页面暂停响应。

示例代码

// index.js
require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
    console.log('index', moduleB);
});

// moduleA.js
define(function(require) {
    const timestamp = require('moduleB');
    setTimeout(() => console.log('moduleA', timestamp), 1000);
});

// moduleB.js
define(function(require) {
    return new Date().getTime();
});

使用说明
如果想要使用 AMD 规范,我们还需要添加一个符合 AMD 规范的加载器脚本在页面中,符合 AMD 规范实现的库很多,比较有名的就是 require.js

3、ESModule

前面我们说到的 CommonJS 规范和 AMD 规范有这么几个特点:

  1. 语言上层的运行环境实现的模块化规范,模块化规范由环境自己定义。
  2. 相互之间不能兼容使用。例如不能在 Node.js 运行 AMD 模块,不能直接在浏览器运行 CommonJS 模块。

在 EcmaScript 2015(也就是我们常说的 ES6)之后,JS 有了语言层面的模块化导入导出语法以及与之匹配的 ESModule 规范。
使用 ESModule 规范,我们可以通过 importexport 两个关键词来对模块进行导入与导出。

示例代码(基于 ESModule 规范改写之前的例子)

// index.js
import './moduleA';
import str from './moduleB';
console.log(str);

// moduleA.js
import timestamp from './moduleB';
setTimeout(() => console.log('moduleA', timestamp), 1000);

// moduleB.js
export default new Date().getTime();

Webpack 配置(webpack.config.js

const path = require('path');

module.exports = {
    mode: 'none',
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'index.bundle.js', // 注意 filename 是小写不是 fileNane
        publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader', // 所有的 js 文件进 webpack 处理的过程中都使用 babel 进行编译
                    options: {
                        presets: ['@babel/preset-env'] // 支持使用环境参数来处理不同的内容
                    }
                }
            }
        ]
    }
};

依赖安装与运行

# 安装依赖
yarn add webpack webpack-cli @babel/core babel-loader @babel/preset-env 

# 编译打包
./node_modules/.bin/webpack 

# 启动静态服务器(可使用 Python 简单启动)
python3 -m http.server 

关于 JS 运行环境与解析器

每个 JS 的运行环境都有一个解析器,否则这个环境也不会认识 JS 语法。
它的作用就是用 ECMAScript 的规范去解释 JS 语法,也就是处理和执行语言本身的内容。
例如按照逻辑正确执行 var a = "123"; function func() { console.log("hahaha"); } 之类的内容。

在解析器的上层,每个运行环境都会在解释器的基础上封装一些环境相关的 API。
例如 Node.js 中的 global 对象、process 对象,浏览器中的 window 对象、document 对象等等。

这些运行环境的 API 规范各自规范的影响,例如浏览器端的 W3C 规范,它们规定了 window 对象和 document 对象上的 API 内容,以使得我们能让 document.getElementById 这样的 API 在所有浏览器上运行正常。

以上就是对 JS 模块化相关规范(CommonJS、AMD、ESModule )以及 JS 运行环境、解析器等相关内容的梳理 ,通过不同规范的对比,能更清晰理解在不同场景(Node.js 服务端、浏览器端等 )下模块化的实现与应用 。

如何将异步操作封装为Promise

作者 古夕
2025年6月8日 00:17

一、传统回调函数与Promise封装对比

1. 传统回调函数示例

// 原始异步函数(带回调)
function dynamicFunc(cb) {
    setTimeout(() => {
        console.log('1s 后显示');
        cb();
    }, 1000);
}

const callback = () => {
    console.log('在异步结束后 log');
}

// 调用方式:传入回调函数
dynamicFunc(callback);

2. 封装为Promise后的版本

// 封装为Promise的异步函数
function dynamicFuncAsync() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('1s 后显示');
            resolve();
        }, 1000);
    });
}

const callback = () => {
    console.log('在异步结束后 log');
}

// 调用方式:链式调用then
dynamicFuncAsync().then(callback);

二、AJAX请求的Promise封装实践

1. 传统AJAX回调写法

function ajax(url, success, fail) {
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = () => {
        if (this.readyState !== 4) return;
        if (this.status === 200) {
            success(this.response);
        } else {
            fail(new Error(this.statusText));
        }
    };
    client.send();
}

// 调用方式:传入成功/失败回调
ajax('/ajax.json', 
    () => console.log('成功'), 
    () => console.log('失败')
);

2. Promise封装后的AJAX函数

function ajaxAsync(url) {
    return new Promise((resolve, reject) => {
        const client = new XMLHttpRequest();
        client.open("GET", url);
        client.onreadystatechange = () => {
            if (this.readyState !== 4) return;
            if (this.status === 200) {
                resolve(this.response);
            } else {
                reject(new Error(this.statusText));
            }
        };
        client.send();
    });
}

// 调用方式:Promise链式处理
ajaxAsync('/ajax.json')
    .then(response => console.log('成功:', response))
    .catch(error => console.log('失败:', error.message));

三、核心封装原则总结

  1. 封装步骤关键点

    • 在函数内部返回一个新的Promise实例
    • 在原异步操作完成时,根据结果调用resolvereject
    • 异步操作的参数通过resolve/reject传递给后续.then回调
  2. 优势对比

    特性 传统回调函数 Promise封装
    代码可读性 多层嵌套易形成回调地狱 链式调用更清晰
    错误处理 需要层层传递错误回调 统一通过.catch捕获
    流程控制 难以实现并行/串行 支持Promise.all/race
  3. 最佳实践建议

    • 对所有异步API(如文件操作、网络请求)进行Promise封装
    • 保持封装函数的参数简洁(如ajaxAsync(url)而非ajaxAsync(url, method, data)
    • .catch中处理全局错误,避免Promise链中断

四、高级封装技巧

  1. 处理多参数回调

    // 原函数(返回多个结果)
    function processData(data, success, fail) {
        // 异步处理data
        success(result1, result2);
    }
    
    // Promise封装
    function processDataAsync(data) {
        return new Promise((resolve, reject) => {
            processData(data, 
                (res1, res2) => resolve({ result1: res1, result2: res2 }),
                error => reject(error)
            );
        });
    }
    
    // 调用时解构参数
    processDataAsync(data)
        .then(({ result1, result2 }) => { /* 处理多结果 */ })
    
  2. 封装Node.js风格的回调函数

    // Node.js传统回调:第一个参数为错误(error-first)
    const fs = require('fs');
    
    // 封装fs.readFile
    function readFileAsync(path) {
        return new Promise((resolve, reject) => {
            fs.readFile(path, (error, data) => {
                if (error) reject(error);
                resolve(data);
            });
        });
    }
    
    // 调用方式
    readFileAsync('/data.txt')
        .then(data => console.log('文件内容:', data))
        .catch(error => console.error('读取失败:', error));
    

五、总结:Promise封装的核心价值

  1. 代码结构优化
    将"回调地狱"转化为线性的链式调用,提升代码可维护性。

  2. 错误处理标准化
    通过统一的.catch机制处理异步错误,避免传统回调中错误传递的遗漏。

  3. 异步流程增强
    支持Promise组合操作(如Promise.all并行执行多个异步任务),简化复杂异步流程控制。

通过上述封装方式,任何异步操作都能转化为Promise接口,从而充分利用ES6异步编程的优势,让代码更简洁、更健壮。

HTML&CSS:3D图片切换效果

作者 前端Hardy
2025年6月9日 14:31

这个页面实现了一个具有 3D 效果的画廊展示,用户可以通过点击缩略图来切换显示的图像。页面使用了 Three.js 库来实现 3D 渲染和动画效果,整体设计风格现代且具有视觉吸引力。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

HTML&CSS

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
    <title>Document</title>
    <style>
        body,
        html {
            margin: 0;
            padding: 0;
            overflow: hidden;
            font-family: sans-serif;
            background: #ffdfc4;
        }

        .container-gallary {
            position: relative;
            width: 100vw;
            height: 100vh;
            background-image: url(https://img.blacklead.work/grid.svg)
        }

        .canvas-wrapper {
            position: absolute;
            top: 50%;
            left: 50%;
            width: 350px;
            height: 350px;
            transform: translate(-50%, -50%);
            clip-path: circle(50% at 50% 50%);
            overflow: hidden;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
        }

        .border-inside {
            position: absolute;
            top: 50%;
            left: 50%;
            width: 340px;
            height: 340px;
            border: 10px solid black;
            border-radius: 100%;
            transform: translate(-50%, -50%);
            clip-path: circle(50% at 50% 50%);
        }


        .border-outside {
            position: absolute;
            top: 50%;
            left: 50%;
            width: 364px;
            height: 364px;
            background: black;
            border-radius: 100%;
            transform: translate(-50%, -50%);
            clip-path: circle(50% at 50% 50%);
        }

        .border-outside::after {
            content: '';
            position: absolute;
            top: 50%;
            left: 50%;
            width: 354px;
            height: 354px;
            background-image: linear-gradient(180deg, #ffff82, #f4d2ba00 50%, #e8a5f3);
            border-radius: 100%;
            transform: translate(-50%, -50%);
            z-index: -1;
        }

        .thumbnails {
            position: absolute;
            bottom: 20px;
            right: 20px;
            display: flex;
            flex-direction: row;
            gap: 10px;
        }

        .thumbnail {
            display: flex;
            justify-content: center;
            align-items: center;
            position: relative;
            width: 74px;
            height: 105px;
            cursor: pointer;
            opacity: 0.6;
            overflow: hidden;
            transition: all 0.4s ease;
        }

        .thumbnail .frame {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-image: url("https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/6762b98cb5e68f0b74323e61_collection-card-frame.svg");
            background-size: cover;
            background-repeat: no-repeat;
            opacity: 0;
            transition: opacity 0.4s ease;
        }

        .thumbnail.active .frame,
        .thumbnail:hover .frame {
            opacity: 1;
        }

        .thumbnail.active {
            opacity: 1;
        }

        .thumbnail img {
            width: 66px;
            height: 99px;
            object-fit: cover;
        }
    </style>
</head>

<body>
    <div class="container-gallary">
        <div class="border-outside">
            <div class="canvas-wrapper" id="canvasWrapper">
                <span class="border-inside"></span>
            </div>
        </div>
        <div class="thumbnails" id="thumbnails"></div>
    </div>
    <script type="module">
        let renderer, scene, camera;
        let plane, material;
        let textures = [];
        let activeImage = 0;
        let transitionImage = null;
        let progress = 1;
        let isAnimating = false;

        const images = [
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c71c61f6db7df2e5218bc_collections-oranith-1.webp",
                title: "Image 1",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c71c6bd8971b3e73ee7c8_collections-anturax-1.webp",
                title: "Image 2",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c71c6648fdd5236d5b972_collections-oranith-2.webp",
                title: "Image 3",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c71c67e1e5c7edbcc0c3f_collections-anturax-3.webp",
                title: "Image 4",
            },
        ];
        const imagesThumbnail = [
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c7c7c41d8916da35baa9c_card-Oraniths-1.webp",
                title: "Image 1",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c7c7c65d779e7cfe7a75a_card-anturax-1.webp",
                title: "Image 2",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c7c7c5225fefdd3302e57_card-Oraniths-2.webp",
                title: "Image 3",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c7c7c8c0dbe0a8563fe55_card-anturax-3.webp",
                title: "Image 4",
            },
        ];

        const PIXELS = new Float32Array(
            [
                1, 1.5, 2, 2.5, 3, 1, 1.5, 2, 2.5, 3, 3.5, 4, 2, 2.5, 3, 3.5, 4, 4.5, 5,
                5.5, 6, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 20, 100,
            ].map((v) => v / 100)
        );

        function init() {
            const containerNext = document.getElementById("canvasWrapper");

            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
            camera.position.z = 10;

            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(350, 350);
            containerNext.appendChild(renderer.domElement);

            const loader = new THREE.TextureLoader();
            let loadCount = 0;
            images.forEach((img, idx) => {
                loader.load(img.url, (tex) => {
                    tex.minFilter = THREE.LinearFilter;
                    tex.magFilter = THREE.LinearFilter;

                    textures[idx] = tex;
                    loadCount++;
                    if (loadCount === images.length) {
                        createScene();
                        animate();
                    }
                });
            });

            createThumbnails();
        }

        function createScene() {
            const vertexShader = `
          varying vec2 vUv;
          void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `;

            const fragmentShader = `
          uniform float uTime;
          uniform vec3 uFillColor;
          uniform float uProgress;
          uniform float uType;
          uniform float uPixels[36];
          uniform vec2 uTextureSize;
          uniform vec2 uElementSize;
          uniform sampler2D uTexture;
          varying vec2 vUv;

          vec2 fade(vec2 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}
          vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
          vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
          vec3 fade3(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}

          float mapf(float value, float min1, float max1, float min2, float max2) {
            float val = min2 + (value - min1) * (max2 - min2) / (max1 - min1);
            return clamp(val, min2, max2);
          }

          float quadraticInOut(float t) {
            float p = 2.0 * t * t;
            return t < 0.5 ? p : -p + (4.0 * t) - 1.0;
          }

          void main() {
            vec2 uv = vUv - vec2(0.5);
            float aspect1 = uTextureSize.x/uTextureSize.y;
            float aspect2 = uElementSize.x/uElementSize.y;
            if(aspect1>aspect2){uv *= vec2( aspect2/aspect1,1.);}
            else{uv *= vec2( 1.,aspect1/aspect2);}
            uv += vec2(0.5);
            vec4 defaultColor = texture2D(uTexture, uv);

            if(uType==3.0){
              float progress = quadraticInOut(1.0-uProgress);
              float s = 50.0;
              float imageAspect = uTextureSize.x/uTextureSize.y;
              vec2 gridSize = vec2(
                s,
                floor(s/imageAspect)
              );

              float v = smoothstep(0.0, 1.0, vUv.y + sin(vUv.x*4.0+progress*6.0) * mix(0.3, 0.1, abs(0.5-vUv.x)) * 0.5 * smoothstep(0.0, 0.2, progress) + (1.0 - progress * 2.0));
              float mixnewUV = (vUv.x * 3.0 + (1.0-v) * 50.0)*progress;
              vec2 subUv = mix(uv, floor(uv * gridSize) / gridSize, mixnewUV);

              vec4 color = texture2D(uTexture, subUv);
              color.a =  mix(1.0, pow(v, 5.0) , step(0.0, progress));
              color.a = pow(v, 1.0);
              color.rgb = mix(color.rgb, uFillColor, smoothstep(0.5, 0.0, abs(0.5-color.a)) * progress);
              gl_FragColor = color;
            }
            gl_FragColor.rgb = pow(gl_FragColor.rgb,vec3(1.0/1.2));
          }
        `;

            material = new THREE.ShaderMaterial({
                vertexShader,
                fragmentShader,
                uniforms: {
                    uTime: { value: 0 },
                    uFillColor: { value: new THREE.Color("#000000") },
                    uProgress: { value: 1 },
                    uType: { value: 3 },
                    uPixels: { value: PIXELS },
                    uTextureSize: { value: new THREE.Vector2(1, 1) },
                    uElementSize: { value: new THREE.Vector2(1, 1) },
                    uTexture: { value: textures[activeImage] },
                },
                transparent: true,
            });

            material.uniforms.uTextureSize.value.set(
                textures[activeImage].image.width,
                textures[activeImage].image.height
            );

            const geometry = new THREE.PlaneGeometry(8.3, 8.3);
            plane = new THREE.Mesh(geometry, material);
            scene.add(plane);
        }

        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
            updateAnimation();
        }

        function updateAnimation() {
            if (transitionImage !== null && isAnimating) {
                progress += 0.015;

                if (
                    progress > 0.1 &&
                    material.uniforms.uTexture.value !== textures[transitionImage]
                ) {
                    material.uniforms.uTexture.value = textures[transitionImage];
                    material.uniforms.uTextureSize.value.set(
                        textures[transitionImage].image.width,
                        textures[transitionImage].image.height
                    );
                }

                if (progress >= 1) {
                    progress = 1;
                    activeImage = transitionImage;
                    transitionImage = null;
                    isAnimating = false;
                }
                material.uniforms.uProgress.value = progress;
            }
        }

        function createThumbnails() {
            const thumbsContainer = document.getElementById("thumbnails");
            imagesThumbnail.forEach((img, idx) => {
                const thumb = document.createElement("div");
                thumb.className = "thumbnail" + (idx === activeImage ? " active" : "");

                const thumbnailImg = document.createElement("img");
                thumbnailImg.src = img.url;
                thumbnailImg.alt = img.title;
                thumb.appendChild(thumbnailImg);

                const frame = document.createElement("div");
                frame.className = "frame";
                thumb.appendChild(frame);

                thumb.addEventListener("click", () => handleThumbnailClick(idx));

                thumbsContainer.appendChild(thumb);
            });
        }

        function handleThumbnailClick(index) {
            if (index === activeImage || isAnimating) return;
            transitionImage = index;
            progress = 0;
            isAnimating = true;

            const thumbs = document.querySelectorAll(".thumbnail");
            thumbs.forEach((t, i) => {
                t.classList.remove("active");
                if (i === index) t.classList.add("active");
            });
        }

        document.addEventListener("DOMContentLoaded", init);

    </script>
</body>

</html>

HTML

  • container-gallary:定义了一个画廊容器,包含一个 3D 渲染的画布和缩略图导航。
  • border-outside:定义了一个外部边框,包含一个画布容器和一个内部边框。
  • canvas-wrapper" id="canvasWrapper:定义了一个画布容器,用于显示 3D 内容。
  • border-inside:定义了一个内部边框。
  • thumbnails" id="thumbnails:定义了一个缩略图导航容器,包含多个缩略图项。

CSS

  • body, html:设置页面的外边距和内边距为 0,隐藏溢出内容,设置字体系列为无衬线字体,并定义背景颜色。
  • .container-gallary:定义了画廊容器的样式,包括宽度、高度和背景图像。
  • .canvas-wrapper:定义了画布容器的样式,包括位置、宽度、高度和变换效果。
  • .border-inside:定义了内部边框的样式,包括位置、宽度、高度、边框和圆角。
  • .border-outside:定义了外部边框的样式,包括位置、宽度、高度、背景颜色和圆角。
  • .border-outside::after:定义了外部边框的伪元素样式,用于创建渐变背景。
  • .thumbnails:定义了缩略图容器的样式,包括位置、底部和右侧的偏移、布局和间隙。
  • .thumbnail:定义了单个缩略图的样式,包括位置、宽度、高度、鼠标指针样式、透明度和过渡效果。
  • .thumbnail .frame:定义了缩略图框架的样式,包括位置、宽度、高度、背景图像和透明度。
  • .thumbnail.active:定义了活动缩略图的样式,包括透明度。
  • .thumbnail img:定义了缩略图图像的样式,包括宽度、高度和对象适应方式。

JavaScript

  • init():初始化 Three.js 场景、相机和渲染器,并加载纹理。
  • createScene():创建 3D 场景,包括几何体和着色器材质。
  • animate():渲染场景并更新动画。
  • updateAnimation():更新动画进度,控制图像切换效果。
  • createThumbnails():创建缩略图项并添加到页面。
  • handleThumbnailClick(index):处理缩略图点击事件,切换显示的图像。

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

✏️Canvas实现环形文字

2025年6月9日 14:09

画画.png

什么是环形文字?

搜索相关的图片可以看到

image-20250609131652149

从上面可以得知, 环形文字其实就是文字围绕一个中心点或一个圆形区域(如圆形徽章、硬币、印章、表盘、圆形按钮、图标等)进行布局.

在前端中,如果想要实现这种环形文字,我们可以借助canvas来实现,也可以用传统的css来实现。

这边采用的是cavans来实现

最终效果

image-20250609132324166

完整代码

<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>文字环绕</title>
    <style>
      html,
      body {
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      canvas {
        background: #1a1a1a;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas" height="800px" width="800px"></canvas>
    <script>
      const canvas = document.getElementById('canvas')
      const ctx = canvas.getContext('2d')
      const num = 360
      if (!ctx) console.log('上下文对象不存在')
      ctx.font = '24px 微软雅黑'
      ctx.fillStyle = '#fff'
      /**
       * @function 环形文本
       * @param text 文本
       * @params spacing 间距
       */
      const circularText = (
        text,
        spacing,
        options = {
          x: 400,
          y: 400,
          radius: 250
        }
      ) => {
        const num = 360
        for (let index = 0; index < num; index += spacing) {
          const startAngle = (Math.PI * index) / 180
          const angle = parseFloat(startAngle)
          ctx.save()
          ctx.beginPath()
          ctx.translate(
            options.x + Math.cos(angle) * options.radius,
            options.y + Math.sin(angle) * options.radius
          )
          ctx.rotate(Math.PI / 2 + angle)
          ctx.font = '24px 微软雅黑'
          ctx.fillStyle = '#fff'
          ctx.fillText(text, 0, 0)
          ctx.restore()
        }
      }
      circularText('/', 3.6)
      circularText('+', 10, {
        x: 400,
        y: 400,
        radius: 200
      })
      circularText('{', 20, {
        x: 400,
        y: 400,
        radius: 150
      })
      circularText('</>', 30, {
        x: 400,
        y: 400,
        radius: 100
      })
      const poetry = '桃之夭夭,灼灼其华。之子于归,宜其室家。'
      const total = num / 20
      // 汉字环绕
      for (let index = 0; index < total; index++) {
        const text = poetry.charAt(index % total)
        const startAngle = (Math.PI * index * 20) / 180
        const angle = parseFloat(startAngle)
        ctx.save()
        ctx.beginPath()
        ctx.translate(400 + Math.cos(angle) * 300, 400 + Math.sin(angle) * 300)
        ctx.rotate(Math.PI / 2 + angle)
        ctx.fillText(text, 0, 0)
        ctx.restore()
      }
      ;(function () {})()
    </script>
  </body>
</html>

实现思路

  1. 根据间距计算在环上面的位置,以及起始弧度
  2. 文字按照环形排列
  3. 对每一个字符,进行旋转
const total = num / 20
for (let index = 0; index < total; index++) {
    // 计算当前文字所在位置的角度(弧度制)。因为每个文字间隔20度,所以当前角度为`index * 20`度,然后转换为弧度(乘以π除以180)
    const startAngle = (Math.PI * index * 20) / 180
    // 确保是浮点数,可以省略
    const angle = parseFloat(startAngle)
   // 在通过sin和cos函数计算 对应的文字点在圆周上面的坐标
    ctx.fillText(
      'A',
      400 + Math.cos(angle) * 300,
      400 + Math.sin(angle) * 300
    )
  }

x=400 + Math.cos(angle) * 300

y=400 + Math.sin(angle) * 300

这个x,y就是文字坐标

公式如下:

x = 圆心的x坐标+Math.cos(弧度)*半径
y = 圆心的y坐标+Math.cos(弧度)*半径

最终画出来的效果是这样的

image-20250609134345431

这时候文字字符以及围成一个圆形了

但是文字本身还没有旋转到对应的角度,看起来就很奇怪。

这时候需要用到下面几个方法

  • save():用于存储当前绘制状态

  • translate():修改当前画布原点,这里要注意 这个方法修改的是整个画布的原点

  • rotate():根据画布原点旋转画布

  • restore():恢复最近的保存状态

旋转字符的步骤如下: 保存状态->将 字符坐标设置画布原点->旋转画布->恢复到上一次的画布绘制状态

ctx.save()
ctx.beginPath()
ctx.translate(400 + Math.cos(angle) * 300, 400 + Math.sin(angle) * 300)
ctx.rotate(Math.PI / 2 + angle)
ctx.fillText(text, 0, 0)
ctx.restore()

之前我们将文本按圆心环绕绘制了,那么这里就按前面获取到的字符坐标设置原点

完整代码如下:

<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>文字环绕</title>
    <style>
      html,
      body {
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      canvas {
        background: #1a1a1a;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas" height="800px" width="800px"></canvas>
    <script>
      const canvas = document.getElementById('canvas')
      const ctx = canvas.getContext('2d')
      const num = 360
      if (!ctx) console.log('上下文对象不存在')
      ctx.font = '24px 微软雅黑'
      ctx.fillStyle = '#fff'
      const total = num / 20
      // 汉字环绕
      for (let index = 0; index < total; index++) {
        const startAngle = (Math.PI * index * 20) / 180
        const angle = parseFloat(startAngle)
        ctx.save()
        ctx.beginPath()
        ctx.translate(400 + Math.cos(angle) * 300, 400 + Math.sin(angle) * 300)
        ctx.rotate(Math.PI / 2 + angle)
        ctx.fillText('A', 0, 0)
        ctx.restore()
      }
      ;(function () {})()
    </script>
  </body>
</html>

画出来的效果就是这样的

image-20250609135626030.png

结尾

到这里环形文本的实现方式已经结束了,下一篇文章我打算用这些知识实现mdn文档官网首页的这个效果

image-20250609140005614

Vue 表情包输入组件实现代码及完整开发流程解析

作者 spionbo
2025年6月9日 13:46

Vue 表情包输入组件的实现方案与应用实例

表情包已成为现代社交应用不可或缺的元素,为用户交流增添了丰富的情感表达。本文将详细介绍如何在 Vue 项目中实现一个功能完善、交互友好的表情包输入组件,并探讨其应用场景。

组件设计思路

表情包输入组件的核心功能是让用户能够便捷地选择表情,并将其插入到输入框中。设计时需要考虑以下几个方面:

  • 表情分类展示:将表情按类别分组,方便用户查找
  • 响应式布局:在不同设备上都能良好展示
  • 与输入框交互:将选中的表情插入到输入框当前光标位置
  • 样式定制:支持自定义主题和布局

技术实现方案

下面介绍实现这个组件的关键技术点:

// EmojiPicker.vue 核心代码
export default {
  data() {
    return {
      activeCategory: 'recent',
      emojiCategories: [], // 表情分类数据
      recentEmojis: [], // 最近使用的表情
      searchText: '', // 搜索文本
      isOpen: false // 控制组件显示/隐藏
    }
  },
  methods: {
    // 选择表情时的处理函数
    selectEmoji(emoji) {
      // 将表情添加到最近使用列表
      this.addToRecent(emoji);
      // 触发事件,通知父组件用户选择了表情
      this.$emit('select', emoji);
    },
    
    // 搜索表情
    searchEmojis() {
      // 根据搜索文本过滤表情
    },
    
    // 切换表情分类
    switchCategory(category) {
      this.activeCategory = category;
    },
    
    // 添加表情到最近使用列表
    addToRecent(emoji) {
      // 更新最近使用表情逻辑
    }
  }
}

组件的模板结构主要包含:表情分类导航栏、表情展示区域、搜索框和最近使用表情区域。通过 Tailwind CSS 实现样式布局,确保组件在各种设备上都有良好的显示效果。

与输入框的交互实现

实现表情包与输入框的交互是关键部分,主要解决两个问题:

  1. 如何将表情插入到输入框的当前光标位置
  2. 如何在用户点击其他区域时关闭表情选择器
// InputWithEmoji.vue
export default {
  data() {
    return {
      message: '',
      showEmojiPicker: false
    }
  },
  methods: {
    // 插入表情到输入框
    insertEmoji(emoji) {
      const textarea = this.$refs.textarea;
      const start = textarea.selectionStart;
      const end = textarea.selectionEnd;
      
      // 将表情插入到当前光标位置
      this.message = this.message.substring(0, start) + 
                    emoji + 
                    this.message.substring(end);
      
      // 更新光标位置
      this.$nextTick(() => {
        textarea.focus();
        textarea.selectionStart = textarea.selectionEnd = start + emoji.length;
      });
    },
    
    // 切换表情选择器显示状态
    toggleEmojiPicker() {
      this.showEmojiPicker = !this.showEmojiPicker;
    },
    
    // 点击外部区域关闭表情选择器
    handleClickOutside(event) {
      if (!this.$refs.container.contains(event.target)) {
        this.showEmojiPicker = false;
      }
    }
  },
  mounted() {
    // 添加点击外部区域的监听
    document.addEventListener('click', this.handleClickOutside);
  },
  beforeDestroy() {
    // 移除监听,防止内存泄漏
    document.removeEventListener('click', this.handleClickOutside);
  }
}

应用实例

下面是一个完整的应用示例,展示如何在聊天界面中集成表情包输入组件:

<!-- ChatApp.vue -->
<template>
  <div class="chat-app">
    <!-- 聊天消息区域 -->
    <div class="messages-container">
      <!-- 消息列表 -->
      <MessageList :messages="messages" />
    </div>
    
    <!-- 输入区域 -->
    <div class="input-area">
      <div class="emoji-button" @click="toggleEmojiPicker">
        <i class="fas fa-smile"></i>
      </div>
      
      <textarea 
        v-model="message" 
        ref="textarea" 
        placeholder="输入消息..."
      ></textarea>
      
      <button @click="sendMessage">发送</button>
      
      <!-- 表情包选择器 -->
      <EmojiPicker 
        v-if="showEmojiPicker"
        @select="insertEmoji"
      />
    </div>
  </div>
</template>

性能优化与扩展

为了确保组件性能和扩展性,可以考虑以下几点:

  1. 表情数据懒加载:对于大量表情数据,采用按需加载策略
  2. 虚拟滚动:对于表情数量较多的情况,使用虚拟滚动提升性能
  3. 自定义主题:通过 CSS 变量或配置选项支持主题定制
  4. 国际化支持:支持不同语言环境下的表情分类名称

通过以上方案实现的表情包输入组件,可以轻松集成到各种 Vue 应用中,为用户提供丰富的表情输入体验。在实际应用中,还可以根据项目需求进一步扩展功能,如添加自定义表情、GIF 搜索等高级功能。


Vue, 表情包输入组件,前端开发,组件实现,JavaScript,HTML,CSS, 热门关键字,Vue 组件开发,输入组件开发,表情包组件,完整开发流程,前端组件,Web 开发,Vue.js



资源地址: pan.quark.cn/s/a85a3b247…


深入理解JavaScript原型机制:从Java到JS的面向对象编程之路

2025年6月9日 12:55

前言

作为一名前端开发者,相信你一定遇到过这样的困惑:为什么JavaScript没有传统的类概念,却能实现面向对象编程?为什么要用function来模拟类?__proto__prototype到底有什么区别?

今天我们就来深入探讨JavaScript独特的原型机制,对比传统面向对象语言(如Java),让你彻底理解JS中的面向对象编程。

传统面向对象 vs JavaScript面向对象

传统OOP:以Java为例

让我们先看一个简单的Java类定义:

// 定义Puppy类
public class Puppy{
    // 成员变量
    int puppyAge;
    // 构造方法
    public Puppy(int age){  // 注意:这里应该声明参数类型
        puppyAge = age;
    }
    // 公有方法
    public void say(){
        System.out.println("汪汪汪");  // 小狗应该汪汪叫,不是喵喵叫哦
    }
}

在Java中,我们有明确的类概念,通过class关键字定义类,类中包含属性和方法,这就是经典的面向对象编程三大特性:封装、继承、多态

JavaScript的困境:没有class的时代

在ES6之前,JavaScript并没有class关键字,但作为企业级开发语言,JS必须支持面向对象编程。那么问题来了:没有类,如何实现面向对象?

最初的尝试可能是这样的:

// 对象字面量方式
var Person = {
    name: '胡一菲',
    hobbies: ['音乐','电影','钓鱼']
}

var pll = {
    name: '黄少天',
    hobbies: ['音乐','篮球','游戏']
}

这种方式的问题显而易见:创建大量相似对象时非常麻烦,代码重复严重

JavaScript的解决方案:构造函数 + 原型

构造函数:让function身兼两职

JavaScript采用了一种巧妙的设计:让函数既是函数,又是类

// 首字母大写的约定:1.类的概念 2.构造函数
function Person(name, age){
    // this 指向当前实例化对象
    this.name = name
    this.age = age
}

// 函数对象的原型对象
Person.prototype = {
    sayHello: function(){
        console.log(`Hello, my name is ${this.name}`)
    }
}

// new 一下,实例化对象
let hu = new Person('黄少天', 20)
hu.sayHello()  // Hello, my name is 黄少天

关键概念解析

  1. 构造函数:首字母大写的函数,用new操作符调用
  2. 实例对象:通过new创建的对象
  3. 原型对象:每个函数都有prototype属性,指向原型对象
  4. 原型链:通过__proto__属性连接的对象链

深入理解原型机制

__proto__ vs prototype

这是最容易混淆的概念:

function Person(name, age){
    this.name = name
    this.age = age
}

Person.prototype.sayHello = function(){
    console.log(`Hello, my name is ${this.name}`)
}

var hu = new Person('黄少天', 20)

// 关键理解:
console.log(hu.__proto__ === Person.prototype)  // true
console.log(Person.prototype.constructor === Person)  // true

重点理解

  • __proto__:每个对象都有的私有属性,指向其构造函数的原型对象
  • prototype:每个函数都有的属性,值是该构造函数的原型对象

原型链的神奇之处

JavaScript的原型机制最强大的地方在于:对象和构造函数之间没有血缘关系

var hu = new Person('黄少天', 20)
console.log(hu.__proto__)  // Person.prototype

// 我们可以动态改变原型指向!
var a = {
    name: '孔子',
    eee: '鹅鹅鹅',
    country: '中国'
}

hu.__proto__ = a
console.log(hu.country)  // 中国
console.log(hu.eee)      // 鹅鹅鹅

这种设计让JavaScript的面向对象更加灵活:

  • 对象的原型可以动态改变
  • 不依赖类的继承关系
  • 通过原型链实现属性和方法的查找

new操作符的工作原理

理解new的执行过程对掌握原型机制至关重要:

new的执行步骤:
1. new -> 创建空对象{}
2. 执行constructor构造函数
3. this指向新创建的对象
4. 构造函数执行完毕,返回对象
5. 设置__proto__指向constructor.prototype
6. 形成原型链,最终指向null

用代码表示就是:

// new Person('黄少天', 20) 的内部实现
function myNew(constructor, ...args) {
    // 1. 创建空对象
    let obj = {}
    
    // 2. 设置原型链
    obj.__proto__ = constructor.prototype
    
    // 3. 执行构造函数
    let result = constructor.apply(obj, args)
    
    // 4. 返回对象
    return result instanceof Object ? result : obj
}

原型链查找机制

当我们访问对象的属性时,JavaScript会按照原型链进行查找:

let hu = new Person('黄少天', 20)

// 访问hu.toString()的查找过程:
// 1. 在hu对象本身查找toString方法 -> 没找到
// 2. 在hu.__proto__(Person.prototype)中查找 -> 没找到  
// 3. 在Person.prototype.__proto__(Object.prototype)中查找 -> 找到了!
// 4. 如果还没找到,继续向上直到null

console.log(hu.__proto__)           // Person.prototype
console.log(hu.__proto__.__proto__) // Object.prototype
console.log(hu.toString())          // [object Object] - 来自Object.prototype

实际应用:原型继承

基于原型机制,我们可以实现继承:

// 父类
function Animal(name) {
    this.name = name
}

Animal.prototype.eat = function() {
    console.log(`${this.name} is eating`)
}

// 子类
function Dog(name, breed) {
    Animal.call(this, name)  // 调用父类构造函数
    this.breed = breed
}

// 设置原型继承
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog

Dog.prototype.bark = function() {
    console.log(`${this.name} is barking`)
}

let myDog = new Dog('旺财', '哈士奇')
myDog.eat()   // 旺财 is eating - 继承自Animal
myDog.bark()  // 旺财 is barking - Dog自己的方法

总结

JavaScript的原型机制虽然看起来复杂,但理解了核心概念后会发现它的强大之处:

核心要点

  1. JavaScript本没有类,用首字母大写的函数来表示类
  2. 构造函数身兼两职:既是函数又是类
  3. 原型链是灵魂:通过__proto__连接对象,实现属性和方法的继承
  4. 动态性是优势:对象的原型可以动态改变,比传统继承更灵活

关键区别

  • 传统OOP:类 -> 实例,关系固定
  • JavaScript OOP:构造函数 -> 实例,通过原型链连接,关系动态

实用建议

  1. 理解__proto__prototype的区别
  2. 掌握原型链的查找机制
  3. 学会使用原型实现继承
  4. 在现代开发中,可以使用ES6的class语法,但理解底层原型机制仍然重要

虽然ES6引入了class关键字,让JavaScript看起来更像传统面向对象语言,但底层仍然是基于原型的。理解原型机制不仅能帮你写出更好的代码,也能让你在面试中脱颖而出!

uniapp图片上传添加水印/压缩/剪裁

2025年6月9日 11:19

一、前言

最近遇到一个需求,微信小程序上传图片添加水印的需求,故此有该文章做总结, 功能涵盖定理地位,百度地址解析,图片四角水印,图片压缩,图片压缩并添加水印,图片剪裁,定位授权,保存图片到相册等

二、效果

4.gif

三、代码实现核心

3.1)添加水印并压缩 核心实现

// 添加水印并压缩
export function addWatermarkAndCompress(options, that, isCompress = false) {
return new Promise((resolve, reject) => {
const {
errLog,
config
} = dealWatermarkConfig(options)

that.watermarkCanvasOption.width = 0
that.watermarkCanvasOption.height = 0
if (!errLog.length) {
const {
canvasId,
imagePath,
watermarkList,
quality = 0.6
} = config

uni.getImageInfo({ // 获取图片信息,以便获取图片的真实宽高信息
src: imagePath,
success: (info) => {
const {
width: oWidth,
height: oHeight,
type,
orientation
} = info; // 获取图片的原始宽高
const fileTypeObj = {
'jpeg': 'jpg',
'jpg': 'jpg',
'png': 'png',
}
const fileType = fileTypeObj[type] || 'png'

let width = oWidth
let height = oHeight

if (isCompress) {
const {
cWidth,
cHeight
} = calcRatioHeightAndWight({
oWidth,
oHeight,
quality,
orientation
})

// 按对折比例缩小
width = cWidth
height = cHeight
}

that.watermarkCanvasOption.width = width
that.watermarkCanvasOption.height = height

that.$nextTick(() => {
// 获取canvas绘图上下文
const ctx = uni.createCanvasContext(canvasId, that);
// 绘制原始图片到canvas上
ctx.drawImage(imagePath, 0, 0, width, height);
// 绘制水印项
const drawWMItem = (ctx, options) => {
const {
fontSize,
color,
text: cText,
position,
margin
} = options
// 添加水印
ctx.setFontSize(fontSize); // 设置字体大小
ctx.setFillStyle(color); // 设置字体颜色为红色

if (isNotEmptyArr(cText)) {
const text = cText.filter(Boolean)
if (position.startsWith('bottom')) {
text.reverse()
}
text.forEach((str, ind) => {
const textMetrics = ctx.measureText(str);
const {
calcX,
calcY
} = calcPosition({
height,
width,
position,
margin,
ind,
fontSize,
textMetrics
})
ctx.fillText(str, calcX, calcY, width);
})
} else {
const textMetrics = ctx.measureText(cText);

const {
calcX,
calcY
} = calcPosition({
height,
width,
position,
margin,
ind: 0,
fontSize,
textMetrics
})
// 在图片底部添加水印文字
ctx.fillText(text, calcX, calcY, width);
}
}

watermarkList.forEach(ele => {
drawWMItem(ctx, ele)
})

// 绘制完成后执行的操作,这里不等待绘制完成就继续执行后续操作,因为我们要导出为图片
ctx.draw(false, () => {
// #ifndef MP-ALIPAY
uni.canvasToTempFilePath({ // 将画布内容导出为图片
canvasId,
x: 0,
y: 0,
width,
height,
fileType,
quality, // 图片的质量,目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。
destWidth: width,
destHeight: height,
success: (res) => {
console.log('res.tempFilePath', res)
resolve(res.tempFilePath)
},
fail() {
reject(false)
}
}, that);
// #endif

// #ifdef MP-ALIPAY
ctx.toTempFilePath({ // 将画布内容导出为图片
canvasId,
x: 0,
y: 0,
width: width,
height: height,
destWidth: width,
destHeight: height,
quality,
fileType,
success: (res) => {
console.log('res.tempFilePath', res)
resolve(res.tempFilePath)
},
fail() {
reject(false)
}
}, that);
// #endif 
});
})
}
});
} else {
const errStr = errLog.join(';')
showMsg(errStr)
reject(errStr)
}
})
}

3.2)剪切图片

// 剪切图片
export function clipImg(options, that) {
return new Promise((resolve, reject) => {
const {
errLog,
config
} = dealClipImgConfig(options)

that.watermarkCanvasOption.width = 0
that.watermarkCanvasOption.height = 0

if (!errLog.length) {
const {
canvasId,
imagePath,
cWidth,
cHeight,
position
} = config

// 获取图片信息,以便获取图片的真实宽高信息
uni.getImageInfo({
src: imagePath,
success: (info) => {
const {
width,
height
} = info; // 获取图片的原始宽高

// 自定义剪裁范围要在图片内
if (width >= cWidth && height >= cHeight) {

that.watermarkCanvasOption.width = width
that.watermarkCanvasOption.height = height
that.$nextTick(() => {
// 获取canvas绘图上下文
const ctx = uni.createCanvasContext(canvasId, that);

const {
calcSX,
calcSY,
calcEX,
calcEY
} = calcClipPosition({
cWidth,
cHeight,
position,
width,
height
})

// 绘制原始图片到canvas上
ctx.drawImage(imagePath, 0, 0, width, height);

// 绘制完成后执行的操作,这里不等待绘制完成就继续执行后续操作,因为我们要导出为图片
ctx.draw(false, () => {
// #ifndef MP-ALIPAY
uni.canvasToTempFilePath({ // 将画布内容导出为图片
canvasId,
x: calcSX,
y: calcSY,
width: cWidth,
height: cHeight,
destWidth: cWidth,
destHeight: cHeight,
success: (res) => {
console.log('res.tempFilePath',
res)
resolve(res.tempFilePath)
},
fail() {
reject(false)
}
}, that);
// #endif

// #ifdef MP-ALIPAY
ctx.toTempFilePath({ // 将画布内容导出为图片
canvasId,
x: 0,
y: 0,
width: width,
height: height,
destWidth: width,
destHeight: height,
// fileType: 'png',
success: (res) => {
console.log('res.tempFilePath',
res)
resolve(res.tempFilePath)
},
fail() {
reject(false)
}
}, that);
// #endif 
});
})
} else {
return imagePath
}
}
})

} else {
const errStr = errLog.join(';')
showMsg(errStr)
reject(errStr)
}
})
}

3.3)canvas画布标签

<!-- 给图片添加的标签 -->
<canvas v-if="watermarkCanvasOption.width > 0 && watermarkCanvasOption.height > 0"
:style="{ width: watermarkCanvasOption.width + 'px', height: watermarkCanvasOption.height + 'px' }"
canvas-id="watermarkCanvas" id="watermarkCanvas" style="position: absolute; top: -10000000rpx;" />

以上代码具体的实现功能不做一一讲解,详细请看下方源码地址

四、源码地址

github: github.com/ArcherNull/…

五、总结

  • 图片的操作,例如压缩/剪裁/加水印都是需要借助canvas标签,也就是说需要有canvas实例通过该api实现这些操作
  • 当执行 ctx.drawImage(imagePath, 0, 0, width, height) 后,后续的操作的是对内存中的数据,而不是源文件

完结撒花,如果对您有帮助,请一键三连

仅仅是发送一封邮件?暴露安全边界!

2025年6月9日 10:52

🕳️ 背景:从简单的“发封邮件”开始

本来只是个普通的需求:

“用脚本写一个,给用户发封提醒邮件就行,流程简单点。”

我想都没多想,就接入了邮箱的 SMTP 服务,并愉快地获取了邮箱授权码(对,就是那个不是密码,但比密码更危险的东西)。然后写下了这段代码——请注意,这是错误示范:

// frontend/mail.js(错误示范!!!)
const smtpToken = "xqyuafxxxxxxbx"; // 邮箱授权码

fetch("https://smtp.qq.com/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${smtpToken}`,
  },
  body: JSON.stringify({
    to: "xxx@qq.com",
    subject: "Test",
    text: "Hello from frontend",
  })
});

localhost 上一切正常,邮件发出、无异常。直到我部署到了生产环境,点开浏览器 DevTools 的那一刻,头皮发麻:

  • Token 明文出现在请求 header 中
  • token 字符串直接写死在 JS 文件里
  • Chrome Sources 面板能轻松全文搜索出 smtpToken

这意味着——任何访问我网站的人,都能顺手牵羊进我邮箱

我试着用 curl 复现了一下,只要有这个 token,连密码都不需要,直接能登录邮箱,读取邮件、伪造发信、批量投毒,真·一秒社会工程。

为什么这是致命的安全漏洞?

邮箱授权码 = “绕过密码的万能钥匙”:

  • 对于 QQ、163 等邮箱,一旦授权码泄露,相当于失去了邮箱控制权
  • 邮箱常被用于系统找回密码、验证码校验、用户注册绑定,一旦被入侵,牵一发而动全身
  • 如果资源被收录(如 GitHub Copilot、Shodan),攻击者可以批量扫描、自动化提取 token,用于攻击

🧠 错误的本质:混淆了“前端能看到”与“后端能做”的边界

我当时想都没想的逻辑是这样的:

“我前端只是在发个请求,token 我加密一下不就行了?”

这是典型的开发者误区: 前端加密 = 把锁和钥匙一起交出去。

前端代码运行在用户设备上,任何 JS、加密逻辑、混淆方法都逃不过 DevTools、F12、抓包器和爬虫——你终究得把 token 还给用户设备,那就是等于公开

✅ 正确的做法:前端发起“意图”,后端执行“动作”。我们要遵守 “最小信任原则”

🟢 前端代码(只发送意图)

fetch("/api/send-email", {
  method: "POST",
  body: JSON.stringify({
    to: "user@example.com",
    subject: "你好",
    body: "邮件内容"
  })
});

🟢 后端代码(执行安全操作)

def send_email(to, subject, body):
    server = smtplib.SMTP_SSL("smtp.qq.com", 465)
    server.login("your@email.com", os.getenv("EMAIL_KEY"))
    server.sendmail(...省略...)

关键点:

  • 授权码保存在后端 .env 文件,不出现在任何 JS 文件中
  • 发送邮件动作由后端执行,可加权限、频控、黑名单
  • 前端只负责触发意图,无需关心 SMTP、Token、登录

🧰 安全架构图:前端 <-> 后端 <-> 邮箱服务器

image.png


🧪 安全版代码 Demo

🔐 .env 文件(服务器本地)

EMAIL_USER=your@qq.com
EMAIL_PASS=your_authorization_code
SMTP_SERVER=smtp.qq.com
SMTP_PORT=465

⚠️ 一定不要提交 .env 到 Git 仓库,加到 .gitignore

🖥️ Flask 后端(app.py)

from flask import Flask, request, jsonify
from flask_cors import CORS
from email.mime.text import MIMEText
import smtplib, os
from dotenv import load_dotenv

load_dotenv()
app = Flask(__name__)
CORS(app)

EMAIL_USER = os.getenv("EMAIL_USER")
EMAIL_PASS = os.getenv("EMAIL_PASS")
SMTP_SERVER = os.getenv("SMTP_SERVER")
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))

@app.route("/api/send-email", methods=["POST"])
def send_email():
    data = request.get_json()
    to, subject, body = data.get("to"), data.get("subject"), data.get("body")

    if not to or not subject or not body:
        return jsonify({"success": False, "error": "缺少字段"}), 400

    try:
        msg = MIMEText(body, "plain", "utf-8")
        msg["Subject"], msg["From"], msg["To"] = subject, EMAIL_USER, to

        server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT)
        server.login(EMAIL_USER, EMAIL_PASS)
        server.sendmail(EMAIL_USER, [to], msg.as_string())
        server.quit()

        return jsonify({"success": True})
    except Exception as e:
        return jsonify({"success": False, "error": str(e)}), 500

if __name__ == "__main__":
    app.run(port=5001)

📋 安全编码 CheckList(务必贴墙上!)

项目 能否出现在前端 说明
接口地址 ✅ 可以 推荐 proxy 转发
公钥 ✅ 可以 非对称加密可用
私钥/授权码 ❌ 禁止 一旦泄露等于暴露控制权
AccessToken 🚫 限制使用 建议短时 + 刷新
OAuth Code 🚫 禁止 不要保存在前端资源中
SMTP Token ❌ 禁止 只能后端持有

🧩 小结

安全不是“上线之后加一层校验”,而是“写代码那一刻就该想好的边界”。

OK,以上便是本次分享~

欢迎加我:atar24,交个盆友,我会第一时间通过

突破性能瓶颈:基于虚拟滚动的大数据需求文档方案——告别卡顿与分页,实现Word级流畅体验

作者 bo52100
2025年6月9日 10:07

背景描述:

在前后端分离架构下,需求文档功能常面临两大核心痛点:

  1. 大数据展示难题:传统分页加载会打断用户浏览连续性,而一次性渲染海量数据(如10万+条目)又会导致DOM爆炸、页面卡顿
  2. 交互体验割裂:现有方案(如懒加载)仍需等待数据分块请求,无法实现类似Word的“无感知”无限滚动,影响用户沉浸感

当前市面主流方案(如分页、简单无限滚动)均存在性能或体验缺陷:

  • 分页:需手动切换页面,破坏长文档阅读流畅性[7]。
  • 基础无限滚动:滚动时DOM节点持续堆积,最终导致内存溢出。
  • 图表集成场景:若每行含动态图表(如ECharts),传统渲染会因重排/重绘引发严重卡顿。

本方案的创新价值

  • 虚拟滚动技术:仅渲染可视区域DOM(如屏幕内20条),动态计算滚动位置替换内容,实现10万+数据下的60FPS流畅滚动。

  • 前后端协同优化:

    • 前端:虚拟列表+动态高度计算(支持图文混排),结合Intersection Observer精准触发加载。

    • 后端:GraphQL分片查询(按需返回偏移量数据),减少网络传输压力。

      • 前端直接做数据切割也可以
  • “Word式”体验:无分页打断、无滚动白屏,支持快速定位与实时筛选。

二:开发过程中的各个原因分析

1、初次加载缓慢

1.1 、vxetable问题

导致此问题的原因是为解决不定高滚动条定位问题,初次加载时将所有数据都手动滚动了一遍导致首次加载很慢,

  • 手动触发滚动所有数据的原因:

    • 由于未加载的行是无法知道行高的。虚拟算法原理是估算虚拟行的行高,通过已渲染的行来纠正行高值。只有当所有数据被查看过一次之后才能达到和同等行高一样的效果。否则快速拉动滚动时会出现鼠标偏移,效果上会比同等行高略差。

好处:

对滚动条定位较为精准

坏处:导致首次加载缓慢,数据量越大,速度越慢,

1.2 树形结构问题

树形结构未采用虚拟树形,也会引发卡顿问题

2、定位滚动条较慢

定位较慢是因为每次定位的时候需要加载本次虚拟表格所需的所有dom,大概8条左右,每个dom存在的内容不一,数据较多时就会存在定位卡顿问题。

3、页面操作较为卡顿

此问题是由以上俩问题综合导致

三:解决方案

优化本质思路:多次优化经验来看以及结合浏览器的当前状况以及chrome提出的优化模型来看,个人感觉前端优化本质从某种意义上来说可以总结为*”规避“*二字。

  • 规避性能瓶颈,避免让浏览器因为一些低效操作而卡顿或崩溃。
  • 规避不必要的资源消耗,减少加载时间、内存占用和带宽消耗。
  • 规避用户体验上的问题,确保页面交互流畅、响应迅速。
  • 规避架构和设计上的问题,使得代码更加可维护、可扩展,减少潜在的 Bug 和性能隐患。

我们现在卡顿基本上总结为短时间内渲染了大量dom

首先将树形改为虚拟树形

改为虚拟树形后可以大幅度减少dom的渲染量

优点:

  • 一定程度解决卡顿问题

不足:

  • 无法解决定位卡顿问题

  • 没有拖动功能

    • 见虚拟树形支持拖动改造文档
  • 没有新增时的占位符功能

    • 见虚拟树形支持拖动改造文档
  • 本身存在的问题

    • 滚动条拖动时整体都被拖动了
    • 右击新增时定位不准

定位卡顿存在不定高问题以及显示内容可能较多引起的卡顿问题。

  • 若想解决显示内容较多引起的卡顿问题,我们可以采用懒加载
  • 若想解决不定高就需要手动触发所有数据的滚动,但是此操作会导致初次加载缓慢;会陷入死循环,所以我们要做*“规避”*

所以衍生出以下几种方案:

方案一:不解决首次加载缓慢,将内容改为懒加载

效果:见演示

优点:

  • 定位速度快,基本上秒定

缺点:

  • 因为加了懒加载,及时手动滚动了一遍,定位仍然不准确
  • 首次加载缓慢

结论:

不能采用

方案二:内容定高

效果:见演示

优点:

  • 定位速度快,基本上秒定
  • 定位最为精准
  • 首次加载迅速,接口响应时页面立马加载

缺点:

  • 定高没有自适应好看,而且可能会出现内部滚动条

建议:

  • 1、直接去掉滚动条,每次只展示一章节内容

  • 2、提供两两种模式

    • 不能跳转,但是能像当前这样查看所有章节
    • 能跳转,但是每次只能查看一章

结论:

不能采用,此方案达不到预期效果,且已有其他平台实现了此功能,例如cb

查看cb需求文档功能,进行思路解析:

  • 一:cb点击左侧菜单栏时有两种现象

    • 1、出现加载中的提示
    • 2、直接定位到内容区
  • 二:cb滚动条有两种现象

    • 1、正常上下滚动
    • 2、到了边界线时出现加载内容的loading提示
  • 三:从控制台观察cb的渲染量

    • 存在最大数量,比如30,超过此数量时会将内容进行替换,不超过时会对内容进行补充
  • 四:network观察点击现象

    • 1、点击菜单栏会进行http请求,且返回的内容为html,此操作可以看着是ssr渲染了,前端压力很小。

结论:

cb平台是采用了分页概念以及ssr手段*,从而达到了流畅的效果,在交互时若跨度较大或超出上限,会存在loading反馈,提示用户正在加载中。*

我们项目本身情况:

  • vue框架、单页面应用。
  • 当前数据是一口气返回来的(后端也可配合修改,当前并无必要,但是当前后端请求时间较久,需优化)。
  • 未采用ssr渲染,若采用ssr渲染,我们的框架体系要多一个node服务端。

总结:

  • 也采用分页概念
  • 内容区无ssr,采用懒加载
  • 此时理论情况上只需解决定位精准问题即可

根据总结得到以下方案三

方案三:树形懒加载+内容区分页+内容区懒加载

效果:见演示

优点:

  • 定位速度快,基本上秒定
  • 定位是所有方案包含改动前的效果中最精准的
  • 首次加载迅速,接口响应时页面立马加载
  • 内容高度自适应,符合需求

缺点:

  • 滚动加载后定位不是很准确

    • 经过算法优化已经较为精准

在实现过程中使用了一些 nextTick、setTimeout的API,来确保当前内容加载完成可以进行滚动了。

浏览器渲染机制:浏览器会在 JavaScript 执行时进行页面的渲染更新,但有时渲染过程不会立即进行,特别是在多次 DOM 更新后,浏览器会合并多个布局计算和渲染任务。nextTick 只确保 DOM 更新完成,但并不保证浏览器已经重新渲染和计算了布局。

nextTick 执行时机:即使 nextTick 回调在 DOM 更新后执行,浏览器的渲染进程可能还没有完成。这会导致滚动条操作没有生效,因为滚动位置是基于页面的当前渲染状态的。

setTimeout 的行为setTimeout 会让浏览器先完成所有当前的渲染和计算工作(即 DOM 更新和布局过程),然后在下一轮事件循环中执行你的回调。这样,滚动操作就可以在渲染完成后进行,确保滚动效果生效。

总结

当在 nextTick 中进行滚动时,滚动操作可能在浏览器完成布局计算之前执行,导致没有效果。为了确保滚动条位置能够生效,可以使用:

  • requestAnimationFrame:确保滚动操作在浏览器渲染完成后执行。
  • setTimeout:通过延迟执行滚动操作,确保在浏览器完成当前的渲染和布局后执行。

两者都能够有效解决在 DOM 更新后立即执行滚动操作的问题,确保滚动效果的正确应用。

为什么我们用了 Vite 还是构建慢?——真正的优化在这几步

作者 ErpanOmer
2025年6月9日 09:57

Vite 凭借其基于原生 ESM 的开发体验和极速冷启动,被誉为“现代前端构建的终极解决方案”。然而,许多开发者在将老项目迁移到 Vite 或新项目上马后,仍会面临以下现实:

  • 冷启动是快了,但热更新开始卡顿
  • 构建产物还是几十秒甚至上百秒
  • vite build 一跑,CPU 风扇飞起,内存飙升

这不禁让人怀疑:Vite 真的快吗?我是不是哪里搞错了?

答案是:Vite 本身没错,但你可能忽略了 “构建速度的决定性因素” 并不只是工具,而是你整个工程的结构与使用方式。


第一步:认清 Vite 的本质——开发快 ≠ 构建快

Vite 的核心价值之一是:

开发时基于原生 ESM 模块按需加载,只构建你当前用到的模块。

所以你在开发时能享受到:

  • 秒级冷启动
  • 快速热更新(HMR)

但是一旦 vite build,它会切换到底层的 Rollup 作为打包器。这时候:

  • 所有依赖和模块都会被打包(并非按需加载)
  • 你项目里隐藏的复杂性就无所遁形了

如果你项目构建慢,问题通常出在这几个地方 👇


第二步:从源码开始拆解瓶颈

1. 依赖体积巨大,tree-shaking 无效

常见现象:

  • 引入了 lodash,但只用了一个 cloneDeep,结果全库都进来了
  • 三方组件库未按需引入
  • import 了整个 moment/antd/dayjs/chart.js 等大块头

建议:

  • 替换为按需引入版本(例如 lodash-esdayjs
  • 配置 vite-plugin-impunplugin-vue-components 等插件实现自动按需加载
  • 使用 esbuild 插件提前预构建大型依赖

2. 依赖过多,预构建时间拉长

Vite 会在第一次启动时对第三方依赖进行 预构建,但如果:

  • node_modules 太大
  • dependenciesdevDependencies 没分清
  • 重复安装多个版本的库

则预构建变成噩梦。

建议:

  • 使用 optimizeDeps.exclude 排除不必要的模块
  • 确保依赖升级一致,避免 lodash@3lodash@4 共存
  • 使用 pnpmyarn workspace 做 hoist 减少重复依赖

3. 大量动态导入 / 动态路由

const page = import(`./pages/${name}.vue`)

这类动态导入虽然在开发阶段无感,但在打包时会造成:

  • 无法静态分析,打包粒度变粗
  • chunk 切割不理想

建议:

  • 避免使用过多复杂路径的 import()
  • 显式声明 chunk 名称:
const page = import(/* webpackChunkName: "page-[request]" */ `./pages/${name}.vue`)
  • 使用插件 vite-plugin-pages 提前生成路由,提升打包可控性

4. 打包时文件过多或模块层级过深

Vite 最怕你这样:

  • 单页项目动辄几千个 .vue 文件
  • 组件层级 6 层嵌套,导入链冗长

这样会造成:

  • Rollup 构建图构造时间大大增加
  • chunk 分析变慢

建议:

  • 控制模块划分粒度
  • 重构庞大的组件结构
  • 使用 vite-plugin-inspectwhy-so-slow 工具分析依赖链路

第三步:构建优化技巧全集

✅ 开启 Rollup 缓存(生产构建)

export default defineConfig({
  build: {
    cacheDir: 'node_modules/.vite_cache',
  },
})

✅ 使用 esbuild 插件压缩而不是 Terser

build: {
  minify: 'esbuild', // 默认为 terser,esbuild 更快
}

✅ 压缩大型依赖提前剥离(optimizeDeps + manualChunks)

optimizeDeps: {
  include: ['axios', 'lodash-es'],
}
rollupOptions: {
  output: {
    manualChunks: {
      vendor: ['vue', 'vue-router'],
    },
  },
}

✅ 设置 SSR-friendly 构建配置

如果你的项目未来有 Nuxt / SSR 的诉求:

ssr: {
  noExternal: ['your-lib'], // 避免库在服务端打包出错
}

第四步:缓存、预构建、硬盘读写也重要

  • .vite 缓存目录删除后构建速度会变慢一次
  • node_modules 删除后会触发重新 optimizeDeps
  • 高并发文件读取(尤其在 HDD 硬盘上)会导致构建性能极差

建议:

  • 使用 SSD 做本地开发盘
  • 保留 .vite 缓存目录
  • 在 CI/CD 中加缓存提升构建速度

Vite 是好工具,但别用错姿势

Vite 真的快,但也真的依赖你的项目结构合理性。如果你像用 Webpack 一样对待 Vite,把所有依赖全部一锅端地丢进来,还觉得构建变慢了,那问题不在工具,而在使用方式。

所以,如果你真的想优化构建速度:

  • 别急着 blame Vite
  • 先看看自己是不是喂了它太多“垃圾”代码

css+javaScript轮播图

作者 星_离
2025年6月9日 09:34

今天和大家分享一个很常见的效果,也就是轮播图,轮播图主要的功能点就是,自动轮播、上一张、下一张、以及跟踪的圆点。接下来就一个一个的来说一下,注意,本文主要针对初学者,也就是已经了解了HTML、Css、以及JavaScript。接下来我们就来看一下具体的代码实现。

image.png

1. Html部分

<div class="box">
        <img src="../../img/slider01.jpg" alt="">
        <p>对人类来说会不会太超前了?</p>
        <ul>
            <li class="active"></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
        </ul>
        <p class="btn"><button class="backBtn"><</button>&nbsp;&nbsp;<button class="nextBtn">></button></p>
    </div>

首先我们需要一个承载图片的标签也就是img标签,这个标签在之后会展示所有的轮播图片,其次就是承载我们的文本内容的标签,这些都是根据自己需要来自行添加的,接下来就需要来设置一个小圆点的盒子,在这里我使用ul和li来实现这部分。接下来的css部分我就不一一讲解了,我们直接来看js部分。

2. Css部分

<style>
      ul,li{
            padding: 0;
            margin: 0;
            list-style: none;
        }
        .box{
            width: 800px;
            color: white;
            margin: 100px auto;
            padding-bottom: 10px;
            background: rgb(100, 67, 68);
            position: relative;
            p{
                padding-left: 10px;
            }
            img{
                width: 100%;
            }
            ul{
                padding-left: 10px;
                display: flex;
                li{
                    margin-right: 10px;
                    width: 10px;
                    height: 10px;
                    border-radius: 50%;
                    background: rgba(0, 0, 0 , .5);
                }
                .active{
                    background: #fff;
                }
            }
            .btn{
                position: absolute;
                bottom: 15px;
                right: 10px;
                font-size: 20px;
            }
        }
    </style>

3. JavaScript部分

<script>
        const sliderData = [
          { url: '../image/slider01.jpg', title: '对人类来说会不会太超前了?', color: 'rgb(100, 67, 68)' },
          { url: '../image/slider02.jpg', title: '开启剑与雪的黑暗传说!', color: 'rgb(43, 35, 26)' },
          { url: '../image/slider03.jpg', title: '真正的jo厨出现了!', color: 'rgb(36, 31, 33)' },
          { url: '../image/slider04.jpg', title: '李玉刚:让世界通过B站看到东方大国文化', color: 'rgb(139, 98, 66)' },
          { url: '../image/slider05.jpg', title: '快来分享你的寒假日常吧~', color: 'rgb(67, 90, 92)' },
          { url: '../image/slider06.jpg', title: '哔哩哔哩小年YEAH', color: 'rgb(166, 131, 143)' },
          { url: '../image/slider07.jpg', title: '一站式解决你的电脑配置问题!!!', color: 'rgb(53, 29, 25)' },
        ]
        let oImg = document.querySelector("img")
        let oBox = document.querySelector(".box")
        let oP = document.querySelector("p")
        let oLi = document.querySelectorAll("li")
        let backBtn = document.querySelector(".backBtn")
        let nextBtn = document.querySelector(".nextBtn")
        let num = 0
        let timer = null
        
        function updateSlider() {
            // Clear all active classes
            for(let i = 0; i < oLi.length; i++) {
                oLi[i].classList.remove('active')
            }
            // Update display
            oImg.src = sliderData[num].url
            oP.innerHTML = sliderData[num].title
            oBox.style.backgroundColor = sliderData[num].color
            oLi[num].classList.add('active')
        }
        
        function startTimer() {
            timer = setInterval(() => {
                num = (num + 1) % sliderData.length
                updateSlider()
            }, 1000)
        }
        
        // Initialize slider
        updateSlider()
        startTimer()
        
        // Back button
        backBtn.onclick = function() {
            clearInterval(timer)
            num = (num - 1 + sliderData.length) % sliderData.length
            updateSlider()
            startTimer()
        }
        
        // Next button
        nextBtn.onclick = function() {
            clearInterval(timer)
            num = (num + 1) % sliderData.length
            updateSlider()
            startTimer()
        }
        
        // Dot navigation
        for(let i = 0; i < oLi.length; i++) {
            oLi[i].onclick = function() {
                clearInterval(timer)
                num = i
                updateSlider()
                startTimer()
            }
        }
    </script>

好,直接来分析一下JavaScript首先我们需要来获取到所有的事件源:

let oImg = document.querySelector("img")
let oBox = document.querySelector(".box")
let oP = document.querySelector("p")
let oLi = document.querySelectorAll("li")
let backBtn = document.querySelector(".backBtn")
let nextBtn = document.querySelector(".nextBtn")

以上是获取的所有DOM节点。 接下来我们来初始化一个变量来表示当前图片的索引,默认索引是0,也就是第一张图片,我们来看一下自动轮播功能。

3.1. 自动轮播

function updateSlider() {
    // Clear all active classes
    for(let i = 0; i < oLi.length; i++) {
        oLi[i].classList.remove('active')
    }
    // Update display
    oImg.src = sliderData[num].url
    oP.innerHTML = sliderData[num].title
    oBox.style.backgroundColor = sliderData[num].color
    oLi[num].classList.add('active')
}

function startTimer() {
    timer = setInterval(() => {
        num = (num + 1) % sliderData.length
        updateSlider()
    }, 1000)
}

// Initialize slider
updateSlider()
startTimer()

轮播的操作也就是在一段时间后将当前的图片替换成另一张,那么我们先来看第一段代码的功能,也就是startTimer(),这个函数是用来更新我们的页面的,一是来更新索引,二是来更新页面,从而做到图片的切换,由于在这里我们是自动播放,所以需要使用到计时器,让num每秒进行加1的操作,直到增加到数组的长度,两个相同的数进行取模操作,会得到0,也就是当达到数组长度的时候就会将num重新赋值为0,继续执行增加1的操作。这样就可以实现自动轮播,那么我们就来看看这个函数中的具体更新操作的函数updateSlider()在这个函数中我们需要去处理小圆点和图片的具体操作,也就是我们在开始的时候需要将所有小圆点中的样式移除掉,然后我们就需要去获取到图片的src属性将其更改为对应num索引下的图片,其余文字和背景色都类似,在最后将对应远点的样式添加上就可以了,最后我们来初始化自动轮播。这样简单的自动轮播就可以实现。那么接下俩就一起来看看返回上一张的操作。

3.2. 上一张

backBtn.onclick = function() {
    clearInterval(timer)
    num = (num - 1 + sliderData.length) % sliderData.length
    updateSlider()
    startTimer()
}

关于返回上一张,不难想到只要去变动num的值然后重新去更新页面就可以,那么关键的就是如何去巧妙的返回到上一张图片。在这里我们同样使用数组的长度来执行这一操作, (num - 1 + sliderData.length) % sliderData.length比如现在我们在图片的最后一张,也就是num等于6,那么返回上一张也就是将num的值返回到5,我们代入理解一下,上面的代码可以理解为(6-1+7)%7最终的结果就是5,也就是说通过这个公式就可以完美的实现num值的回退。回退num值后重新初始化更新函数以及计时器。接下来的下一张就简单多了。

3.3. 下一张

 nextBtn.onclick = function() {
    clearInterval(timer)
    num = (num + 1) % sliderData.length
    updateSlider()
    startTimer()
}

忘了说明一点,不论是在执行上一张的操作还是下一张的操作,我们都需要去将原有的计时器清除掉,不然会出现bug,也就是你点击之后,你的计时器还在执行,就会出现小圆点和图片来回跳动的bug,所以需要去清除计时器。 那么接下来就继续说下一张的操作,在这里更改num值的公式值需要和计时器中的一样就可以了,都是让num进行加1的操作,所以放在这里依然适用。接着初始化更新函数以及计时器函数就可以了。 最后我们就来看看点击小圆点来查看图片。

3.4. 小圆点查看

 for(let i = 0; i < oLi.length; i++) {
    oLi[i].onclick = function() {
        clearInterval(timer)
        num = i
        updateSlider()
        startTimer()
    }
}

不难发现在上边的代码中关键的操作依然是去变动num的值来实现指定图片的查看,所以在这里我们先去遍历所有的小圆点节点,为每一个远点都添加点击事件,当我们点击的时候清除掉计时器,也就是自动播放的计时器,然后将num更改为小圆点索引对应的图片索引,在这里直接将i赋值给num就可以了,因为小圆点的数量和图片的数量是一样的是,所以都是一一对应的,最后我们只要重新初始化更新函数和计时器就可以了,那么说到这里我们的轮播图就做好了。

4. 结语

通过本文的学习,我们实现了一个功能完整的轮播图组件,包含了以下核心功能:

  1. 自动轮播功能(通过setInterval实现)
  2. 上一张/下一张导航按钮
  3. 小圆点指示器导航
  4. 平滑的内容切换效果

关键点总结

  1. 数据结构设计:使用数组存储轮播内容,便于管理和扩展
  2. 状态管理:通过num变量跟踪当前显示的幻灯片索引
  3. 模块化函数:将更新逻辑封装在updateSlider函数中,提高代码复用性
  4. 定时器管理:在用户交互时清除并重启定时器,避免冲突
  5. 循环逻辑:使用取模运算实现无限循环轮播效果

希望这个教程能帮助你理解轮播图的基本实现原理。掌握了这些基础知识后,你可以尝试实现更复杂的轮播效果,或者将其封装成可复用的组件。

记住,前端开发最重要的是理解原理而非死记代码。建议你尝试自己从头实现一遍,遇到问题再回来看解决方案,这样学习效果会更好!

🚀 由Tony Stark 带你入门 JavaScript(新手向)🚀

作者 MrSkye
2025年6月9日 09:00

📜 前言

在编程的世界里,JavaScript 就像漫威宇宙的 无限宝石,掌握它,你就能操控网页的动态、构建交互式应用,甚至开发全栈系统!但和所有超级英雄一样,学习 JS 也需要从基础力量开始 —— 变量、数据类型、函数、对象,这些就是你的 “蜘蛛感应”“钢铁战甲”

在这篇文章中,我们将用 钢铁侠 来讲解 JavaScript 对象(Object),让新手不仅能理解代码逻辑,还能像编写 复仇者联盟剧本 一样享受编程的乐趣!

💡 你会学到:

  • 对象(Object) 如何存储数据,比如钢铁侠的战甲、灭霸的无限手套
  • 方法(Method) 如何让对象“动”起来,比如 ironMan.attack()
  • 数组(Array)、字符串(String)、数字(Number) 在对象里的使用
  • 动态属性访问,像 ironMan.weaponKind[0]

准备好了吗?让我们进入 “JavaScript 漫威宇宙”,用代码书写你的超级英雄传奇!


如何运行JavaScript?

前端运行

JS是一门脚本语言,最初是用来给页面增加动态交互效果的,所以它可以在浏览器中被运行,我们可以创建一个index.html1.js,将js文件引入html的script中,利用右键,选择Open with Live Server打开文件。

image.png 当然要先下载在浏览器中打开的两个插件,才会显示相应选项:

屏幕截图 2025-06-09 084337.png

后端运行

我们可以利用node.js来运行JavaScript代码,在 nodejs.org/zh-cn 下载node.js到C盘,安装后点击右键1.js,选择在终端打开,之后在终端输入命令node 1.js即可打开文件。

利用node打开js文件命令:node filename.js

注意:这个文件必须在当前打开的目录下!

初识 JavaScript 对象:Ironman / Thanos

代码世界就是现实世界的数字分身

在编程中,我们通过 对象(Object) 来映射现实世界的实体——就像钢铁侠的战甲档案存储了他的能力参数,灭霸的宝石手套记录着毁灭宇宙的规则。

  • 每个 属性(property) 都是特征(如 thanos.power = 'Infinity Gauntlet'
  • 每个 方法(method) 都是行为(如 ironman.fly()
  • 数据与功能的结合,就是编程对现实的 数字建模

那么我们的Tony Stark在数字世界的抽象就是这样的:

const ironMan = {  
name: "Tony·Stark", 
age: 48, 
weaponKind: ["Mark-50", "Jericho Missile", "Arc Pulse Cannon"], 
job: "Leader of The Avengers/Engineer/Billionaire", 
hobby:['make Armor','play with girls'],
alive:true,
}

动态语言 / 弱类型语言

从上面的例子我们可以看到,我们抽象托尼屎大颗到软件的世界中时,对于他的各个属性都是由Key:Value的结构组成,即直接写变量名,再写对应的内容,也就是说:变量的类型是由变量值决定的,不需要提前定义变量的类型,这就体现了JavaScript的动态性,其相对于C、Java这种静态性语言来说是很随意的,哎!我可以先声明一个变量,但不给他赋值 (动态性),我愿意这个变量是什么值,它就可以改成什么值 (弱类型),比如:

var name;
console.log(name); // undefined 未定义
name = "Tony·Stark" 
console.log(name); // JS中的printf,作用为输出内容   输出:Tony·Stark
name = 1;
console.log(name);  // 输出: 1
name = ['1','2','3'];
console.log(name); // 输出:['1','2','3']
/*嘿!同一个变量,即使后面重新赋值改变为不同的类型也不会报错,这就是弱类型语言的魅力!*/

JavaScript可以做到先创建对象再赋值也不会报错,体现了其动态性,到了执行时会自动判断变量的类型,而C或Java等静态性语言则必须在编译之前确定好某个变量的类型,否则在编译时会报错,而对于弱类型,JavaScript的变量可以根据环境变化自己转变类型,不需要强转,而强类型语言则必须要强转才能改变变量的类型

JS的数据类型

我们可以从上面的例子中看到,Stark同志的各个属性都对应着不同的数据类型

const ironMan = {} // 对应的是Object(对象)数据类型
// 对象数据类型的声明就是 一个变量名 + 一个{}即可,随意吧哈哈哈哈哈
name: "Tony·Stark",  // String(字符串)数据类型
age: 48,   // Number(数字)数据类型
weaponKind: ["Mark-50", "Jericho Missile", "Arc Pulse Cannon"], // 数组(也属于 Object )
job: "Leader of The Avengers/Engineer/Billionaire", // String
hobby:['make Armor','play with girls'], // 数组
alive:true, // Boolean(布尔)数据类型,其只有两个值:true/false

JS一共有几种数据类型?(面试必考)

当然上面的不是全部的数据类型,实际上,JavaScript一共有7种数据类型,它们分别是:

Numeric、String、Boolean、null、undefined、Object、Symbol(ES6新增)

Numeric 包括 Number和bigInt(ES6新增)

而JavaScript的数据类型又可以分为简单数据类型复杂数据类型,复杂数据类型只有Object,其他均为简单数据类型。

🕳️关于null和undefined

nullundefined虽然都表示“无”的概念,但它们的语义完全不同:

// 就像灭霸的响指造成的两种不同结果:
const vanishedHeroes = null;     // 明确被消灭的英雄
const missingHeroes = undefined; // 从未出现过的英雄
Undefined
  • 类型undefined

  • 含义"这里应该有值,但还未定义"

  • 产生方式

    • 变量声明但未赋值
    • 函数参数未传递
    • 访问对象不存在的属性
    • 函数没有返回值
// 像未激活的战甲功能
let newFeature;      // 自动获得 undefined
const suit = { name: "Mark XLII" };
console.log(suit.color);          // undefined (不存在该属性)

function deployArmor(version) {
  // version 可能为 undefined
  return this.version;
}

Null
  • 类型object (历史遗留的 bug,实际应为 null 类型)
  • PS: 后来想改的,但是历史遗留代码太多,官方不敢改了hhhhhhh
  • 含义"这里有值,但值为空"
  • 使用场景:主动赋值表示空值
// 就像主动清空战甲库存
let starkArmory = null;  // 明确表示"没有战甲"

// 典型应用场景:
1. 作为函数的参数,表示"不需要传入值"
2. 作为对象属性的值,表示"清空该属性"
3. DOM 获取元素不存在时的返回值

🛠️ 为Iron Man装配方法函数:从数据类型到行为能力

现在我们的屎大颗同志已经有了基本的属性,但他目前像个植物人一样没有行动能力,接下来就让我们利用函数(Function) 来给他加装能力吧!

首先,我们的Tony Stark同志是一个非常花心的人,那么他就很喜欢勾搭其他美丽的异性,就会有以下能力:

Function hitOnGirl(girlName) { 
const pickUpLines = [ `Hey ${girlName}, 我的战甲需要充电,而你就是我的电源。`, 
`${girlName},你是方舟反应堆吗?因为你让我的心跳加速了。`, 
`我不是在找J.A.R.V.I.S,我是在找Mrs.Stark,你觉得怎么样?`, 
`我的Mark战甲能承受40000英尺高空,但承受不住你的眼神。`, 
`比起拯救世界,我更想拯救你的通讯录——把我的号码存进去。` ];
return pickUpLines[Math.floor(Math.random() * pickUpLines.length)];
}

当我们把这一段代码加入到Tony Stark的虚拟人设中,他就有了到处勾搭妹妹的能力了,那么他会怎么勾搭妹妹呢?取决于 return的随机撩妹话语~

接下来让我们从这一段花心男人的鬼话继续了解JavaScript的设计思想和语法~

函数(Function)

为什么要有函数?

函数的结构如下:

image.png

其功能是封装一段逻辑,就像把这一段逻辑打包了一样,装在了一个箱子里,通过函数名我们可以实现对此函数的调用,使用里面的逻辑,这个函数名就像是贴在箱子上的标签,也就是这段逻辑暴露在外的接口

为什么要把一段逻辑封装成函数呢?就像我们的花心男Stark一样,他不局限于1个美女,他喜欢好多美女,所以当他遇见美女时要多次运用这些话术,来获得妹妹的心!也就是说,同样的一些话Stark会经常使用,所以我们就可以封装成一个函数,使其可以复用

如何去调用一个函数?

const ironMan = {  
name: "Tony·Stark", 
age: 48, 
weaponKind: ["Mark-50", "Jericho Missile", "Arc Pulse Cannon"], 
job: "Leader of The Avengers/Engineer/Billionaire", 
hobby:['make Armor','play with girls'],
alive:true,
hitOnGirl: Function(girlName) {  // 在这里hitOnGirl是一个变量,它的值是一个函数
const pickUpLines = [ `Hey ${girlName}, 我的战甲需要充电,而你就是我的电源。`, 
`${girlName},你是方舟反应堆吗?因为你让我的心跳加速了。`, 
`我不是在找J.A.R.V.I.S,我是在找Mrs.Stark,你觉得怎么样?`, 
`我的Mark战甲能承受40000英尺高空,但承受不住你的眼神。`, 
`比起拯救世界,我更想拯救你的通讯录——把我的号码存进去。` ];
return pickUpLines[Math.floor(Math.random() * pickUpLines.length)];
}

// 如何在全局调用函数呢?可以像下面这么做
ironMan.hitOnGirl('Pepper');
}

我们使用了ironMan.hitOnGirl('Pepper'),意思就是我们调用了ironMan中的hitOnGirl函数,传递了参数girlNamePepper,这体现了面向对象的思维,动态的访问属性。

//比如我们要访问Stark的其他属性,都可以用对象名.属性名(或方法名)来访问:
ironMan.name;       // Tony·Stark
ironMan.age;        // 48
ironMan.weaponKind; // ["Mark-50", "Jericho Missile", "Arc Pulse Cannon"]

hitOnGirl方法(Method)详解

不需要纠结,方法就是函数,只不过是在对象之中嘛,成了对象专用的一个函数,我们一般把它叫做方法(Method).

Function hitOnGirl(girlName) {  
const pickUpLines = [ `Hey ${girlName}, 我的战甲需要充电,而你就是我的电源。`, 
`${girlName},你是方舟反应堆吗?因为你让我的心跳加速了。`, 
`我不是在找J.A.R.V.I.S,我是在找Mrs.Stark,你觉得怎么样?`, 
`我的Mark战甲能承受40000英尺高空,但承受不住你的眼神。`, 
`比起拯救世界,我更想拯救你的通讯录——把我的号码存进去。` ];
return pickUpLines[Math.floor(Math.random() * pickUpLines.length)];
}

这一段逻辑究竟有啥用呢?且听我娓娓道来~

字符串模板

首先映入眼帘的是这段代码:

const pickUpLines = [ `Hey ${girlName}, 我的战甲需要充电,而你就是我的电源。`, 
`${girlName},你是方舟反应堆吗?因为你让我的心跳加速了。`, 
`我不是在找J.A.R.V.I.S,我是在找Mrs.Stark,你觉得怎么样?`, 
`我的Mark战甲能承受40000英尺高空,但承受不住你的眼神。`, 
`比起拯救世界,我更想拯救你的通讯录——把我的号码存进去。` ];

这是一个const常量,其中的值不可以后续修改,它是一个数组,里面有5句话,每一句都是Tony在女孩子身上用过无数次的。

两个 ` ` (反引号,在tab键上方) 就组成了字符串模板,其作用就是使得我们的数据表达更加方便,让我们来看同一段话在C语言和JavaScript(不用字符串模板)的表达:

printf("Hello %s, your armor is %d%% charged.\n", name, charge);
console.log( "Hello " + name + ", your armor is " + (10 - 2) + "% charged.");

而利用字符串模板:

console.log(`Hello ${Name},your armor is ${10-2}% charged.`)

加上反引号,可以对${}中的内容进行识别,其内容可以是参数,也可以是一段逻辑运算,字符串模板还有保留换行等作用。

面向对象的思想

接下来就是这一段代码:

return pickUpLines[Math.floor(Math.random() * pickUpLines.length)];

这是hitOnGirl函数的返回值,返回的是pickUpLines中的随机一句话,这里运用了Math对象,我们的JavaScript也是有面向对象的特性的,对于randomfloor等数学方法,我们都将其封装在了Math对象中,从Math对象中获得引用。

return pickUpLines[] 显示是获取数组中的某个值,

Math.floor() 对某个数向下取整

Math.random() 从0~1中取随机数(不包括1)

pickUpLines.length pickUpLines数组的长度,其一共有5句话所以pickUpLines.length=5,

这样的话Math.floor(Math.random() * pickUpLines.length)的取值范围就控制在了0~4之间,可以随机到任何一句话(因为数组下标从0开始,由0~4正好对应5句话)

结语:从Iron Man到编程,从新手到开发者

在 Tony Stark 的实验室里,J.A.R.V.I.S. 通过 JavaScript 对象组织数据,用函数完成复杂的逻辑运算,用模板字符串优雅地拼接对话。这些概念不仅是代码的组成部分,也是构建智能系统的基石——就像钢铁侠的战甲一样,每一行代码都是精密的零件,组合起来才有强大的威力。

学习 JavaScript 的数据类型、对象、函数和模板字符串,就像解锁了战甲的第一层功能。未来,你还会接触更多高级概念(如 Symbol、原型链、异步编程),就像 Tony 不断升级 Mark 系列战甲,从基础款进化到纳米战甲。

记住:

  • 对象(Object) 是组织数据的工具箱
  • 函数(Function) 是让代码灵活运转的 AI 助手
  • 模板字符串 让字符串拼接更优雅,就像 J.A.R.V.I.S. 的流畅交互

Keep coding, and you'll build your own suit someday. ⚡💻

React Hooks 的优势和使用场景

作者 Riesenzahn
2025年6月9日 07:38
# React Hooks 的优势和使用场景

## 1. React Hooks 的优势

### 1.1 简化组件逻辑
Hooks 允许在不编写 class 的情况下使用 state 和其他 React 特性。这使得函数组件能够拥有类组件的功能,同时保持简洁性。

```jsx
// 类组件
class Counter extends React.Component {
  state = { count: 0 };
  
  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

// 函数组件 + Hooks
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

1.2 更好的逻辑复用

Hooks 解决了高阶组件和渲染属性带来的"嵌套地狱"问题,通过自定义 Hook 可以更优雅地复用状态逻辑。

// 自定义 Hook
function useCounter(initialValue) {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  
  return { count, increment, decrement };
}

// 使用自定义 Hook
function CounterA() {
  const { count, increment } = useCounter(0);
  return <button onClick={increment}>A: {count}</button>;
}

function CounterB() {
  const { count, decrement } = useCounter(10);
  return <button onClick={decrement}>B: {count}</button>;
}

1.3 更细粒度的代码组织

Hooks 允许按照功能而非生命周期方法来组织代码,使相关代码更加集中。

function FriendStatus({ friendId }) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    
    ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange);
    };
  }, [friendId]);

  return isOnline === null ? 'Loading...' : isOnline ? 'Online' : 'Offline';
}

1.4 性能优化

Hooks 提供了更精细的控制能力,如 useMemouseCallback 可以避免不必要的计算和渲染。

function ExpensiveComponent({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items, filter]);

  return <List items={filteredItems} />;
}

2. React Hooks 的使用场景

2.1 状态管理

useState 适用于管理组件内部的状态,替代类组件的 this.state

function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
    </form>
  );
}

2.2 副作用处理

useEffect 适用于处理副作用,如数据获取、订阅、手动 DOM 操作等。

function DataFetcher({ url }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(url);
      const json = await response.json();
      setData(json);
      setLoading(false);
    };

    fetchData();
  }, [url]);

  return loading ? <Spinner /> : <DataView data={data} />;
}

2.3 上下文访问

useContext 提供了一种在组件树中共享数据的方式,无需显式地通过组件逐层传递 props。

const ThemeContext = React.createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Themed Button</button>;
}

如何提高前端应用的性能?

作者 Riesenzahn
2025年6月9日 07:38
# 前端性能优化实战指南

## 1. 资源加载优化

### 1.1 代码拆分与懒加载
```javascript
// 动态导入实现懒加载
const LazyComponent = React.lazy(() => import('./LazyComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

1.2 资源预加载

<!-- 关键资源预加载 -->
<link rel="preload" href="critical.css" as="style">
<link rel="prefetch" href="next-page.js" as="script">

1.3 图片优化策略

  • 使用WebP格式替代JPEG/PNG
  • 实现响应式图片(srcset)
  • 懒加载非首屏图片(loading="lazy")

2. 渲染性能优化

2.1 减少重排重绘

// 批量DOM操作
const fragment = document.createDocumentFragment();
items.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item;
  fragment.appendChild(li);
});
list.appendChild(fragment);

2.2 使用CSS硬件加速

.transform-layer {
  transform: translateZ(0);
  will-change: transform;
}

2.3 虚拟列表实现

// React中使用react-window
import { FixedSizeList as List } from 'react-window';

<List
  height={600}
  itemCount={1000}
  itemSize={35}
  width={300}
>
  {Row}
</List>

3. JavaScript优化

3.1 防抖与节流

// 节流实现
function throttle(fn, delay) {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= delay) {
      fn.apply(this, args);
      lastCall = now;
    }
  };
}

3.2 Web Worker应用

// 主线程
const worker = new Worker('worker.js');
worker.postMessage(data);
worker.onmessage = (e) => handleResult(e.data);

// worker.js
self.onmessage = (e) => {
  const result = heavyCalculation(e.data);
  self.postMessage(result);
};

4. 缓存策略

4.1 Service Worker缓存

// 注册Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}

// sw.js缓存策略
self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/styles/main.css',
        '/scripts/main.js'
      ]);
    })
  );
});

4.2 HTTP缓存头设置

Cache-Control: public, max-age=31536000
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

5. 监控与持续优化

5.1 性能指标采集

// 使用Performance API
const timing = performance.timing;
const loadTime = timing.loadEventEnd - timing.navigationStart;

// 使用web-vitals库
import {getCLS, getFID, getLCP} from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getLCP(console.log);

5.2 性能预算

// package.json
"performance": {
  "budgets": [
    {
      "resourceType": "script",
      "budget": 125
    },
    {
      "resourceType": "image",
      "budget": 50
    }
  ]
}

6. 现代框架优化技巧

6.1 React性能优化

// 使用React.memo
const MemoComponent = React.memo(function MyComponent(props) {
  /* 渲染逻辑 */
});

// 使用useMemo/useCallback
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);

6.2 Vue优化策略

// 使用v-once
<div v-once>{{ staticContent }}</div>
❌
❌