阅读视图

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

每日一题-找出第 K 个字符 I🟢

Alice 和 Bob 正在玩一个游戏。最初,Alice 有一个字符串 word = "a"

给定一个正整数 k

现在 Bob 会要求 Alice 执行以下操作 无限次 :

  • word 中的每个字符 更改 为英文字母表中的 下一个 字符来生成一个新字符串,并将其 追加 到原始的 word

例如,对 "c" 进行操作生成 "cd",对 "zb" 进行操作生成 "zbac"

在执行足够多的操作后, word至少 存在 k 个字符,此时返回 word 中第 k 个字符的值。

注意,在操作中字符 'z' 可以变成 'a'

 

示例 1:

输入:k = 5

输出:"b"

解释:

最初,word = "a"。需要进行三次操作:

  • 生成的字符串是 "b"word 变为 "ab"
  • 生成的字符串是 "bc"word 变为 "abbc"
  • 生成的字符串是 "bccd"word 变为 "abbcbccd"

示例 2:

输入:k = 10

输出:"c"

 

提示:

  • 1 <= k <= 500

3304. 找出第 K 个字符 I

解法一

思路和算法

最直观的思路是模拟每次操作。初始时字符串中只有一个字符 $\text{`a'}$,每次操作将字符串中已有的每个字符的后一个字符拼接到字符串的末尾,直到字符串中至少有 $k$ 个字符时结束操作。当字符串中至少有 $k$ 个字符时,答案字符为字符串的下标 $k - 1$ 的字符。

代码

###Java

class Solution {
    public char kthCharacter(int k) {
        StringBuffer sb = new StringBuffer("a");
        while (sb.length() < k) {
            int length = sb.length();
            for (int i = 0; i < length; i++) {
                sb.append((char) (sb.charAt(i) + 1));
            }
        }
        return sb.charAt(k - 1);
    }
}

###C#

public class Solution {
    public char KthCharacter(int k) {
        StringBuilder sb = new StringBuilder("a");
        while (sb.Length < k) {
            int length = sb.Length;
            for (int i = 0; i < length; i++) {
                sb.Append((char) (sb[i] + 1));
            }
        }
        return sb[k - 1];
    }
}

复杂度分析

  • 时间复杂度:$O(k)$,其中 $k$ 是给定的正整数。结束操作时的字符串的长度一定小于 $2k$,每个字符的生成时间都是 $O(1)$。

  • 空间复杂度:$O(k)$,其中 $k$ 是给定的正整数。结束操作时的字符串的长度一定小于 $2k$。

解法二

思路和算法

为了计算字符串中第 $k$ 个字符的值,需要首先计算使字符串中至少有 $k$ 个字符的操作次数。由于初始字符串中有 $1$ 个字符,每次操作之后都会将字符串中的字符个数乘以 $2$,因此对于任意非负整数 $x$,经过 $x$ 次操作之后字符串中的字符个数等于 $2^x$,计算满足 $2^x \ge k$ 的最小整数 $x$,可以得到 $x \ge \lceil \log k \rceil$,为了方便计算,当 $k > 1$ 时可以转换成 $x \ge \lfloor \log (k - 1) \rfloor + 1$,即 $x$ 的最小值等于 $k$ 的二进制位数。计算字符串中第 $k$ 个字符的值需要考虑前 $x$ 次操作。

当位于下标 $\textit{index}$ 时,计算方法如下。

  • 如果 $\textit{index} < 0$,则不执行操作,字符的值等于 $\text{`a'}$。

  • 如果 $\textit{index} \ge 0$,则前 $\textit{index}$ 次操作之后的字符串中的字符个数等于 $2^{\textit{index}}$,需要比较 $k$ 和 $2^{\textit{index}}$ 的大小,然后执行计算。

    • 如果 $k \le 2^{\textit{index}}$,则下标 $\textit{index} - 1$ 的操作不影响第 $k$ 个字符,因此计算位于下标 $\textit{index} - 1$ 时的第 $k$ 个字符的值。

    • 如果 $k > 2^{\textit{index}}$,则下标 $\textit{index} - 1$ 的操作影响第 $k$ 个字符,第 $k$ 个字符由第 $k - 2^{\textit{index}}$ 个字符经过一次操作得到,因此计算位于下标 $\textit{index} - 1$ 时的第 $k - 2^{\textit{index}}$ 个字符的值,该字符的后一个字符的值即为第 $k$ 个字符的值。

上述过程是一个递归的过程。

递归的终止条件是 $\textit{index} < 0$,此时字符的值等于 $\text{`a'}$。当 $\textit{index} \ge 0$ 时,比较 $k$ 和 $2^{\textit{index}}$ 的大小,递归计算第 $k$ 个字符的值。

代码

###Java

class Solution {
    public char kthCharacter(int k) {
        int binaryLength = getBinaryLength(k - 1);
        return findCharacter(k, binaryLength - 1);
    }

    public int getBinaryLength(long num) {
        int length = 0;
        while (num > 0) {
            num /= 2;
            length++;
        }
        return length;
    }

    public char findCharacter(long k, int index) {
        if (index < 0) {
            return 'a';
        }
        long prevPosition = 1L << index;
        if (k <= prevPosition) {
            return findCharacter(k, index - 1);
        } else {
            char prev = findCharacter(k - prevPosition, index - 1);
            return (char) (prev + 1);
        }
    }
}

###C#

public class Solution {
    public char KthCharacter(int k) {
        int binaryLength = GetBinaryLength(k - 1);
        return FindCharacter(k, binaryLength - 1);
    }

    public int GetBinaryLength(long num) {
        int length = 0;
        while (num > 0) {
            num /= 2;
            length++;
        }
        return length;
    }

    public char FindCharacter(long k, int index) {
        if (index < 0) {
            return 'a';
        }
        long prevPosition = 1L << index;
        if (k <= prevPosition) {
            return FindCharacter(k, index - 1);
        } else {
            char prev = FindCharacter(k - prevPosition, index - 1);
            return (char) (prev + 1);
        }
    }
}

复杂度分析

  • 时间复杂度:$O(\log k)$,其中 $k$ 是给定的正整数。操作次数是 $O(\log k)$,每次操作的计算时间是 $O(1)$。

  • 空间复杂度:$O(\log k)$,其中 $k$ 是给定的正整数。递归调用栈的深度是 $O(\log k)$。

解法三

思路和算法

递归实现可以改成迭代实现。

首先计算使字符串中至少有 $k$ 个字符的操作次数 $x$,然后反向遍历 $x$ 次操作的过程,计算第 $k$ 个字符的值的增加次数,即可得到字符串中第 $k$ 个字符的值。

代码

###Java

class Solution {
    public char kthCharacter(int k) {
        int increments = 0;
        int binaryLength = getBinaryLength(k - 1);
        for (int i = binaryLength - 1; i >= 0; i--) {
            long prevPosition = 1L << i;
            if (k > prevPosition) {
                k -= prevPosition;
                increments++;
            }
        }
        return (char) ('a' + increments);
    }

    public int getBinaryLength(long num) {
        int length = 0;
        while (num > 0) {
            num /= 2;
            length++;
        }
        return length;
    }
}

###C#

public class Solution {
    public char KthCharacter(int k) {
        int increments = 0;
        int binaryLength = GetBinaryLength(k - 1);
        for (int i = binaryLength - 1; i >= 0; i--) {
            long prevPosition = 1L << i;
            if (k > prevPosition) {
                k = (int) (k - prevPosition);
                increments++;
            }
        }
        return (char) ('a' + increments);
    }

    public int GetBinaryLength(long num) {
        int length = 0;
        while (num > 0) {
            num /= 2;
            length++;
        }
        return length;
    }
}

复杂度分析

  • 时间复杂度:$O(\log k)$,其中 $k$ 是给定的正整数。操作次数是 $O(\log k)$,每次操作的计算时间是 $O(1)$。

  • 空间复杂度:$O(1)$。

O(1) 做法,一行代码解决(Python/Java/C++/Go)

本题相当于周赛第四题所有 $\textit{operations}[i]=1$ 的版本,做法是一样的,请先看 我的题解

优化前

为了方便大家看出怎么优化,代码中先把 $k$ 减一。

###py

class Solution:
    def kthCharacter(self, k: int) -> str:
        k -= 1
        m = k.bit_length()
        inc = 0
        for i in range(m - 1, -1, -1):
            if k >= 1 << i:  # k 在右半边
                inc += 1
                k -= 1 << i
        return ascii_lowercase[inc]

###java

class Solution {
    public char kthCharacter(int k) {
        k--;
        char ans = 'a';
        for (int i = 31 - Integer.numberOfLeadingZeros(k); i >= 0; i--) {
            if (k >= (1 << i)) { // k 在右半边
                ans++;
                k -= 1 << i;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    char kthCharacter(int k) {
        k--;
        int m = bit_width((uint32_t) k);
        char ans = 'a';
        for (int i = m - 1; i >= 0; i--) {
            if (k >= (1 << i)) { // k 在右半边
                ans++;
                k -= 1 << i;
            }
        }
        return ans;
    }
};

###go

func kthCharacter(k int) byte {
k--
ans := byte('a')
for i := bits.Len(uint(k)) - 1; i >= 0; i-- {
if k >= 1<<i { // k 在右半边
ans++
k -= 1 << i
}
}
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(\log k)$。
  • 空间复杂度:$\mathcal{O}(1)$。

优化

上面的代码相当于,每次遇到 $k-1$ 二进制中的 $1$,就把答案加一。

所以答案为 $\texttt{a}$ 加上 $k-1$ 二进制中的 $1$ 的个数。

注意在本题的数据范围下,无需和 $26$ 取模。

###py

class Solution:
    def kthCharacter(self, k: int) -> str:
        return ascii_lowercase[(k - 1).bit_count()]

###java

class Solution {
    public char kthCharacter(int k) {
        return (char) ('a' + Integer.bitCount(k - 1));
    }
}

###cpp

class Solution {
public:
    char kthCharacter(int k) {
        return 'a' + popcount((uint32_t) k - 1);
    }
};

###go

func kthCharacter(k int) byte {
return 'a' + byte(bits.OnesCount(uint(k-1)))
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(1)$。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

模拟

解法:模拟

按题意模拟即可。复杂度 $\mathcal{O}(k)$。

参考代码(c++)

###cpp

class Solution {
public:
    char kthCharacter(int k) {
        string s = "a";
        while (s.size() < k) {
            string t;
            for (char c : s) t.push_back((c - 'a' + 1) % 26 + 'a');
            s += t;
        }
        return s[k - 1];
    }
};

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、边缘计算,定期分析慢接口。
  • 错误处理不统一:统一响应格式,前端友好提示。

片尾

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

❌