阅读视图

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

JavaScript 闭包作用域、原型链继承面试题解析教程

说到 JavaScript 面试,闭包、作用域、原型链、继承这些关键词几乎是“必考题”。很多同学一刷题就头大,明明看过好几遍原理,结果一到面试官面前,还是词不达意、思路混乱。这是为什么?

其实不是你不懂,而是没能用“讲故事”的方式把它讲清楚。面试不只是考概念,更是在考你能不能把复杂问题讲“简单”。比如,闭包到底是“函数里面套函数”这么简单吗?作用域链和执行上下文到底谁先谁后?原型链继承又是怎么一层一层传下去的?

在这篇文章里,我会带你一口气理清这些高频知识点,不讲花哨术语,只用最通俗的例子和最常见的面试题,帮你把零散的知识点串成“系统的知识树”。看完这篇,下一次再遇到相关题目,不仅能答对,还能讲得漂亮!

闭包与作用域

闭包的定义与原理

闭包(Closure)是 JavaScript 的核心特性,指一个函数能够“记住”并访问其定义时所在的作用域,即使该函数在其他作用域中执行。闭包由两部分组成:

  • 函数本身:定义的函数体。
  • 词法环境(Lexical Environment):函数定义时绑定的变量环境。

理论背景

  • JavaScript 使用词法作用域(Lexical Scoping),变量的作用域在代码编写时静态确定。
  • 每个函数创建时,会绑定其定义时的作用域链(Scope Chain),包含外部变量引用。
  • 闭包通过维持对外部变量的引用,延长变量的生命周期。

简单示例

function outer() {
    let count = 0;
    function inner() {
        count++;
        console.log(count);
    }
    return inner;
}

const counter = outer();
counter(); // 输出: 1
counter(); // 输出: 2

逐步分析

  1. outer 定义了变量 count 和函数 inner
  2. inner 引用了外部的 count,形成闭包。
  3. outer 返回 innercountinner 捕获,保存在内存中。
  4. 每次调用 counter()inner 更新并访问 count,实现计数器功能。

闭包的内存机制

  • count 存储在 inner 的词法环境中,不会因 outer 执行结束而销毁。
  • 垃圾回收器(GC)无法回收闭包引用的变量,可能导致内存泄漏,需谨慎管理。

作用域与作用域链

作用域(Scope)定义了变量的可见性和生命周期。JavaScript 有以下作用域类型:

  • 全局作用域:全局变量,生命周期贯穿整个程序。
  • 函数作用域:函数内定义的变量,仅在函数内可见。
  • 块级作用域:使用 letconst{} 内定义的变量(ES6 引入)。

作用域链

  • 当访问变量时,JavaScript 引擎从当前作用域开始,沿作用域链向上查找,直到全局作用域。
  • 作用域链由函数定义时的词法环境决定。

示例

let globalVar = "global";
function outer() {
    let outerVar = "outer";
    function inner() {
        let innerVar = "inner";
        console.log(innerVar, outerVar, globalVar);
    }
    inner();
}
outer();

输出

inner outer global

逐步分析

  1. inner 访问 innerVar(本地),outerVar(外层函数),globalVar(全局)。
  2. 作用域链:inner -> outer -> global
  3. 查找顺序:先本地作用域,再逐级向上。

闭包的常见面试题

面试题 1:闭包计数器

问题:修改以下代码,使每次调用返回不同的计数器实例。

function createCounter() {
    let count = 0;
    return function() {
        return count++;
    };
}
const counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1

答案

function createCounter() {
    let count = 0;
    return function() {
        return count++;
    };
}

const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 0
console.log(counter1()); // 1
console.log(counter2()); // 0
console.log(counter2()); // 1

分析

  • 每次调用 createCounter 创建新的闭包,count 是独立的。
  • counter1counter2 引用不同的词法环境。

面试题 2:循环中的闭包

问题:以下代码输出什么?如何修复?

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1000);
}

输出

3
3
3

原因

  • var 具有函数作用域,i 是全局变量,setTimeout 回调执行时,i 已变为 3。
  • 闭包捕获的是变量引用,而非值。

修复方法 1:使用 let(块级作用域):

for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 1000);
}

输出

0
1
2

修复方法 2:使用 IIFE(立即执行函数表达式):

for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(() => console.log(j), 1000);
    })(i);
}

分析

  • let 为每次循环创建新的绑定。
  • IIFE 每次循环创建新的作用域,捕获当前 i 的值。

面试题 3:私有变量

问题:实现一个带有私有变量的模块。

function createPerson(name) {
    let _age = 0; // 私有变量
    return {
        getName: () => name,
        getAge: () => _age,
        setAge: (age) => { _age = age; }
    };
}

const person = createPerson("Alice");
console.log(person.getName()); // Alice
console.log(person.getAge()); // 0
person.setAge(25);
console.log(person.getAge()); // 25
console.log(person._age); // undefined

分析

  • _age 是闭包中的私有变量,无法直接访问。
  • 通过返回对象的方法控制访问,模拟封装。

闭包的应用场景

  1. 数据封装:如上例的私有变量。
  2. 状态维护:如计数器、事件处理。
  3. 函数柯里化
function curryAdd(a) {
    return function(b) {
        return a + b;
    };
}

const add5 = curryAdd(5);
console.log(add5(3)); // 8
  1. 事件处理
function setupButton(id) {
    let count = 0;
    document.getElementById(id).addEventListener('click', () => {
        console.log(`Clicked ${++count} times`);
    });
}
setupButton('myButton');

分析

  • 闭包维护 count,确保按钮点击次数持久化。
  • 避免全局变量污染。

原型链与继承

原型链的定义与原理

JavaScript 使用原型链(Prototype Chain)实现继承。每个对象有一个内部 [[Prototype]] 属性(通过 __proto__Object.getPrototypeOf 访问),指向其原型对象。原型链是对象查找属性的路径。

核心概念

  • 原型对象:每个函数有一个 prototype 属性,指向原型对象。
  • 构造函数:通过 new 创建对象时,对象的 [[Prototype]] 指向构造函数的 prototype
  • 属性查找:访问对象属性时,若对象本身没有,则沿原型链向上查找。

示例

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

Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};

const alice = new Person("Alice");
alice.sayHello(); // Hello, I'm Alice
console.log(alice.__proto__ === Person.prototype); // true

逐步分析

  1. Person 是一个构造函数,其 prototype 属性指向原型对象。
  2. new Person("Alice") 创建对象 alice,其 [[Prototype]] 指向 Person.prototype
  3. alice.sayHello() 查找 sayHello,在 alice 自身找不到,沿原型链找到 Person.prototype.sayHello

原型链的继承

JavaScript 通过原型链实现继承,子类原型指向父类实例。

示例

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

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

function Dog(name, type) {
    Animal.call(this, type); // 继承属性
    this.name = name;
}

Dog.prototype = Object.create(Animal.prototype); // 继承方法
Dog.prototype.constructor = Dog; // 修复构造函数
Dog.prototype.bark = function() {
    console.log(`${this.name} barks`);
};

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog is eating
dog.bark(); // Max barks

逐步分析

  1. Animal.call(this, type) 调用父类构造函数,继承 type 属性。
  2. Object.create(Animal.prototype) 创建新对象,继承 Animal.prototype 的方法。
  3. 修复 constructor 确保 dog instanceof Dog 正确。
  4. 原型链:dog -> Dog.prototype -> Animal.prototype -> Object.prototype

原型链的常见面试题

面试题 1:原型链查找

问题:以下代码输出什么?

function Foo() {}
Foo.prototype.x = 1;

const foo = new Foo();
console.log(foo.x); // 1
foo.x = 2;
console.log(foo.x); // 2
console.log(Foo.prototype.x); // 1

分析

  • foo.x 初始查找 Foo.prototype.x,输出 1。
  • foo.x = 2foo 自身创建属性 x,不影响原型。
  • Foo.prototype.x 仍为 1。

面试题 2:继承实现

问题:实现一个继承方法,支持多级继承。

function inherit(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function() {
    console.log(`${this.type} eats`);
};

function Dog(name, type) {
    Animal.call(this, type);
    this.name = name;
}
inherit(Dog, Animal);
Dog.prototype.bark = function() {
    console.log(`${this.name} barks`);
};

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog eats
dog.bark(); // Max barks

分析

  • inherit 函数封装原型链继承,复用性高。
  • Object.create 避免直接修改父类原型。

面试题 3:instanceof 原理

问题:以下代码输出什么?

console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true

分析

  • instanceof 检查对象原型链是否包含构造函数的 prototype
  • dog 的原型链:Dog.prototype -> Animal.prototype -> Object.prototype

ES6 Class 继承

ES6 引入 class 语法,简化继承:

class Animal {
    constructor(type) {
        this.type = type;
    }
    eat() {
        console.log(`${this.type} eats`);
    }
}

class Dog extends Animal {
    constructor(name, type) {
        super(type);
        this.name = name;
    }
    bark() {
        console.log(`${this.name} barks`);
    }
}

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog eats
dog.bark(); // Max barks

分析

  • class 是原型继承的语法糖,super 调用父类构造函数。
  • 更直观,但底层仍是原型链。

数据结构与算法在前端面试中的重要性

为什么重要

数据结构与算法(DSA)在前端面试中至关重要,原因如下:

  • 性能优化:高效算法减少 DOM 操作、渲染时间,提升用户体验。
  • 逻辑能力:算法题考察逻辑思维和问题解决能力。
  • 跨领域应用:前端与后端(如 Node.js)、机器学习(如 CNN 可视化)交互需要 DSA 知识。
  • 竞争力:顶级公司(如 Google、Meta)要求扎实的算法基础。

前端场景

  • 数组操作:过滤、排序、去重(如用户列表处理)。
  • 树结构:DOM 树遍历、组件树优化。
  • 图算法:依赖解析(如 Webpack 模块依赖)。
  • 时间复杂度:优化大数据量渲染(如虚拟列表)。

常见数据结构与算法

数组与字符串

面试题:反转字符串

问题:编写函数反转字符串,不使用内置方法。

function reverseString(s) {
    let arr = s.split('');
    let left = 0, right = arr.length - 1;
    while (left < right) {
        [arr[left], arr[right]] = [arr[right], arr[left]];
        left++;
        right--;
    }
    return arr.join('');
}

console.log(reverseString("hello")); // "olleh"

分析

  • 时间复杂度:O(n),空间复杂度:O(n)。
  • 使用双指针交换字符,避免额外空间。

链表

面试题:反转链表

class ListNode {
    constructor(val, next = null) {
        this.val = val;
        this.next = next;
    }
}

function reverseList(head) {
    let prev = null, curr = head;
    while (curr) {
        let next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

const list = new ListNode(1, new ListNode(2, new ListNode(3)));
const reversed = reverseList(list);
console.log(reversed.val); // 3

分析

  • 时间复杂度:O(n),空间复杂度:O(1)。
  • 链表在前端用于事件队列、历史记录。

面试题:二叉树前序遍历

class TreeNode {
    constructor(val, left = null, right = null) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

function preorderTraversal(root) {
    const result = [];
    function traverse(node) {
        if (!node) return;
        result.push(node.val);
        traverse(node.left);
        traverse(node.right);
    }
    traverse(root);
    return result;
}

const tree = new TreeNode(1, new TreeNode(2), new TreeNode(3));
console.log(preorderTraversal(tree)); // [1, 2, 3]

分析

  • 时间复杂度:O(n),空间复杂度:O(h)(h 为树高)。
  • 前端应用:DOM 树遍历、组件树解析。

面试题:深度优先搜索(DFS)

function dfs(graph, start) {
    const visited = new Set();
    function traverse(node) {
        visited.add(node);
        console.log(node);
        for (let neighbor of graph[node]) {
            if (!visited.has(neighbor)) {
                traverse(neighbor);
            }
        }
    }
    traverse(start);
}

const graph = {
    A: ['B', 'C'],
    B: ['A', 'D', 'E'],
    C: ['A', 'F'],
    D: ['B'],
    E: ['B', 'F'],
    F: ['C', 'E']
};
dfs(graph, 'A'); // A, B, D, E, F, C

分析

  • 时间复杂度:O(V + E),空间复杂度:O(V)。
  • 应用:依赖解析、组件关系图。

算法在前端的实际应用

虚拟列表优化

处理大数据量列表(如 10,000 条记录):

function createVirtualList(container, items, itemHeight, visibleHeight) {
    let startIndex = 0;
    let endIndex = Math.ceil(visibleHeight / itemHeight);
    
    function render() {
        container.innerHTML = '';
        for (let i = startIndex; i < endIndex; i++) {
            const div = document.createElement('div');
            div.style.height = `${itemHeight}px`;
            div.textContent = items[i];
            container.appendChild(div);
        }
    }
    
    container.addEventListener('scroll', () => {
        startIndex = Math.floor(container.scrollTop / itemHeight);
        endIndex = startIndex + Math.ceil(visibleHeight / itemHeight);
        render();
    });
    
    render();
}

const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
createVirtualList(document.getElementById('list'), items, 50, 500);

分析

  • 仅渲染可视区域,降低 DOM 操作开销。
  • 时间复杂度:O(k),k 为可视项数。

CNN 结果可视化

结合 Python CNN 项目,前端可视化训练结果:

fetch('/api/cnn_results')
    .then(response => response.json())
    .then(data => {
        const ctx = document.getElementById('chart').getContext('2d');
        new Chart(ctx, {
            type: 'line',
            data: {
                labels: data.epochs,
                datasets: [{
                    label: '验证准确率',
                    data: data.val_accuracy,
                    borderColor: '#007bff',
                    fill: false
                }]
            }
        });
    });

分析

  • 使用 Chart.js 绘制 CNN 训练曲线。

  • 后端(Node.js 或 Python Flask)提供数据:

    from flask import Flask, jsonify
    app = Flask(__name__)
    
    @app.route('/api/cnn_results')
    def cnn_results():
        return jsonify({
            'epochs': list(range(1, 51)),
            'val_accuracy': [0.65, 0.70, 0.75, ...]
        })
    

企业级实践

Node.js 与 Python 交互

前端通过 Node.js 调用 Python CNN 模型:

const { spawn } = require('child_process');

function runPythonScript(scriptPath, args) {
    return new Promise((resolve, reject) => {
        const python = spawn('python', [scriptPath, ...args]);
        let output = '';
        python.stdout.on('data', (data) => {
            output += data.toString();
        });
        python.stderr.on('data', (data) => {
            reject(data.toString());
        });
        python.on('close', () => {
            resolve(output);
        });
    });
}

runPythonScript('cifar10_project/scripts/predict.py', ['image.jpg'])
    .then(result => console.log(result))
    .catch(err => console.error(err));

Python 脚本 (predict.py):

import sys
import tensorflow as tf
import numpy as np

model = tf.keras.models.load_model('cifar10_project/models/cifar10_model.h5')
image = tf.keras.preprocessing.image.load_img(sys.argv[1], target_size=(32, 32))
image = tf.keras.preprocessing.image.img_to_array(image) / 255.0
image = np.expand_dims(image, axis=0)
prediction = model.predict(image)
print(np.argmax(prediction[0]))

分析

  • Node.js 使用 child_process 调用 Python 脚本。
  • 适合前端展示 CNN 预测结果。

Docker 部署

部署前端与 CNN 后端:

echo 'FROM node:16
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["node", "server.js"]' > Dockerfile
docker build -t frontend_app .

server.js:

const express = require('express');
const { runPythonScript } = require('./utils');

const app = express();
app.use(express.static('public'));

app.get('/api/predict', async (req, res) => {
    const result = await runPythonScript('predict.py', ['image.jpg']);
    res.json({ prediction: result });
});

app.listen(3000, () => console.log('Server running on port 3000'));

分析

  • 前端通过 Express 提供静态文件和 API。
  • Docker 容器化部署,确保环境一致。

深入闭包与作用域

闭包的底层实现

闭包的实现依赖于 JavaScript 引擎(如 V8)的词法环境(Lexical Environment)和执行上下文(Execution Context)。以下是其底层机制:

  • 词法环境:每个函数创建时,V8 为其生成一个词法环境对象,包含:
    • 变量对象:存储本地变量(如 letconst)。
    • 外部引用:指向外层函数的词法环境。
  • 执行上下文:包含变量环境、词法环境和 this 绑定,栈式管理(调用栈)。
  • 闭包捕获:当函数返回时,其词法环境被保留,外部变量引用不会被垃圾回收。

示例(深入分析):

function createCounter() {
    let count = 0;
    return {
        increment: () => ++count,
        getCount: () => count
    };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.getCount()); // 1
console.log(counter.increment()); // 2

逐步分析

  1. createCounter 创建词法环境,包含 count = 0
  2. 返回对象 { increment, getCount },两个函数共享同一词法环境。
  3. V8 引擎为 count 分配堆内存,闭包函数通过引用访问。
  4. 垃圾回收器无法回收 count,因为 incrementgetCount 仍在使用。

内存管理

  • 内存泄漏风险:闭包可能导致未释放的变量累积。例如,事件监听器未移除:
function setupLeak() {
    let data = new Array(1000000).fill(0); // 大数组
    document.getElementById('button').addEventListener('click', () => {
        console.log(data.length); // 闭包引用 data
    });
}

解决

  • 手动移除监听器:

    const button = document.getElementById('button');
    const handler = () => console.log('Clicked');
    button.addEventListener('click', handler);
    // 移除
    button.removeEventListener('click', handler);
    

作用域的进阶应用

块级作用域与 Temporal Dead Zone(TDZ)

ES6 的 letconst 引入块级作用域,并伴随 TDZ(暂时性死区),防止变量在声明前使用。

面试题:以下代码输出什么?

function testTDZ() {
    console.log(x); // ReferenceError
    let x = 10;
}
testTDZ();

分析

  • let x 在声明前不可访问,触发 TDZ 错误。
  • var 无 TDZ,可能导致 undefined

模块作用域

ES6 模块(ESM)引入模块作用域,变量默认私有。

// counter.js
let count = 0;
export function increment() {
    return ++count;
}
export function getCount() {
    return count;
}

// main.js
import { increment, getCount } from './counter.js';
console.log(increment()); // 1
console.log(getCount()); // 1
console.log(increment()); // 2

分析

  • 模块作用域类似闭包,count 仅在模块内可访问。
  • ESM 支持静态分析,优化 Tree Shaking。

面试题 4:闭包与模块

问题:使用闭包重写模块模式。

const counterModule = (function() {
    let count = 0;
    return {
        increment: () => ++count,
        getCount: () => count
    };
})();

console.log(counterModule.increment()); // 1
console.log(counterModule.getCount()); // 1

分析

  • IIFE(立即执行函数表达式)创建私有作用域,模拟模块。
  • 与 ESM 相比,IIFE 动态但不支持 Tree Shaking。

原型链与继承进阶

原型链的底层机制

原型链基于 JavaScript 的对象模型,V8 引擎通过 [[Prototype]] 实现属性查找。以下是关键点:

  • 原型对象Function.prototypeObject.prototype 是原型链的根。
  • 属性遮蔽:对象自身属性优先于原型属性。
  • 性能:深层原型链查找可能影响性能。

示例(属性遮蔽):

function Person(name) {
    this.name = name;
}
Person.prototype.name = "Default";

const person = new Person("Alice");
console.log(person.name); // Alice
delete person.name;
console.log(person.name); // Default

分析

  • delete person.name 移除自身属性,暴露原型属性。
  • 原型链:person -> Person.prototype -> Object.prototype

高级继承模式

寄生组合继承

寄生组合继承是高效的继承方式,避免重复调用父类构造函数。

function inherit(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

function Animal(type) {
    this.type = type;
}
Animal.prototype.eat = function() {
    console.log(`${this.type} eats`);
};

function Dog(name, type) {
    Animal.call(this, type);
    this.name = name;
}
inherit(Dog, Animal);
Dog.prototype.bark = function() {
    console.log(`${this.name} barks`);
};

const dog = new Dog("Max", "Dog");
dog.eat(); // Dog eats
dog.bark(); // Max barks

分析

  • Object.create 创建中间对象,避免 Dog.prototype = new Animal() 的副作用。
  • Animal.call 继承属性,inherit 继承方法。

Mixin 模式

Mixin 允许多重继承,复用代码。

const canRun = {
    run() {
        console.log(`${this.name} runs`);
    }
};

function Dog(name) {
    this.name = name;
}
Object.assign(Dog.prototype, canRun);

const dog = new Dog("Max");
dog.run(); // Max runs

分析

  • Object.assign 将 Mixin 方法复制到原型。
  • 适合复用独立功能,如日志、事件处理。

面试题 5:原型链修改

问题:以下代码输出什么?如何避免问题?

function Person() {}
Person.prototype.name = "Shared";

const p1 = new Person();
const p2 = new Person();
p1.name = "Alice";
console.log(p1.name); // Alice
console.log(p2.name); // Shared
Person.prototype.name = "Modified";
console.log(p1.name); // Alice
console.log(p2.name); // Modified

分析

  • p1.name = "Alice"p1 自身创建属性,不影响原型。
  • 修改 Person.prototype.name 影响未遮蔽的实例(如 p2)。
  • 避免问题:避免直接修改原型,使用 Mixin 或实例属性。

面试题 6:instanceof 实现

问题:手动实现 instanceof

function myInstanceof(obj, constructor) {
    let proto = Object.getPrototypeOf(obj);
    while (proto) {
        if (proto === constructor.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

console.log(myInstanceof(dog, Dog)); // true
console.log(myInstanceof(dog, Animal)); // true

分析

  • 遍历 obj 的原型链,检查是否包含 constructor.prototype
  • 时间复杂度:O(n),n 为原型链长度。

数据结构与算法进阶

动态规划

面试题:最长公共子序列(LCS)

问题:求两个字符串的最长公共子序列长度。

function longestCommonSubsequence(text1, text2) {
    const m = text1.length, n = text2.length;
    const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
    
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (text1[i - 1] === text2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[m][n];
}

console.log(longestCommonSubsequence("ABCD", "ACDF")); // 3 (ACD)

分析

  • 时间复杂度:O(m_n),空间复杂度:O(m_n)。
  • 前端应用:文本差异比较(如代码编辑器高亮)。

图算法

面试题:广度优先搜索(BFS)

function bfs(graph, start) {
    const visited = new Set();
    const queue = [start];
    visited.add(start);
    
    while (queue.length) {
        const node = queue.shift();
        console.log(node);
        for (let neighbor of graph[node]) {
            if (!visited.has(neighbor)) {
                visited.add(neighbor);
                queue.push(neighbor);
            }
        }
    }
}

const graph = {
    A: ['B', 'C'],
    B: ['A', 'D', 'E'],
    C: ['A', 'F'],
    D: ['B'],
    E: ['B', 'F'],
    F: ['C', 'E']
};
bfs(graph, 'A'); // A, B, C, D, E, F

分析

  • 时间复杂度:O(V + E),空间复杂度:O(V)。
  • 前端应用:组件依赖解析、路由导航。

LeetCode 高频题

面试题:两数之和

问题:给定数组和目标值,找出两个数的索引,使其和等于目标值。

function twoSum(nums, target) {
    const map = new Map();
    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        if (map.has(complement)) {
            return [map.get(complement), i];
        }
        map.set(nums[i], i);
    }
    return [];
}

console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]

分析

  • 使用哈希表,时间复杂度:O(n),空间复杂度:O(n)。
  • 前端应用:快速查找 DOM 元素对。

前端性能优化

节流与防抖

节流(Throttle):限制函数在固定时间间隔内执行一次。

function throttle(fn, delay) {
    let last = 0;
    return function(...args) {
        const now = Date.now();
        if (now - last >= delay) {
            fn.apply(this, args);
            last = now;
        }
    };
}

const scrollHandler = throttle(() => console.log('Scrolled'), 1000);
window.addEventListener('scroll', scrollHandler);

防抖(Debounce):延迟执行,直到事件停止触发。

function debounce(fn, delay) {
    let timer;
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}

const resizeHandler = debounce(() => console.log('Resized'), 500);
window.addEventListener('resize', resizeHandler);

分析

  • 节流适合高频事件(如滚动),防抖适合输入验证。
  • 优化前端交互性能,减少不必要的计算。

虚拟 DOM 优化

React 的虚拟 DOM 优化 DOM 操作:

class List extends React.Component {
    shouldComponentUpdate(nextProps) {
        return this.props.items !== nextProps.items;
    }
    
    render() {
        return (
            <div>
                {this.props.items.map(item => <div key={item.id}>{item.text}</div>)}
            </div>
        );
    }
}

分析

  • shouldComponentUpdate 避免不必要的重新渲染。
  • 时间复杂度:O(n) 比较虚拟 DOM 树。

与 CNN 项目的整合

前端可视化 CNN 结果

使用 Chart.js 可视化 Python CNN 训练结果:

fetch('/api/cnn_results')
    .then(response => response.json())
    .then(data => {
        const ctx = document.getElementById('accuracyChart').getContext('2d');
        new Chart(ctx, {
            type: 'line',
            data: {
                labels: data.epochs,
                datasets: [
                    {
                        label: '训练准确率',
                        data: data.accuracy,
                        borderColor: '#007bff',
                        fill: false
                    },
                    {
                        label: '验证准确率',
                        data: data.val_accuracy,
                        borderColor: '#28a745',
                        fill: false
                    }
                ]
            },
            options: {
                scales: {
                    y: {
                        beginAtZero: true,
                        max: 1
                    }
                }
            }
        });
    });

Python 后端(Flask):

from flask import Flask, jsonify
app = Flask(__name__)

@app.route('/api/cnn_results')
def cnn_results():
    return jsonify({
        'epochs': list(range(1, 51)),
        'accuracy': [0.65, 0.70, 0.75, ...], # 训练数据
        'val_accuracy': [0.60, 0.65, 0.70, ...]
    })

if __name__ == '__main__':
    app.run(port=5000)

分析

  • 前端通过 Fetch API 获取数据,Chart.js 绘制曲线。
  • 后端使用 Flask 提供 REST API,结合 Anaconda 环境运行。

WebAssembly 调用 CNN

使用 TensorFlow.js 或 ONNX.js 运行 CNN 模型:

import * as tf from '@tensorflow/tfjs';

async function predict(imageElement) {
    const model = await tf.loadLayersModel('/models/cifar10_model.json');
    const img = tf.browser.fromPixels(imageElement).resizeNearestNeighbor([32, 32]).toFloat().div(255).expandDims();
    const prediction = model.predict(img);
    const result = await prediction.data();
    console.log(result);
}

const img = document.getElementById('inputImage');
predict(img);

分析

  • TensorFlow.js 在浏览器运行 CNN 模型,无需后端。

  • 需将 Python 模型转换为 TF.js 格式:

    tensorflowjs_converter --input_format keras cifar10_project/models/cifar10_model.h5 cifar10_project/models/web_model
    

Node.js 与 Python 交互

Node.js 调用 Python CNN 预测:

const { spawn } = require('child_process');

function runPrediction(imagePath) {
    return new Promise((resolve, reject) => {
        const python = spawn('python', ['predict.py', imagePath]);
        let output = '';
        python.stdout.on('data', (data) => output += data);
        python.stderr.on('data', (data) => reject(data.toString()));
        python.on('close', () => resolve(output));
    });
}

runPrediction('image.jpg').then(result => console.log(`Prediction: ${result}`));

predict.py

import sys
import tensorflow as tf
import numpy as np

model = tf.keras.models.load_model('cifar10_project/models/cifar10_model.h5')
image = tf.keras.preprocessing.image.load_img(sys.argv[1], target_size=(32, 32))
image = tf.keras.preprocessing.image.img_to_array(image) / 255.0
image = np.expand_dims(image, axis=0)
prediction = model.predict(image)
print(np.argmax(prediction[0]))

分析

  • 使用 child_process 调用 Python 脚本。

  • Linux 命令管理进程:

    ps aux | grep python
    kill -9 <pid>
    

企业级实践

微前端架构

使用 Module Federation 实现微前端:

// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
    plugins: [
        new ModuleFederationPlugin({
            name: 'host',
            remotes: {
                app1: 'app1@http://localhost:3001/remoteEntry.js'
            }
        })
    ]
};

// app1.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
    plugins: [
        new ModuleFederationPlugin({
            name: 'app1',
            filename: 'remoteEntry.js',
            exposes: {
                './Chart': './src/Chart.js'
            }
        })
    ]
};

分析

  • 微前端分解大型应用,独立部署。
  • 适合 CNN 可视化模块的动态加载。

CI/CD 集成

使用 GitHub Actions 自动化部署:

name: Deploy Frontend
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '16'
      - run: npm install
      - run: npm run build
      - name: Deploy to S3
        run: aws s3 sync ./dist s3://my-bucket

分析

  • 自动化构建和部署前端代码。

  • 结合 Docker 部署 CNN 后端:

    docker push myrepo/cnn_app:latest
    

Kubernetes 部署

部署前端与 CNN 服务:

kubectl create -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cnn-frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: cnn-frontend
  template:
    metadata:
      labels:
        app: cnn-frontend
    spec:
      containers:
      - name: frontend
        image: frontend_app:latest
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: cnn-frontend-service
spec:
  selector:
    app: cnn-frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: LoadBalancer
EOF

分析

  • 部署前端服务,负载均衡提高可用性。
  • 可扩展到 CNN 后端,分配 GPU 资源。

【拒绝平庸】Vue+ECharts图表美化--柱状图展示优化教程

优化后的饼图效果

6.gif

Scss样式部分

html,body{
  width: 100%;
  height: 100%;
  padding:0px; 
  box-sizing: border-box;
  overflow: hidden;
}
 
body{
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
}
 
.layout-demo-box{
  display: flex;
  flex-direction: column;
  width: 540px;
  height: 300px;
  background: linear-gradient(
    to bottom,
  #000e2a 0%,
  #000000 10%,
  #001134 100%
  );
  border: 1px solid #00436e;
  border-radius: 5px;
  *{
    box-sizing: border-box;
  }
  .title-box{
    display: flex;
    align-items: center;
    width: 100%; 
    height: 50px;
    flex-shrink: 0;
    padding: 20px 30px 0px 20px; 
    span{
      flex-shrink: 0;

      &:nth-child(1){
        width: 0px;
        flex-grow: 1;
      }
    }
    .btn-box{
      display: block;
      color:#6bf6fc;
      cursor: pointer;
    }
    h1{
      font-size: 14px; 
      line-height: 16px; 
      margin: 0px;
      background: linear-gradient(to top, #00d1fe, #fff);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;  
    }
    p{
      font-size: 12px;
      margin:2px 0px;
      color:#416387;
    }
  }
  .chart-box{
    width: 100%;
    height: 0px;
    flex:1;
  } 
}  

HTML页面部分

<div id="app">
  <!-- demo内容 start -->  
  <div class="layout-demo-box">
    <div class="title-box">
      <span>
        <h1>柱状图面板</h1>
        <p>统计日期(2025-07-02 12:00:00)</p> 
      </span>  
    </div> 
    <div class="chart-box" id="chartId"></div>
  </div>
  <!-- demo内容 end --> 
</div>

JS页面部分

  methods: {
    /**
     * 初始化并渲染 ECharts 图表
     * 功能说明:
     * 1. 创建 ECharts 实例并渲染图表
     * 2. 自动响应窗口大小变化
     * 3. 组件销毁时自动清理资源防止内存泄漏 
     */
    initEcharts() {
      // 1. 获取 DOM 元素 - 添加空检查
      const chartDom = document.getElementById('chartId'); 
      if (!chartDom) {
        console.warn(' 图表容器不存在');
        return;
      }
  
      // 2. 初始化图表实例
      this.myChart  = echarts.init(chartDom); 
      
      // 3. 设置图表配置 
      const option = {
        // option 配置 start ---------------------------------------
        
        // option 配置 end ---------------------------------------
      };
      
      // 4. 应用配置
      try {
        this.myChart.setOption(option); 
      } catch (error) {
        console.error(' 图表配置错误:', error);
      }
  
      // 5. 响应式处理 - 使用防抖优化性能
      this.handleResize  = debounce(() => {
        this.myChart  && this.myChart.resize(); 
      }, 300);
      
      window.addEventListener('resize',  this.handleResize); 
    },
    
    // 清理资源 
    destroyEcharts() {
      if (this.myChart)  {
        window.removeEventListener('resize',  this.handleResize); 
        this.myChart.dispose(); 
        this.myChart  = null;
      }
    }
  },
  
  // Vue生命周期钩子-组件挂载完成后调用
  mounted() {
    this.$nextTick(() => {
      this.initEcharts(); 
    });
  }, 

  // Vue生命周期钩子-组件销毁前调用
  beforeDestroy() {
    this.destroyEcharts(); 
  }

定义data数据

  // 数据
  chartData:{
    xAxisData: ['语文','数学','英语','科学','历史'],
    seriesData : [20, 80, 100, 40, 34, 90, 60]
  }, 

柱状图的option配置

tooltip:{
  trigger: 'axis',
  axisPointer: {
    type: 'shadow',
    shadowStyle: { // 鼠标经过背景色
      color: 'rgba(0, 67, 110, 0.1)',
    } 
  },
  formatter: function(params) { 
    return params[0].marker + params[0].name + "成绩:" + params[0].data
  }
}, 
animation: true,
grid: {
  top: "40",
  bottom: "40",
  left: "50", 
  right: "20"  
},
xAxis: {
  data: chartData.xAxisData,
  axisLine: {
    show: true, //隐藏X轴轴线
    lineStyle: {
      color: '#0A376C'
    }
  },
  axisTick: {
    show: false //隐藏X轴刻度
  },
  axisLabel: {
    show: true,
    margin: 14,
    fontSize: 12,
    textStyle: {
      color: "#A3C0DF" //X轴文字颜色
    }
  }  
},
yAxis: [
  {
    type: "value",
    gridIndex: 0,  
    splitLine: {
      show: true,
      lineStyle: {
        type: 'dashed', // 关键属性:虚线
        color: '#011731',
        width: 1
      },
    },
    axisTick: {
      show: false
    },
    axisLine: {
      show: false, 
    },
    axisLabel: {
      show: true,
      margin: 14,
      fontSize: 10,
      textStyle: {
        color: "#A3C0DF" //X轴文字颜色
      }
    }
  }
], 
series: [
  {
    name: "单科成绩",
    type: "bar",
    barWidth: 16,
    itemStyle: {
      normal: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
            offset: 0,
            color: "#07ecd9"
          },
          {
            offset: 1,
            color: "#034881"
          }
        ]), 
      }
    }, 
    data: chartData.seriesData,
    z: 10,
    zlevel: 2,
    label: {
      show: true,
      position: "top",
      distance: 5,
      fontSize:12,
      color: "#01fff4"
    }
  },   
  {
    // 分隔
    type: "pictorialBar",
    itemStyle: {
      normal:{
        color:"#0F375F"
      }
    },
    symbolRepeat: "fixed",
    symbolMargin: 2,
    symbol: "rect",
    symbolClip: true,
    symbolSize: [16, 2],
    symbolPosition: "start",
    symbolOffset: [0, -1], 
    data: chartData.seriesData, 
    z: 0,
    zlevel: 3,
  }
]      

在线Demo

下方可在线查看Demo完整代码

总结

通过以上步骤,我们成功地使用 Echarts 制作并优化了一个柱状图。在实际应用中,大家可以根据具体的数据和业务需求,进一步调整图表的样式和交互效果,让数据可视化更加美观和实用。希望这篇教程能对大家在前端数据可视化开发中有所帮助

告别FLIP动画:View Transition API带来的革命性变革

前端动画发展这么多年了,transition、animation等,但他们都有一个问题,就是页面元素结构发生变化时,无法应用动画。所以就有了FLIP动画,它可以在页面元素结构发生变化时,应用动画。

FLIP动画: 曾经的王者

FLIP是First, Last, Invert, Play的缩写,它是一种动画效果,它的核心思想是:在元素结构发生变化时,先记录元素的初始状态,然后记录元素的最终状态,然后计算出元素的变化量,最后应用这些变化量,就可以实现动画效果。

  1. First : 记录元素的当前状态(位置、大小)
  2. Last : 修改元素,记录元素的最终状态(位置、大小)
  3. Invert : 计算元素的变化量, 并通过transform将元素恢复到初始状态
  4. Play : 对transform应用动画,将元素移动到最终状态

这种方式被广泛应用于拖拽, 列表重排等场景中。但是它太麻烦了。需要手动计算元素初始及结束状态,及变化量,然后应用到元素上。

View Transition API: 革命性的变革

View Transition API是一种新的API,它可以在页面元素结构发生变化时,通过动画的方式完成变化

document.startViewTransition(() => {
  // 这里是修改页面元素的代码
  // 比如更新DOM,调整元素位置等
});

只需要将DOM变更的代码放在startViewTransition的回调函数中,就可以实现动画效果了。浏览器会自动:

  • 捕获前后状态
  • 自动为旧视图和新视图创建snapshot
  • 管理动画过程

使用这个新的api你只需要专注于我想改什么, 不再关注怎么应用动画

CSS

View Transition API 还有配套的CSS属性,用于控制动画效果,主要有以下几个属性:

::view-transition, ::view-transition-old, ::view-transition-new, ::view-transition-group, ::view-transition-image-pair

兼容性

除了Firefox, 主流现代浏览器都已经支持了


曾经我们用FLIP实现流畅的动画效果,是因为浏览器不给力。今天,浏览器已经足够给力,是时候说一句

再见,FLIP。你好,View Transition API。

告别盲测:Jest--JavaScript测试之道

为什么要测试,测试什么?

为什么要测试?

  • 提升代码质量和可靠性: 尽早发现并修复bug,减少线上事故。
  • 增强重构信心: 有测试用例保驾护航,你可以大胆地优化和重构代码,因为你知道它们会立即发现潜在的回归错误。
  • 提高开发效率: 避免了手动重复测试的繁琐,让你可以更快地迭代新功能。

测试什么?

我们通常将测试分为几个层次:

  • 单元测试 (Unit Tests): 针对代码中最小的可独立测试单元进行测试,如单个函数、类的方法。它们应该快速、独立、可重复。这是JTest的基础和核心。

  • 集成测试 (Integration Tests): 测试多个单元或模块协同工作时的行为,验证它们之间的接口和交互是否正确。例如,测试一个UI组件与数据层API的交互。


JTest初体验——告别盲测的第一步 (Jest入门)

环境搭建

首先,我们来安装Jest。这是一个零配置的测试框架,非常适合快速上手。

# 进入你的项目目录
cd your-js-project

# 安装 Jest
npm install --save-dev jest

然后,在 package.json 中添加一个 test 脚本:

{
  "name": "your-js-project",
  "version": "1.0.0",
  "scripts": {
    "test": "jest"
  },
  "devDependencies": {
    "jest": "^30.0.3"
  }
}

2.2 你的第一个JTest用例

让我们从一个最简单的纯函数开始:加法。

src/sum.js

// 这是一个简单的加法函数
function sum(a, b) {
  return a + b;
}

module.exports = sum; // 导出函数

接下来,我们为 sum.js 创建一个测试文件。根据Jest的约定,测试文件通常与源文件放在同一目录下,并以 .test.js.spec.js 结尾。

src/sum.test.js

const sum = require('./sum'); // 导入要测试的函数

// describe 块用于组织相关的测试用例
describe('sum 函数', () => {
    // test (或 it) 定义一个具体的测试用例
    test('应该正确计算两个正数的和', () => {
        // expect(value) 是 Jest 的全局函数,用于声明一个断言
        // .toBe(expected) 是一个匹配器 (matcher),用于比较值是否相等
        expect(sum(1, 2)).toBe(3);
    });

    test('应该正确计算一个正数和一个负数的和', () => {
        expect(sum(5, -3)).toBe(2);
    });

    test('应该正确计算两个零的和', () => {
        expect(sum(0, 0)).toBe(0);
    });
});

2.3 运行测试

现在,打开你的终端,运行测试命令:

npm test

你将看到类似以下的输出:

> jest

 PASS  ./sum.test.js
  sum 函数
     应该正确计算两个正数的和 (2 ms)
     应该正确计算一个正数和一个负数的和
     应该正确计算两个零的和

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.387 s, estimated 1 s
Ran all test suites.

恭喜你!你已经成功编写并运行了你的第一个JTest用例。PASS 意味着你的代码通过了测试,一切正常。


深入浅出:JTest核心断言与组织

更多的断言匹配器 (Matchers)

Jest提供了丰富的匹配器,用于检查各种条件。

匹配器 描述 示例
toBe(value) 严格相等 (===),用于基本类型 expect(1).toBe(1);
toEqual(value) 递归比较对象或数组的内容相等,用于引用类型 expect({a:1}).toEqual({a:1});
not.toBe(value) 不严格相等 expect(1).not.toBe(2);
toBeTruthy() 检查是否为真值 (truthy) expect(1).toBeTruthy();
toBeFalsy() 检查是否为假值 (falsy) expect(0).toBeFalsy();
toBeNull() 检查是否为null expect(null).toBeNull();
toBeUndefined() 检查是否为undefined expect(undefined).toBeUndefined();
toBeDefined() 检查是否已定义 expect(1).toBeDefined();
toBeInstanceOf(Class) 检查是否是某个类的实例 expect(new Array()).toBeInstanceOf(Array);
toContain(item) 检查数组中是否包含某个元素 expect([1, 2, 3]).toContain(2);
toMatch(regexp) 检查字符串是否匹配正则表达式 expect('hello').toMatch(/ll/);
toThrow(error?) 检查函数是否抛出错误 expect(() => { throw new Error(); }).toThrow();
resolves.toBe(value) 检查Promise是否成功解决并匹配值 await expect(Promise.resolve(1)).resolves.toBe(1);
rejects.toThrow(error?) 检查Promise是否失败并抛出错误 await expect(Promise.reject('error')).rejects.toThrow('error');

示例:src/stringUtils.js

function capitalize(str) {
  if (typeof str !== 'string' || str.length === 0) {
    throw new Error('Input must be a non-empty string.');
  }
  return str.charAt(0).toUpperCase() + str.slice(1);
}

module.exports = { capitalize };

src/stringUtils.test.js

const { capitalize } = require('./stringUtils');

describe('capitalize 函数', () => {
    test('应该将字符串的第一个字母大写', () => {
        expect(capitalize('hello')).toBe('Hello');
    });

    test('应该返回相同的大写字符串,如果第一个字母已经是大写', () => {
        expect(capitalize('World')).toBe('World');
    });

    test('应该处理单字符字符串', () => {
        expect(capitalize('a')).toBe('A');
    });

    test('应该抛出错误,如果输入不是字符串', () => {
        expect(() => capitalize(123)).toThrow('Input must be a non-empty string.');
        expect(() => capitalize(null)).toThrow('Input must be a non-empty string.');
    });

    test('应该抛出错误,如果输入是空字符串', () => {
        expect(() => capitalize('')).toThrow('Input must be a non-empty string.');
    });
});

测试生命周期函数:beforeEachafterEach

在某些场景下,你可能需要在每个测试用例运行之前或之后执行一些设置或清理工作。Jest提供了 beforeEach, afterEach, beforeAll, afterAll 等生命周期函数。

  • beforeEach(fn): 在每个 test (或 it) 运行之前执行。
  • afterEach(fn): 在每个 test (或 it) 运行之后执行。
  • beforeAll(fn): 在当前 describe 块中的所有 test 运行之前执行一次。
  • afterAll(fn): 在当前 describe 块中的所有 test 运行之后执行一次。

示例:计数器模块的测试

src/counter.js

let count = 0;

function increment() {
    count++;
}

function decrement() {
    count--;
}

function getCount() {
    return count;
}

function reset() {
    count = 0;
}

module.exports = {
    increment,
    decrement,
    getCount,
    reset
};

src/counter.test.js

const counter = require('./counter');

describe('计数器模块', () => {
    // 在每个测试用例运行前,将计数器重置为0,确保每个测试的独立性
    beforeEach(() => {
        counter.reset();
    });

    test('increment 应该使计数器加一', () => {
        counter.increment();
        expect(counter.getCount()).toBe(1);
    });

    test('decrement 应该使计数器减一', () => {
        counter.decrement();
        expect(counter.getCount()).toBe(-1);
    });

    test('连续调用 increment 应该正确累加', () => {
        counter.increment();
        counter.increment();
        expect(counter.getCount()).toBe(2);
    });

    test('reset 应该将计数器重置为0', () => {
        counter.increment();
        counter.increment();
        counter.reset();
        expect(counter.getCount()).toBe(0);
    });
});

测试覆盖率

衡量你的测试质量的一个重要指标是测试覆盖率 (Test Coverage) 。Jest内置了此功能。

package.jsontest 脚本中添加 --coverage 标志:

{
  "scripts": {
    "test": "jest --coverage"
  }
}

运行 npm test 后,你会在终端看到一个报告,也会在项目根目录生成一个 coverage 文件夹,其中包含详细的HTML报告,你可以打开 coverage/lcov-report/index.html 查看。

覆盖率指标:

  • Statements (语句): 代码中的语句有多少被执行了。
  • Branches (分支): if/else, switch, 三元表达式等分支有多少被执行了。
  • Functions (函数): 函数有多少被调用了。
  • Lines (行): 代码行有多少被执行了。

注意: 100% 覆盖率不代表代码没有bug,它只能说明你的测试执行了所有代码路径,但无法保证这些路径的逻辑都是正确的。你应该追求有意义的覆盖率,而不是盲目追求数字。


结语


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

深入理解BFC:前端布局中的“隔离结界”

在前端开发中,CSS布局始终是核心技能之一。无论是新手还是资深开发者,都会遇到诸如浮动元素导致的父容器高度塌陷、外边距重叠、元素重叠等问题。而解决这些问题的核心概念之一,就是BFC(Block Formatting Context,块级格式化上下文)。本文将从BFC的定义、触发条件、特性、应用场景及注意事项等方面进行深入解析,帮助你掌握这一布局利器。


一、什么是BFC?

BFC(Block Formatting Context)是一个独立的渲染区域,它遵循特定的布局规则。简单来说,BFC就像一个“结界”:内部元素的布局不会影响外部元素,外部元素也不会干扰内部布局。这种隔离性使得BFC成为解决复杂布局问题的强大工具。

1.1 BFC的核心特性

  • 垂直排列:BFC内的块级元素会垂直排列,间距由margin决定。
  • 外边距折叠:同一BFC内的相邻块级元素的垂直外边距会合并(如margin-top:20pxmargin-bottom:30px合并为30px),但不同BFC之间的外边距不会折叠。
  • 包含浮动元素:BFC会计算内部浮动元素的高度,避免父容器高度塌陷。
  • 隔离浮动:BFC区域不会与浮动元素重叠。
  • 独立布局:BFC内部的布局规则不会影响外部元素,反之亦然。

二、如何触发BFC?

BFC的触发条件是理解其应用的关键。以下是一些常见的触发方式:

2.1 根元素

HTML的根元素<html>默认就是一个BFC。

2.2 浮动元素

当元素的float属性值为leftright时,会创建BFC。

.float-element {
  float: left;
  width: 100px;
  height: 100px;
}

2.3 绝对定位元素

设置positionabsolutefixed的元素会脱离文档流并创建BFC。

.absolute-element {
  position: absolute;
  top: 20px;
  left: 20px;
}

2.4 行内块元素

display设置为inline-blocktable-celltable-caption等值时,元素会创建BFC。

.inline-block-element {
  display: inline-block;
}

2.5 溢出容器

当元素的overflow属性值不为visible(如hiddenautoscroll)时,会触发BFC。

.overflow-container {
  overflow: hidden;
}

2.6 弹性盒子和网格布局

display设置为flexgrid等现代布局模式时,也会创建BFC。

.flex-container {
  display: flex;
}

三、BFC的应用场景

BFC的核心价值在于解决布局中的常见问题。以下是几个典型的应用场景:


3.1 清除浮动(解决父容器高度塌陷)

问题描述:

当子元素使用浮动时,父容器的高度会塌陷为0,导致布局混乱。

解决方案:

通过触发父容器的BFC,使其包裹浮动子元素。

<div class="container">
  <div class="float-box">浮动内容</div>
</div>
.container {
  overflow: hidden; /* 触发BFC */
}

.float-box {
  float: left;
  width: 200px;
  height: 100px;
  background: lightblue;
}

原理:

BFC会计算内部浮动元素的高度,从而避免父容器高度塌陷。


3.2 防止外边距折叠

问题描述:

相邻块级元素的垂直外边距会合并,导致间距不符合预期。

解决方案:

为其中一个元素包裹BFC容器,阻止外边距折叠。

<div class="bfc-container">
  <div class="element1">元素A</div>
</div>
<div class="element2">元素B</div>
.bfc-container {
  overflow: hidden; /* 触发BFC */
}

.element1 {
  margin-bottom: 30px;
}

.element2 {
  margin-top: 20px;
}

原理:

不同BFC之间的外边距不会折叠,因此element1element2的间距为30px + 20px = 50px


3.3 实现两栏布局

问题描述:

左侧固定宽度,右侧自适应宽度的布局。

解决方案:

左侧浮动,右侧触发BFC以自适应剩余空间。

<div class="container">
  <div class="sidebar">侧边栏</div>
  <div class="content">内容区域</div>
</div>
.sidebar {
  float: left;
  width: 200px;
  background: lightgray;
}

.content {
  overflow: hidden; /* 触发BFC */
}

原理:

BFC区域不会与浮动元素重叠,右侧内容会自动填充剩余空间。


3.4 防止元素重叠

问题描述:

浮动元素可能与非浮动元素重叠。

解决方案:

为非浮动元素触发BFC,避免重叠。

<div class="float-box">浮动元素</div>
<div class="bfc-box">非浮动元素</div>
.float-box {
  float: left;
  width: 100px;
  height: 100px;
  background: lightblue;
}

.bfc-box {
  overflow: hidden; /* 触发BFC */
  background: lightgreen;
}

原理:

BFC区域不会与浮动元素重叠,非浮动元素会自动避开浮动元素。


四、BFC的注意事项

尽管BFC强大且实用,但在使用时仍需注意以下几点:

4.1 性能影响

创建过多BFC可能会增加浏览器的计算负担,尤其是在复杂页面中。建议合理使用,避免不必要的触发。

4.2 兼容性

大多数现代浏览器都支持BFC,但在某些旧版本浏览器中可能存在兼容性问题。建议测试后再部署。

4.3 与现代布局技术的结合

随着Flexbox和Grid的普及,许多传统BFC场景已被更直观的现代布局技术替代。例如:

  • Flexbox替代BFC清除浮动

    .container {
      display: flex; /* 自动包含子元素 */
    }
    
  • Grid替代BFC实现多栏布局

    .container {
      display: grid;
      grid-template-columns: 200px 1fr;
    }
    

五、总结

BFC是前端布局中不可或缺的概念,它通过创建独立的渲染区域,帮助开发者解决浮动、外边距折叠、元素重叠等常见问题。掌握BFC的触发条件和应用场景,不仅能提升布局效率,还能避免许多潜在的兼容性问题。

在实际开发中,BFC常与现代布局技术(如Flexbox、Grid)结合使用,既保留了传统方法的灵活性,又兼顾了现代布局的简洁性。通过不断实践和优化,你将能够更高效地构建稳定、美观的网页布局。


附录:BFC触发条件自查清单

在设计布局时,可以通过以下清单检查是否需要触发BFC:

场景 是否需要触发BFC
需要清除浮动
防止外边距折叠
实现自适应布局
避免元素重叠
使用Flexbox/Grid时 ❌(优先使用现代布局技术)

通过本文的讲解,相信你已经对BFC有了更深入的理解。在未来的开发中,合理运用BFC,定能让你的布局更加得心应手!

十万级设备接入的微前端架构设计实践

十万级设备接入的微前端架构设计实践

本文以我实际参与的某大厂的十万级设备接入的项目为例,详细阐述在高并发、大数据量场景下,如何构建可扩展、高可用的微前端架构。结合 "领域解耦、性能优先、安全可控" 的设计思想,从架构拆分到落地实现形成完整解决方案。

一、核心挑战

  1. 设备搜索体验:十万级设备列表中,如何实现亚秒级搜索与分页加载?
  2. 第三方集成:与 10 + 外部系统互联时,如何保障鉴权时效性与操作安全性?
  3. 模块通信:5 + 微应用间需共享设备状态、全局筛选条件,如何实现低耦合通信?
  4. 技术栈兼容:在公司强制使用 OpenTiny 组件库的约束下,如何适配多技术栈?
  5. 旧系统迁移:需保留 50 + 旧系统页面功能,如何实现平滑集成与双向通信?

二、架构设计:基于领域驱动设计(DDD)模块拆分

以"业务域 - 微应用" 一一映射原则,将系统划分为四个核心子域,每个子域都是一个独立的微应用,通过主应用网关聚合:

核心子域 核心功能 技术栈选型 部署策略
设备管理域 设备接入、状态监控、批量操作 Angular + OpenTiny 独立容器化部署
认证鉴权域 统一身份管理、第三方系统授权 React + Redux 鉴权中台单独集群
数据可视化域 实时数据看板、设备拓扑渲染 Vue3 + ECharts CDN 静态资源加速
集成兼容域 旧系统嵌套、多技术栈适配 原生 JS + Web Component 混合部署兼容层

架构设计原则

  • 独立性:微应用可单独开发、测试、部署,主应用仅通过注册中心管理
  • 扩展性:支持单域横向扩展(如设备管理域按区域拆分微应用)
  • 兼容性:预留旧系统迁移接口,支持 "渐进式替换" 而非 "一刀切" 重构

三、工程化实践

1. 项目结构标准化

采用 Lerna 管理多包架构,实现 "源码集中管理、应用独立部署":

graph TD
    A[root] --> B[packages]
    A --> C[shared]
    A --> D[config]
    
    B --> E[main-app]
    B --> F[device-manager]
    B --> G[auth-center]
    B --> H[data-visualization]
    B --> I[legacy-adapter]
    
    C --> J[components]
    C --> K[utils]
    C --> L[types]
    
    D --> M[eslint]
    D --> N[typescript]
    D --> O[build]

2. 依赖管理优化

  • 跨应用依赖:通过 Lerna 符号链接(symlink)实现共享模块本地修改实时生效lerna bootstrap --hoist 自动提升公共依赖至根目录,减少冗余安装

  • 版本控制:采用独立版本模式(independent),微应用可单独发布

    // lerna.json
    {
      "version": "independent",
      "npmClient": "pnpm",
      "command": {
        "publish": {
          "conventionalCommits": true,
          "message": "chore(release): publish",
          "registry": "https://registry.npmjs.org/",
          "ignoreChanges": [
            "**/*.md",
            "**/test/**",
            "**/docs/**"
          ]
        },
        "bootstrap": {
          "ignore": "component-*",
          "npmClientArgs": ["--no-package-lock"]
        }
      },
      "changelogPreset": "angular",
      "stream": true,
      "useWorkspaces": true
    }
    

3. 构建与部署流程

  • 差异化构建:支持单应用构建(提升开发效率)lerna run build --scope=device-app
  • 增量构建:仅构建变更过的应用(结合 Git 提交记录)lerna run build --since=last-release
  • 统一部署:通过主应用 nginx 配置反向代理,微应用独立部署至 CDN

4. 代码规范与质量

  • 统一校验:根目录配置 ESLint/Prettier,所有微应用继承规范

  • 提交检查:husky+commitlint 强制 Conventional Commits 格式

    # 安装钩子
    npx husky add .husky/commit-msg "npx commitlint --edit $1"
    

5. 开发体验优化

  • 本地调试:主应用通过qiankun配置本地微应用地址,实现联合调试
  • 热更新支持:各微应用独立开启 HMR(热模块替换),修改即时生效

四、 核心问题解决方案

问题1:万级设备搜索性能优化

目标: 实现”输入即反馈“的搜索体验,本地查询响应≤100ms

分层搜索架构设计:

graph LR
A[本地缓存层] --> B[设备ID索引库]
C[Web Worker线程] --> D[本地模糊匹配]
E[体验优化] --> F[预加载]
  • 本地缓存层
    • 采用分页策略,首次加载 1000 条设备基础信息(ID、名称、状态)至 IndexedDB,建立 Bloom Filter 索引,实现 O (1) 复杂度的设备 ID 存在性检测。
    • 缓存策略:设备基础信息 24 小时过期,状态变更通过 WebSocket 实时更新。
  • 搜索分层处理
    • 第一层:Web Worker 中执行设备 ID 前缀匹配,支持设备名称模糊查询、状态筛选等基础条件
    • 第二层:复杂条件查询(如所属区域、所属产品)触发后端请求,采用分片加载协议(每次加载 500 条),通过请求优先级队列(用户输入停顿 0.5 秒后发送请求)减少无效请求
  • 体验优化
    • 本地保存用户查询与分页状态,再次进入页面时自动恢复
    • 实现“预请求后10页”机制,当用户进行查询时,如果有切换分页操作,预先请求后10页数据。
// 初始化IndexedDB与Bloom Filter
async initDeviceCache() {
  const db = await openIndexedDB('deviceDB', 1, {
    devices: { keyPath: 'id', indexes: [{ name: 'status', keyPath: 'status' }] }
  });
  // 首次加载1000条基础数据
  const initialData = await fetch('/api/devices?page=1&size=1000');
  await db.devices.bulkAdd(initialData);
  // 构建Bloom Filter(预估1000万条数据,误判率0.01%)
  window.deviceBloomFilter = new BloomFilter(10000000, 0.0001);
  initialData.forEach(device => deviceBloomFilter.add(device.id));
}
// 前端缓存层实现(IndexedDB + BloomFilter)
class DeviceCache {
  constructor() {
    this.db = new Dexie('DeviceCacheDB');
    this.db.version(1).stores({ devices: 'id' });
  }

  async initCache(deviceList) {
    // BloomFilter初始化(简化版)
    this.bloom = new BloomFilter(32 * 1024 * 1024, 3); 
    
    await this.db.devices.bulkPut(deviceList);
    deviceList.forEach(d => this.bloom.add(d.id));
  }

  async search(keyword) {
    if (!this.bloom.test(keyword)) return []; // 布隆过滤器预判
    
    return this.db.devices
      .where('id').startsWithIgnoreCase(keyword)
      .limit(50)
      .toArray();
  }
}

// Web Worker搜索处理
const worker = new Worker('search.worker.js');
worker.postMessage({ type: 'INIT_INDEX', data: allDeviceIds });
worker.onmessage = (e) => {
  if (e.data.type === 'SEARCH_RESULT') {
    renderResults(e.data.results);
  }
};

问题2:第三方系统集成与鉴权保鲜

  • 鉴权中台设计,设计统一令牌管理中心:

    • 第三方系统通过 OAuth2.0 协议获取短期访问令牌(2 小时有效期)
    • 主应用通过定时任务(每 30 分钟)向第三方系统推送刷新令牌,避免鉴权过期
    // 鉴权中台核心逻辑
    class AuthManager {
      private tokens = new Map<string, { token: string; expires: number }>();
      
      // 注册第三方系统
      registerSystem(systemId: string, refreshFn: () => Promise<string>) {
        this.scheduleRefresh(systemId, refreshFn);
      }
    
      private scheduleRefresh(systemId: string, refreshFn: () => Promise<string>) {
        setInterval(async () => {
          const newToken = await refreshFn();
          this.tokens.set(systemId, {
            token: newToken,
            expires: Date.now() + 55 * 60 * 1000
          });
          this.notifySystems(systemId, newToken);
        }, 54 * 60 * 1000); // 提前1分钟刷新
      }
    
      private notifySystems(systemId: string, token: string) {
        // 通过消息总线通知所有相关模块
        eventBus.emit('TOKEN_UPDATE', { systemId, token });
      }
    }
    
    
  • 安全沙箱机制

    • 第三方页面通过**<iframe sandbox="allow-scripts allow-same-origin">**嵌入,限制其对主应用 DOM 和本地存储的访问
    • 通信方式:主应用与第三方页面通过postMessage传递加密令牌(AES-256 加密,密钥定期轮换),示例如下:
    // 主应用发送令牌
    function sendTokenToThirdParty(iframeEl, token) {
      const encryptedToken = encrypt(token, getDynamicKey());
      iframeEl.contentWindow.postMessage({
        type: 'TOKEN_REFRESH',
        data: encryptedToken,
        timestamp: Date.now()
      }, 'https://trusted-thirdparty.com');
    }
    
    // 第三方页面接收令牌
    window.addEventListener('message', (e) => {
      if (e.origin !== 'https://main-app.com') return;
      if (e.data.type === 'TOKEN_REFRESH') {
        const token = decrypt(e.data.data, getDynamicKey());
        updateLocalToken(token); // 更新本地令牌
      }
    });
    

问题 3:跨模块通信方案

sequenceDiagram
    AppA->>EventBus: emit('deviceSelected', {id:123})
    EventBus->>AppB: on('deviceSelected', callback)
    AppB->>AppC: 通过SharedState更新全局设备状态
  • 通信分层
    • 轻量级通信:使用CustomEvent发布订阅模式
    • 状态共享:通过Redux维护跨模块共享状态(设备选择态、全局筛选条件)
    • 深度耦合:采用qiankun的initGlobalState机制
// 基于RxJS的跨应用通信总线
const eventBus = new Subject();

// 设备选择事件发布
const publishDeviceSelect = (deviceId) => {
  eventBus.next({
    type: 'DEVICE_SELECTED',
    payload: deviceId,
    source: 'device-manager'
  });
};

// 在监控模块订阅事件
eventBus.subscribe(event => {
  if (event.type === 'DEVICE_SELECTED') {
    loadDeviceDetails(event.payload);
  }
});

// Qiankun全局状态管理
import { initGlobalState } from 'qiankun';

const initialState = { currentDevice: null };
const actions = initGlobalState(initialState);

// 设备模块更新状态
actions.setGlobalState({ currentDevice: selectedDevice });

// 数据模块监听变化
actions.onGlobalStateChange((state, prev) => {
  if (state.currentDevice !== prev.currentDevice) {
    fetchDeviceData(state.currentDevice);
  }
});

问题4:多技术栈整合

技术栈 适配方案 示例场景
Vue3 直接挂载微应用 设备实时监控面板
Angular 封装为Web Component 设备批量操作向导
旧系统 iframe+消息通道 传统设备配置页面

关键实现

// Angular组件封装为Web Component
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { createCustomElement } from '@angular/elements';

@Component({
  selector: 'device-batch-operation',
  template: `...`
})
export class BatchOperationComponent {
  @Input() deviceIds: string[];
  @Output() complete = new EventEmitter<void>();
}

// 注册为Web Component
const BatchOperationElement = createCustomElement(BatchOperationComponent, { injector: injector });
customElements.define('device-batch-op', BatchOperationElement);

// 微应用中直接使用
// <device-batch-op device-ids="['id1','id2']" on-complete="handleComplete()"></device-batch-op>

// Vue3微应用接入
export const deviceMicroApp = {
  name: 'device-monitor',
  entry: '//dev.example.com/vue-app',
  container: '#vue-container',
  activeRule: '/monitor',
  props: {
    onEvent: (event) => eventBus.next(event)
  }
};

问题5:旧系统页面集成

  • 智能路由代理:
    • 主应用路由拦截旧系统 URL,自动判断使用 iframe 嵌入还是跳转至新页面
    • 示例:/legacy/config?deviceId=123 路由自动映射至 <iframe src="/legacy-app/config?deviceId=123">
# 前端网关路由配置
location ~ ^/legacy/ {
  proxy_pass http://old-system:8080;
  add_header X-Frame-Options "ALLOW-FROM https://new-system.com";
}
  • 双向通信桥接
    • 旧系统通过window.parent.postMessage发送操作指令(如 "设备配置完成")
    • 主应用通过注入script标签向旧系统注入 API(如获取当前选中设备,需做好防抖)
// 智能路由代理中间件
app.use('/legacy/:path', (req, res) => {
  const legacyUrl = buildLegacyUrl(req.params.path);
  
  // 添加安全头
  res.setHeader('X-Frame-Options', `ALLOW-FROM ${currentDomain}`);
  res.setHeader('Content-Security-Policy', "default-src 'self' legacy.example.com");
  
  // 代理请求
  axios.get(legacyUrl, {
    headers: { 'X-Auth-Token': generateToken() }
  }).then(response => res.send(response.data));
});

// iframe双向通信桥
class LegacyBridge {
  constructor(iframe) {
    this.iframe = iframe;
    window.addEventListener('message', this.handleMessage);
  }

  handleMessage = (event) => {
    if (event.origin !== LEGACY_ORIGIN) return;
    
    switch (event.data.type) {
      case 'AUTH_EXPIRED':
        this.renewToken();
        break;
      case 'DATA_UPDATE':
        eventBus.next(event.data);
        break;
    }
  };

  sendCommand(command) {
    this.iframe.contentWindow.postMessage({
      type: 'COMMAND',
      payload: command
    }, LEGACY_ORIGIN);
  }
}
}

五、性能优化关键实现

1. 微应用预加载策略

  • 预测性加载:基于用户行为分析(如 80% 用户进入设备管理后会访问监控面板),在主应用初始化时预加载 高频微应用
  • 优先级调度:首屏微应用(如设备列表)优先加载,非首屏微应用(如报表统计)延迟至空闲时加载
// 基于qiankun的预加载配置
import { preloadApps } from 'qiankun';

// 首屏应用立即加载
start();

// 空闲时预加载其他应用
window.addEventListener('load', () => {
  requestIdleCallback(() => {
    preloadApps([
      { name: 'data-visualization', entry: '/apps/visualization' },
      { name: 'report-center', entry: '/apps/report' }
    ]);
  });
});

2. 分片加载

class ChunkedDataLoader {
  private chunkSize = 1000;// 每片数据大小private loadedChunks = new Set<number>();

  async loadChunk(chunkIndex: number) {
    if (this.loadedChunks.has(chunkIndex)) return;

    const start = chunkIndex * this.chunkSize;
    const devices = await this.api.getDevices({
      skip: start,
      limit: this.chunkSize
    });

    await this.localCache.addDevices(devices);
    this.loadedChunks.add(chunkIndex);
  }

// 预测性加载async preloadNextChunks(currentIndex: number) {
    const nextChunk = currentIndex + 1;
    if (!this.loadedChunks.has(nextChunk)) {
      await this.loadChunk(nextChunk);
    }
  }
}

3. 资源加载优化

  • 静态资源 CDN 加速:微应用 JS/CSS 资源部署至 CDN,启用 HTTP/2 多路复用
  • 组件懒加载:非核心组件(如高级筛选器)采用动态 import,减少初始包体积
  • 图片优化:设备图标等静态资源使用 WebP 格式,配合响应式图片加载(srcset 属性

六、安全控制实现

1. CSP策略配置

通过 Content-Security-Policy 限制资源加载与脚本执行,示例:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' https://trusted-cdn.com 'unsafe-inline' (仅开发环境);
  frame-src 'self' https://trusted-thirdparty.com;
  img-src 'self' data: https://device-icons-cdn.com;
  style-src 'self' https://trusted-cdn.com;
  object-src 'none'

2. 令牌安全处理

  • 令牌存储:访问令牌存储在内存,刷新令牌加密后存储在 HttpOnly Cookie
  • 传输安全:所有 API 请求启用 HTTPS,关键接口(如批量操作)额外添加签名参数(时间戳 + 设备 ID + 密钥哈希)
// HttpOnly + Secure Cookie设置
app.post('/login', (req, res) => {
  const token = generateJWT(req.user);

  res.cookie('auth_token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'Strict',
    maxAge: 3600000 // 1小时
  });

  res.sendStatus(200);
});

// JWT刷新中间件
const refreshMiddleware = (req, res, next) => {
  if (req.path.startsWith('/api') && isTokenExpiring(req.token)) {
    const newToken = refreshToken(req.token);
    res.setHeader('X-Refresh-Token', newToken);
  }
  next();
};

七、监控体系实现

1.全链路监控

  • 微应用性能:监控微应用加载时间(JS 下载、渲染完成)、资源加载成功率
  • 运行时错误:通过 window.onerror 捕获 JS 错误,结合 source-map 还原真实报错位置
  • 用户行为:记录关键操作(如设备搜索、批量操作)的响应时间,建立性能基准线

2.告警机制

  • 当微应用加载失败率 > 1% 或平均响应时间 > 500ms 时,触发邮件 + 钉钉告警
  • 第三方系统令牌刷新失败时,立即通知运维团队
// 微应用性能监控
const startPerfMonitor = () => {
  const perfMetrics = {
    appLoadStart: Date.now(),
    resourcesLoaded: 0
  };

  // 资源加载监控
  performance.getEntriesByType('resource').forEach(res => {
    if (res.initiatorType === 'script') {
      perfMetrics.resourcesLoaded++;
    }
  });

  // 帧率监控
  const fpsMonitor = new FPSMonitor();
  fpsMonitor.start();

  // 错误监控
  window.addEventListener('error', (e) => {
    sendErrorLog({
      type: 'RUNTIME_ERROR',
      message: e.message,
      stack: e.error.stack,
      timestamp: Date.now()
    });
  });

  // 应用加载完成事件
  window.addEventListener('DOMContentLoaded', () => {
    perfMetrics.loadTime = Date.now() - perfMetrics.appLoadStart;
    sendPerfMetrics(perfMetrics);
  });
};

// 设备列表滚动性能追踪
const trackScrollPerf = (container) => {
  let lastKnownScrollPosition = 0;
  let frameCount = 0;

  container.addEventListener('scroll', () => {
    frameCount++;

    if (frameCount % 10 === 0) {
      const scrollPosition = container.scrollTop;
      const scrollSpeed = Math.abs(scrollPosition - lastKnownScrollPosition);
      lastKnownScrollPosition = scrollPosition;

      sendPerfData({
        type: 'SCROLL_PERF',
        speed: scrollSpeed,
        position: scrollPosition
      });
    }
  });
};

八、架构拓扑实现

graph TD
  A[主应用网关] -->|路由分发| B[设备管理Vue应用]
  A -->|状态共享| C[认证中心Angular应用]
  A -->|事件总线| D[数据可视化React应用]
  A -->|API代理| E[旧系统接入层]
  
  subgraph 微前端集群
    B --> F[设备列表]
    B --> G[设备详情]
    C --> H[OAuth认证]
    C --> I[令牌管理]
    D --> J[实时监控]
    D --> K[拓扑图]
  end
  
  E -->|iframe| L[传统设备配置]
  E -->|API桥接| M[旧设备管理]

九、其他问题及优化方案

1. 数据一致性问题

挑战:本地缓存(IndexedDB)与服务端数据同步可能产生不一致。

解决方案

  • 乐观更新策略
class DeviceDataManager {
  private localCache: IndexedDB;
  private serverApi: DeviceApi;
  private versionMap: Map<string, number> = new Map();

  async updateDevice(deviceId: string, updates: Partial<Device>) {
// 1. 乐观更新本地缓存const currentVersion = this.versionMap.get(deviceId) || 0;
    const newVersion = currentVersion + 1;

    await this.localCache.update(deviceId, {
      ...updates,
      _version: newVersion,
      _pendingSync: true
    });

    try {
// 2. 异步同步到服务器await this.serverApi.updateDevice(deviceId, {
        ...updates,
        _version: newVersion
      });

// 3. 确认同步成功await this.localCache.update(deviceId, { _pendingSync: false });
      this.versionMap.set(deviceId, newVersion);
    } catch (error) {
// 4. 同步失败,回滚本地更新await this.localCache.update(deviceId, {
        _version: currentVersion,
        _pendingSync: false
      });
      throw new SyncError('设备更新同步失败', error);
    }
  }
}

  • 定期全量同步
class DataSyncManager {
  private syncInterval = 5 * 60 * 1000; // 5分钟

  async startPeriodicSync() {
    setInterval(async () => {
      const pendingSyncs = await this.localCache.getPendingSyncs();
      
      for (const item of pendingSyncs) {
        await this.syncItem(item);
      }
      
      // 获取服务器端的最新变更
      const serverChanges = await this.serverApi.getChangesSince(
        this.lastSyncTimestamp
      );
      
      await this.applyServerChanges(serverChanges);
    }, this.syncInterval);
  }
}

2. 版本管理方案

挑战:微应用版本兼容性和平滑升级。

解决方案

  1. 版本兼容性管理
interface VersionCompatibility {
  microApp: string;
  version: string;
  compatibleWith: {
    mainApp: string[];
    microApps: Record<string, string[]>;
  };
}

class VersionManager {
  private compatibilityMatrix: VersionCompatibility[] = [];

  async checkCompatibility(microApp: string, version: string): Promise<boolean> {
    const compatibility = this.compatibilityMatrix.find(
      c => c.microApp === microApp && c.version === version
    );

    if (!compatibility) return false;

// 检查主应用兼容性const mainAppVersion = this.getMainAppVersion();
    if (!compatibility.compatibleWith.mainApp.includes(mainAppVersion)) {
      return false;
    }

// 检查其他微应用兼容性const loadedApps = this.getLoadedMicroApps();
    for (const [app, version] of Object.entries(loadedApps)) {
      if (
        !compatibility.compatibleWith.microApps[app]?.includes(version)
      ) {
        return false;
      }
    }

    return true;
  }
}

3. 国际化支持

挑战:多语言、多时区支持。

解决方案

  • 统一的国际化框架
class I18nManager {
  private translations: Record<string, Record<string, string>> = {};
  private currentLocale: string = 'zh-CN';
  private fallbackLocale: string = 'en-US';

  async loadTranslations(locale: string) {
    if (this.translations[locale]) return;

    const translations = await fetch(`/i18n/${locale}.json`);
    this.translations[locale] = await translations.json();
  }

  translate(key: string, params: Record<string, string> = {}) {
    let template = this.translations[this.currentLocale]?.[key]
      || this.translations[this.fallbackLocale]?.[key]
      || key;

    return template.replace(/\${(\w+)}/g, (_, param) => params[param] || '');
  }
}

  • 时区处理
class TimeZoneManager {
  private userTimeZone: string;

  constructor() {
    this.userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  }

  formatDate(date: Date, format: string = 'full'): string {
    return new Intl.DateTimeFormat('zh-CN', {
      timeZone: this.userTimeZone,
      ...this.getFormatOptions(format)
    }).format(date);
  }

// 转换时间戳到用户时区convertToUserTime(timestamp: number): Date {
    return new Date(timestamp);
  }

// 转换用户时间到UTCconvertToUTC(localDate: Date): number {
    return localDate.getTime();
  }
}

// 在Vue组件中使用@Component
class DeviceTimeline extends Vue {
  @Inject() timeZoneManager!: TimeZoneManager;

  formatEventTime(timestamp: number) {
    const localDate = this.timeZoneManager.convertToUserTime(timestamp);
    return this.timeZoneManager.formatDate(localDate, 'short');
  }
}

基于 ethers.js 的区块链事件处理与钱包管理实践指南

前言

本文将围绕 事件检索与监听HD 钱包批量生成与加密存储静态调用与 callData 构造ERC 标准合约识别 等关键场景,结合代码示例与最佳实践,展示如何利用 ethers.js 完成从基础交互到高级功能的完整流程。无论是初学者还是有经验的开发者,都能通过本指南快速掌握 ethers.js 的核心用法,并将其应用于实际项目中

Event事件

检索事件

const { ethers } = require("hardhat");
async function SearchEvent() {
    try {
        const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
        const signer = await provider.getSigner();
        const TokenAddress = "0xxxxx";//合约地址
        const TokenABI =[]//合约的abi;
        const TokenContract = new ethers.Contract(TokenAddress, TokenABI, signer);//创建合约
        //读取合约
        const name = await TokenContract.name();
        console.log("Contract Name:", name);
        const symbol = await TokenContract.symbol();
        console.log("Contract Symbol:", symbol);
        const totalSupply = await TokenContract.totalSupply();
        console.log("Total Supply:", totalSupply.toString());
        //合约转eth
        const arr1="0xxxxxxxx"
        await TokenContract.transfer(arr1,10);//给arr1转10;
        
        const block = await provider.getBlockNumber()//得到当前block
         const transferEvents = await TokenContract.queryFilter('Transfer', block - x, block);//检索合约Transfer,从block - x,到block之间的解析事件
          console.log(`Transfer事件数量: ${transferEvents.length}`);
         //transferEvents是个数组,我们可以解析他的参数
         console.log(...transferEvents[0].args);//返回form,to ,value
       }catch (error) {
        console.error("Error:", error);
    }
    }

监听事件

//以上同上
TokenContract.on("Transfer", (from, to, value, event) => {
            console.log(`Transfer事件触发:`);
            console.log(`From: ${from}`);
            console.log(`To: ${to}`);
            console.log(`Value: ${value.toString()}`);
            console.log(` 从 ${from}=> 到 ${to} = ${value.toString()}`); 
            console.log(`Event Details:`, event);   
        });

过滤事件

设置过滤规则:contract.filters.EVENT_NAME( ...args )说明:EVENT_NAME:过滤事件,...args:过滤规则

基础规则汇总

规则 含义 示例
null 该位置不限制,匹配任意值 contract.filters.Transfer(null, addr)
单个值 必须完全匹配 contract.filters.Transfer(addr)
数组 至少匹配数组中任意一个值 contract.filters.Transfer(null, [addr1, addr2])
以上代码如上
//设置规则
# 规则1
let addr1="0xf39Fd6e51aad88F6F4ce6axxxxxxx"
let addr2="0x70997970C51812dc3A010C7xxxxxx"
let addr3="0xb0997970C51812dcxxxxxxxxxxxxx"
let rule1 = TokenContract.filters.Transfer(addr1);//过滤来自`addr1`地址的`Transfer`事件
let rule2 = TokenContract.filters.Transfer(null,addr2);//过滤所有发给 addr2`地址的`Transfer`事件
let rule3 = TokenContract.filters.Transfer(addr1,addr2);//过滤所有从 `addr1`发给`addr2`的`Transfer`事件
let rule3 = TokenContract.filters.Transfer(addr1,addr2);//过滤所有从 `addr1`发给`addr2`的`Transfer`事件
let rule4 = TokenContract.filters.Transfer(null,[addr2,addr3]);//过滤所有发给 addr2`地址的或者addr3`的Transfer`事件
# 其他就是各种组合使用了
# 过滤使用
TokenContract.on(rule1, (res) => {
  console.log('---------监听开始过滤--------');
  console.log(
    `${res.args[0]} -> ${res.args[1]} ${res.args[2]}`
  )
})
# 其他同上 把过滤规则给监听事件即可

批量生成HD钱包

BIP汇总
BIP编号 主要用途 典型格式示例
BIP-32 HD 钱包路径 m/44'/0'/0'/0/0
BIP-39 助记词生成种子 12/24 个单词
BIP-44 多币种路径 m/44'/60'/0'/0/0
BIP-49 隔离见证兼容地址 m/49'/0'/0'/0/0
BIP-84 原生隔离见证地址 m/84'/0'/0'/0/0
BIP-173 Bech32 地址编码 bc1q...
BIP-350 Taproot 地址编码 bc1p...
以BIP-44为例代码实践
  • 助记词生成
 const mnemonic = ethers.Mnemonic.entropyToPhrase(ethers.randomBytes(32))
  • 创建HD基钱包
    BIP-44
    基路格式:"m / purpose' / coin_type' / account' / change" 参数说明
    • m:主密钥(Master Key)
    • purpose':固定为 44'(表示遵循 BIP-44 多账户标准)
    • coin_type':币种标识(如 0' = BTC,60' = ETH,501' = SOL)详细可查看SLIP-44
    • account':账户编号(从 0' 开始)
    • change:比特币专用(0 = 外部地址,1 = 找零地址);其他链通常为 0
    • address_index:地址索引(从 0 开始)
 # BIP-44
 // 基路径:
 const basePath = "44'/60'/0'/0"
 # 生成第一对外的链接
 const baseWallet = ethers.HDNodeWallet.fromPhrase(mnemonic, basePath)
  • 批量生成
const WalletNumber = 10;//钱包数
 for (let i = 0; i < WalletNumber; i++) {
     let NewBaseWallet = baseWallet.derivePath(i.toString());
     console.log(`第${i+1}个钱包地址: ${baseWalletNew.address}`)
     wallets.push(baseWalletNew);//生成10个钱包
 }
console.log("钱包地址列表:", wallets.map(wallet => wallet.address));
  • 加密JSON保存
async function saveWalletJson() {
 const wallet = ethers.Wallet.fromPhrase(mnemonic);//助记词
 console.log("通过助记词创建钱包:")
 console.log(wallet)
 // 加密json用的密码,可以更改成别的
 const pwd = "XXXX";
 const json = await wallet.encrypt(pwd)
 console.log("钱包的加密json:")
 console.log(json)
 require("fs").writeFileSync("keystoreBatch.json", json);//在当前文件夹下生成一个 keystoreBatch.json文件
 }
 saveWalletJson();
  • 通过加密json读取钱包信息
async function ReadWalletJson() {
console.log("开始读取json文件");
const json=require("fs").readFileSync("keystoreBatch.json", "utf8");
const walletJson =await ethers.Wallet.fromEncryptedJson(json, "xxx");//生成json时设置的密码
console.log("Wallet from JSON:",walletJson);
console.log("Address:", walletJson.address);
console.log("Private Key:", walletJson.privateKey);
console.log("Mnemonic:", walletJson.mnemonic.phrase);
}
ReadWalletJson();

staticCall和callStatic:

名称 所属模块 作用 返回值 适用场景
staticCall ethers.Contract 实例方法 只读方式 调用合约函数,不修改状态 函数返回值 任何函数(读/写)
callStatic ethers.Contract 实例方法(v6 新增) 只读方式 调用合约函数,不修改状态 函数返回值 任何函数(读/写)
# 代码实例
# staticCall
const from="0xf39xxx"
const to="0x70xxx"
const result = await TokenContract.transfer.staticCall(to,10,{  
                     // 可选 overrides
                    from: from, // 指定调用者(模拟不同账户)
                });
                console.log('模拟结果:', result);
# callStatic
const result = await TokenContract.transfer.staticCall(to,10,{                 
                    // 可选 overrides
                    from: from, // 指定调用者(模拟不同账户)
                });
                console.log('模拟结果:', result);

callData

  • 接口abi:infce=new ethers.Interface(abi);//两者是一样的功能

  • callData:infce=TokenContract.interface;//两者是一样的功能

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
const signer = await provider.getSigner();
const TokenAddress = "0xxxx";//合约地址
const TokenABI =[];//abi
const TokenContract = new ethers.Contract(TokenAddress, TokenABI, signer);
const param = TokenContract.interface.encodeFunctionData(
    "balanceOf",
    ["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]
  );
  console.log("param:", param);
  const tx = {
    to: TokenAddress,
    data: param
}
// 发起交易,可读操作(view/pure)可以用 provider.call(tx)
const balanceWETH = await provider.call(tx)
console.log(`存款前WETH持仓: ${ethers.formatEther(balanceWETH)}\n`)

encodeFunctionData

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
const signer = await provider.getSigner();
const TokenAddress = "0xxxxxxx";//合约地址
const TokenContract = new ethers.Contract(TokenAddress, TokenABI, signer);//构造合约
# 使用合约的transfer 向0x70997970C51812dc3A010C7d01b50e0d17dc79C8 转10n
const calldata = TokenContract.interface.encodeFunctionData('transfer', [
  '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', // 收款地址
  10n                                           // 转账数量 (BigInt)
]);
console.log(calldata)//生成callData
const wallet = new ethers.Wallet("钱包的私钥", provider);
const tx = await wallet.sendTransaction({
  to: "0x5Fxxxxxxx",//合约地址
  data: calldata,
});
await tx.wait();
console.log("交易成功生成的txHash:", tx.hash);
//通过交易hash 
//交易的详细信息
const hash = await provider.getTransaction(tx.hash);
//交易收据
const receipt = await provider.getTransactionReceipt(tx.hash);

识别ERC20、ERC721、ERC115标准合约

识别关键说明:所有现代标准(ERC721、ERC1155)都实现了 ERC165,通过 supportsInterface(bytes4 interfaceId) 函数声明支持的接口,ERC20 不支持 ERC165

  • ERC20

    说明:识别关键ERC20不是基于ERC165,但是ERC20包含totalSupply,识别关键通过totalSupply
    const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
    const signer = await provider.getSigner();//
    const TokenAddress = "0x5Fbxxxxx";//合约地址
    const TokenABI = []//abi
    const TokenContract = new ethers.Contract(TokenAddress, TokenABI, signer);//创建合约
    const totalSupplyValue=await TokenContract.totalSupply(); 
    console.log(totalSupplyValue)//说明是ERC20
    
  • ERC721

    说明:识别关键是ERC721基于ERC165,ERC165标准包含supportsInterface(bytes4 interfaceId)
     创建合约如上
     const isERC721 = await contract.supportsInterface("0x80ac58cd");
     console.log(isERC721); // true 或 false
    
  • ERC1155

    说明:识别关键是ERC721基于ERC165,ERC165标准包含supportsInterface(bytes4 interfaceId)
     创建合约如上
     const isERC721 = await contract.supportsInterface("0xd9b67a26");
     console.log(isERC721); // true 或 false
    
  • 总结

    调用函数/方法 返回值 识别结果 备注
    supportsInterface(0x80ac58cd) true ERC721 NFT 标准接口标识符
    supportsInterface(0xd9b67a26) true ERC1155 多代币标准接口标识符
    totalSupply() 等函数调用成功 成功 ERC20 同质化代币标准(无 ERC165)

总结

以上就是系统介绍了使用 ethers.js 进行区块链开发的关键技术,涵盖事件处理、钱包管理、合约交互及标准识别四大核心模块,并通过代码示例与最佳实践提供完整解决方案;

vue3中pinia

Pinia 是 Vue 官方推荐的新一代状态管理库,专为 Vue 3 设计(同时兼容 Vue 2),它简化了状态管理流程,并优化了 TypeScript 支持。以下从核心概念、使用方法和与 Vuex 的对比三方面详细说明:


一、Pinia 的核心概念与特点

  1. 精简架构

    • 无 Mutation:直接通过 actions 修改状态(同步/异步均可),无需 commit
    • 扁平化 Store:每个 Store 独立管理(如 useUserStoreuseCartStore),无需嵌套模块或命名空间。
    • 响应式状态:基于 Vue 3 的 reactive 实现,直接修改状态自动触发更新。
  2. TypeScript 友好

    • 开箱即用的类型推断,无需额外类型声明。
  3. 轻量高效

    • 体积仅约 1KB(gzip),性能优于 Vuex。

二、基本使用

<template>
  <div>实际参数={{count}}</div>
  <div>
    <button @click="addByPina">点击</button>
  </div>
</template>

<script setup lang="ts">
import {useCounterStore} from "@/stores/counter.ts";
import {storeToRefs} from "pinia";

const useCounter = useCounterStore()
let {count} = storeToRefs(useCounter)

// 第1种直接修改pina中数据
function add () {
  count.value++
}
// 第2种利用$patch 批量修改
useCounter.$patch({
  count: 100
})
// 可直接使用pina的方法
function addByPina () {
  useCounter.increment()
}
// 通过subscribe方法监听state变化
useCounter.$subscribe((mutation, state)=>{
  console.log('$subscribe ===', mutation, state)
})
</script>


三、Pinia 与 Vuex 的核心区别

特性 Pinia Vuex
架构 多 Store 独立管理(扁平化) 单一 Store + 嵌套 Modules
状态更新 直接修改 state无需 Mutation 必须通过 commit 触发 Mutation
异步处理 Actions 直接修改状态 Actions 需调用 Mutations 更新状态
TypeScript 原生类型推断,零配置支持 需手动声明类型,配置复杂
模块化 文件即模块(如 userStore.ts namespaced: true 避免命名冲突
代码简洁度 减少 40% 样板代码(无 Mutation) 冗余代码多(State + Mutation + Action)
DevTools 支持时间旅行调试,结构扁平更清晰 支持但嵌套模块路径深
适用场景 Vue 3 新项目、TS 项目、快速迭代 大型遗留项目、需兼容 Vue 2

四、pinia原理

  1. 模块化设计
    每个 Store 独立定义(defineStore),通过唯一 ID 注册到全局 Pinia 实例的 Map 中。Store 之间完全隔离,无命名空间冲突。

  2. 响应式绑定
    使用 Vue 的 reactive() 将 State 转为响应式对象,Getter 通过 computed 实现缓存。解构时用 storeToRefs 将状态转为 Ref 保持响应性。

  3. 状态直改机制
    移除 Vuex 的 Mutation 层,允许在 Action 中直接修改 State(通过 this.state 操作),同步/异步操作统一处理。

  4. 依赖注入
    通过 Vue 的 provide/inject 实现跨组件访问:

    • useStore() 内部 inject 获取 Pinia 实例
    • 从全局 Map 中检索对应 Store
  5. 初始化流程

    • 执行 useStore() 时,若 Store 不存在:
      • 选项式:自动包装 State(reactive)、Getter(computed
      • 组合式:直接执行 setup 函数(类似组件逻辑)
    • 缓存 Store 实例避免重复创建
  6. 插件扩展
    通过 pinia.use() 添加插件,可拦截:

    • Store 创建(添加新属性/方法)
    • Action 执行(日志/监控)
    • 状态变更(持久化存储)
  7. 性能优化

    • markRaw 标记 Pinia 实例避免被转为响应式
    • 精准响应式绑定(仅 State/Getter 响应,Action 不代理)

本质:Pinia 是 Vue 响应式系统的增强应用,通过精简 API 设计(去 Mutation)+ 复用 Composition API 能力,提供比 Vuex 更简洁高效的状态管理。

五、何时选择 Pinia?

  • 推荐 Pinia
    • Vue 3 新项目、需深度 TypeScript 集成、追求简洁代码。
    • 示例:动态权限管理、跨组件表单状态共享。
  • 保留 Vuex
    • 维护 Vue 2 旧项目、需兼容深度嵌套模块的复杂场景。

迁移建议:小型项目直接重写为 Pinia;大型项目逐步替换模块,利用 $patch 兼容旧逻辑。


附:Pinia 进阶技巧

插件扩展
使用 pinia-plugin-persistedstate 实现状态持久化。

Pinia 通过简化 API 和强化 TypeScript 支持,大幅提升了开发体验。其设计理念更贴合 Vue 3 的响应式系统,是未来 Vue 生态状态管理的首选方案。

【React Native】自定义轮播图组件 Banner

一、组件简介

Banner 是基于 react-native-pager-view 实现的高性能轮播组件,支持无限循环滚动自动播放垂直/水平方向切换自定义分页指示器等功能,适用于广告轮播、内容展示、产品推荐等场景。组件通过封装底层滚动逻辑,提供简洁的 API 接口,降低开发成本。


二、核心功能

功能 描述
无限循环滚动 支持首尾无缝衔接,循环展示数据(需开启 loop 属性)
自动播放 自动切换轮播项(可配置延迟时间 autoplayDelay 和间隔 autoplayInterval
垂直/水平滚动 支持垂直(vertical={true})或水平(默认)滚动方向
自定义分页指示器 支持自定义分页点样式(颜色、大小、间距)、容器样式(背景、边距等)
手动/自动滚动控制 可禁用自动播放(autoplay={false}),或通过 scrollEnabled 控制手动滚动
滚动事件回调 提供 onScrollIndex(切换回调)和 onScroll(滚动过程回调)

三、属性详解(Props)

1. 基础样式与容器

属性名 类型 默认值 描述
style StyleProp<ViewStyle> undefined 自定义 Banner 容器样式(如背景色、边距、圆角等)
vertical boolean false 是否垂直滚动(默认水平滚动)
scrollEnabled boolean true 是否允许手动滚动(禁用后仅自动播放)

2. 数据与渲染

属性名 类型 默认值 描述
data any[] | undefined undefined 轮播数据源(必须为数组,长度需 ≥1)
renderItem (item: any, index: number) => React.ReactElement undefined 渲染单个轮播项的函数(必传)
keyExtractor (item: any, index: number) => string undefined 生成唯一 key 的方法(建议提供,避免渲染警告)

3. 循环与自动播放

属性名 类型 默认值 描述
loop boolean true 是否开启无限循环(需 data.length ≥ 2,否则无效)
autoplay boolean true 是否自动播放(默认开启)
autoplayDelay number 1000 自动播放前的延迟时间(毫秒,仅在首次加载时生效)
autoplayInterval number 5000 自动切换间隔时间(毫秒)

4. 分页指示器

属性名 类型 默认值 描述
showsPagination boolean false 是否显示分页指示器(默认隐藏)
paginationStyle StyleProp<ViewStyle> undefined 分页指示器容器样式(如背景色、内边距、位置等)
dotStyle StyleProp<ViewStyle> undefined 普通分页点样式(如大小、颜色、间距等,与 dotColor 合并生效)
activeDotStyle StyleProp<ViewStyle> undefined 当前分页点样式(如大小、颜色、边框等,与 activeDotColor 合并生效)
dotColor string #CCCCCC 普通分页点颜色(默认浅灰色)
activeDotColor string #FFFFFF 当前分页点颜色(默认白色)

5. 回调函数

属性名 类型 默认值 描述
onScrollIndex (index: number) => void undefined 切换到指定轮播项时的回调(参数为真实数据索引)
onScroll (e: { offset: number; position: number }) => void undefined 滚动过程中的回调(offset 为偏移量,position 为当前页位置)

四、使用示例

1. 基础用法(水平轮播)

import React from 'react';
import { View, StyleSheet } from 'react-native';
import Banner from './Banner'; // 引入组件

const App = () => {
  const data = [
    { id: 1, image: 'https://example.com/banner1.jpg' },
    { id: 2, image: 'https://example.com/banner2.jpg' },
    { id: 3, image: 'https://example.com/banner3.jpg' },
  ];

  const renderItem = ({ item }) => (
    <View style={styles.bannerItem}>
      <Image source={{ uri: item.image }} style={styles.image} />
    </View>
  );

  return (
    <View style={styles.container}>
      <Banner
        data={data}
        renderItem={renderItem}
        loop={true}
        autoplay={true}
        autoplayInterval={3000}
        showsPagination={true}
        dotColor="#999"
        activeDotColor="#FF5500"
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  bannerItem: {
    width: '100%',
    height: 200,
  },
  image: {
    width: '100%',
    height: '100%',
    resizeMode: 'cover',
  },
});

export default App;

2. 自定义分页指示器(垂直滚动)

<Banner
  data={data}
  renderItem={renderItem}
  vertical={true} // 垂直滚动
  loop={true}
  autoplay={true}
  showsPagination={true}
  paginationStyle={{
    backgroundColor: 'rgba(0,0,0,0.3)', // 分页容器背景
    paddingHorizontal: 16, // 分页点左右边距
  }}
  dotStyle={{
    width: 6, // 普通分页点宽度
    height: 6,
    marginHorizontal: 4, // 分页点间距
  }}
  activeDotStyle={{
    borderWidth: 2, // 当前分页点边框
    borderColor: '#FFF',
  }}
  onScrollIndex={(index) => console.log('当前索引:', index)} // 切换回调
/>

2025年最强React插件,支持大量快捷操作

现在到处都是一人公司,数字游民,独立开发。

我也按耐不住寂寞,希望能够开启自己的副业。

于是我做了一个React的Vscode插件。

这是一个收费的插件,

不过我会在评论区免费送出激活码。

激活码获取方法请看文末

React Transformer

这个插件名叫React Transformer

插件主页

做这个插件的动机就是厌倦了繁琐的鼠标操作,

每当修改页面DOM结构,或者要把组件转换成forwardRef,手要从键盘挪动到鼠标,

然后小心翼翼的选中需要修改的地方,

但还是会漏掉一个括号,导致满屏幕的红色。

烦得一b

现在这个插件解决了我这个烦恼,提供了常见的修改DOM结构的操作,和一键添加forwardRef

用法:

  1. 把光标放在你想要重构的地方
  2. 或者选中你想要重构的代码块
  3. 使用快捷键shift+alt+r (shift+cmd+r),打开Vscode的重构菜单:

1. 用新标签包裹

Wrap with new tag demo

2. 与下一个兄弟元素交换

Swap with next sibling demo

3. 创建 forwardRef

Create forwardRef demo

4. 移除 JSX 元素

Remove JSX demo

5.转换为条件语句

Remove JSX demo

6. 转换为三元表达式

Remove JSX demo

免费赠送激活码!

现在下载安装,左下角状态栏会出现一个按钮显示:“未激活React transformer”。

点击它,获取你的机器码

发到评论区,我会给你永久有效的激活码!

vue.use和app.use

一、核心定义与应用场景

1. Vue.use(Vue 2 时代的用法)
  • 作用:在 Vue 2 中注册全局插件,通过操作 Vue 构造函数实现功能扩展(如注册组件、挂载原型方法)。
  • 场景:适用于 Vue 2 项目,在创建实例前调用(如 Vue.use(VueRouter))。
2. app.use(Vue 3 组合式 API 用法)
  • 作用:在 Vue 3 中通过应用实例 app 注册插件,替代 Vue 2 中对全局 Vue 构造函数的直接操作。
  • 场景:适用于 Vue 3 项目,在创建应用实例后调用(如 app.use(router))。

二、底层实现与流程对比

1. Vue.use(Vue 2 源码简化)
// Vue 2 中 Vue.use 核心逻辑
Vue.use = function (plugin) {
  // 防止重复注册
  if (this._installedPlugins.includes(plugin)) return this
  
  // 处理参数并调用 install 方法
  const args = [this, ...toArray(arguments, 1)]
  if (plugin.install) plugin.install.apply(plugin, args)
  else if (typeof plugin === 'function') plugin.apply(null, args)
  
  this._installedPlugins.push(plugin)
  return this
}

特点:直接修改全局 Vue 构造函数,影响所有后续创建的实例。

2. app.use(Vue 3 源码简化)
// Vue 3 中 app.use 核心逻辑
app.use = function (plugin, options) {
  // 防止重复注册
  if (this._plugins.has(plugin)) return this
  
  // 处理参数并调用 install 方法
  const args = [this, options]
  if (plugin.install) plugin.install.apply(plugin, args)
  else if (typeof plugin === 'function') plugin.apply(null, args)
  
  this._plugins.add(plugin)
  return this
}

特点:基于应用实例 app 注册插件,每个实例的插件相互隔离(如多应用场景)。

三、使用方式对比(以路由插件为例)

1. Vue 2 写法
// main.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'

// 注册插件到全局 Vue 构造函数
Vue.use(VueRouter)

// 创建路由实例
const router = new VueRouter(...)

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
2. Vue 3 写法
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

// 创建应用实例
const app = createApp(App)

// 注册插件到应用实例
app.use(router)

// 挂载应用
app.mount('#app')

四、核心差异点总结(面试高频考点)

维度 Vue.use(Vue 2) app.use(Vue 3)
操作对象 全局 Vue 构造函数 应用实例 app(通过 createApp 创建)
作用范围 所有 Vue 实例共享插件(全局污染风险) 插件仅作用于当前应用实例(支持多实例隔离)
设计理念 面向类的编程(Class-based) 组合式 API(Composition API)
插件参数 第一个参数为 Vue 构造函数 第一个参数为 app 实例
重复注册 通过 _installedPlugins 数组校验 通过 _plugins Set 数据结构校验

五、问题

1. 问:Vue 3 为什么用 app.use 替代 Vue.use?
  • 避免全局污染:Vue 2 中 Vue.use 直接修改全局构造函数,可能导致多应用场景下的冲突(如微前端);
  • 符合组合式设计:Vue 3 的 app.use 基于实例隔离,每个应用可独立管理插件(如不同路由配置);
  • 性能优化:实例级插件注册可减少全局状态耦合,更适合大型项目的按需加载。
2. 问:插件在 Vue 2 和 Vue 3 中是否兼容?
  • 基础兼容:若插件遵循 install(Vue, options) 规范,在 Vue 3 中需调整参数:
    • Vue 2 中 install 第一个参数是 Vue 构造函数;
    • Vue 3 中第一个参数是 app 实例(包含 componentdirective 等方法)。
  • 高阶兼容:涉及全局状态(如 Vue.prototype)的插件需重构,推荐使用 app.config.globalProperties 替代(Vue 3 特性)。
3. 问:如何在 Vue 3 中开发兼容两种版本的插件?
// 兼容 Vue 2 和 Vue 3 的插件示例
const CompatPlugin = {
  install(target, options) {
    // target 在 Vue 2 中是 Vue 构造函数,在 Vue 3 中是 app 实例
    const isVue3 = target.config !== undefined
    
    // 注册全局组件(兼容写法)
    if (isVue3) {
      target.component('CompatComponent', { /* ... */ })
    } else {
      target.component('CompatComponent', { /* ... */ })
    }
    
    // 挂载全局方法(兼容写法)
    if (isVue3) {
      target.config.globalProperties.$compat = { /* ... */ }
    } else {
      target.prototype.$compat = { /* ... */ }
    }
  }
}

// 使用方式
// Vue 2: Vue.use(CompatPlugin)
// Vue 3: app.use(CompatPlugin)

双向绑定原理

Vue 双向绑定原理(面试级深度解析)

一、双向绑定的本质与核心流程

定义:双向绑定指视图与数据的自动同步,即修改数据时视图更新,用户操作视图时数据也随之改变(如 v-model 指令)。

核心流程(三要素)

  1. 数据监听:通过 Object.defineProperty 劫持数据的 getter/setter;
  2. 视图更新:依赖收集与派发更新(Watcher 与 Dep 模式);
  3. 用户输入处理:通过事件监听(如 input 事件)更新数据。

二、数据监听的实现(Object.defineProperty)

1. 核心代码示例
// Vue 2 中数据响应式的核心实现(简化版)
function defineReactive(obj, key, value) {
  // 依赖收集器(每个属性对应一个 Dep 实例)
  const dep = new Dep();
  
  // 劫持 getter/setter
  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集:将当前 Watcher 存入 Dep
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set(newVal) {
      if (newVal === value) return;
      value = newVal;
      // 派发更新:通知所有依赖该属性的 Watcher 刷新视图
      dep.notify();
    }
  });
}

// 递归遍历对象所有属性
function observe(obj) {
  if (!obj || typeof obj !== 'object') return;
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}
2. 关键概念解析
  • Dep 类:每个响应式属性对应一个 Dep 实例,用于收集依赖它的 Watcher(订阅者);
  • Watcher 类:当数据变化时,Watcher 会收到通知并触发视图更新(如重新渲染组件);
  • 依赖收集时机:当视图读取数据(触发 getter)时,将当前 Watcher 存入 Dep;
  • 派发更新时机:当数据被修改(触发 setter)时,Dep 通知所有 Watcher 执行更新。

三、双向绑定的完整流程(以 v-model 为例)

1. 模板编译阶段
<!-- 模板 -->
<input v-model="message" />

<!-- 编译后等价于 -->
<input 
  :value="message" 
  @input="message = $event.target.value"
/>
2. 运行时双向绑定流程
  1. 初始化阶段

    • 视图渲染时读取 message,触发 getter,将当前组件的 Watcher 存入 message 对应的 Dep;
    • 输入框的 value 属性绑定到 message,视图显示 message 的值。
  2. 用户输入阶段

    • 用户修改输入框内容,触发 input 事件;
    • 事件回调将 $event.target.value 赋值给 message,触发 setter;
    • setter 通知 Dep 派发更新,Dep 遍历所有 Watcher(组件 Watcher),触发视图重新渲染。
  3. 数据修改阶段

    • 代码中修改 message(如 this.message = 'new value'),触发 setter;
    • setter 通知 Dep 派发更新,组件 Watcher 重新渲染视图,输入框 value 同步更新。

四、双向绑定的缺陷与 Vue 3 的优化

1. Vue 2 双向绑定的限制
  • 数组变异方法的特殊处理
    由于 Object.defineProperty 无法监听数组索引和长度的变化,Vue 2 对数组的 pushpop 等方法进行了重写(通过 Array.prototype 拦截),而直接修改索引(如 arr[0] = value)不会触发更新,需使用 Vue.set(arr, index, value)arr.splice(index, 1, value)

  • 对象新增属性的响应式问题
    新增属性不会被 defineReactive 劫持,需使用 Vue.set(obj, 'newProp', value)this.$set

  • 性能问题
    深层嵌套对象会递归监听所有属性,导致初始化性能开销较大。

2. Vue 3 的优化(Proxy 替代 Object.defineProperty)
// Vue 3 中使用 Proxy 实现响应式(简化版)
function createReactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      // 依赖收集(比 Vue 2 更高效,可捕获整个对象的访问)
      track(target, key);
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value);
      // 派发更新(精准通知变化的属性)
      if (oldValue !== value) {
        trigger(target, key);
      }
      return result;
    }
  });
}

优势

  • 原生支持数组索引和长度变化:Proxy 可直接监听数组的所有操作;
  • 动态新增属性响应式:Proxy 可捕获任意属性的访问与修改;
  • 性能优化:按需监听(仅在属性被访问时收集依赖),避免递归全量监听。

五、问题

1. 问:Vue 双向绑定的核心原理是什么?请用代码简单说明。

  • Vue 通过 Object.defineProperty 劫持数据的 getter/setter,结合 Watcher 和 Dep 实现依赖收集与更新派发。当视图读取数据时,触发 getter 收集依赖;当数据修改时,触发 setter 通知 Watcher 刷新视图。以 v-model 为例,它本质是 :value@input 的语法糖,实现视图与数据的双向同步。
2. 问:Vue 2 中数组直接通过索引修改元素为什么不会触发更新?如何解决?

  • 因为 Object.defineProperty 无法监听数组索引的变化,直接修改 arr[0] = value 不会触发 setter。解决方案:
    • 使用 Vue.set(arr, index, value)this.$set
    • 使用数组的变异方法(如 splice):arr.splice(index, 1, value)
    • 替换整个数组:this.arr = [...this.arr]
3. 问:Vue 3 为什么用 Proxy 替代 Object.defineProperty?
    • 功能更完整:Proxy 原生支持监听数组索引、长度变化及动态新增属性;
    • 性能更优:Object.defineProperty 需递归遍历所有属性,而 Proxy 可直接代理整个对象,且依赖收集是按需进行的(仅在属性被访问时收集);
    • 语法更简洁:Proxy 以声明式方式定义拦截行为,代码结构更清晰。

六、总结

双向绑定三要素:数据监听、依赖收集、更新派发;
Vue 2 用 Object.defineProperty 劫持 getter/setter,配合 Dep 和 Watcher;
v-model 是语法糖,等价于 :value + @input
数组索引修改不触发更新,需用 Vue.setsplice
Vue 3 用 Proxy 优化,支持动态属性、数组操作,性能更优。

i18n-前端国际化实现原理与架构设计

国际化(Internationalization,简称 i18n)  是指设计、开发产品或服务(尤其是软件、网站、内容、业务流程等)时,使其能够轻松适应不同语言、地区和文化环境的过程和技术。它的核心目标是消除地域和文化障碍,为全球用户提供无障碍的体验。

为什么叫 i18n?
这是一个常见的缩写,源自单词 “Internationalization” 的首字母 I 和末字母 n,中间有 18 个字母,因此简写为 i18n

实现原理

  1. 使用键值对存储不同语言的翻译包
  2. 用一个管理器使用对应的语言进行翻译
  3. 代码中原本写死的中文,用函数替换,参数是语言包对应的key值,返回值就是对应的语言

构架设计

1.整体架构思路

// 架构层次
项目根目录
└── src/
    └── modules/
        └── i18n/
            ├── I18nManager.ts       // 核心管理器
            └── languages/           // 语言包目录
                ├── zh-CN.ts        // 中文语言包
                └── en-US.ts        // 英文语言包

管理器模式 + 语言包的架构:

  • I18nManager: 作为国际化的核心管理器,负责语言包管理、文本翻译、语言切换等功能
  • 语言包: 按语言分离的配置文件,便于维护和扩展
  • 统一导出: 全局单例模式,确保一致性

2.核心管理器设计

type LanguagePack = {
  [key: string]: string | LanguagePack;
};

interface I18nConfig {
  defaultLanguage: string;
}

class I18nManager {
  private static instance: I18nManager;
  private currentLanguage: string;
  private languagePacks: Map<string, LanguagePack> = new Map();

  constructor(config: I18nConfig) {
    this.currentLanguage = config.defaultLanguage;
  }

  static getInstance(config?: I18nConfig): I18nManager {
    if (!I18nManager.instance && config) {
      I18nManager.instance = new I18nManager(config);
    }
    return I18nManager.instance;
  }

  /**
   * 设置语言包
   */
  setLanguagePack(language: string, pack: LanguagePack): void {
    this.languagePacks.set(language, pack);
  }

  /**
   * 获取当前语言
   */
  getCurrentLanguage(): string {
    return this.currentLanguage;
  }

  /**
   * 设置当前语言
   */
  setCurrentLanguage(language: string): void {
    if (this.currentLanguage !== language) {
      this.currentLanguage = language;
    }
  }

  /**
   * 获取文本
   */
  t(key: string, params?: Record<string, string | number>): string {
    const pack = this.languagePacks.get(this.currentLanguage);

    if (!pack) {
      console.warn(`${this.currentLanguage} 语言包没有找到`);
      return key;
    }

    const value = this.getNestedValue(pack, key);
    if (typeof value !== 'string') {
      console.warn(`${key} 翻译键没有找到`);
      return key;
    }

    return this.interpolate(value, params);
  }

  /**
   * 获取嵌套值
   */
  private getNestedValue(obj: any, path: string): any {
    return path.split('.').reduce((current, key) => {
      return current && current[key] !== undefined ? current[key] : undefined;
    }, obj);
  }

  /**
   * 插值替换
   */
  private interpolate(text: string, params?: Record<string, string | number>): string {
    if (!params) return text;

    return text.replace(/\{(\w+)\}/g, (match, key) => {
      return params[key] !== undefined ? String(params[key]) : match;
    });
  }
}
  1. 单例模式: 确保全局唯一的国际化实例,避免状态不一致
  2. 类型安全: 使用 TypeScript 定义清晰的类型接口
  3. 嵌套支持: LanguagePack 类型支持多层嵌套结构
  4. 延迟初始化: 只有在需要时才创建实例

3.语言包组织

export const zhCN: Record<string, any> = {
  // 通用模块
  common: {
    close: '关闭',
    confirm: '确认',
    cancel: '取消',
    // ...
  },

  // 功能模块 - 设置
  settings: {
    title: '渲染设置',
    renderSettings: '渲染设置',
    showBorder: '显示边框',
    showBorderTip: '显示模型构件的边线',
    // ...
  },

  // 更多模块...
};
export const enUS: Record<string, any> = {
  // Common
  common: {
    close: 'Close',
    confirm: 'Confirm',
    cancel: 'Cancel',
    // ...
  },

  // Settings
  settings: {
    title: 'Render Settings',
    renderSettings: 'Render Settings',
    showBorder: 'Show Border',
    showBorderTip: 'Display model component edges',
    // ...
  },
  
  // 更多模块...
};
  1. 按功能模块分组: 将相关的文本放在同一个命名空间下
  2. 层次化结构: 使用嵌套对象组织复杂的文本结构
  3. 一致性命名: 保持命名规范的一致性,如 xxxTip 表示提示文本
  4. 完整性保证: 确保所有语言包的结构完全一致

初始化调用

1.全局初始化

import { zhCN } from './languages/zh-CN';
import { enUS } from './languages/en-US';

// 创建全局实例
const i18n = I18nManager.getInstance({
  defaultLanguage: 'zh-CN'
});

// 注册语言包
i18n.setLanguagePack('zh-CN', zhCN);
i18n.setLanguagePack('en-US', enUS);

// 设置当前语言
i18n.setCurrentLanguage('en-US');

export { i18n };

2.实际使用示例

// 在业务代码中的使用
import { i18n } from '../../i18n/I18nManager';

export class TooltipManager {
  private getAnnotationTooltipMessages() {
    return {
      [AnnotationDwgType.CLOUD]: {
        initial: i18n.t('annotationDwg.initialTip'),
        drawing: i18n.t('annotationDwg.drawingTip'),
        mobileInitial: i18n.t('annotationDwg.mobileInitialTip') + i18n.t('annotationDwg.cloud'),
        mobileDrawing: i18n.t('annotationDwg.mobileDrawingTip') + i18n.t('annotationDwg.cloud')
      },
      // ...
    };
  }
}

扩展其他语言

后续如果要支持日语,只需提供日语对应的键值对,然后在manager中使用即可。需要注意的是,不同的语言长短不一样,可能对界面有所影响,css需要微调

JS中Object.defineProperty 详解

Object.defineProperty 详解

Object.defineProperty() 是 JavaScript 中一个强大的方法,用于直接在对象上定义新属性或修改现有属性,并返回该对象。它提供了对属性行为的精细控制。

基本语法

Object.defineProperty(obj, prop, descriptor)
  • obj:要在其上定义属性的对象
  • prop:要定义或修改的属性的名称
  • descriptor:将被定义或修改的属性描述符

属性描述符

属性描述符有两种主要类型:数据描述符存取描述符

数据描述符

数据描述符是一个具有以下可选键的对象:

  • value:属性的值,默认为 undefined
  • writable:是否可写,true 表示可修改,默认为 false
  • enumerable:是否可枚举,true 表示会出现在对象的枚举属性中,默认为 false
  • configurable:是否可配置,true 表示该属性的类型可以改变,且属性可以从对象中删除,默认为 false

存取描述符

存取描述符是一个具有以下可选键的对象:

  • get:作为该属性的 getter 函数,默认为 undefined
  • set:作为该属性的 setter 函数,默认为 undefined
  • enumerable:同数据描述符
  • configurable:同数据描述符

注意:描述符不能同时是数据描述符和存取描述符(即不能同时有 value/writableget/set)。

示例

1. 基本使用

const obj = {};

// 添加数据属性
Object.defineProperty(obj, 'name', {
  value: 'John',
  writable: true,
  enumerable: true,
  configurable: true
});

console.log(obj.name); // "John"

2. 不可写属性

const obj = {};

Object.defineProperty(obj, 'readOnly', {
  value: 42,
  writable: false
});

obj.readOnly = 100; // 静默失败,严格模式下会报错
console.log(obj.readOnly); // 42

3. 不可枚举属性

const obj = {};

Object.defineProperty(obj, 'hidden', {
  value: 'secret',
  enumerable: false
});

console.log(obj.hidden); // "secret"
console.log(Object.keys(obj)); // []

4. 使用 getter 和 setter

const obj = {};
let internalValue = '';

Object.defineProperty(obj, 'greeting', {
  get: function() {
    return internalValue;
  },
  set: function(value) {
    internalValue = 'Hello, ' + value;
  },
  enumerable: true,
  configurable: true
});

obj.greeting = 'World';
console.log(obj.greeting); // "Hello, World"

5. 不可配置属性

const obj = {};

Object.defineProperty(obj, 'fixed', {
  value: 'cannot change',
  configurable: false
});

// 尝试删除或修改属性描述符会失败
delete obj.fixed; // false
console.log(obj.fixed); // "cannot change"

// 严格模式下会抛出错误
Object.defineProperty(obj, 'fixed', { configurable: true }); // 抛出 TypeError

注意事项

  1. 默认值:如果不显式指定,writableenumerableconfigurable 的默认值都是 false
  2. 严格模式:在非严格模式下,违反属性描述符限制的操作会静默失败;在严格模式下会抛出错误。
  3. 继承:通过 Object.defineProperty() 定义的属性默认是不可枚举的,因此不会出现在 for...in 循环中(除非显式设置 enumerable: true)。
  4. 性能:与普通属性相比,访问器属性(getter/setter)可能会有轻微的性能开销。

实际应用

Object.defineProperty() 常用于:

  • 创建不可变属性
  • 实现数据绑定和观察(如 Vue 2.x 的响应式系统)
  • 定义隐藏属性(不可枚举)
  • 实现高级属性行为(如计算属性、验证等)

浏览器兼容性

Object.defineProperty() 在 IE9+ 和所有现代浏览器中都支持,但在 IE8 中仅能用于 DOM 对象。

Next.js 教程系列(六)API Routes 与全栈开发基础

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

第六章:API Routes 与全栈开发基础

教程简介

本章将带你深入理解 Next.js 的 API Routes 功能,掌握如何在同一个项目中实现前后端一体化开发。你将学会如何编写高质量的 API 接口,处理数据请求、鉴权、错误处理、性能优化等企业级场景。我们还会结合移动端适配、接口安全、全栈开发最佳实践,帮助你构建健壮、可维护的全栈应用。


理论讲解

1.1 API Routes 概述与架构演进

  • API Routes 是 Next.js 提供的后端接口开发能力,允许你在 /pages/api/app/api 目录下直接编写 Node.js 风格的接口。
  • 支持 RESTful、GraphQL、Webhooks、BFF(Backend For Frontend)等多种接口风格。
  • 与前端页面共享同一项目、同一依赖、同一部署流程,极大提升开发效率。
  • 适合中小型全栈项目、BFF 模式、原型开发、企业级微服务网关等场景。
  • 推荐分层架构:API 层(路由)、服务层(业务逻辑)、数据访问层(DAO),便于维护和扩展。
  • 支持接口版本管理(如 /api/v1/),便于平滑升级。

1.2 API Routes 的基本用法与进阶

  • /pages/api 目录下创建任意 .ts/.js 文件,即可自动成为一个 API 路由。
  • 每个文件导出一个默认函数,接收 req(请求对象)和 res(响应对象)。
  • 支持 GET、POST、PUT、DELETE 等 HTTP 方法。
  • 支持中间件、Cookie、Session、文件上传、数据库操作、Edge API Routes(边缘计算)、Server Actions(App Router)。
  • 推荐按业务模块拆分目录,如 /api/user//api/order//api/admin/

1.3 API Routes 与全栈开发

  • 前端页面通过 fetchaxiosSWRReact Query 等方式请求本地 API。
  • API 层可集成数据库(如 Prisma、TypeORM)、第三方服务(如 Stripe、微信支付)、缓存(如 Redis)、消息队列等。
  • 支持 SSR/SSG/ISR 等多种渲染模式下的数据获取。
  • 可作为微服务网关,聚合/转发后端服务。

1.4 企业级安全与权限控制

  • 鉴权:结合 JWT、Session、OAuth2、API Key、第三方登录(如 GitHub、微信)实现用户身份校验。
  • 接口限流:防止恶意刷接口,可用 Redis、内存、第三方服务实现。
  • CSRF/XSS 防护:合理设置 CORS、校验 Referer、过滤输入。
  • 敏感信息保护:环境变量、加密存储、日志脱敏、接口审计。
  • 接口签名与幂等性:对关键接口请求参数签名校验,防止篡改和重复提交。
  • 多租户支持:通过租户ID、Token、Header 实现多租户隔离。

1.5 性能优化与高可用

  • 缓存:HTTP 缓存头、Redis、CDN、接口预热、缓存穿透防护。
  • 批量/合并请求:减少接口数量,提升移动端体验。
  • 异步处理:如队列、定时任务,避免接口阻塞主线程。
  • 边缘计算:利用 Vercel Edge Functions 实现低延迟接口。
  • 接口降级与容灾:主服务异常时自动降级到备用方案。
  • 日志与监控:接口需有日志、埋点、告警,便于排查问题。

1.6 Mock、自动化测试与文档

  • Mock Service Worker(MSW):前端可独立开发调试,后端未完成时模拟接口。
  • Jest/Supertest:为 API Routes 编写单元测试、集成测试。
  • 契约测试:保证前后端数据结构一致。
  • OpenAPI/Swagger:自动生成接口文档,支持在线调试。
  • 类型注释:结合 TypeScript 类型,提升文档准确性。
  • CI 持续集成:自动化测试覆盖,保障接口质量。

1.7 多端适配与国际化

  • 响应式接口:根据 UA/参数返回不同数据结构,适配 Web/移动/小程序/桌面端。
  • 国际化:接口支持多语言返回,结合 i18n、next-intl。
  • 图片/多媒体优化:返回合适尺寸的图片链接,支持 WebP、AVIF。
  • 网络异常处理:接口需返回明确错误码和提示,前端可友好降级。

1.8 错误处理与监控

  • 统一错误码与响应格式:便于前端处理和埋点。
  • Sentry/LogRocket:接入错误监控,自动上报异常。
  • 慢接口告警:接口超时自动告警,便于性能优化。
  • 日志采集:接口请求日志、用户行为日志、异常日志。

代码示例

2.1 创建基础 API 路由

// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ message: 'Hello, Next.js API!' });
}

2.2 支持多种 HTTP 方法

// pages/api/user.ts
import type { NextApiRequest, NextApiResponse } from 'next';

let users = [
  { id: 1, name: '小明' },
  { id: 2, name: '小红' },
];

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    res.status(200).json(users);
  } else if (req.method === 'POST') {
    const { name } = req.body;
    const newUser = { id: Date.now(), name };
    users.push(newUser);
    res.status(201).json(newUser);
  } else {
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

2.3 接口鉴权与 JWT 校验

// pages/api/profile.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET || 'demo_secret';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: '未登录' });
  }
  try {
    const user = jwt.verify(token, SECRET);
    res.status(200).json({ user });
  } catch {
    res.status(401).json({ error: 'Token 无效' });
  }
}

2.4 接口限流中间件

// lib/rateLimit.ts
const rateLimitMap = new Map<string, { count: number; last: number }>();

export function rateLimit(ip: string, limit = 10, windowMs = 60_000) {
  const now = Date.now();
  const entry = rateLimitMap.get(ip) || { count: 0, last: now };
  if (now - entry.last > windowMs) {
    rateLimitMap.set(ip, { count: 1, last: now });
    return false;
  }
  if (entry.count >= limit) return true;
  rateLimitMap.set(ip, { count: entry.count + 1, last: entry.last });
  return false;
}
// pages/api/secure-data.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { rateLimit } from '@/lib/rateLimit';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const ip = req.headers['x-forwarded-for']?.toString() || req.socket.remoteAddress || '';
  if (rateLimit(ip, 5, 60_000)) {
    return res.status(429).json({ error: '请求过于频繁,请稍后再试' });
  }
  res.status(200).json({ data: '安全数据' });
}

2.5 文件上传与表单处理

// pages/api/upload.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import formidable from 'formidable';
import fs from 'fs';

export const config = {
  api: {
    bodyParser: false,
  },
};

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const form = new formidable.IncomingForm();
  form.parse(req, (err, fields, files) => {
    if (err) return res.status(500).json({ error: '上传失败' });
    // 假设保存到本地
    const file = files.file as formidable.File;
    fs.renameSync(file.filepath, `./public/uploads/${file.originalFilename}`);
    res.status(200).json({ url: `/uploads/${file.originalFilename}` });
  });
}

2.6 数据库操作(以 Prisma 为例)

// pages/api/products.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    const products = await prisma.product.findMany();
    res.status(200).json(products);
  } else if (req.method === 'POST') {
    const { name, price } = req.body;
    const product = await prisma.product.create({ data: { name, price } });
    res.status(201).json(product);
  } else {
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

2.7 移动端分页与懒加载接口

// pages/api/feed.ts
import type { NextApiRequest, NextApiResponse } from 'next';

const allItems = Array.from({ length: 100 }).map((_, i) => ({ id: i + 1, title: `Item ${i + 1}` }));

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const page = parseInt(req.query.page as string) || 1;
  const limit = parseInt(req.query.limit as string) || 10;
  const start = (page - 1) * limit;
  const end = start + limit;
  res.status(200).json({
    items: allItems.slice(start, end),
    total: allItems.length,
    page,
    limit,
  });
}

2.8 错误处理与统一响应格式

// lib/response.ts
export function success(data: any) {
  return { code: 0, data };
}
export function error(message: string, code = 1) {
  return { code, message };
}
// pages/api/unified.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { success, error } from '@/lib/response';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    // ...业务逻辑
    res.status(200).json(success({ msg: 'ok' }));
  } catch (e) {
    res.status(500).json(error('服务器异常'));
  }
}

2.9 GraphQL API 基础

// pages/api/graphql.ts
import { ApolloServer, gql } from 'apollo-server-micro';

const typeDefs = gql`
  type User { id: ID! name: String! }
  type Query { user(id: ID!): User }
`;
const resolvers = {
  Query: {
    user: (_: any, { id }: { id: string }) => ({ id, name: '小明' }),
  },
};
const apolloServer = new ApolloServer({ typeDefs, resolvers });
export const config = { api: { bodyParser: false } };
export default apolloServer.createHandler({ path: '/api/graphql' });

2.10 API Key 鉴权与签名校验

// pages/api/secure.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

const API_KEY = process.env.API_KEY || 'demo_key';
const SECRET = process.env.API_SECRET || 'demo_secret';

function verifySignature(req: NextApiRequest) {
  const signature = req.headers['x-signature'] as string;
  const timestamp = req.headers['x-timestamp'] as string;
  const raw = `${timestamp}${API_KEY}${SECRET}`;
  const expected = crypto.createHash('sha256').update(raw).digest('hex');
  return signature === expected;
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.headers['x-api-key'] !== API_KEY || !verifySignature(req)) {
    return res.status(401).json({ error: '无效签名' });
  }
  res.status(200).json({ data: '安全数据' });
}

2.11 幂等性与防重复提交

// lib/idempotency.ts
const idempotencyMap = new Map<string, number>();
export function isDuplicate(id: string) {
  if (idempotencyMap.has(id)) return true;
  idempotencyMap.set(id, Date.now());
  setTimeout(() => idempotencyMap.delete(id), 60_000);
  return false;
}
// pages/api/order.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { isDuplicate } from '@/lib/idempotency';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const id = req.headers['x-idempotency-key'] as string;
  if (!id || isDuplicate(id)) {
    return res.status(409).json({ error: '重复提交' });
  }
  // ...创建订单逻辑
  res.status(201).json({ message: '订单创建成功' });
}

2.12 Mock 与自动化测试

// __tests__/api/user.test.ts
import handler from '../../pages/api/user';
import { createMocks } from 'node-mocks-http';

test('GET /api/user', async () => {
  const { req, res } = createMocks({ method: 'GET' });
  await handler(req, res);
  expect(res._getStatusCode()).toBe(200);
  expect(JSON.parse(res._getData())).toEqual([
    { id: 1, name: '小明' },
    { id: 2, name: '小红' },
  ]);
});

2.13 OpenAPI/Swagger 自动生成文档

// scripts/generate-openapi.js
// 使用 swagger-jsdoc 自动生成 openapi.json
const swaggerJSDoc = require('swagger-jsdoc');
const options = { ... };
const openapiSpec = swaggerJSDoc(options);
require('fs').writeFileSync('openapi.json', JSON.stringify(openapiSpec, null, 2));

2.14 多语言接口

// pages/api/i18n.ts
import type { NextApiRequest, NextApiResponse } from 'next';
const messages = {
  zh: { hello: '你好' },
  en: { hello: 'Hello' },
};
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const lang = req.query.lang || 'zh';
  res.status(200).json({ message: messages[lang as string]?.hello || messages.zh.hello });
}

2.15 Sentry 错误监控集成

// pages/api/_middleware.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({ dsn: process.env.SENTRY_DSN });
export default function middleware(req, ev) {
  try {
    return NextResponse.next();
  } catch (e) {
    Sentry.captureException(e);
    throw e;
  }
}

实战项目

3.1 构建全栈商品管理系统

目标:实现一个支持商品增删改查、图片上传、用户鉴权、移动端适配的全栈商品管理系统。

主要功能:
  1. 商品列表页:支持分页、搜索、移动端自适应。
  2. 商品详情页:展示商品图片、价格、描述。
  3. 后台管理页:支持商品的新增、编辑、删除。
  4. 用户登录鉴权:JWT 登录、接口权限控制。
  5. 图片上传:支持多图上传、进度显示。
  6. API 接口:全部基于 Next.js API Routes 实现。
  7. 错误处理与统一响应格式。
  8. 性能优化:接口缓存、按需加载。
  9. 日志与监控:接口请求日志、错误告警。
  10. 国际化与移动端适配。
关键代码片段:
// pages/products/index.tsx
import useSWR from 'swr';
import { useState } from 'react';

const fetcher = (url: string) => fetch(url).then(res => res.json());

export default function ProductsPage() {
  const [page, setPage] = useState(1);
  const { data, error } = useSWR(`/api/feed?page=${page}&limit=10`, fetcher);
  if (error) return <div>加载失败</div>;
  if (!data) return <div>加载中...</div>;
  return (
    <div>
      <ul>
        {data.items.map((item: any) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
      <button disabled={page === 1} onClick={() => setPage(page - 1)}>上一页</button>
      <button disabled={data.items.length < 10} onClick={() => setPage(page + 1)}>下一页</button>
    </div>
  );
}
// pages/admin/products.tsx
import { useState } from 'react';

export default function AdminProducts() {
  const [name, setName] = useState('');
  const [price, setPrice] = useState('');
  const [msg, setMsg] = useState('');
  const handleAdd = async () => {
    const res = await fetch('/api/products', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, price: Number(price) }),
    });
    const data = await res.json();
    setMsg(data.code === 0 ? '添加成功' : data.message);
  };
  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} placeholder="商品名" />
      <input value={price} onChange={e => setPrice(e.target.value)} placeholder="价格" type="number" />
      <button onClick={handleAdd}>添加商品</button>
      <div>{msg}</div>
    </div>
  );
}

3.2 多租户商城 API 设计

  • 支持多租户(如不同商家/品牌)数据隔离。
  • 每个租户有独立的商品、订单、用户数据。
  • 通过 Header、Token、子域名等区分租户。
  • 接口返回结构需兼容多端(Web/移动/小程序)。
  • 支持租户级别的权限、限流、定制化配置。

3.3 订单系统与支付回调

  • 订单创建接口需防止重复提交(幂等性)。
  • 支付回调接口需校验签名、防止伪造。
  • 订单状态流转需有日志、告警。
  • 支持异步通知、消息推送。

3.4 实时消息与 WebSocket

  • 使用 nextjs-websocket、socket.io 实现实时订单状态推送。
  • 支持移动端、桌面端多端同步。
  • 接口需有鉴权、限流、断线重连机制。

3.5 复杂参数校验与批量导入导出

// lib/validate.ts
import * as z from 'zod';
export const productSchema = z.object({
  name: z.string().min(2),
  price: z.number().positive(),
});
// pages/api/products.ts
import { productSchema } from '@/lib/validate';
export default async function handler(req, res) {
  if (req.method === 'POST') {
    const parse = productSchema.safeParse(req.body);
    if (!parse.success) {
      return res.status(400).json({ error: '参数校验失败', details: parse.error.errors });
    }
    // ...创建逻辑
  }
}
  • 支持 Excel/CSV 批量导入商品,接口需校验格式、异步处理。
  • 导出接口支持大数据量分片导出,防止超时。

3.6 长轮询与 WebSocket

  • 长轮询接口需有超时、重试机制。
  • WebSocket 需支持断线重连、心跳包。

常见问题与最佳实践

  • API 目录混乱:建议按业务模块拆分,统一命名规范。
  • 环境变量泄漏:敏感信息只放 .env,不要暴露到前端。
  • SSR/CSR 下接口复用:建议统一用 API Routes,避免重复实现。
  • 接口 Mock 不一致:Mock 数据与真实接口保持同步,自动化测试覆盖。
  • 接口文档滞后:用 OpenAPI/Swagger 自动生成,CI 校验。
  • 多端适配遗漏:接口返回结构需兼容 Web/移动/小程序。
  • 接口安全被忽视:务必加鉴权、限流、签名、日志。
  • 性能瓶颈:接口加缓存、CDN、边缘计算,定期分析慢接口。
  • 错误处理不统一:统一响应格式,前端友好提示。

片尾

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

JS中的类,类的原型,实例三者的异同

JavaScript 中的类、类的原型和实例的异同

在 JavaScript 中,类(Class)、类的原型(Prototype)和实例(Instance)是面向对象编程的核心概念。下面我将详细解释它们的异同,并提供代码示例。

1. 基本概念

类 (Class)

  • ES6 引入的语法糖,基于原型继承的封装
  • 用于创建对象的模板
  • 包含构造函数和方法的定义

类的原型 (Prototype)

  • 每个 JavaScript 函数(包括类)都有一个 prototype 属性
  • 包含该类的共享方法和属性
  • 实例通过原型链继承这些属性和方法

实例 (Instance)

  • 通过 new 关键字调用类创建的具体对象
  • 每个实例都有自己的属性,但共享原型上的方法

2. 三者的关系

类 (Class) → 类的原型 (Prototype) ← 实例 (Instance)
  • 类定义了如何创建实例
  • 类的原型是所有实例共享的方法和属性的存储位置
  • 实例通过原型链访问原型上的方法和属性

3. 代码示例

// 定义一个类
class Person {
  // 构造函数,用于初始化实例属性
  constructor(name, age) {
    this.name = name; // 实例属性
    this.age = age;   // 实例属性
  }

  // 实例方法(实际上会被添加到Person.prototype上)
  greet() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }

  // 静态方法(属于类本身,不属于实例)
  static species() {
    return 'Homo sapiens';
  }
}

// 1. 类本身
console.log(typeof Person); // "function"(类实际上是函数)
console.log(Person.species()); // "Homo sapiens"(调用静态方法)

// 2. 类的原型
console.log(Person.prototype); // 包含greet方法
console.log(Person.prototype.constructor === Person); // true

// 3. 创建实例
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

// 实例访问属性和方法
console.log(person1.name); // "Alice"(实例属性)
person1.greet(); // 调用原型上的方法

// 检查原型链关系
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1 instanceof Person); // true

// 添加方法到原型(会影响所有实例)
Person.prototype.sayGoodbye = function() {
  console.log(`Goodbye from ${this.name}!`);
};

person1.sayGoodbye(); // "Goodbye from Alice!"
person2.sayGoodbye(); // "Goodbye from Bob!"

4. 三者的主要区别

特性 类 (Class) 类的原型 (Prototype) 实例 (Instance)
定义位置 使用 class 关键字定义 自动创建,通过 ClassName.prototype 访问 通过 new ClassName() 创建
包含内容 构造函数、静态方法 共享的方法和属性 实例特有的属性
访问方式 直接通过类名访问 通过 ClassName.prototypeinstance.__proto__ 通过变量名访问
是否可被继承 是(通过 extends 是(通过原型链)
内存中的存在 只有一个类定义 只有一个原型对象 可以有多个实例

5. 更深入的原型链示例

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name} is barking!`);
  }
}

const myDog = new Dog('Rex', 'Labrador');

// 原型链关系
console.log(myDog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true

// 方法查找过程
myDog.eat(); // 先在Dog.prototype找,没有就去Animal.prototype找
myDog.bark(); // 在Dog.prototype找到
myDog.toString(); // 最终在Object.prototype找到

6. 总结

  • 是创建对象的模板,提供了更清晰的语法来创建构造函数和原型
  • 原型是所有实例共享的方法和属性的存储位置,实现了方法的共享和继承
  • 实例是根据类创建的具体对象,拥有自己的属性,但共享原型上的方法

理解这三者的关系对于掌握 JavaScript 的面向对象编程至关重要。原型继承是 JavaScript 的核心特性,即使使用 class 语法,底层仍然是基于原型的实现。

1、作用域链彻底搞懂:词法环境 vs 执行上下文

💡 本篇目标:彻底理解 JavaScript 中作用域的本质、形成机制、作用域链的运行逻辑,并通过可视化图解 + 代码演示 + 实战示例,建立你对 JS 底层执行模型的“源代码级”理解。


🎯 一、你以为的作用域,其实只是“静态规则”

❓什么是作用域(Scope)?

作用域就是变量访问的范围,决定了在什么位置能访问到某个变量

JS 是词法作用域(Lexical Scope)语言,也就是说:

  • 作用域在你写代码时就已经决定了
  • 跟你怎么调用这个函数无关

🧠 对比图:词法作用域 vs 动态作用域

类型 特点 举例
词法作用域 静态、由代码结构决定 JavaScript、C、Go
动态作用域 动态、由调用栈决定 bash、某些 Lisp

📦 二、执行上下文(Execution Context)vs 词法环境(Lexical Environment)

✅ 术语区分:

概念 说明
执行上下文 当前 JS 执行时的状态(变量、作用域链、this 等)
词法环境 是执行上下文的一部分,维护变量-值映射的结构
作用域链 多个词法环境通过 outer 连接形成的“查找路径”

🧩 执行过程简图:

代码结构决定:
function foo() {
  var a = 10
  function bar() {
    console.log(a)
  }
  bar()
}

执行栈过程如下:

  1. 进入全局执行上下文(创建 globalEnv)
  2. 创建 foo 函数 → 记录其词法环境指向 globalEnv
  3. 调用 foo() → 创建 fooEC,fooEnv → outer → globalEnv
  4. 调用 bar() → 创建 barEC,barEnv → outer → fooEnv

🔬 三、作用域链的运行逻辑(图解)

const a = 1
function outer() {
  const b = 2
  function inner() {
    const c = 3
    console.log(a, b, c)
  }
  inner()
}
outer()

内部结构可视化(作用域链):

innerEnv → outerEnv (outer) → outerEnv (global)
   c           b                a

变量查找机制是:从当前作用域向外一层一层找,直到 global,找不到就报错


✅ 四、工程实战:作用域链是如何用于权限控制的?

示例:构建一个私有变量模块(闭包 + 作用域链)

function createCounter() {
  let count = 0 // 作用域链上的私有变量
  return {
    increment() {
      count++
      return count
    },
    getCount() {
      return count
    }
  }
}

const counter = createCounter()
console.log(counter.increment()) // 1
console.log(counter.getCount()) // 1

📌 本质解析:

  • createCounter 执行后,其 count 被保存在 [[Environment]]
  • incrementgetCount 拿到的是 闭包 + 作用域链形成的访问权
  • 外部无法直接访问 count实现了封装与数据安全

🧨 五、常见误区与注意点

1. 作用域不是调用时才决定的

var a = 1
function foo() {
  console.log(a)
}
function bar() {
  var a = 2
  foo() // 输出 1,而不是 2
}
bar()

因为 foo 在定义时,其作用域链已经锁定在 global,不会因为在 bar 中调用而切换作用域。


2. 不理解作用域链会导致“闭包陷阱”

const fns = []
for (var i = 0; i < 3; i++) {
  fns.push(function () {
    console.log(i) // 全部输出 3
  })
}
fns[0]() // 3 ❌

✅ 修正方式一:使用 IIFE

for (var i = 0; i < 3; i++) {
  (function(i) {
    fns.push(function () {
      console.log(i)
    })
  })(i)
}

✅ 修正方式二:使用 let(块级作用域)

for (let i = 0; i < 3; i++) {
  fns.push(() => console.log(i)) // 输出 0,1,2
}

🚀 六、总结:你现在应该掌握了

知识点 理解程度
JS 是词法作用域语言
执行上下文 vs 词法环境
作用域链查找顺序
如何利用作用域封装私有数据
容易出错的场景与修复方法

📘 下一篇《第2篇:变量提升与函数提升的编译原理剖析》将深入源码层级,揭示 JS 是如何在执行前构建作用域和变量声明的。

Three.js 贴图:给 3D 世界穿上花衣裳

在 Three.js 的魔法世界里,我们搭建的 3D 模型就像一个个等待盛装出席舞会的 “裸模”。它们光秃秃地站在那里,虽然已经有了迷人的身材(几何形状)和挺拔的身姿(空间位置),但总觉得缺了点什么 —— 没错,就是那件能让它们惊艳全场的 “衣裳”,而这件衣裳,就是我们今天要讲的贴图

一、揭开贴图的神秘面纱

从底层原理来看,贴图本质上就是一张二维图像,它会被 “包裹” 在三维模型表面,给模型赋予丰富的视觉细节。这就好比你给一个素色的陶瓷花瓶贴上精美的贴纸,原本平淡无奇的花瓶瞬间变得五彩斑斓。在计算机的世界里,显卡就像是一位心灵手巧的裁缝,它会按照一定的规则,把二维图像准确地 “缝制” 到三维模型上。

1.1 贴图的基本类型

Three.js 支持多种类型的贴图,常见的有纹理贴图法线贴图高光贴图等。

  • 纹理贴图:这是最基础、最常用的贴图类型,就像给模型贴上一张高清照片。比如你想创建一个木纹桌面,只需要找一张逼真的木纹图片作为纹理贴图,贴到桌面模型上,瞬间就能让桌面看起来质感十足。
// 加载纹理贴图
const textureLoader = new THREE.TextureLoader();
const woodTexture = textureLoader.load('wood.jpg');
// 创建材质并应用纹理
const material = new THREE.MeshBasicMaterial({ map: woodTexture });
  • 法线贴图:它就像给模型添加了 “凹凸滤镜”,通过改变模型表面的光照计算方式,让模型看起来有凹凸不平的效果。想象一下,你在平面上贴了一张有岩石纹路的法线贴图,原本平滑的表面在光线照射下,就能呈现出岩石那种坑坑洼洼的立体感,而实际上模型的几何形状并没有改变。
const normalMapLoader = new THREE.TextureLoader();
const normalMap = normalMapLoader.load('rock_normal.jpg');
const bumpMaterial = new THREE.MeshPhongMaterial({ 
    map: woodTexture,
    normalMap: normalMap,
    normalScale: new THREE.Vector2(1, 1) 
});
  • 高光贴图:决定了模型表面哪些地方更亮、更反光,就像是给模型的 “皮肤” 调整光泽度。比如制作一个金属质感的物体,通过高光贴图可以让它看起来闪闪发亮,仿佛镀了一层金属膜。
const specularMapLoader = new THREE.TextureLoader();
const specularMap = specularMapLoader.load('metal_specular.jpg');
const shinyMaterial = new THREE.MeshPhongMaterial({ 
    map: metalTexture,
    specularMap: specularMap,
    specular: 0x111111 
});

二、贴图的 “穿衣” 过程

当我们把贴图加载好之后,该怎么让它准确地 “穿” 到模型身上呢?这就涉及到UV 映射。UV 映射可以理解为给三维模型制作的 “裁剪图”,它定义了二维贴图上的每个点对应到三维模型表面的位置。

想象你要把一张世界地图贴到一个地球仪上,如果随便乱贴,肯定会变得乱七八糟。UV 映射就是告诉你,地图上的某个角落应该贴在地球仪的北极,另一个地方应该贴在赤道附近。在 Three.js 中,很多基础的几何模型都已经内置了合理的 UV 映射,我们可以直接使用。但如果是自定义的复杂模型,可能就需要手动调整 UV 映射了。

// 创建一个立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 为立方体指定材质(包含纹理贴图)
const cubeMaterial = new THREE.MeshBasicMaterial({ map: someTexture });
const cube = new THREE.Mesh(geometry, cubeMaterial);

三、进阶技巧:让贴图更 “丝滑”

3.1 纹理重复与偏移

有时候,我们的贴图尺寸可能不够大,覆盖不了整个模型,或者想实现一些有趣的图案效果,这时候就可以用到纹理重复和偏移。

  • 纹理重复:就像用同一块瓷砖铺满整个地面,通过设置repeat属性,可以让贴图在模型表面重复显示。
woodTexture.wrapS = THREE.RepeatWrapping;
woodTexture.wrapT = THREE.RepeatWrapping;
woodTexture.repeat.set(2, 2); // 在UV两个方向上都重复2次
  • 纹理偏移:则是把贴图在模型表面 “挪动” 一下位置,通过offset属性来实现。比如你想让木纹图案从模型的某个角落开始显示,就可以调整偏移值。
woodTexture.offset.set(0.5, 0.5); // 让纹理在UV方向上都偏移0.5

3.2 纹理过滤

当模型离我们很远或者进行快速移动时,为了防止贴图出现模糊、锯齿等难看的效果,我们需要设置纹理过滤。纹理过滤就像是给贴图加上一个 “美颜滤镜”,让它在各种情况下都能保持良好的视觉效果。Three.js 提供了多种过滤方式,如THREE.NearestFilter(最近邻过滤,速度快但可能有锯齿)、THREE.LinearFilter(线性过滤,效果平滑但计算稍复杂)等。

const filteredTexture = new THREE.TextureLoader().load('image.jpg');
filteredTexture.minFilter = THREE.LinearFilter;
filteredTexture.magFilter = THREE.LinearFilter;

四、实战演练:打造一个奇幻小屋

现在,我们就用刚刚学到的知识,来搭建一个充满童话色彩的小屋。

  1. 加载屋顶的瓦片纹理贴图和墙壁的砖块纹理贴图。
const roofTexture = textureLoader.load('tiles.jpg');
const wallTexture = textureLoader.load('bricks.jpg');
  1. 创建屋顶和墙壁的几何模型,并分别赋予对应的材质。
// 屋顶
const roofGeometry = new THREE.ConeGeometry(5, 2, 32);
const roofMaterial = new THREE.MeshBasicMaterial({ map: roofTexture });
const roof = new THREE.Mesh(roofGeometry, roofMaterial);
// 墙壁
const wallGeometry = new THREE.BoxGeometry(4, 3, 4);
const wallMaterial = new THREE.MeshBasicMaterial({ map: wallTexture });
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
  1. 将屋顶和墙壁组合在一起,调整位置和角度,一个可爱的小屋就诞生啦!

在 Three.js 的世界里,贴图就像是我们手中的调色盘和画笔,能让原本单调的 3D 模型变得栩栩如生。掌握了贴图的应用技巧,你就能成为这个魔法世界里的顶级设计师,创造出无数令人惊叹的作品。快去发挥你的创意,给你的 3D 模型们都穿上华丽的 “衣裳” 吧!

上述内容从多方面展示了 Three.js 贴图应用。若你对某个部分想深入了解,或有其他功能添加需求,欢迎随时告诉我。

彻底搞懂浏览器事件循环:从原理到最佳实践

概述

要真正理解事件循环,我们需要先了解浏览器的多进程架构:

  • 浏览器主进程:负责界面显示、用户交互

  • GPU进程:处理图形渲染

  • 网络进程:处理网络请求

  • 渲染进程(核心):每个标签页一个渲染进程,包含:

    • 主线程:执行JS、解析HTML/CSS、布局、绘制(就是我们常说的JS线程)
    • 合成线程:负责图层分割
    • 光栅线程:将图层转换为像素

🔍 关键点:JS引擎(如V8)只是渲染进程的一部分,JS的"单线程"指的是主线程的单线程,浏览器整体是多线程的。

1. 事件循环的完整运行机制

1.1 核心组件详解

(1) 调用栈(Call Stack)

  • 本质:记录函数调用的数据结构(LIFO栈)

  • 特点:

    • 每次函数调用都会创建新的栈帧(包含参数、局部变量等)
    • 栈溢出:当递归深度超过最大调用栈大小(Chrome约1万层)
// 栈溢出示例
function stackOverflow() {
  stackOverflow()
}
stackOverflow() // Uncaught RangeError

(2) 堆内存(Heap)

  • 存储引用类型(对象、数组等)的内存区域

  • 与栈的区别:

    • 栈:自动分配固定大小内存(基础类型、指针)
    • 堆:动态分配内存,需要垃圾回收

(3) 任务队列系统

队列类型 触发方式 优先级 示例
微任务队列 JS引擎直接管理 Promise.then, queueMicrotask
宏任务队列 由浏览器宿主环境管理 setTimeout, 事件回调
动画回调队列 requestAnimationFrame 特殊 动画更新
空闲回调队列 requestIdleCallback 最低 非关键任务

💡 重要细节:微任务会在每个宏任务执行完后立即清空,包括渲染前和事件循环的每个阶段之间。

1.2 完整事件循环流程

image.png

关键阶段说明:

  1. 微任务检查点

    • 在每个宏任务结束后
    • 在每次事件循环迭代开始时
  2. 渲染时机

    • 约60fps(每16.6ms)
    • 受垂直同步信号影响
  3. 任务优先级

    用户交互 > 微任务 > 宏任务 > 空闲任务
    

2. 深度解析异步任务

2.1 宏任务(MacroTask)详解

  • 本质:由浏览器环境(而非JS引擎)管理的任务

  • 完整分类

    • DOM事件(click等)
    • 网络回调(XHR/Fetch)
    • IndexedDB操作
    • History API
    • setTimeout/setInterval
    • postMessage
    • MessageChannel
// 宏任务执行顺序测试
setTimeout(() => console.log('timeout1'), 0)
const channel = new MessageChannel()
channel.port1.postMessage(null)
channel.port2.onmessage = () => console.log('message channel')
setTimeout(() => console.log('timeout2'), 0)
// 输出顺序:message channel → timeout1 → timeout2

2.2 微任务(MicroTask)深度解析

  • 运行时机

    • 在每个宏任务之后
    • 在每次事件循环开始前
  • 特殊行为

    • 微任务中可以递归添加微任务
    • 微任务队列必须完全清空才会继续事件循环
// 微任务递归示例
function recursiveMicrotask() {
  Promise.resolve().then(() => {
    console.log('微任务执行')
    recursiveMicrotask()
  })
}
// 会导致页面卡死,因为微任务不断产生

2.3 requestAnimationFrame的特殊性

  • 执行时机:在样式计算和布局之后,绘制之前

  • 与事件循环的关系:

image.png

3. 浏览器渲染机制与事件循环

3.1 渲染管线关键阶段

  1. JavaScript:改变DOM/CSS
  2. 样式计算:计算最终CSS
  3. 布局:计算元素几何信息
  4. 绘制:生成绘制指令
  5. 合成:图层合并

3.2 事件循环与渲染的协同

function testRender() {
  box.style.width = '100px' // 触发重排
  requestAnimationFrame(() => {
    console.log('RAF:', box.offsetWidth) // 读取最新布局
  })
  box.style.height = '200px' // 再次重排
}
// 现代浏览器会合并样式修改

4. 实战中的性能优化

4.1 避免布局抖动

// 反模式:强制同步布局
function layoutThrashing() {
  for(let i = 0; i < 100; i++) {
    el.style.width = el.offsetWidth + 1 + 'px'
  }
}

// 优化方案:批量读取和修改
function optimizedLayout() {
  const width = el.offsetWidth
  for(let i = 0; i < 100; i++) {
    width += 1
  }
  el.style.width = width + 'px'
}

4.2 合理使用任务拆分

function processLargeTask() {
  const chunkSize = 1000
  let i = 0
  
  function processChunk() {
    const end = Math.min(i + chunkSize, data.length)
    for (; i < end; i++) {
      // 处理数据
    }
    if (i < data.length) {
      // 使用宏任务让出主线程
      setTimeout(processChunk, 0)
      // 或者使用更现代的API
      // scheduler.postTask(() => processChunk(), {priority: 'background'})
    }
  }
  
  processChunk()
}

5. Node.js与浏览器事件循环差异

特性 浏览器 Node.js
微任务执行时机 每个宏任务之后 每个事件循环阶段之间
setImmediate 不存在 专门阶段
优先级 微任务 > 宏任务 微任务 > setImmediate
渲染时机 每帧检查 不适用

6. 最新规范变化

  • isInputPending API:检测是否有挂起的用户输入
  • scheduler API:更细粒度的任务调度
  • 优先级API:可以指定任务优先级

总结与思考题

关键结论

  1. 微任务会在当前宏任务结束后立即执行
  2. 渲染发生在微任务执行后、下一个宏任务前
  3. RAF回调在渲染前执行
  4. 长时间运行的微任务会阻塞渲染

思考题

console.log('script start')

setTimeout(() => {
  console.log('timeout1')
  Promise.resolve().then(() => console.log('promise1'))
}, 0)

requestAnimationFrame(() => {
  console.log('rAF')
  Promise.resolve().then(() => console.log('promise2'))
})

Promise.resolve().then(() => {
  console.log('promise3')
  setTimeout(() => console.log('timeout2'), 0)
})

console.log('script end')

请分析输出顺序,并说明每一部分的执行时机,可以尝试判断输出结果


如果觉得本文有帮助,给个点赞收藏+关注  🚀

❌