普通视图

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

每日一题-24 点游戏🔴

2025年8月18日 00:00

给定一个长度为4的整数数组 cards 。你有 4 张卡片,每张卡片上都包含一个范围在 [1,9] 的数字。您应该使用运算符 ['+', '-', '*', '/'] 和括号 '(' 和 ')' 将这些卡片上的数字排列成数学表达式,以获得值24。

你须遵守以下规则:

  • 除法运算符 '/' 表示实数除法,而不是整数除法。
    • 例如, 4 /(1 - 2 / 3)= 4 /(1 / 3)= 12 。
  • 每个运算都在两个数字之间。特别是,不能使用 “-” 作为一元运算符。
    • 例如,如果 cards =[1,1,1,1] ,则表达式 “-1 -1 -1 -1”不允许 的。
  • 你不能把数字串在一起
    • 例如,如果 cards =[1,2,1,2] ,则表达式 “12 + 12” 无效。

如果可以得到这样的表达式,其计算结果为 24 ,则返回 true ,否则返回 false 。

 

示例 1:

输入: cards = [4, 1, 8, 7]
输出: true
解释: (8-4) * (7-1) = 24

示例 2:

输入: cards = [1, 2, 1, 2]
输出: false

 

提示:

  • cards.length == 4
  • 1 <= cards[i] <= 9

Java 回溯, 经典面试题

作者 airmelt
2021年6月9日 14:14

微软一面平行面出了这个题, 比这个还麻烦, 给的输入是 n 个数字
用的 Java 回溯解决的, 第一轮已经通过
提一下需要注意的细节

  1. 不要使用魔法数字 24, 1e-6 等, 需要使用有意义的变量代替
  2. double 类型不能使用 "==", 需要用做差和一个较小的值比较判断
  3. 将函数拆分成几个小的函数分别求解, 可以先提出思路和写一个空函数
  4. 从 2 个数字开始逐步扩展
  5. 注意不能产生除 0 错误
  6. 一旦回溯有一条路能产生 true 需要立即返回

###Java

class Solution {
    private static final double TARGET = 24;
    private static final double EPISLON = 1e-6;
    
    public boolean judgePoint24(int[] cards) {
        return helper(new double[]{ cards[0], cards[1], cards[2], cards[3] });
    }
    
    private boolean helper(double[] nums) {
        if (nums.length == 1) return Math.abs(nums[0] - TARGET) < EPISLON;
        // 每次选择两个不同的数进行回溯
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                // 将选择出来的两个数的计算结果和原数组剩下的数加入 next 数组
                double[] next = new double[nums.length - 1];
                for (int k = 0, pos = 0; k < nums.length; k++) if (k != i && k != j) next[pos++] = nums[k];
                for (double num : calculate(nums[i], nums[j])) {
                    next[next.length - 1] = num;
                    if (helper(next)) return true;
                }
            }
        }
        return false;
    }
    
    private List<Double> calculate(double a, double b) {
        List<Double> list = new ArrayList<>();
        list.add(a + b);
        list.add(a - b);
        list.add(b - a);
        list.add(a * b);
        if (!(Math.abs(b) < EPISLON)) list.add(a / b);
        if (!(Math.abs(a) < EPISLON)) list.add(b / a);
        return list;
    }
}

Python一行大法好!

###Python

class Solution:
    def judgePoint24(self, nums: List[int]) -> bool:
        return sorted(nums) in [[1, 1, 1, 8], [1, 1, 2, 6], [1, 1, 2, 7], [1, 1, 2, 8], [1, 1, 2, 9], [1, 1, 3, 4], [1, 1, 3, 5], [1, 1, 3, 6], [1, 1, 3, 7], [1, 1, 3, 8], [1, 1, 3, 9], [1, 1, 4, 4], [1, 1, 4, 5], [1, 1, 4, 6], [1, 1, 4, 7], [1, 1, 4, 8], [1, 1, 4, 9], [1, 1, 5, 5], [1, 1, 5, 6], [1, 1, 5, 7], [1, 1, 5, 8], [1, 1, 6, 6], [1, 1, 6, 8], [1, 1, 6, 9], [1, 1, 8, 8], [1, 2, 2, 4], [1, 2, 2, 5], [1, 2, 2, 6], [1, 2, 2, 7], [1, 2, 2, 8], [1, 2, 2, 9], [1, 2, 3, 3], [1, 2, 3, 4], [1, 2, 3, 5], [1, 2, 3, 6], [1, 2, 3, 7], [1, 2, 3, 8], [1, 2, 3, 9], [1, 2, 4, 4], [1, 2, 4, 5], [1, 2, 4, 6], [1, 2, 4, 7], [1, 2, 4, 8], [1, 2, 4, 9], [1, 2, 5, 5], [1, 2, 5, 6], [1, 2, 5, 7], [1, 2, 5, 8], [1, 2, 5, 9], [1, 2, 6, 6], [1, 2, 6, 7], [1, 2, 6, 8], [1, 2, 6, 9], [1, 2, 7, 7], [1, 2, 7, 8], [1, 2, 7, 9], [1, 2, 8, 8], [1, 2, 8, 9], [1, 3, 3, 3], [1, 3, 3, 4], [1, 3, 3, 5], [1, 3, 3, 6], [1, 3, 3, 7], [1, 3, 3, 8], [1, 3, 3, 9], [1, 3, 4, 4], [1, 3, 4, 5], [1, 3, 4, 6], [1, 3, 4, 7], [1, 3, 4, 8], [1, 3, 4, 9], [1, 3, 5, 6], [1, 3, 5, 7], [1, 3, 5, 8], [1, 3, 5, 9], [1, 3, 6, 6], [1, 3, 6, 7], [1, 3, 6, 8], [1, 3, 6, 9], [1, 3, 7, 7], [1, 3, 7, 8], [1, 3, 7, 9], [1, 3, 8, 8], [1, 3, 8, 9], [1, 3, 9, 9], [1, 4, 4, 4], [1, 4, 4, 5], [1, 4, 4, 6], [1, 4, 4, 7], [1, 4, 4, 8], [1, 4, 4, 9], [1, 4, 5, 5], [1, 4, 5, 6], [1, 4, 5, 7], [1, 4, 5, 8], [1, 4, 5, 9], [1, 4, 6, 6], [1, 4, 6, 7], [1, 4, 6, 8], [1, 4, 6, 9], [1, 4, 7, 7], [1, 4, 7, 8], [1, 4, 7, 9], [1, 4, 8, 8], [1, 4, 8, 9], [1, 5, 5, 5], [1, 5, 5, 6], [1, 5, 5, 9], [1, 5, 6, 6], [1, 5, 6, 7], [1, 5, 6, 8], [1, 5, 6, 9], [1, 5, 7, 8], [1, 5, 7, 9], [1, 5, 8, 8], [1, 5, 8, 9], [1, 5, 9, 9], [1, 6, 6, 6], [1, 6, 6, 8], [1, 6, 6, 9], [1, 6, 7, 9], [1, 6, 8, 8], [1, 6, 8, 9], [1, 6, 9, 9], [1, 7, 7, 9], [1, 7, 8, 8], [1, 7, 8, 9], [1, 7, 9, 9], [1, 8, 8, 8], [1, 8, 8, 9], [2, 2, 2, 3], [2, 2, 2, 4], [2, 2, 2, 5], [2, 2, 2, 7], [2, 2, 2, 8], [2, 2, 2, 9], [2, 2, 3, 3], [2, 2, 3, 4], [2, 2, 3, 5], [2, 2, 3, 6], [2, 2, 3, 7], [2, 2, 3, 8], [2, 2, 3, 9], [2, 2, 4, 4], [2, 2, 4, 5], [2, 2, 4, 6], [2, 2, 4, 7], [2, 2, 4, 8], [2, 2, 4, 9], [2, 2, 5, 5], [2, 2, 5, 6], [2, 2, 5, 7], [2, 2, 5, 8], [2, 2, 5, 9], [2, 2, 6, 6], [2, 2, 6, 7], [2, 2, 6, 8], [2, 2, 6, 9], [2, 2, 7, 7], [2, 2, 7, 8], [2, 2, 8, 8], [2, 2, 8, 9], [2, 3, 3, 3], [2, 3, 3, 5], [2, 3, 3, 6], [2, 3, 3, 7], [2, 3, 3, 8], [2, 3, 3, 9], [2, 3, 4, 4], [2, 3, 4, 5], [2, 3, 4, 6], [2, 3, 4, 7], [2, 3, 4, 8], [2, 3, 4, 9], [2, 3, 5, 5], [2, 3, 5, 6], [2, 3, 5, 7], [2, 3, 5, 8], [2, 3, 5, 9], [2, 3, 6, 6], [2, 3, 6, 7], [2, 3, 6, 8], [2, 3, 6, 9], [2, 3, 7, 7], [2, 3, 7, 8], [2, 3, 7, 9], [2, 3, 8, 8], [2, 3, 8, 9], [2, 3, 9, 9], [2, 4, 4, 4], [2, 4, 4, 5], [2, 4, 4, 6], [2, 4, 4, 7], [2, 4, 4, 8], [2, 4, 4, 9], [2, 4, 5, 5], [2, 4, 5, 6], [2, 4, 5, 7], [2, 4, 5, 8], [2, 4, 5, 9], [2, 4, 6, 6], [2, 4, 6, 7], [2, 4, 6, 8], [2, 4, 6, 9], [2, 4, 7, 7], [2, 4, 7, 8], [2, 4, 7, 9], [2, 4, 8, 8], [2, 4, 8, 9], [2, 4, 9, 9], [2, 5, 5, 7], [2, 5, 5, 8], [2, 5, 5, 9], [2, 5, 6, 6], [2, 5, 6, 7], [2, 5, 6, 8], [2, 5, 6, 9], [2, 5, 7, 7], [2, 5, 7, 8], [2, 5, 7, 9], [2, 5, 8, 8], [2, 5, 8, 9], [2, 6, 6, 6], [2, 6, 6, 7], [2, 6, 6, 8], [2, 6, 6, 9], [2, 6, 7, 8], [2, 6, 7, 9], [2, 6, 8, 8], [2, 6, 8, 9], [2, 6, 9, 9], [2, 7, 7, 8], [2, 7, 8, 8], [2, 7, 8, 9], [2, 8, 8, 8], [2, 8, 8, 9], [2, 8, 9, 9], [3, 3, 3, 3], [3, 3, 3, 4], [3, 3, 3, 5], [3, 3, 3, 6], [3, 3, 3, 7], [3, 3, 3, 8], [3, 3, 3, 9], [3, 3, 4, 4], [3, 3, 4, 5], [3, 3, 4, 6], [3, 3, 4, 7], [3, 3, 4, 8], [3, 3, 4, 9], [3, 3, 5, 5], [3, 3, 5, 6], [3, 3, 5, 7], [3, 3, 5, 9], [3, 3, 6, 6], [3, 3, 6, 7], [3, 3, 6, 8], [3, 3, 6, 9], [3, 3, 7, 7], [3, 3, 7, 8], [3, 3, 7, 9], [3, 3, 8, 8], [3, 3, 8, 9], [3, 3, 9, 9], [3, 4, 4, 4], [3, 4, 4, 5], [3, 4, 4, 6], [3, 4, 4, 7], [3, 4, 4, 8], [3, 4, 4, 9], [3, 4, 5, 5], [3, 4, 5, 6], [3, 4, 5, 7], [3, 4, 5, 8], [3, 4, 5, 9], [3, 4, 6, 6], [3, 4, 6, 8], [3, 4, 6, 9], [3, 4, 7, 7], [3, 4, 7, 8], [3, 4, 7, 9], [3, 4, 8, 9], [3, 4, 9, 9], [3, 5, 5, 6], [3, 5, 5, 7], [3, 5, 5, 8], [3, 5, 5, 9], [3, 5, 6, 6], [3, 5, 6, 7], [3, 5, 6, 8], [3, 5, 6, 9], [3, 5, 7, 8], [3, 5, 7, 9], [3, 5, 8, 8], [3, 5, 8, 9], [3, 5, 9, 9], [3, 6, 6, 6], [3, 6, 6, 7], [3, 6, 6, 8], [3, 6, 6, 9], [3, 6, 7, 7], [3, 6, 7, 8], [3, 6, 7, 9], [3, 6, 8, 8], [3, 6, 8, 9], [3, 6, 9, 9], [3, 7, 7, 7], [3, 7, 7, 8], [3, 7, 7, 9], [3, 7, 8, 8], [3, 7, 8, 9], [3, 7, 9, 9], [3, 8, 8, 8], [3, 8, 8, 9], [3, 8, 9, 9], [3, 9, 9, 9], [4, 4, 4, 4], [4, 4, 4, 5], [4, 4, 4, 6], [4, 4, 4, 7], [4, 4, 4, 8], [4, 4, 4, 9], [4, 4, 5, 5], [4, 4, 5, 6], [4, 4, 5, 7], [4, 4, 5, 8], [4, 4, 6, 8], [4, 4, 6, 9], [4, 4, 7, 7], [4, 4, 7, 8], [4, 4, 7, 9], [4, 4, 8, 8], [4, 4, 8, 9], [4, 5, 5, 5], [4, 5, 5, 6], [4, 5, 5, 7], [4, 5, 5, 8], [4, 5, 5, 9], [4, 5, 6, 6], [4, 5, 6, 7], [4, 5, 6, 8], [4, 5, 6, 9], [4, 5, 7, 7], [4, 5, 7, 8], [4, 5, 7, 9], [4, 5, 8, 8], [4, 5, 8, 9], [4, 5, 9, 9], [4, 6, 6, 6], [4, 6, 6, 7], [4, 6, 6, 8], [4, 6, 6, 9], [4, 6, 7, 7], [4, 6, 7, 8], [4, 6, 7, 9], [4, 6, 8, 8], [4, 6, 8, 9], [4, 6, 9, 9], [4, 7, 7, 7], [4, 7, 7, 8], [4, 7, 8, 8], [4, 7, 8, 9], [4, 7, 9, 9], [4, 8, 8, 8], [4, 8, 8, 9], [4, 8, 9, 9], [5, 5, 5, 5], [5, 5, 5, 6], [5, 5, 5, 9], [5, 5, 6, 6], [5, 5, 6, 7], [5, 5, 6, 8], [5, 5, 7, 7], [5, 5, 7, 8], [5, 5, 8, 8], [5, 5, 8, 9], [5, 5, 9, 9], [5, 6, 6, 6], [5, 6, 6, 7], [5, 6, 6, 8], [5, 6, 6, 9], [5, 6, 7, 7], [5, 6, 7, 8], [5, 6, 7, 9], [5, 6, 8, 8], [5, 6, 8, 9], [5, 6, 9, 9], [5, 7, 7, 9], [5, 7, 8, 8], [5, 7, 8, 9], [5, 8, 8, 8], [5, 8, 8, 9], [6, 6, 6, 6], [6, 6, 6, 8], [6, 6, 6, 9], [6, 6, 7, 9], [6, 6, 8, 8], [6, 6, 8, 9], [6, 7, 8, 9], [6, 7, 9, 9], [6, 8, 8, 8], [6, 8, 8, 9], [6, 8, 9, 9], [7, 8, 8, 9]]

【详解】递归回溯,考察基本功 | 679. 24点游戏

作者 xiao_ben_zhu
2020年8月22日 08:17

思路

  • 游戏的第一步是挑出两个数,算出一个新数替代这两个数。

  • 然后,在三个数中玩 24 点,再挑出两个数,算出一个数替代它们。

  • 然后,在两个数中玩 24 点……

很明显的递归思路。每次递归都会挑出两个数,我们尝试挑出不同的两数组合

  • 挑 1、2,基于它,继续递归。
  • 挑 1、3,基于它,继续递归。
  • 挑 ……

即通过两层 for 循环,枚举所有的两数组合,展开出不同选择所对应的递归分支。

挑出的每一对数,我们…

  • 枚举出所有可能的运算操作:加减乘除…——(对应不同的递归调用)
  • 逐个尝试每一种运算——(选择进入一个递归分支)
  • 传入长度变小的新数组继续递归——(递归计算子问题)
  • 当递归到只剩一个数——(到达了递归树的底部),看看是不是 24 。
    • 是就返回 true——结束当前递归,并且控制它不进入别的递归分支,整个结束掉。
    • 否则返回 false,离开错误的分支,进入别的递归分支,尝试别的运算。

剪枝小技巧

当递归返回 true,代表游戏成功,不用继续探索了,剩下的搜索分支全部砍掉。怎么做到?

  • 代码如下。标识变量isValid初始为 false,默认会执行||后面的递归。
  • 一旦某个递归返回真,isValid就变为真,由于||的短路特性,后面的递归不会执行。
  • 所有递归子调用都这么写,isValid就像一个开关,避免写很多判断语句。

###js

isValid = isValid || judgePoint24([...newNums, n1 + n2]);
isValid = isValid || judgePoint24([...newNums, n1 - n2]);
isValid = isValid || judgePoint24([...newNums, n2 - n1]);
isValid = isValid || judgePoint24([...newNums, n1 * n2]);
// ...

代码

const judgePoint24 = (nums) => {
    const len = nums.length;
    if (len == 1) {                // 递归的出口
        return Math.abs(nums[0] - 24) < 1e-9;
    }
    let isValid = false;           // 用于控制是否进入递归

    for (let i = 0; i < len; i++) { // 两层循环,枚举出所有的两数组合
        for (let j = i + 1; j < len; j++) {
            const n1 = nums[i];
            const n2 = nums[j];     // 选出的两个数 n1 n2
            const newNums = [];     // 存放剩下的两个数
            for (let k = 0; k < len; k++) {
                if (k != i && k != j) {  // 剔除掉选出的两个数
                    newNums.push(nums[k]);
                }
            }
            // 加
            isValid = isValid || judgePoint24([...newNums, n1 + n2]);
            // 减与被减
            isValid = isValid || judgePoint24([...newNums, n1 - n2]);
            isValid = isValid || judgePoint24([...newNums, n2 - n1]);
            // 乘
            isValid = isValid || judgePoint24([...newNums, n1 * n2]);
            if (n2 !== 0) { // 除
                isValid = isValid || judgePoint24([...newNums, n1 / n2]);
            }
            if (n1 !== 0) { // 被除
                isValid = isValid || judgePoint24([...newNums, n2 / n1]);
            }
            if (isValid) {
                return true;
            }
        }
    }
    return false; // 遍历结束,始终没有返回真,则返回false
};
func judgePoint24(nums []int) bool {
floatNums := make([]float64, len(nums))
for i := range floatNums {
floatNums[i] = float64(nums[i])
}
return dfs(floatNums)
}

func dfs(nums []float64) bool {
if len(nums) == 1 {
return math.Abs(nums[0]-24) < 1e-9
}
flag := false
for i := 0; i < len(nums); i++ {
for j := i + 1; j < len(nums); j++ {
n1, n2 := nums[i], nums[j]
newNums := make([]float64, 0, len(nums))
for k := 0; k < len(nums); k++ {
if k != i && k != j {
newNums = append(newNums, nums[k])
}
}
flag = flag || dfs(append(newNums, n1+n2))
flag = flag || dfs(append(newNums, n1-n2))
flag = flag || dfs(append(newNums, n2-n1))
flag = flag || dfs(append(newNums, n1*n2))
if n1 != 0 {
flag = flag || dfs(append(newNums, n2/n1))
}
if n2 != 0 {
flag = flag || dfs(append(newNums, n1/n2))
}
if flag {
return true
}
}
}
return false
}

执行情况

Runtime: 68 ms, faster than 100.00% of JavaScript online submissions for 24 Game.
Runtime: 0 ms, faster than 100.00% of Go online submissions for 24 Game.

感谢阅读,文字经过反复修改打磨,希望你能感受到这份真诚。欢迎提出建议。

最后修改于:2022-01-10

24 点游戏

2020年8月21日 23:25

方法一:回溯

一共有 $4$ 个数和 $3$ 个运算操作,因此可能性非常有限。一共有多少种可能性呢?

首先从 $4$ 个数字中有序地选出 $2$ 个数字,共有 $4 \times 3=12$ 种选法,并选择加、减、乘、除 $4$ 种运算操作之一,用得到的结果取代选出的 $2$ 个数字,剩下 $3$ 个数字。

然后在剩下的 $3$ 个数字中有序地选出 $2$ 个数字,共有 $3 \times 2=6$ 种选法,并选择 $4$ 种运算操作之一,用得到的结果取代选出的 $2$ 个数字,剩下 $2$ 个数字。

最后剩下 $2$ 个数字,有 $2$ 种不同的顺序,并选择 $4$ 种运算操作之一。

因此,一共有 $12 \times 4 \times 6 \times 4 \times 2 \times 4=9216$ 种不同的可能性。

可以通过回溯的方法遍历所有不同的可能性。具体做法是,使用一个列表存储目前的全部数字,每次从列表中选出 $2$ 个数字,再选择一种运算操作,用计算得到的结果取代选出的 $2$ 个数字,这样列表中的数字就减少了 $1$ 个。重复上述步骤,直到列表中只剩下 $1$ 个数字,这个数字就是一种可能性的结果,如果结果等于 $24$,则说明可以通过运算得到 $24$。如果所有的可能性的结果都不等于 $24$,则说明无法通过运算得到 $24$。

实现时,有一些细节需要注意。

  • 除法运算为实数除法,因此结果为浮点数,列表中存储的数字也都是浮点数。在判断结果是否等于 $24$ 时应考虑精度误差,这道题中,误差小于 $10^{-6}$ 可以认为是相等。

  • 进行除法运算时,除数不能为 $0$,如果遇到除数为 $0$ 的情况,则这种可能性可以直接排除。由于列表中存储的数字是浮点数,因此判断除数是否为 $0$ 时应考虑精度误差,这道题中,当一个数字的绝对值小于 $10^{-6}$ 时,可以认为该数字等于 $0$。

还有一个可以优化的点。

  • 加法和乘法都满足交换律,因此如果选择的运算操作是加法或乘法,则对于选出的 $2$ 个数字不需要考虑不同的顺序,在遇到第二种顺序时可以不进行运算,直接跳过。

###Java

class Solution {
    static final int TARGET = 24;
    static final double EPSILON = 1e-6;
    static final int ADD = 0, MULTIPLY = 1, SUBTRACT = 2, DIVIDE = 3;

    public boolean judgePoint24(int[] nums) {
        List<Double> list = new ArrayList<Double>();
        for (int num : nums) {
            list.add((double) num);
        }
        return solve(list);
    }

    public boolean solve(List<Double> list) {
        if (list.size() == 0) {
            return false;
        }
        if (list.size() == 1) {
            return Math.abs(list.get(0) - TARGET) < EPSILON;
        }
        int size = list.size();
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (i != j) {
                    List<Double> list2 = new ArrayList<Double>();
                    for (int k = 0; k < size; k++) {
                        if (k != i && k != j) {
                            list2.add(list.get(k));
                        }
                    }
                    for (int k = 0; k < 4; k++) {
                        if (k < 2 && i > j) {
                            continue;
                        }
                        if (k == ADD) {
                            list2.add(list.get(i) + list.get(j));
                        } else if (k == MULTIPLY) {
                            list2.add(list.get(i) * list.get(j));
                        } else if (k == SUBTRACT) {
                            list2.add(list.get(i) - list.get(j));
                        } else if (k == DIVIDE) {
                            if (Math.abs(list.get(j)) < EPSILON) {
                                continue;
                            } else {
                                list2.add(list.get(i) / list.get(j));
                            }
                        }
                        if (solve(list2)) {
                            return true;
                        }
                        list2.remove(list2.size() - 1);
                    }
                }
            }
        }
        return false;
    }
}

###C++

class Solution {
public:
    static constexpr int TARGET = 24;
    static constexpr double EPSILON = 1e-6;
    static constexpr int ADD = 0, MULTIPLY = 1, SUBTRACT = 2, DIVIDE = 3;

    bool judgePoint24(vector<int> &nums) {
        vector<double> l;
        for (const int &num : nums) {
            l.emplace_back(static_cast<double>(num));
        }
        return solve(l);
    }

    bool solve(vector<double> &l) {
        if (l.size() == 0) {
            return false;
        }
        if (l.size() == 1) {
            return fabs(l[0] - TARGET) < EPSILON;
        }
        int size = l.size();
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (i != j) {
                    vector<double> list2 = vector<double>();
                    for (int k = 0; k < size; k++) {
                        if (k != i && k != j) {
                            list2.emplace_back(l[k]);
                        }
                    }
                    for (int k = 0; k < 4; k++) {
                        if (k < 2 && i > j) {
                            continue;
                        }
                        if (k == ADD) {
                            list2.emplace_back(l[i] + l[j]);
                        } else if (k == MULTIPLY) {
                            list2.emplace_back(l[i] * l[j]);
                        } else if (k == SUBTRACT) {
                            list2.emplace_back(l[i] - l[j]);
                        } else if (k == DIVIDE) {
                            if (fabs(l[j]) < EPSILON) {
                                continue;
                            }
                            list2.emplace_back(l[i] / l[j]);
                        }
                        if (solve(list2)) {
                            return true;
                        }
                        list2.pop_back();
                    }
                }
            }
        }
        return false;
    }
};

###C

const int TARGET = 24;
const double EPSILON = 1e-6;
const int ADD = 0, MULTIPLY = 1, SUBTRACT = 2, DIVIDE = 3;

bool solve(double *l, int l_len) {
    if (l_len == 0) {
        return false;
    }
    if (l_len == 1) {
        return fabs(l[0] - TARGET) < EPSILON;
    }
    int size = l_len;
    for (int i = 0; i < size; i++) {
        for (int j = 0; j < size; j++) {
            if (i != j) {
                double list2[20];
                int l2_len = 0;
                for (int k = 0; k < size; k++) {
                    if (k != i && k != j) {
                        list2[l2_len++] = l[k];
                    }
                }
                for (int k = 0; k < 4; k++) {
                    if (k < 2 && i > j) {
                        continue;
                    }
                    if (k == ADD) {
                        list2[l2_len++] = l[i] + l[j];
                    } else if (k == MULTIPLY) {
                        list2[l2_len++] = l[i] * l[j];
                    } else if (k == SUBTRACT) {
                        list2[l2_len++] = l[i] - l[j];
                    } else if (k == DIVIDE) {
                        if (fabs(l[j]) < EPSILON) {
                            continue;
                        }
                        list2[l2_len++] = l[i] / l[j];
                    }
                    if (solve(list2, l2_len)) {
                        return true;
                    }
                    l2_len--;
                }
            }
        }
    }
    return false;
}

bool judgePoint24(int *nums, int numsSize) {
    double l[20];
    int l_len = 0;
    for (int i = 0; i < numsSize; i++) {
        l[l_len++] = nums[i];
    }
    return solve(l, l_len);
}

###Python

class Solution:
    def judgePoint24(self, nums: List[int]) -> bool:
        TARGET = 24
        EPSILON = 1e-6
        ADD, MULTIPLY, SUBTRACT, DIVIDE = 0, 1, 2, 3

        def solve(nums: List[float]) -> bool:
            if not nums:
                return False
            if len(nums) == 1:
                return abs(nums[0] - TARGET) < EPSILON
            for i, x in enumerate(nums):
                for j, y in enumerate(nums):
                    if i != j:
                        newNums = list()
                        for k, z in enumerate(nums):
                            if k != i and k != j:
                                newNums.append(z)
                        for k in range(4):
                            if k < 2 and i > j:
                                continue
                            if k == ADD:
                                newNums.append(x + y)
                            elif k == MULTIPLY:
                                newNums.append(x * y)
                            elif k == SUBTRACT:
                                newNums.append(x - y)
                            elif k == DIVIDE:
                                if abs(y) < EPSILON:
                                    continue
                                newNums.append(x / y)
                            if solve(newNums):
                                return True
                            newNums.pop()
            return False

        return solve(nums)

###golang

const (
    TARGET = 24
    EPSILON = 1e-6
    ADD, MULTIPLY, SUBTRACT, DIVIDE = 0, 1, 2, 3
)

func judgePoint24(nums []int) bool {
    list := []float64{}
    for _, num := range nums {
        list = append(list, float64(num))
    }
    return solve(list)
}

func solve(list []float64) bool {
    if len(list) == 0 {
        return false
    }
    if len(list) == 1 {
        return abs(list[0] - TARGET) < EPSILON
    }
    size := len(list)
    for i := 0; i < size; i++ {
        for j := 0; j < size; j++ {
            if i != j {
                list2 := []float64{}
                for k := 0; k < size; k++ {
                    if k != i && k != j {
                        list2 = append(list2, list[k])
                    }
                }
                for k := 0; k < 4; k++ {
                    if k < 2 && i < j {
                        continue
                    }
                    switch k {
                    case ADD:
                        list2 = append(list2, list[i] + list[j])
                    case MULTIPLY:
                        list2 = append(list2, list[i] * list[j])
                    case SUBTRACT:
                        list2 = append(list2, list[i] - list[j])
                    case DIVIDE:
                        if abs(list[j]) < EPSILON {
                            continue
                        } else {
                            list2 = append(list2, list[i] / list[j])
                        }
                    }
                    if solve(list2) {
                        return true
                    }
                    list2 = list2[:len(list2) - 1]
                }
            }
        }
    }
    return false
}

func abs(x float64) float64 {
    if x < 0 {
        return -x
    }
    return x
}

复杂度分析

  • 时间复杂度:$O(1)$。一共有 $9216$ 种可能性,对于每种可能性,各项操作的时间复杂度都是 $O(1)$,因此总时间复杂度是 $O(1)$。

  • 空间复杂度:$O(1)$。空间复杂度取决于递归调用层数与存储中间状态的列表,因为一共有 $4$ 个数,所以递归调用的层数最多为 $4$,存储中间状态的列表最多包含 $4$ 个元素,因此空间复杂度为常数。

昨天 — 2025年8月17日技术

vite和webpack打包结构控制

作者 gnip
2025年8月17日 22:45

概述

在工程化项目中,Vite 和 Webpack 作为当前最流行的两大构建工具,它们在打包输出目录结构的配置上各有特点,webpack和vite默认打包构建的输出目录结构可能不满足我们的需求,因此需要根据实际情况进行控制。

默认输出结构对比

webpack

默认情况下,基本上都不会根据情况进行分块,所有资源都是默认被打包到了一个文件中。

dist/
  ├── main.js
  ├── index.html
  |—— .....

vite

dist/
  ├── assets/
  │   ├── index.[hash].js
  │   ├── vendor.[hash].js
  │   └── style.[hash].css
  └── index.html

Vite目录结构精细控制

文件指纹策略

Webpack 提供了多种 hash 类型:

  • [hash]: 项目级hash
  • [chunkhash]: chunk级hash
  • [contenthash]: 内容级hash

基础配置方案

// vite.config.js
export default {
  build: {
    outDir: 'dist',
    assetsDir: 'static',
    emptyOutDir: true
  }
}

Rollup 输出配置

由于vite内部打包使用rollup,因此打包输出相关配置需参考rollup的配置

export default {
  build: {
    rollupOptions: {
      output: {
          //资源块输出目录配置
        chunkFileNames: 'static/js/[name]-[hash].js',
        //入口文件输出目录配置
        entryFileNames: 'static/js/[name]-[hash].js',
        //静态输出目录配置(图片、音频、字体)
        assetFileNames: ({ name }) => {
          const ext = name.split('.').pop()
          //函数形式动态返回文件输出名及其位置
          return `static/${ext}/[name]-[hash].[ext]`
        }
      }
    }
  }
}

webpack 目录结构精细控制

基础输出配置

// webpack.config.js
module.exports = {
  output: {
    path: path.resolve(__dirname, 'build'), // 修改输出目录
    filename: 'js/[name].[contenthash:8].js', // JS文件输出路径
    chunkFilename: 'js/[name].[contenthash:8].chunk.js', // 异步chunk
    assetModuleFilename: 'media/[name].[hash:8][ext]', // 静态资源
    clean: true // 构建前清空目录
  }
}

高级资源管理

使用 mini-css-extract-plugin 控制 CSS 输出:

  module: {
    rules: [
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
     
    ],
  },

Webpack 5 引入了资源模块类型,取代了传统的 file-loader/url-loader,用来处理之前繁琐的配置

 module: {
   rules: [
{
       test: /.(png|jpe?g|gif|svg)$/i,
       type: 'asset/resource' // 替换 file-loader
     },
     {
       test: /.(mp4|webm|ogg)$/i,
       type: 'asset/resource'
     }
   ],
 },

四种资源模块类型

类型 作用 等价 loader
asset/resource 导出单独文件并生成 URL file-loader
asset/inline 导出资源的 Data URI url-loader
asset/source 导出资源的源代码 raw-loader
asset 自动选择 resource 或 inline url-loader + 限制

总结

上面列举的部分配置,更多的详细配置,可以查阅官网解析。

在二维 Canvas 中模拟三角形绕 X、Y 轴旋转

作者 excel
2025年8月17日 22:24

在 Web 前端绘图中,我们通常使用 Canvas 2D API 来绘制图形。Canvas 是二维的,无法直接进行三维建模和旋转。但如果我们希望一个简单的三角形能像 3D 物体一样「绕 X 轴翻转」或「绕 Y 轴旋转」,可以通过数学变换在 2D 平面里模拟这种效果。

本文将介绍实现思路,并给出 JavaScript 示例代码。


一、二维平面旋转 vs 三维旋转

在二维平面中,常见的旋转公式是 绕 Z 轴旋转

x=(x−cx)cosθ−(y−cy)sinθ+cx

y =(x−cx)sinθ+(y−cy)cosθ+cy

这只是平面内的转动。
但在三维空间中,还存在 绕 X 轴绕 Y 轴 的旋转:

  • X 轴:y 与 z 发生变化,看起来图形会「上下翻转」。
  • Y 轴:x 与 z 发生变化,看起来图形会「左右摇摆」。

由于 Canvas 是 2D 的,我们不能真正使用 z 坐标。但我们可以通过对 x 或 y 做 余弦缩放 来模拟这种效果。


二、核心数学思路

  1. 绕 X 轴旋转(上下翻转):

    • 在三维中,y 会随角度压缩或伸展。

    • 在二维里,我们可以这样近似:

      y′=(y−cy)cosθ+cy

  2. 绕 Y 轴旋转(左右摇摆):

    • 在三维中,x 会随角度压缩或伸展。

    • 在二维里,我们可以这样近似:

      x′=(x−cx)cosθ+cx

其中 (cx,cy) 是旋转中心点。


三、Canvas 实现代码

<canvas id="canvas" width="500" height="500"></canvas>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

const w = canvas.width;
const h = canvas.height;

// 定义三角形
let triangle = [
  {x: 200, y: 200},
  {x: 300, y: 200},
  {x: 250, y: 100}
];

// 旋转中心(画布中心)
const cx = w / 2;
const cy = h / 2;

// 绕 X 轴旋转(模拟:改变 y)
function rotateX2D(p, angle) {
  return {
    x: p.x,
    y: (p.y - cy) * Math.cos(angle) + cy
  };
}

// 绕 Y 轴旋转(模拟:改变 x)
function rotateY2D(p, angle) {
  return {
    x: (p.x - cx) * Math.cos(angle) + cx,
    y: p.y
  };
}

// 绘制三角形
function drawTriangle(points) {
  ctx.beginPath();
  ctx.moveTo(points[0].x, points[0].y);
  ctx.lineTo(points[1].x, points[1].y);
  ctx.lineTo(points[2].x, points[2].y);
  ctx.closePath();
  ctx.fillStyle = "rgba(0,150,255,0.6)";
  ctx.fill();
  ctx.stroke();
}

let angleX = 0;
let angleY = 0;

function animate() {
  ctx.clearRect(0, 0, w, h);

  // 先绕 X 再绕 Y
  let rotated = triangle.map(p => rotateX2D(p, angleX));
  rotated = rotated.map(p => rotateY2D(p, angleY));

  drawTriangle(rotated);

  angleX += 0.02;  // 控制绕 X 旋转
  angleY += 0.015; // 控制绕 Y 旋转
  requestAnimationFrame(animate);
}

animate();
</script>

四、效果与扩展

运行后可以看到:

  • X 轴:三角形上下压缩,像是「翻书」。
  • Y 轴:三角形左右压缩,像是「开门」。
  • 两个叠加,就能产生类似 3D 物体的旋转感。

进一步扩展:

  • 可以改旋转中心为三角形重心,实现「绕自身旋转」。
  • 可以结合鼠标拖拽或键盘按键,动态控制旋转角度。
  • 可以给多个点应用同样的变换,实现更复杂的多边形或图形旋转。

五、总结

虽然 Canvas 2D 本身没有三维功能,但通过 余弦缩放 的方式,我们能在二维平面里模拟绕 X/Y 轴旋转的效果。这是一种简化的「伪 3D」方法,足以满足一些轻量级的动画和视觉效果需求。

如果需要更真实的 3D 效果,可以考虑使用 WebGL 或 Three.js 等三维引擎;但在学习和小型项目中,使用纯数学和 Canvas 就能实现有趣的旋转体验。

AI 代理是什么,其有助于我们实现更智能编程

作者 Jimmy
2025年8月17日 22:01

原文链接 AI Agents: What Are They, Your Key to Smarter Coding - 作者 Eleftheria Batsou

介绍

谈到 AI 代理,我感觉很兴奋,它们是怎么改变开发者局势。它们不止是聊天机器人 - 它们是聪明的工具,处理一些任务,比如发送邮件或者调试代码,节省我们的时间去做更有趣的事情。AI 可以自动化帮我们做那些无聊繁琐的事情,这样我们可以集中精力做创造性的事情。

本文将讲解 AI 代理是什么,为什么它们如此优秀,并且我们怎么使用它们让编码更智能。读过之后,我们将会知道怎么让代理为我们工作并且我们为什么必须试试。

读过本文之后,我们将会:

  • 明白 AI 代理,它们和聊天机器人的区别
  • 在我们的工作流中使用它们的方法
  • 知道它们的挑战和怎么去处理它们

AI 代理是什么?

AI 代理就像很聪明的助理,它们不仅会回答问题还会做其他的事情。不像基本的 AI,比如 ChatGPT,输出文本,AI 代理会计划,决定,和使用工具,比如邮件客户端或者 IDEs

💡例如Jeff Su’s video 展示一个代理起草一个关于项目更新的邮件给老板,然后在老板批准邮件后,将其发送出去。

通过自我迭代,它们也可以安排日程或者调试代码。对开发者而言,这意味着更少的时间在这些重复的任务上,更多的时间在编码上。代理使用框架,比如 LangChain 来和我们的工具关联。

为什么要关注 AI 代理

我很关心可以让编码更容易的工具,然后 AI 代理它们正在做这件事。它们通过处理一些任务,比如团队间的邮件或者安排我们的日程来节省我们的时间。

💡 一份 2025 年的研究表明,使用 AI 代理的开发者,有 65% 表示工作效率有所提升。

关注这个:当我们深入一个项目,AI 代理发送一个升级状态的邮件或者预约一个代码审核的会议。使用工具,比如有望成为标准的 AutoGPT,每个人都可以接触到代理,而不是大团队的专属。它们并不完美,但是它们是让人保持专注的巨大胜利。

为什么说它们很酷:

  • 处理无聊繁琐的任务,比如发邮件或者处理日程
  • 释放编码和调试代码的时间
  • 和现有的工具安全地工作
  • 使用开发框架很容易尝试

如何在我们的工作流中使用 AI 代理

准备好来使用 AI 代理了吗?

起草邮件

我们可以使用类似这样地提示来起草一份邮件,"编写一份关于项目进展的邮件给我的老板"。它会创造一份清晰的邮件,和你的 Gmail 关联,然后在得到我们的批准后将其发送。我曾尝试过这种方法快速更新团队的信息,这很简单-只需要在发送前审核即可。

会议日程安排

AI 代理一份提示,"预约个周四的团队会议"。它会检查我们的日历,找到一个空档,然后发送一个邀请。请务必检查邀请信息以免混淆。

调试代码

问下 AI 代理,“在我的 Python 脚本中找缺陷”,然后它会通过 IDE 插件来建议修复的点。像LangChain 工具可以实现无缝迭代,直到代码正常运行。

测试一切

AI 代理可以搞砸,比如使用错误的语调来起草邮件。建议在沙盒中测试输出,比如一个测试的邮件账号,使用清晰的提示,比如 “编写一个简短专业的邮件”,确保准确性。

成功的技巧

  • 从简单的任务开始,比如起草邮件
  • 用自己的工具安全连接代理
  • 为了更好的结果,使用清晰的提示
  • 测试结果,更早获取错误

需要关注的挑战

AI 代理不是完美的。开始可能很困难。模糊的提示导致糟糕的输出,比如正式的邮件。数据隐私是一个关注点,我们并不想我们项目的细节泄露。尽管 AutoGPT 等开源的工具免费,但是优质的代理工具会增加成本。为了解决这点,我们需要使用准确的提示,然后在安全的环境中测试。稍加关注,代理可以极大促进我们的工作流。

成功修复:

  • 编写特定,清晰的提示
  • 在沙盒环境中测试输出
  • 工具连接时使用安全的 APIs

我对 AI 代理的看法

我喜欢 AI 代理,因为它让我生活得更轻松。它们并不是来替换我们的 - 对不起,怀疑论者 - 而来来处理枯燥繁琐的事情,以便我们可以集中编码。不管是起草邮件或者调试脚本,AI 代理都可以帮我节省时间并让我保持专注。我们现在仍然需要通过好的提示语来引导它们,但这正是乐趣所在。我已经将 AI 代理应用在简单的任务,并且到目前为止我都很满意。读者可以尝试它们并且跟我们分享。

总结

AI 代理可以改变开发者的工作,自动化任务,比如邮件处理和调试代码,减轻我们压力。我们可以从小的任务开始,并测试它们。它们并不是完美的,但是对于任何想保持领先的开发者,这是必须要尝试的。想要深入 AI 工具?可以到 aidd.io 尝试。

参考

Trae助力,可视化面板登录页面接口联调与提测一气呵成

2025年8月17日 21:57

前言

上回说到,我用 Trae 给甲方爸爸的可视化面板项目搭建了一个登录页面的 UI,甲方爸爸对页面效果很满意,那接下来自然就是接口联调的环节了。

接口联调这脏活累活,说难不难,说容易也不容易,各种细节得处理好,不然上线后问题频出,那可就麻烦了。

不过,有 Trae 在,我这效率肯定能拉满,也不怕出错,有bug就让Trae修复,我们现在就开始接口联调吧。

开始联调

1. 环境准备

我先和后端小伙伴确认了一下接口文档,把开发环境、测试环境的接口地址都拿到了。

  1. 后端的接口地址是/login,参数是username和password
  2. 验证码是code,用户输入手机号,前端正则校验手机号是否正确,正确才可以发验证码
  3. 输入验证码之后也要校验验证码是否是六位,不是六位不提交到后端接口

帮我把可视化面板的接口接上,代码要优雅

image.png

2. 账号密码登录接口联调

Trae把用户名和密码改上对应的后端字段,post请求,点击登录按钮,浏览器控制太发起了请求,返回了登录成功的 token。

image.png

image.png

看看有没有将其后端返回的token存到本地缓存,如果有,账号密码登录就成功了,没有写一行代码就完成了第一个接口的联调

非常好,Trae太强了

image.png

3. 手机号登录接口联调

接下来是手机号登录,这个接口相对复杂一些,涉及到验证码的发送和验证。

我先测试了验证码发送接口,点击发送验证码按钮,前端调用接口,后端返回验证码。

我注意到验证码的有效期是 5 分钟,这个细节很重要,得在前端提示用户。然后是验证码验证接口,我输入了正确的验证码,接口返回登录成功,同样返回了 token。

为了测试完整性,我还故意输入了错误的验证码,接口返回了错误提示,一切正常。

image.png

提测

接口联调完成后,我开始进行整体的测试。

我模拟了各种场景,包括正常登录、密码错误、手机号格式不正确、验证码过期等情况,确保每个环节都没有问题。

测试过程中,我发现了一个小问题,验证码倒计时的显示会有延迟。我通过 Trae 调整了前端代码,优化了倒计时的逻辑,问题顺利解决。

image.png

后续

项目顺利通过测试,上线后运行稳定,甲方爸爸对整个登录页面的功能和体验都非常满意。

我也通过 Trae 的帮助,高效地完成了这个任务,再次感受到了 Trae 的强大,下次有接口联调的脏活累活,先让Trae试试,看看能不能十分钟完成,哈哈哈哈。

接下来,我还会继续探索 Trae 的更多功能,用它来提升我的开发效率,让更多的项目顺利落地,高效的完成甲方爸爸的任务。

在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例

作者 zhanshuo
2025年8月17日 21:53

在这里插入图片描述

摘要

在做鸿蒙(HarmonyOS)应用开发时,网络请求基本是跑不掉的。无论是加载首页数据、拉取列表,还是提交表单,背后都依赖 HTTP 请求。可一旦遇到断网、超时、服务端 500 错误,应用就可能直接崩溃,用户体验会非常糟糕。 所以,如何优雅地处理网络错误,保证用户看到的不是“白屏”而是合理的提示,是开发过程中必须考虑的事情。

引言

随着鸿蒙生态越来越完善,应用也越来越复杂。以前我们可能只需要在局域网里访问接口,现在很多场景都涉及到公网请求,这也就意味着网络的不确定性大大增加。 比如:

  • 用户在地铁里,信号忽强忽弱;
  • 服务端升级中,短时间返回 503;
  • 网络延迟过大,导致超时;

这些问题都会在真实应用里遇到,所以处理网络错误并不仅仅是“加一个 try-catch”,而是需要一个完整的容错方案。下面我会结合实际场景,展示具体的代码和解决办法。

鸿蒙里怎么捕获网络错误

最基本的做法就是在发起请求时对错误进行捕获和处理。鸿蒙提供了 @ohos.net.http 模块,可以很方便地发起请求并拿到结果。

基本 Demo

下面给一个最小可运行的示例:

import http from '@ohos.net.http';

@Entry
@Component
struct NetworkErrorDemo {
  @State message: string = '点击按钮请求数据';

  doRequest() {
    let httpRequest = http.createHttp();

    httpRequest.request(
      "https://example.com/api/data",
      {
        method: http.RequestMethod.GET,
        connectTimeout: 5000, // 超时时间
        readTimeout: 5000,
      },
      (err, data) => {
        if (err) {
          // 网络层异常,比如断网、超时
          this.message = `网络错误: ${err.message}`;
          return;
        }
        if (data.responseCode !== 200) {
          // 业务层异常,比如 404/500
          this.message = `请求失败: ${data.responseCode}`;
        } else {
          // 成功返回
          this.message = `成功: ${data.result}`;
        }
      }
    );
  }

  build() {
    Column() {
      Text(this.message)
        .fontSize(18)
        .margin(10)
      Button("发起请求")
        .onClick(() => this.doRequest())
        .margin(10)
    }
  }
}

这里的处理逻辑非常直接:

  • err 代表网络层的错误(比如超时、断网)。
  • data.responseCode 可以拿到 HTTP 状态码(比如 404、500)。
  • 成功的情况再去解析 data.result

这样就能避免“用户点了按钮→没反应→程序崩了”的情况。

统一错误处理工具函数

实际项目里,我们不可能每个请求都写一堆 if (err) { ... }。为了简化,可以写一个 统一的错误处理函数

function handleHttpError(err?: Error, data?: http.HttpResponse): string {
  if (err) {
    return `网络异常: ${err.message}`;
  }
  if (!data) {
    return '未知错误';
  }
  if (data.responseCode >= 500) {
    return `服务器异常: ${data.responseCode}`;
  }
  if (data.responseCode >= 400) {
    return `请求错误: ${data.responseCode}`;
  }
  return '';
}

在业务代码里,就能这样写:

httpRequest.request("https://example.com/api/data", {}, (err, data) => {
  const errorMsg = handleHttpError(err, data);
  if (errorMsg) {
    this.message = errorMsg;
    return;
  }
  this.message = `成功: ${data?.result}`;
});

这样做的好处是,错误处理逻辑都收敛在一个地方,后期维护和扩展会方便很多。

实际场景案例

登录接口失败重试

用户点登录按钮时,如果网络瞬时断开,可以提示用户“重试”。

async function login(username: string, password: string): Promise<string> {
  return new Promise((resolve, reject) => {
    let httpRequest = http.createHttp();
    httpRequest.request(
      "https://example.com/api/login",
      {
        method: http.RequestMethod.POST,
        extraData: { username, password }
      },
      (err, data) => {
        const errorMsg = handleHttpError(err, data);
        if (errorMsg) {
          reject(errorMsg);
          return;
        }
        resolve(data?.result ?? '');
      }
    );
  });
}

在 UI 层就可以:

Button("登录").onClick(async () => {
  try {
    let result = await login("test", "123456");
    this.message = `登录成功: ${result}`;
  } catch (error) {
    this.message = `登录失败: ${error}`;
  }
});

列表加载失败时的兜底

当首页列表请求失败,可以展示一个“重试按钮”,而不是让用户看见空白。

if (this.message.startsWith("请求失败") || this.message.startsWith("网络错误")) {
  Button("重试")
    .onClick(() => this.doRequest())
}

异常埋点上报

很多团队会在网络异常时,把错误日志上报到服务器,用于后续分析。比如:

function reportError(error: string) {
  console.log("上报错误:", error);
  // 实际中可以发送到日志服务
}

调用时:

const errorMsg = handleHttpError(err, data);
if (errorMsg) {
  reportError(errorMsg);
  this.message = errorMsg;
  return;
}

这样就能知道“哪些用户经常遇到 500”,“是不是某个版本里断网频繁”等问题。

常见问题 QA

Q1:网络请求要不要统一加超时? 建议加。因为如果不加超时,用户可能卡死在加载中,体验会非常差。

Q2:不同的接口要不要写不同的错误处理逻辑? 可以通用处理大部分错误(比如断网、500),但关键业务(比如支付、登录)最好再做特殊处理。

Q3:能不能全局捕获所有网络错误? 可以。思路是对 http.request 再封装一层,把错误统一在 Promise.catch 里处理。这样项目里的调用者就只需要关注业务逻辑。

总结

网络错误处理在鸿蒙应用开发里是绕不开的话题。一个健壮的应用,不能只考虑“正常返回”,还要考虑“异常兜底”。 通过:

  • 基础的错误捕获;
  • 统一的错误处理函数;
  • 结合实际业务场景(登录、重试、埋点);

我们就能构建一个既稳定又易维护的网络层,保证应用在各种网络环境下都能“有话可说”,而不是让用户看到一个冷冰冰的白屏。

promise & async await总结

作者 ZXT
2025年8月17日 21:48

promise

  • promise 有三种状态 等待(pedding),成功(resolve) 拒绝(reject)
  • promise.then 会等待promise内部resolve,并且promise的executor执行完,接着将.then里面的东西放入微任务队列。new promise.resolve().then(callback) callback也会放入微任务队列
  • promise.then()的返回值始终是一个新的promise对象。promise的状态是.then回调的返回值。
  • promise.then中的回调会等待前一个promise的resolve,前一个.then函数没返回值会等到执行完成之后自动resolve
  • Promise.then会立即返回一个状态为pending的新Promise对象,这正是它能够支持链式调用的关键。
  • new 的promise不会自动resolve,但是.then里面会自动resolve。

碰到.then 跳过 找同步代码执行完成 回来

catch跟then一样,都会将任务放入微任务队列中。

async await

  • await 会等待右侧的promise完成之后将剩余代码放入微任务队列。
  • async对显式返回创建的Promise的处理会安排一个额外的微任务。

前端必会:如何创建一个可随时取消的定时器

作者 烛阴
2025年8月17日 20:58

一、原生的取消方式

JavaScript 原生就提供了取消定时器的方法。setTimeoutsetInterval 在调用时都会返回一个数字类型的 ID,我们可以将这个 ID 传递给 clearTimeoutclearInterval 来取消它。

// 1. 设置一个定时器
const timerId: number = setTimeout(() => {
  console.log("这个消息可能永远不会被打印");
}, 2000);

// 2. 在它触发前取消它
clearTimeout(timerId);

常见痛点:

  • timerId 变量需要被保留在组件或模块的作用域中,状态分散。
  • 启动、暂停、取消的逻辑是割裂的,代码可读性和可维护性差。

二、封装一个可取消的定时器类

我们可以简单的封装一个 CancellableTimer 类,将定时器的状态和行为内聚在一起。后续可以扩展,把项目中的所有定时器进行统一管理。

// 定义定时器ID类型
type TimeoutId = ReturnType<typeof setTimeout>;

class CancellableTimer {
    private timerId: TimeoutId | null = null;

    constructor(private callback: () => void, private delay: number) {}

    public start(): void {
        // 防止重复启动
        if (this.timerId !== null) {
            this.cancel();
        }

        this.timerId = setTimeout(() => {
            this.callback();
            // 执行完毕后重置 timerId
            this.timerId = null;
        }, this.delay);
    }

    public cancel(): void {
        if (this.timerId !== null) {
            clearTimeout(this.timerId);
            this.timerId = null;
        }
    }
}

// 使用示例
console.log('定时器将在3秒后触发...');
const myTimer = new CancellableTimer(() => {
    console.log('定时器任务执行!');
}, 3000);

myTimer.start();

// 模拟在1秒后取消
setTimeout(() => {
    console.log('用户取消了定时器。');
    myTimer.cancel();
}, 1000);

三、实现可暂停和恢复的定时器

在很多场景下,我们需要的不仅仅是取消,还有暂停恢复

要实现这个功能,我们需要在暂停时记录剩余时间

type TimeoutId = ReturnType<typeof setTimeout>;

class AdvancedTimer {
    private timerId: TimeoutId | null = null;
    private startTime: number = 0;
    private remainingTime: number;
    private callback: () => void;
    private delay: number;


    constructor(callback: () => void, delay: number) {
        this.remainingTime = delay;
        this.callback = callback;
        this.delay = delay;
    }

    public resume(): void {
        if (this.timerId) {
            return; // 已经在运行
        }

        this.startTime = Date.now();
        this.timerId = setTimeout(() => {
            this.callback();
            // 任务完成,重置
            this.remainingTime = this.delay;
            this.timerId = null;
        }, this.remainingTime);
    }

    public pause(): void {
        if (!this.timerId) {
            return;
        }

        clearTimeout(this.timerId);
        this.timerId = null;
        // 计算并更新剩余时间
        const timePassed = Date.now() - this.startTime;
        this.remainingTime -= timePassed;
    }

    public cancel(): void {
        if (this.timerId) {
            clearTimeout(this.timerId);
        }
        this.timerId = null;
        this.remainingTime = this.delay; // 重置
    }
}

// 使用示例
console.log('定时器启动,5秒后执行...');
const advancedTimer = new AdvancedTimer(() => console.log('Done!'), 5000);
advancedTimer.resume();

setTimeout(() => {
    console.log('2秒后暂停定时器');
    advancedTimer.pause();
}, 2000);

setTimeout(() => {
    console.log('4秒后恢复定时器 , 应该还剩3秒');
    advancedTimer.resume();
}, 4000);

总结

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

Swift 应用在安卓系统上会怎么样?

作者 JarvanMo
2025年8月17日 20:34

欢迎关注我的公众号:OpenFlutter,感谢。

当我们想到 Swift 时,通常会想到 iPhone、iPad、Mac。基本上,任何带有闪亮 Apple 标志的产品。

但安卓?

那感觉有点……不对劲。

然而,一些有趣的事情正在发生:Swift——这门由苹果创建的语言——正在安卓上悄然使用。这不仅仅是实验,也不仅仅是业余项目。它通过 Swift.org 支持的一项正式工作,拥有适当的工具、文档和日益增长的社区支持。

那么,当你在安卓上运行 Swift 时,究竟会发生什么呢?

让我们清晰、实际地,并基于截至 2025 年正在进行的实际工作来分解一下。


Swift 在安卓上的核心要点

  • Swift 可以在安卓上使用,但不是用于用户界面 (UI)。
  • 它主要用于跨平台共享业务逻辑
  • 截至 2025 年 6 月,有一个官方的 Swift Android 工作组
  • 它使用 JNI将 Swift 代码桥接到安卓应用中。
  • 你需要使用 CMake + Gradle 设置自定义工具链

这不是要用 Swift 完全重写安卓开发。但它确实是一种在 iOS 和安卓之间共享逻辑的合法方式,而且无需放弃你熟悉的语言。

背景:Swift Android 工作组 (2025)

2025 年 6 月,Swift.org 团队宣布成立一个新的正式工作组:

🔗 安卓工作组

他们的使命是?

“为了指导和支持 Swift 不断壮大的安卓用户社区,确保提供高质量、可持续的工具,以及出色的开发者体验。”

这不是一次实验。它是 Swift 旨在扩展到苹果平台之外的更广泛目标的一部分——就像他们的 WebAssembly 和 C++ 互操作工作组一样。

该工作组的重点是:

  • 维护安卓工具链
  • 改进 Swift 的核心库(例如 Foundation)以支持安卓
  • 支持与安卓的 Gradle、NDK 和 JNI 集成
  • 让编写共享 Swift 库并在安卓应用中使用变得更容易

那么……它到底是如何工作的呢?

假设你有一些 Swift 逻辑——比如你应用的认证层:

public class AuthManager {
    public static func hash(password: String) -> String {
        return password.reversed() + "123" // 虚拟逻辑
    }
}

以下是你如何在安卓上使用它:

1. 将 Swift 编译成安卓 .so

你需要使用 swift-android-toolchain 将 Swift 代码编译成一个共享的本地库(例如 libauth.so)。

为此,你需要准备:

  • Android NDK
  • Swift 工具链(目标是 aarch64-linux-android
  • CMake/Gradle 构建脚本

2. 使用 JNI 将 Swift 桥接到 Kotlin

在 Kotlin 中:

external fun hash(password: String): String
init {
    System.loadLibrary("auth")
}

现在你的 Kotlin 应用可以调用你的 Swift 逻辑了——就像调用原生的 C 代码一样。

虽然这有点粗糙,但确实可行。

你能做什么,不能做什么

你可以:

  • 用 Swift 编写业务逻辑
  • 构建静态或动态 Swift 库
  • 使用 Foundation、Dispatch 和其他核心库(存在一些限制)
  • 通过 JNI 从 Kotlin 调用 Swift

你不能:

  • 用 Swift 编写 Android UI(不支持 SwiftUI 或 UIKit)
  • 使用依赖于 Apple 专属框架(例如 AVKit)的 Swift 包
  • 期望在 Android Studio 或 Xcode 中获得完整的 Swift 工具链支持

工具、文档和示例

以下是关键资源:

  • 🔧 swift-android-toolchain: 由 Swift Android 工作组维护的官方工具链
  • 📄 Android Workgroup Charter: 官方使命和职责章程
  • 📦 Readdle Swift Android Sample: 一个使用 CMake 和 Swift 共享代码的实际工作示例
  • 💬 Swift 论坛 — Android 讨论区: 活跃的贡献者社区和最新动态

架构是怎样的?

以下是其工作原理的简化图:

[iOS]
Swift UI + Swift 逻辑
         ↑
     共享 Swift 代码
         ↓
[Android]
Kotlin UI + JNI 桥接 → Swift .so 库

你用 Swift 编写一次代码(例如,模型、逻辑),然后将其桥接到两个平台,使用各自平台原生的 UI。

注意事项与陷阱

这不是即插即用的。你会遇到:

  • 繁琐的 JNI 桥接
  • 陡峭的工具链设置(除非你习惯了原生开发)
  • 针对 Swift 崩溃的 Android 调试能力有限
  • Android 目标尚不支持 Swift 包管理器 (SPM)

最大的问题是什么?你需要手动编写 CMake + Gradle 脚本。目前还没有图形用户界面工具。

跨平台 Swift UI 会出现吗?

目前还不会。

目前,没有迹象表明 SwiftUI 会被移植到安卓。Swift 安卓工作组只专注于非 UI 集成——即逻辑、库和系统支持。

但基础正在奠定。

如果社区的势头持续增长,未来可能有人会在原生安卓视图之上构建一个 类似 SwiftUI 的抽象层。就像 Flutter 一样,但使用 Swift。

现在预测还为时过早,但并非不可能。

总结:为什么要费这个劲?

你可能会问——如果 Kotlin 在安卓上是官方且原生的,为什么还要用 Swift?

这是个合理的问题。

而这是一个合理的答案:

如果你的团队主要使用 Swift 并且以 iOS 开发为主,那么在安卓上使用 Swift 能让你复用自身优势,避免逻辑重复,并加快开发速度

这并非适用于所有团队。但对合适的团队来说,它能实现真正的跨平台开发,同时又不牺牲原生性能或 Swift 的易用性。

Oxc 最新 Transformer Alpha 功能速览! 🚀🚀🚀

2025年8月17日 20:16

前言

刚刚看到尤雨溪推特转发了 OXC 团队的最新成果,并介绍了该成果背后的一些故事!

尤雨溪推特

今天介绍下这些详细成果!

往期精彩推荐

正文

Oxc Transformer Alpha 内置 React Refresh,以及无需 TypeScript 编译器的独立 .d.ts 文件生成。相较于 SWC 和 Babel,Oxc 在性能、内存占用和包体积上表现出色,堪称前端构建的实用利器。

以下是其核心特性的详细解析。

1. TypeScript 和 JSX 到 ESNext 转换

Oxc 支持将 TypeScript 和 React JSX 代码转换为 ESNext,性能显著优于传统工具:

  • 3-5 倍于 SWC:处理 100 到 10,000 行代码,Oxc 耗时仅 0.14ms 至 14.9ms,而 SWC 为 0.7ms 至 35.9ms。
  • 20-50 倍于 Babel:Babel 处理同样代码耗时 11.5ms 至 492ms,Oxc 效率遥遥领先。

2. 内置 React Refresh

Oxc 集成了 React Refresh,支持开发中的热重载,速度比 SWC 快 5 倍,比 Babel 快 50 倍。这让 React 开发更流畅,减少等待时间。

3. TypeScript 独立声明生成

Oxc 提供无需 TypeScript 编译器的 .d.ts 文件生成,性能惊人:

  • 40 倍于 TSC:处理 100 行代码仅需 0.1ms(TSC 为 23.1ms)。
  • 20 倍于大文件:10,000 行代码耗时 3.5ms(TSC 为 115.2ms)。

示例

import { transform } from 'oxc-transform';
const transformed = transform('file.ts', sourceCode, {
  typescript: {
    onlyRemoveTypeImports: true,
    declaration: { stripInternal: true },
  },
});
await fs.writeFile('out.js', transformed.code);
await fs.writeFile('out.d.ts', transformed.declaration);

4. 轻量级与低内存占用

Oxc 仅需 2 个 npm 包(总计 2MB),对比 SWC 的 37.5MB 和 Babel 的 21MB(170 个包)。内存占用上,Oxc 处理 10,777 行代码仅用 51MB 内存,SWC 用 67MB,Babel 高达 172MB。

5. 实际应用案例

  • Vue.js:实验性使用 oxc-transform 优化构建流程。
  • vue-macros:通过 unplugin-isolated-decl.d.ts 生成时间从 76s 降至 16s。
  • Airtable:在 Bazel 构建中集成 Oxc 的 .d.ts 生成。
  • Rolldown:直接使用 Rust oxc_transformer crate。

最后

Oxc Transformer Alpha 以 Rust 的高性能和轻量级设计,为 JavaScript 编译带来新可能。无论是加速 TypeScript 转换还是优化 React 开发体验,它都展现了朴实无华的实用力量!

今天的分享就这些了,感谢大家的阅读!如果文章中存在错误的地方欢迎指正!

往期精彩推荐

Vue3之计算属性

作者 littleding
2025年8月17日 18:33

Vue3的计算属性为了解决依赖响应式状态的复杂逻辑

基本用法

<script setup> import { reactive, computed } from 'vue' 

const author = reactive({ 
    name: 'John Doe', 
    books: [ 
        'Vue 2 - Advanced Guide', 
        'Vue 3 - Basic Guide', 
        'Vue 4 - The Mystery' 
    ] 
}) 

// 一个计算属性 ref 
const publishedBooksMessage = computed(() => {
    return author.books.length > 0 ? 'Yes' : 'No' 
}) 
    
</script> 

<template> 
    <p>Has published books:</p> <span>{{ publishedBooksMessage }}</span>     
</template>

这里定义了一个计算属性 publishedBooksMessagecomputed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value

Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage 依赖于 author.books,所以当 author.books 改变时,任何依赖于 publishedBooksMessage 的绑定都会同时更新。

不同写法

要绑定一个动态类

写法一

const isActive = ref(true) 
const error = ref(null) 
const classObject = computed(() => ({ 
    active: isActive.value && !error.value, 
    'text-danger': error.value && error.value.type === 'fatal' 
}))

<div :class="classObject"></div>

写法二

const isActive = ref(true) 
const error = ref(null) 
const classObject = computed(() => { 
    return {
        active: isActive.value && !error.value, 
        'text-danger': error.value && error.value.type === 'fatal' 
    }
})

<div :class="classObject"></div>

两种计算属性写法

写法一 用了对象字面量的简写形式(省略了return和大括号),直接返回一个对象。这是 ES6 的箭头函数语法:当函数体只有一句返回语句且为对象时,可以省略return和外层大括号,用小括号包裹对象。

写法二 用了完整的函数体语法,显式写出return语句。这种写法更适合函数体可能扩展的场景(比如未来需要添加更多逻辑,如条件判断、变量计算等)

当我把前端条件加载做到极致

作者 Linsk
2025年8月17日 18:28

各位网友晚上好,相信大家都对使用type="module"和nomodule这种技巧有所耳闻。今天我将为大家分享我把这种技巧应用到极致的经验:4 条件加载方案。

什么是分条件加载

在前端构建中,我们通常需要进行 Babel 语法降级和 polyfill 插入等操作以兼容低版本浏览器。但这会导致一个问题:在现代浏览器中也会加载大量兼容性代码,无法享受浏览器原生支持的新特性带来的性能优势。

分条件加载的核心思想是:为不同能力的浏览器提供差异化的构建产物。最基础的实现就是利用 HTML5 标准中的type="module"和nomodule属性进行区分:

<!-- 仅供参考 -->
<script type="module" src="modern-bundle.js"></script>
<script nomodule src="legacy-bundle.js"></script>

这样在支持 ES Module 的浏览器中会加载type="module"的脚本,而不支持的浏览器会忽略这个标签并加载带有nomodule属性的脚本。通过这种方式,现代浏览器就能直接运行原生 class、ES Module、解构赋值等新特性,无需加载冗余的兼容性代码。

3 个条件的分条件加载

由于IE8与IE9之间有巨大的差异,因此在上述2个版本的构建产物之上,使用条件注释在分出第3个版本专用于IE8及以下。

<!-- 仅供参考 -->
<!--[if gte IE 9]><!-->
<script type="module" src="modern-bundle.js"></script>
<script nomodule src="legacy-bundle.js"></script>
<!--><![endif]-->
<!--[if lte IE 8]>
  <script src="ie8-bundle.js"></script>
<![endif]-->

这样IE8及以下加载特制兼容包可以更加精简,其他版本也能避开恶心的兼容代码。

为什么需要分4条件加载

分3个版本似乎己经够用了,但是随着top-level-await的出现,变得不够用了。使用type="module"和nomodule分版本的一个目的就是能使用原生esmodule。而top-level-top出现后,无法用旧有的esmodule实现top-level-await。因此只能使用传统方式,比如构建成AMD格式,用AMD加载器加载。因此我们可以进一步细分出一个版本。 现在我们有4个版本了

  • IE8及以下
  • 不支持原生esm的版本(chrome4~chrome60)
  • 支持原生esm但不支持top-level-await(chrome61~chrome89)
  • 原生支持top-level-await的浏览器(chrome90+)

注意哦,上面最新的版本是chrome90,现在都chrome130+了,随着浏览器的更新,我希望用新浏览器的用户能够直接运行新特性,因此我需要用一个较新的特性做区分,于是我选择了Promise.try。现在的4个版本改变为

  • IE8及以下
  • 不支持原生esm的版本(chrome4~chrome60)
  • 支持原生esm但不支持Promise.try(chrome61~chrome127)
  • 原生支持Promise.try的浏览器(chrome128+)

这里我选择了Promise.try是因为我还没有在公司推开es2025的使用,业务人员是不允许使用es2025的特性,代码中也必然不存在es2025的代码。等我在公司构建工具链中做完了es2025的降级处理,就会把用Promise.try判断,改成用更新的特性来判断。

<!-- 仅供参考 -->
<!--[if gte IE 9]><!-->
<script type="module">
if(Promise.try){__import__("xxxx")}else{__import__("yyyy")}
</script>
<script nomodule src="legacy-bundle.js"></script>
<!--><![endif]-->
<!--[if lte IE 8]>
  <script src="ie8-bundle.js"></script>
<![endif]-->

最后我们看看打包后的运行效果。我们发现在最新浏览器中没有polyfill引用,也可以原生运行top-level-await。

总结

通过多条件加载,可以在全浏览器兼容的前提下,减少新式浏览器的加载内容大小。分4版本条件加载,是做到极致的水平。

献给前端小白的首次全栈项目心得:Uniapp X全栈项目开发实录

作者 Zeetenium
2025年8月17日 18:25

这里是项目的 Github仓库 ,里面既有简单的完善代码质量任务,也有作者完全搞不定的各式难题,无论你是单纯想熟悉一下PR流程,还是愿意帮助作者完善项目,亦或是看完文章之后想了解一下实现方式,这里都欢迎你的到来!

你觉得,实现这么一个看起来只有两页的简单APP需要多久?

主页.gif用户.gif

很多人的第一反应可能是:两天?

而我的答案是:整整18天,并且每天肝将近16个小时

为什么?因为作为一名即将大二的野路子前端,这不仅是我第一次接触 Uniapp,更是第一次真正独立完成一个全栈项目。它与我之前照着B站视频亦步亦趋做项目有着根本上的不同

因此,我决定写下这篇文章,把我这18天里总结的所有经验教训分享出来。虽然项目基于Uniapp X,但我认为不管使用哪个技术栈进行开发,这篇文章都会对你有所启示。

事先声明

本人只是一名即将大二的前端初学者,文内总结的内容都只是我的个人经验和看法,是面向和我一样从未开发过全栈项目的前端小白所作。

如果你是经验丰富的前端大佬,欢迎你移步我的Github仓库,为这个小小项目提出一些宝贵的 Issue 或者 PR。你们的任何一句指点,对我来说都是巨大的帮助

当然,你也可以继续看下去,如果有哪些地方我说得片面或者有误,也欢迎在评论区里指出或纠正。

选型:光环之下的 “隐形成本”

为什么选择 Uniapp X?

我的初衷非常简单。放眼国内市场,移动端(尤其是小程序和APP)的开发需求依旧旺盛。作为一名 Vue 技术栈的开发者,Uniapp 那句 “一端编译,多端运行” 的口号深深吸引了我。而 Uniapp X,作为其未来的迭代方向,主打能编译成原生应用,性能更强,这听起来简直是完美的解决方案。于是,我毫不犹豫地将它定为项目的主要技术框架。

然而,经过一番实践和复盘,我才意识到,对于许多开发者(尤其是新手)而言,技术的“新”和“强”,往往伴随着你必须仔细评估的隐性成本。

隐性成本一:功能覆盖 —— 你需要的功能它支持吗?

选择一个框架,我们不能只看它“有什么”,更要看它 “没什么”

Uniapp X 为了实现代码到原生的编译,舍弃了大量在 Web 端常见但在原生端难以兼容的特性。比如 CSS 中的 3D 变换、径向渐变、伪元素、伪类选择器……这些在 Web 开发中信手拈来的工具,在 Hbuilder 编辑器里都变成了刺眼的黄色波浪:“不支持”。许多酷炫的想法,在诞生的那一刻就因为框架的限制而被迫放弃。

不支持.png

你可能会以为,做出了如此大的牺牲,至少跨端兼容性会很好吧?事实并非如此。各个平台(iOS, Android, 不同的小程序)支持的属性和事件不尽相同。你几乎每写一行代码,都需要去翻阅文档,确认这个功能在目标平台上是否支持。如果不支持,你将面临两个选择:要么放弃这个功能,要么通过复杂的终端判断和替代方案来“曲线救国”。这样写出来的代码,其复杂度和维护成本,甚至还不如为不同平台单独编写两套来得清晰。

在挣扎了两天后,我最终放弃了同时开发小程序版本的想法。

选型思考一: 在评估一个框架时,请优先列出你的核心需求,然后去验证这个框架是否会因为功能限制而阻碍这些需求的实现。

隐性成本二:学习曲线 —— 文档和资源对新手友好吗?

这里有一个常见的误区:有文档,不等于文档对新手友好。

举个项目中的例子。在使用 webview (相当于内置小型浏览器 可以通过挂载Vue页面来实现原生所不具备的功能)时,我发现从 webview 返回的事件(event)对象,虽然 console.log 能完整打印出其内部所有内容,但我却无法通过 event.detail 读取其属性。官方文档对此问题的描述只有短短一行,几乎没有任何参考价值。

为了解决这个问题,我尝试了定类型、写接口、转语法等各种方案,均以失败告终。最后,我只能采取“隐瞒编译器”的方法:将整个 event 对象强制转换为字符串,然后通过关键词匹配和字符串切割来“提取”我需要的信息。项目就以这种方式跑了十几天,直到后来我偶然在文档的另一个角落,发现官方为这个 event 定义了一个专门的事件类型。

学习资源方面同样匮乏。你在B站搜索“UTS语法”,会发现有且仅有一位UP(还是在我完成项目之后才发布的)出了简短的相关视频,更不用提像 Vue 那样成体系的教学课程了。所有的知识获取,几乎完全依赖于自己逐行阅读和理解官方文档。

UTS语法.png

一个成熟的技术,其文档会成为你的向导,丰富的社区资源会为你指路。而一个新兴的技术,则需要你自己摸索前行。

选型思考二: 参考自身情况,你是否愿意为探索一门新技术投入大量的时间和精力?

隐性成本三:社区生态 —— 遇到问题时,有地方求助吗?有轮子可用吗?

这在项目能顺利进行的时候容易被忽视,但一旦遇到问题,就会意识到周围环境有多么贫瘠。

在开发初期,我被 webview 的加载问题卡住了。我能成功加载官方示例中的单文件 HTML,却无法加载我自己用 Vue CLI 打包的多文件网页应用。整个原生端的调用代码只有短短 7 行,逻辑清晰,但我百思不得其解。我将问题发布在官方论坛,十几个小时后才收到一条回复,但即便是这位社区等级很高的热心大佬,也未能给出解决方案。

再比如,当我想在项目里集成一个常用的模态框(Modal)插件时,整个官方插件市场,居然只有两个相关结果,其中一个甚至 0 人下载。面对这样的插件,别说风格是否符合自己的项目,你敢用吗?

无下载.png

诚然,这些基础组件我们可以自己“造轮子”。但当你不想把宝贵的时间浪费在手搓日历选择器这类基础组件上时,你才会发现一个完善的生态系统是多么可贵。一个活跃的社区、丰富的插件生态,是我们在遇到难题时最坚实的后盾。

选型思考三: 如果在开发中遇到棘手问题,社区里是否能快速找到答案?是否有现成的、可靠的“轮子”能帮你节省时间?

写下这段文字,不是为了劝退新手使用某项特定技术,只是希望你在进行技术选型时,暂时忘掉那些 “性能猛兽”、“未来趋势”的耀眼光环 ,回归到项目的本质需求上。

决策:独立开发的“不可能三角”

在编写项目的过程中,你会发现在做技术决策时经常陷入无休止的纠结。

就拿项目里账号的登录功能举例,我是该用 uni-id 这种现成的、封装完美的方案,还是自己手搓一套账户系统呢?

用 uni-id?学一下怎么调用方法就成,实现速度快,用户体验好, 然而它把所有东西都封装好了,我根本不知道它是如何实现的,只知道调用这个或那个方法,我如何通过项目去了解账号的管理方式、token的加密验证?

那手搓一个?原理确实能学到, 但效率极低,并且想实现诸如一键登录的功能,对我一个萌新来说完全不现实,不仅如此,自己写的简单逻辑,安全性也远远不如用uni-id集成的处理方法。

再举个例子,一个大概率不打算上线的项目,为了防范永远不会出现的简易恶意攻击,要不要浪费几天时间做这些对技术没有一点提升的苦力活——专门给三端(Webview端-Uniapp端-服务器端)通信全部加上大量重复的卫语句,进行所谓的防御性编程呢?

云端卫语句.png
(已经在前端校验过的信息,为了“不信任”原则必须重新校验)

实际上,这种折磨的根源,来自于三种想法的冲突,而我把它们总结为开发中的不可能三角:

  • 原理导向:追求的是知其所以然,想把每个技术的底层原理都搞得明明白白。
  • 交付导向:追求的是效率和速度,目标就是用最快的方式把功能实现出来。
  • 产品导向:追求的是用户体验和程序的健壮性,希望交付一个稳定、好用的产品。

这三种思想虽然大多数时候相辅相成,但是一旦发生冲突就极其难以取舍;如果不对这三者预设一个优先级,你就会像我一样,在遇到这类问题时,陷入无休止的左右脑互搏和精神内耗。影响进度不说,做出取舍之后还难以释怀。

所以,在你新建项目文件夹之前,先问自己一个最核心的问题:

我做这个项目的首要目的是什么?

  • 是为了掌握工作技能、准备面试?那交付导向就是第一位的,你要展示的是你快速实现功能的能力。
  • 是为了吃透某个框架、学习底层?那原理导向就排第一,哪怕慢一点,效果差一点,也要消化知识。
  • 是为了上架运营、给真实用户使用?那产品导向必须是最高优先级,不容置疑。

在项目开始前,狠下心,给它们的重要性排个序。在开发过程中,一旦遇到冲突,就严格按照这个排序来做决策,这能帮你减少至少 90% 的精神内耗!

当然,我这么说,不是让你变成一个极端的人。只看重其中某一种,你可能会成为:

  • 只会重复造轮子的理论派
  • 知其然不知其所以然的调包侠
  • 不切实际的完美主义者

我提供的思路,只是让你确实不得不在某些功能上舍弃掉其中之一时,能有一份快速决策的“指导方案”。

执行:完美的蓝图与现实的碰撞

一个完整的产品,它要面对的,不是安稳的开发环境,而是 无法预测的网络波动、千奇百怪的设备硬件、甚至匪夷所思的用户操作 。这是在我以前开发Web端的demo项目时很难体会到的,无非是注意一下响应式布局,保证大部分情况下布局不会错乱罢了。而真正开发一个能称为产品的全栈项目,以上所有因素你都不得不进行考虑。

举个在移动端项目开发时最容易遇到的问题:设备兼容性

你们前面看到的这个“四不像”的主页,曾经是一个 充满了各种华丽特效、酷炫动画的3D界面 ,这也是我做这个项目当时最引以为豪、最独特的 “亮点”

为了实现它,我专门花了好几天去研究 webview 的搭建和通信。在我电脑的安卓模拟器上,它流畅、优雅,简直完美

但就在我把安装包打包,发给朋友进行测试的时候,问题来了。

不断出现的闪烁、黑块与卡顿,甚至没有规律可循:有的问题出现在旗舰机而非性能差得多的中端机,所以你甚至没办法推理出它们是因为什么出现;而安卓模拟器哪怕限制它只使用1核CPU与1GB内存,都比那些高性能的手机更稳定,完全无法复现问题。而我手里只有一部手机,想测试问题也无从下手。租个远程真机测试平台?看了一眼价格,100分钟100块,只能是无奈笑笑。

为了保留这个3D界面,我开始疯狂地做减法:砍特效、砍动画、砍元素…… 直到删无可删,中低端机上仍然卡顿。

最后,我只能咬着牙,亲手把这个项目最独特、最亮眼的功能彻底阉割掉

  • 先是把 3D 效果,降级成用数字模拟透视的 2D 效果。
  • 发现还是卡,又把 2D 动画继续简化。
  • 最后实在没办法,我在设置页专门加了一个性能模式,连最基础的快速翻页动画都换成了渐隐渐显,才算勉强让这个主页不至于成为废稿,但看着最后的“四不像”,只能是有口难言。

3D主页.gif
(降格2D前最后一刻,此时还保留着开局立牌效果和滑动时的倾斜效果)

而这个主页问题,只是在整个项目实践的意外中某个缩影罢了。你永远不知道,在开发环境外,你的项目究竟会经历何种情况,所以,与其像我一样设计出一个“光鲜亮丽”的外表再不断做减法,你们更应该从一个“原型产品”上不断做加法(实际上就是著名的MVP思想):

  1. 先保证存活:优先保证你的核心功能,在信号最差、性能最烂、操作最奇葩的极限情况下,依然能稳定运行。在做项目前看看能不能通过各种方式取得一两部你认为是“最低限度”的设备,在这种设备上先实现最基础的功能;
  2. 再考虑健壮:为你的应用增加“安全气囊”。你需要预设各种异常流程,比如网络请求失败时的重试机制、用户提交非法数据时的清晰提示、甚至是应用崩溃后的日志记录。一个能优雅地处理错误的应用,远比一个在顺利时表现完美、一遇异常就崩溃的应用要更像一个“产品”。
  3. 最后追求体验:当你的产品已经能在最差的环境下稳定运行时,再来锦上添花。把你那些酷炫的想法,一层层地加上去。每加一层,都回头看看,它是否对低端设备造成了负担?是否引入了新的不稳定性?这种自下而上的构建方式,能让你清晰地看到每一步优化的成本,从而做出更明智的决策,而不是像我一样,从一开始就陷入对“完美功能”的执念,最终不得不亲手将它摧毁。

这种从“能用”到“好用”的渐进式开发,才是从蓝图跨越到现实最稳妥的桥梁。

心态:独立开发中的心理难关

前面都是在讲方法,也许你在最终做完项目后对这些方法会有更深层的理解与看法,能总结出更适合自己的方法论,但接下来讲的,却是一套套方法所无法解决的问题——心态问题。

第一 数不清的妥协

就像上文3D主页的故事和“字符串切割”方法,这种妥协在整个项目里无处不在: 比如,服务器有限的读写次数限制,让我不得不放弃消息的实时推送,改成最原始的用户手动刷新;比如,Uniapp X所不支持的各种事件与属性,让我尝试了一天后放弃了为项目制作一个精美彩蛋动画的想法。

服务被停用.png

这些妥协,把我脑海里那个完美的、优雅的实现方案,一点一点地踢倒在地。这种感觉,就像一个想画出鸿篇巨作的画家,却被告知只能在巴掌大的纸上用三种颜色作画。这对于习惯了纯前端开发的我来说,是一种巨大的心理落差。

我不知道其他独立开发者是怎么样的,但如果你也体会过这种束手束脚的难受感觉,请一定放平心态,因为你已经知道了,至少有一个人和你感同身受

第二 看不见的努力

我这个项目总共花了18天,但实际上,后9天的工作,几乎都是“看不见的努力”。

我做了什么?修补各种诡异的bug、优化已经有保底方案的主页性能、给交互加上乐观更新、解耦不同模块的逻辑、抽离公共的样式、统一后端的接口格式……

优化.png
(从每个组件200多行的通信逻辑中抽离出这段还不到200行的JS代码,专门用于Webview端对Uniapp的通信)

九天前的APP,和现在的APP,从表面看几乎没什么差别。这种原地踏步的感觉,真的很容易让人气馁,甚至怀疑自己这些天是不是在浪费时间。但请你相信,这些努力恰恰是区分一个玩具Demo和一个可靠产品的关键。如果你也正在经历这个阶段,请一定认识到,这并非原地踏步,而是从“能用”到“好用”的必经之路,是产品健壮性的基石。

第三 无止境的优化

“看不见的努力”的极端,是“追求完美”的陷阱。

每当我完成一部分重构重新审视代码时,总能发现新的bug,总会出现可以优化的地方。我在这样反复重构了几天之后,才终于醒悟:优化是无止境的,完美是一个永远无法抵达的终点。

消息1.jpg消息2.jpg
(没错,30号那天晚上我傻傻的认为项目已经进入尾声了)

一个项目可以永远不“完成”,总有值得改进的地方。所以,你要学会“完成”,而不是追求“完美”。为你的项目设定一个明确的优化期限。一旦超过时间,就勇敢地为它画上一个句号。先发布,再迭代。这不仅是对你个人努力的交代,也是推动项目向前的唯一方式。

收获:代码之外的成长

讲了这么多技术上的坑、决策上的难,你可能会问,既然这么折磨,你到底为啥去做这个项目,这不是自讨没趣吗?

说实话,完成项目之后,你问我是否掌握了Uniapp开发,我只能很遗憾地说“没有”,如果再打一个Uniapp项目,我仍然要一边翻文档一边写代码。

真正能从中收获的,是对“开发”这件事本身,一次彻头彻尾的认知升级

在这之前,我对前端开发的认知仅限于拿那些常用手脚架搭几个页面,拿后端的文档做好对接而已,至于后端接口的数据结构为什么要设计得那么复杂,产品的需求为什么不停改来改去,我根本就没有关心过。

但当我自己一个人,同时扮演了前端、后端、产品、测试这四个角色后,我才真正理解了他们。

  • 我理解了后端:当我看到uniCloud那有限的免费读写次数时,我瞬间明白了为什么后端要那么“抠门”,为什么他们要对数据进行严格的校验和限制。因为服务器的每一个资源,都是真金白银的成本,每一次不必要的数据库查询,都是在烧钱。从那以后,我会在写前端代码时,主动思考如何减少网络请求,如何设计前端缓存,来减轻服务器的压力。

  • 我理解了产品:当我为了一个“完美”的3D动效,在各种机型上反复测试、反复碰壁时,我才明白为什么产品经理有时会拒绝我们那些“酷炫”的提议。因为一个产品首先要保证的是稳定和可用,是能覆盖最广大的用户群体,而不是在一个小众的亮点上,牺牲掉大部分用户的体验。

更重要的是,我终于弄明白了那些曾经让我困惑的技术本身。过去跟着教程做项目,vue-router, pinia, element-plus……这些似乎是Vue开发的“全家桶”,一股脑装上就对了。至于为什么要装?不知道,反正我知道怎么用,所有能用的地方全部改成这些,就能体现出自己 “技术高超” ,实际上是知其然而不知其所以然

但在这个项目中的Uniapp X部分,这些库全都无法使用,迫使我通过原生方案进行管理,再回头看Vue项目,我开始思考:

  • 我真的需要一个路由库吗?
  • 我真的需要一个状态管理库吗?
  • 我真的需要一个UI组件库吗?

当我开始问这些“为什么”的时候,我才真正开始理解这些工具的本质。它们不是“必需品”,而是一个个为了解决特定问题而生的“工具”。我学会了从需求出发,去选择最合适的工具,而不是盲目地堆砌技术

package内容.png
(在删去了一切不必要的组件之后,极其干净的package.json文件)

这,才是我在这18天里所收获的知识,它们对日常的切图写界面可能没什么直接帮助,但我相信它们会在我们的技术路上不断发挥作用——至少我不再是那个只会照着教程敲代码的萌新了

结语

所以,回到文章开头那个问题:做一个简单的APP,是否真的需要18天?

现在我可以回答:对于一个曾经的我来说,需要。但这18天踩过的坑,让我总结出了文章内可能对大佬来说无关痛痒,但是对于当时的我来说极其重要的宝贵经验。

我写这篇文章,不是为了劝退任何一个想尝试独立开发的新手,恰恰相反,是想把这份避坑地图交给你。希望它能帮你把这18天,缩短哪怕一个钟头。过程中即使遇到了新的问题,也无需气馁,欢迎你在评论区或私信与我交流,我不是什么高手,只是比你先迈出了半步

拯救你的app/小程序审核!一套完美避开审核封禁的URL黑名单机制

2025年8月17日 18:25

app/微信小程序审核又双叒叕被拒了?因为一个历史遗留页面导致整个小程序被封禁搜索?别让精心开发的app/小程序毁在几个不起眼的URL上!本文将揭秘我们在多次惨痛教训后总结出的终极解决方案。

前言:每个小程序开发者都经历过的噩梦

凌晨两点,微信审核通知:"您的小程序因存在违规页面,搜索功能已被限制"。看着辛苦运营的用户量断崖式下跌,排查三天才发现是因为一个早已下架但还能访问的历史页面。这不是假设,而是真实发生的灾难场景

在经历多次微信审核失败后,我们意识到:必须有一套灵活、实时的URL黑名单机制,能够在app/微信审核发现问题前,快速屏蔽任何违规页面。这套系统需要:

  1. 分钟级响应:新发现的违规URL,1分钟内全局生效

  2. 精准打击:既能拦截整个页面,也能封禁特定参数组合

  3. 零误杀:确保正常页面不受影响

  4. 优雅降级:被拦截用户跳转到友好提示页,且可一对一设置兜底页。

下面是我们用血泪教训换来的完整解决方案,已成功帮助我们通过n多次app审核。

app/微信小程序审核的致命陷阱:你未必意识到的风险点

真实审核失败案例

  • 案例1:三年前的活动页仍可通过直接URL访问(违反现行规则)

  • 案例2:用户生成内容包含敏感关键词(UGC页面)

  • 案例3:第三方合作伙伴的H5页面突然变更内容

  • 最致命案例:历史页面被微信爬虫索引,导致整个小程序搜索功能被封禁

核心需求清单(微信审核视角)

  1. 实时封堵能力:无需发版即可封禁任意URL

  2. 精准匹配:支持完整URL和带参数的URL匹配

  3. 全类型覆盖:原生页面 + H5页面统一处理

  4. 优雅降级:被封禁用户看到友好提示而非404

  5. 安全兜底:系统异常时自动放行,不影响正常业务

系统架构设计:三重防护盾

核心流程

  1. 所有跳转请求经过黑名单检查

  2. 命中规则则跳转到兜底页

  3. 系统异常时降级放行

  4. 后台配置秒级生效

核心技术实现

参数级精准打击 - 只封禁违规内容

// 黑名单配置
["pages/user/content?type=sensitive"]

// 结果:
"pages/user/content?type=normal" => 放行 ✅
"pages/user/content?type=sensitive" => 拦截 ⛔

微信审核场景:当只有特定参数组合违规时,最小化业务影响

匹配规则详解:如何应对app审核

场景1:紧急封禁整个页面(后台配置示例)

{
  "YourBlackList": [
    {
      "nowUrl": "https://baidu.com",
      "ToUrl": "www.juejin.cn"
    }
  ]
}

只要命中 baidu.com 无论实际跳转页面后面参数是什么,都命中了黑名单,直接跳转到自己的兜底页](url)

场景2:精准封禁违规内容

// 配置黑名单
{
  "YourBlackList": [
    {
      "nowUrl": "pages/news/detail?id=12345",
      "ToUrl": "www.baidu.com"
    }
  ]
}
// 效果:
仅拦截id=12345的新闻,如果命中,则跳转到百度(你设置的兜底页)。其他正常展示

场景3:批量处理历史内容

// 配置黑名单
{
  "YourBlackList": [
    {
      "nowUrl": "pages/history/?year=2020",
      "ToUrl": "www.baidu.com"
    }
  ]
}

// 效果:
拦截2020年的所有历史页面,其他年份正常

实际应用:拯救审核的最后一公里

在路由跳转处拦截

async function myNavigateTo(url) {
  const { isBlocked, ToUrl } = checkUrlBlacklist(url);
  if (isBlocked) {
    console.warn('审核风险页面被拦截:', url);
    // 跳转到安全页
    return wx.navigateTo({ url: ToUrl });
  }
  
  // 正常跳转逻辑...
}

性能与安全:双保险设计

二重保障机制

  1. 性能优化:黑名单为空时短路返回
if (!blackUrlList.length) return { isBlocked: false };
  1. 频率控制:避免相同URL重复解析

更新时机

app/小程序初始化时,如果想更精细一些,可以监听app/小程序后台切到前台onShow时

  // 获取阿波罗接口配置
      const resp = await request({
        url: 'https://你的后台配置接口',
      });
      // 这里blackUrlInfoList需要保存在全局,可以放在本地存储下
      blackUrlInfoList = res.blackUrlInfoList || []

校验时机:每次跳转时。

具体判断逻辑在此不做阐述,

总结:从此告别审核噩梦

通过实施这套URL黑名单系统,我们实现了:

  • 审核通过率从63% → 98%

  • 问题响应时间从2天 → 5分钟

  • 搜索封禁事故0发生

关键收获

  1. 提前拦截比事后补救更重要

  2. 参数级控制最大化保留正常功能

  3. 实时配置能力是应对审核的关键

现在点击右上角收藏本文,当app审核再次亮红灯时,你会感谢今天的自己!


分享你的审核故事:你在微信/app审核中踩过哪些坑?欢迎在评论区分享你的经历和解决方案!

React深入浅出理解

作者 luckyJian
2025年8月17日 18:06

1. JSX代码如何转化为DOM

JSX是JavaScript的语法扩展,会被Babel编译为React.createElement()函数调用,该函数返回一个称为“React Element”的JavaScript对象。Babel作为工具链,主要用于将ECMAScript 2015+代码转化为向后兼容的JavaScript语法,以支持旧版浏览器。JSX语法允许开发者使用类似HTML的标签语法,降低学习成本并提升研发效率。

关键点

  • createElement函数需要三个参数:

    type: 标识节点类型(如组件或HTML标签)。

    config: 以对象形式传入,存储组件的所有属性(键值对)。

    children: 以对象形式传入,记录组件标签间的嵌套内容。

image.png

  • ReactDOM.render参数说明:

    • element: 需要渲染的React Element。

    • container: 元素挂载的目标容器(真实DOM节点)。

    -[callback]: 可选回调函数,用于处理渲染结束后的逻辑。

      整体转化流程:JSX → `createElement`→ React Element → `ReactDOM.render`→ 真实DOM。
    

2. React生命周期详解

生命周期描述了组件从初始化到更新或卸载的全过程,核心是渲染工作流的“封闭”和“开放”特性:

  • 封闭:每个组件仅处理内部渲染逻辑。

  • 开放:基于单向数据流原则实现组件间通信,通过数据变更影响渲染结果。

    生命周期方法(如render)构成了组件的“灵魂”与“躯干”。

React 15生命周期过程
  • 父组件导致子组件重新渲染时,即使props未更改,也会触发componentReceiveProps(非props变化驱动)。

  • shouldComponentUpdate返回值决定是否执行后续生命周期及重渲染。

  • componentWillUnmount触发条件:

    • 组件在父组件中被移除。

    • 父组件render中key值与上一次不一致。

React 16生命周期过程
  • 废除componentWillMount,新增getDerivedStateFromProps

  • render方法改进:支持返回元素数组或字符串(React 16前仅支持单个元素)。

  • getDerivedStateFromProps用途:

    • 仅用于派生/更新state(非依赖组件,无法访问this)。

    • 接收propsstate参数,返回对象(无返回值需return null)。

    • 更新机制为定向属性更新(非整体覆盖)。

  • 组件初始化渲染(挂载)对比:

  • 组件更新流程对比:

    • getDerivedStateFromProps替代componentReceiveProps(不等价)。

    • 移除componentWillUpdate,新增getSnapshotBeforeUpdate

      • 执行时机在render后、真实DOM更新前。

      • 返回值作为参数传递给componentDidUpdate,用于获取更新前的真实DOM及state/props。

3. Fiber架构核心原理

Fiber是React 16对核心算法的重写,旨在解决同步渲染导致的性能问题:

  • •将同步渲染变为异步(仅在Concurrent模式下)。

  • •拆解大更新任务为小任务,支持中断、恢复和优先级调度。

  • •将生命周期划分为render(可中断)和commit(同步执行)两阶段。

影响与变更

  • 废除API:componentWillMountcomponentWillUpdatecomponentWillReceiveProps(因异步渲染可能导致多次请求或数据不一致)。

  • 动机:配合异步渲染机制,确保数据安全性和生命周期行为的可预测性。

  • 初始化阶段流程:

    • 请求当前Fiber节点的lane(优先级)。

    • 创建update对象。

    • 调节当前节点(root Fiber)。

Render阶段工作流
  • WorkInProgress节点创建

    • createWorkInProgress生成新节点,与current节点通过alternate互指。

    • 循环调用performUnitOfWork触发beginWork创建Fiber树。

  • beginWork逻辑:基于Fiber节点的tag属性调用不同创建函数。

  • childReconciler作用:处理Fiber节点的创建、增删改(如placeXXXdeleteXXX)。

  • effectList设计

    • 记录需处理的副作用(如DOM变更)。

    • 实现原理:Fiber节点的flags属性(旧称effectTag)标识副作用类型(如placement表示新增DOM节点)。

  • Fiber树创建过程

Commit阶段工作流
  • before mutation:DOM未渲染前的逻辑。

  • mutation:负责DOM渲染。

  • layout:处理DOM渲染后的收尾工作。

  • 最终将Fiber树的current指针指向workInProgress树。

4. React组件数据流动

单向数据流
  • 数据流向:父组件 → 子组件(通过props传递)。

  • 实现方式:

    • 父组件通过props传递数据或函数(子组件调用函数以参数形式返回数据)。

    • 兄弟组件通信:需通过父组件中介。

发布-订阅模式(EventBus)
  • • 示例:Socket.io或Node.js的EventEmitter。

  • API设计:

    • on():注册事件监听器。

    • emit():触发事件(可携带数据)。

    • off():移除监听器。

Context全局通信
  • 作用:跨层级组件数据传递。

  • 实现:React.createContext创建Provider和Consumer。

  • 问题:过时Context代码不优雅,新版本确保数据一致性(即使shouldComponentUpdate返回false)。

5. Redux核心概念

Redux是JavaScript状态容器,提供可预测的状态管理:

  • 组成

    • store:单一数据源(只读)。

    • action:描述变化的对象(type为必填唯一标识)。

    • reducer:处理action并更新状态。

  • 原则:state变更必须由action驱动。

6. React Hooks设计

Hooks是使函数组件更强大的“钩子”,本质是一个链表:

函数组件 vs 类组件
  • 类组件:继承class、访问生命周期、维护state、使用this

  • 函数组件:轻量、无生命周期、无this、更契合React声明式理念(数据与渲染绑定)。

核心Hooks

  • useState:返回状态变量和更新函数(mountState初始化,updateState更新)。

  • useEffect:弥补生命周期缺失(回调中返回清除函数)。

问题与原则

  • 问题:无法完全替代类组件能力,复杂性处理较弱。

  • 使用原则:

    • 仅在React函数中调用Hook。

    • 避免在循环、条件或嵌套函数中调用。

    • 确保Hooks执行顺序一致。

7. 虚拟DOM与diff算法

虚拟DOM是描述DOM结构的JS对象,优化渲染性能:

  • 工作流

    • 挂载阶段:JSX → 虚拟DOM树 → ReactDOM.render→ 真实DOM。

    • 更新阶段:变更作用于虚拟DOM → diff算法对比差异 → 差量更新真实DOM。

  • 价值

    • 提升研发体验与效率。

    • 支持跨平台渲染(Web、iOS、Android)。

    • 实现批量更新。

diff算法原理

  • 分层对比:仅同层级节点比较。

  • 类型一致:节点类型相同才继续diff。

  • key优化:重用同层级节点。

  • 树递归:基于key进行深度比较。

8. setState工作流

  • 行为差异

    • 异步:在React钩子函数和合成事件中。

    • 同步:在setTimeoutsetInterval或原生DOM事件中(因逃脱React管控)。

  • Transaction机制:封装方法(如initializeperform),确保更新原子性。

9. React事件系统

原生DOM事件流
  • 三阶段:事件捕获 → 目标阶段 → 事件冒泡。

  • 事件委托:合并子元素监听逻辑到父元素。

React事件流实现
  • 绑定:通过completeWork完成。

  • 特点:document上统一事件分发函数(多次调用仅触发一次)。

  • 合成事件:封装原生事件,提供跨浏览器一致性。

HTML5语义化标签和“<div>的一招鲜吃遍天”

作者 挽淚
2025年8月17日 17:56

什么是 <div> 的万能性?

<div> 是 HTML 中最基础的容器标签,它的特点是 没有语义、只有布局,可以用来包裹任何内容,并通过 CSS 进行样式控制。
因此,它具备几个优点:

  1. 灵活性高:随意套用 CSS、做复杂布局都没有限制。
  2. 兼容性好:几乎所有浏览器都支持,老旧浏览器不会出问题。
  3. 历史原因:早期 HTML 不支持语义化标签,布局全靠 <div> + CSS,形成了使用习惯。
  4. 开发效率快:不必考虑标签语义,直接套用 <div> 就能完成页面结构。

这也是为什么很多初创项目或内部系统喜欢“一招鲜吃遍天”,直接用 <div> 完成所有布局。

就比如掘金首页就是一个很典型的例子:

wechat_2025-08-17_162512_664.png

wechat_2025-08-17_165409_354.png

图中大量使用了 <div> 标签做布局和内容承载,语义化程度低。像页面头部、导航、主要内容区等,没有用 <header><nav><main> 等更具语义的标签,这并不利于浏览器、搜索引擎及开发者快速理解页面结构与内容层级 。比如页面中各种功能模块的包裹,单纯用 <div> 堆叠,语义比较模糊。

我们再来看看英国政府官网(www.gov.uk/)

wechat_2025-08-17_171825_773.png 这网站可以说是相当板正了,<main> 区内容层级分明,<section> 区块 + 标题(虽然代码里标题是 <h2> 类名,但实际应该有合理层级 ),关键词分布自然。比如 “HMRC account” 这类热门服务,放在 Popular on GOV.UK 区,既满足用户需求,又给关键词 “送分” 。

语义化标签能带来什么?

HTML5 提供了丰富的 语义化标签

  • <header>:定义页面的头部区域,通常包含网站的标题和导航。
  • <nav>:表示导航链接的区域,帮助搜索引擎和辅助技术识别页面的导航部分。
  • <main>:表示页面的主要内容区域,应该是页面中唯一的 <main> 元素。
  • <section>:用于定义文档中的节(section),通常包含一个标题和一组相关内容。
  • <article>:表示独立的内容块,如博客文章、新闻报道等。
  • <footer>:定义页面的页脚区域,通常包含版权信息、联系信息等。

而它们的优点是:

  1. 内容结构清晰:一眼就能知道页面哪部分是导航、哪部分是文章。
  2. 利于 SEO:搜索引擎更容易抓取核心内容,提升页面权重。
  3. 提高可访问性:屏幕阅读器可以根据标签理解页面结构,辅助设备用户体验更好。

这里我们就用语义化标签简单搭一个网页练练手:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>HTML5 语义化示例</title>
</head>
<body>
  <header>
    <h1>我的博客</h1>
    <nav>
      <ul>
        <li><a href="#home">首页</a></li>
        <li><a href="#about">关于我</a></li>
        <li><a href="#contact">联系</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <section id="home">
      <h2>欢迎来到我的博客</h2>
      <p>这是我的个人博客,分享我的技术文章和生活点滴。</p>
    </section>

    <section id="articles">
      <h2>最新文章</h2>
      <article>
        <h3>HTML5 语义化标签详解</h3>
        <p>本文详细介绍了 HTML5 中的语义化标签及其应用。</p>
      </article>
      <article>
        <h3>CSS Flexbox 实战指南</h3>
        <p>深入讲解 CSS Flexbox 布局模型的使用方法。</p>
      </article>
    </section>
  </main>

  <footer>
    <p>© 2025 我的博客</p>
  </footer>
</body>
</html>

效果如图:

16234797-5d41-4ab5-abb5-38a199a0b467.png

实际应用的选择

考虑工程效率为主

  • 开发速度快:用 <div> 不用考虑标签语义,写页面速度快。
  • 布局灵活:无论是弹性布局、复杂网格还是响应式界面,<div> 都能胜任。
  • 组件化场景中语义化价值低:例如聊天框、仪表盘、后台管理系统,这类页面主要是交互界面,SEO 或无障碍价值有限,用 <div> 最省心。

二者如何权衡

  • 核心内容(例如文章、博客、新闻) :语义化标签有价值 → 用 <article><section>
  • 界面组件(例如聊天框、工具栏、按钮) :语义化价值低 → 用 <div> + CSS/JS 快速实现即可。

换句话说,对话界面更多是 UI 组件,而非内容承载,所以 <div> “一招鲜”是合理选择。

小结

  • 网页不是只有 SEO 才重要:很多地方是给用户交互用的组件,这时候灵活性比语义化更重要。
  • 语义化 vs div 的权衡本质上是 “内容 vs 布局” 的问题。
  • 对话页面大量 <div> 并不代表语义化没用,而是 在合理场景下的工程选择
❌
❌