阅读视图

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

你以为 Props 只是传参? 不,它是 React 组件设计的“灵魂系统”

90% 的 React 初学者,都低估了 Props。
他们以为它只是“从父组件往子组件传点数据”。

但真正写过复杂组件、设计过通用组件的人都知道一句话:

Props 决定了一个组件“好不好用”,而不是“能不能用”。

这篇文章,我们不讲 API 清单、不背概念,
而是围绕 Props 系统的 5 个核心能力,一次性讲透 React 组件化的底层逻辑:

  • Props 传递
  • Props 解构
  • 默认值(defaultProps / 默认参数)
  • 类型校验(PropTypes)
  • children 插槽机制(React 的核武器)

👉 看完你会明白:
React 真正厉害的不是 JSX,而是 Props 设计。


一、Props 的本质:组件的“对外接口”

先抛一个结论:

React 组件 ≈ 一个函数 + 一套 Props 接口

来看一个最简单的组件 👇

function Greeting(props) {
  return <h1>Hello, {props.name}</h1>
}

使用时:

<Greeting name="白兰地" />

很多人到这里就停了,但问题是:

name 到底是什么?

答案是:

name 不是变量,是组件对外暴露的能力。

Props 本质上是:

  • 父组件 👉 子组件的输入
  • 组件作者 👉 使用者的约定

二、Props 解构:不是语法糖,而是“设计声明”

对比两种写法 👇

❌ 不推荐

function Greeting(props) {
  return <h1>Hello, {props.name}</h1>
}

✅ 推荐

function Greeting({ name }) {
  return <h1>Hello, {name}</h1>
}

为什么?

解构不是为了少写字,而是为了表达意图。

当你看到函数签名:

function Greeting({ name, message, showIcon }) {}

你立刻就知道:

  • 这个组件“需要什么”
  • 组件的“输入边界”在哪里

👉 好的组件,从函数签名就能读懂。


三、Props 默认值:组件“健壮性”的第一步

看这个组件 👇

function Greeting({ name, message }) {
  return (
    <div>
      <h1>Hello, {name}</h1>
      <p>{message}</p>
    </div>
  )
}

如果使用者这么写:

<Greeting name="空瓶" />

会发生什么?

message === undefined

这时候就轮到 默认值 出场了。


方式一:defaultProps(经典)

Greeting.defaultProps = {
  message: 'Welcome!'
}

方式二:解构默认值(更推荐)

function Greeting({ name, message = 'Welcome!' }) {}

💡默认值不是兜底,而是组件设计的一部分。

它代表的是:

  • “在你不配置的情况下”
  • “组件应该表现成什么样”

四、Props 类型校验:组件的“自说明文档”

来看一段很多人忽略、但非常值钱的代码 👇

import PropTypes from 'prop-types'

Greeting.propTypes = {
  name: PropTypes.string.isRequired,
  message: PropTypes.string,
  showIcon: PropTypes.bool,
}

很多人会说:

“这不是可有可无吗?”

但在真实项目里,它解决的是:

  • ❌ 参数传错没人发现
  • ❌ 新人不知道组件怎么用
  • ❌ 组件一多,全靠猜

🔍 PropTypes 的真正价值

不是防 bug,而是“降低理解成本”。

当你看到 propTypes,就等于看到一份说明书:

  • 哪些 props 必须传?
  • 哪些是可选?
  • 类型是什么?

👉 一个没有 propTypes 的通用组件,本质上是“黑盒”。


五、children:React Props 系统的“王炸”

如果只能选一个 Props 机制,我会毫不犹豫选:

🧨 children

来看一个 Card 组件 👇

const Card = ({ children, className = '' }) => {
  return (
    <div className={`card ${className}`}>
      {children}
    </div>
  )
}

使用时:

<Card className="user-card">
  <h2>张三</h2>
  <p>高级前端工程师</p>
  <button>查看详情</button>
</Card>

这里发生了一件非常重要的事情:

组件不再关心“内容是什么”。


🧠 children 的设计哲学

组件负责“骨架”,使用者负责“填充”。

  • Card 只负责:边框、阴影、间距
  • children 决定:展示什么内容

这让组件具备了两个特性:

  • ✅ 高度复用
  • ✅ 永不过期

六、children + Props = 通用组件的终极形态

再看一个更高级的例子:Modal 👇

<Modal HeaderComponent={MyHeader} FooterComponent={MyFooter}>
  <p>这是一个弹窗</p>
  <p>你可以在这里显示任何 JSX</p>
</Modal>

Modal 的实现:

function Modal({ HeaderComponent, FooterComponent, children }) {
  return (
    <div>
      <HeaderComponent />
      {children}
      <FooterComponent />
    </div>
  )
}

这背后是一个非常高级的思想:

Props 不只是数据,也可以是组件。


七、请记住这 5 条 Props 设计铁律

🔥 如果你只能记住一段话,请记住这里

  1. Props 是组件的“对外接口”,不是随便传的变量
  2. 解构 Props,是在声明组件的能力边界
  3. 默认值,决定组件的“基础体验”
  4. 类型校验,让组件自带说明书
  5. children,让组件从“可用”变成“好用”

八、写在最后

当你真正理解 Props 之后,你会发现:

  • React 不只是 UI 库
  • 它在教你如何设计 API
  • 如何让别人“用得爽”

Props 写得好不好,决定了一个人 React 水平的上限。

每日一题-两个最好的不重叠活动🟡

给你一个下标从 0 开始的二维整数数组 events ,其中 events[i] = [startTimei, endTimei, valuei] 。第 i 个活动开始于 startTimei ,结束于 endTimei ,如果你参加这个活动,那么你可以得到价值 valuei 。你 最多 可以参加 两个时间不重叠 活动,使得它们的价值之和 最大 。

请你返回价值之和的 最大值 。

注意,活动的开始时间和结束时间是 包括 在活动时间内的,也就是说,你不能参加两个活动且它们之一的开始时间等于另一个活动的结束时间。更具体的,如果你参加一个活动,且结束时间为 t ,那么下一个活动必须在 t + 1 或之后的时间开始。

 

示例 1:

输入:events = [[1,3,2],[4,5,2],[2,4,3]]
输出:4
解释:选择绿色的活动 0 和 1 ,价值之和为 2 + 2 = 4 。

示例 2:

Example 1 Diagram

输入:events = [[1,3,2],[4,5,2],[1,5,5]]
输出:5
解释:选择活动 2 ,价值和为 5 。

示例 3:

输入:events = [[1,5,3],[1,5,1],[6,6,5]]
输出:8
解释:选择活动 0 和 2 ,价值之和为 3 + 5 = 8 。

 

提示:

  • 2 <= events.length <= 105
  • events[i].length == 3
  • 1 <= startTimei <= endTimei <= 109
  • 1 <= valuei <= 106

两个最好的不重叠活动

方法一:时间戳排序

思路与算法

我们可以将所有活动的左右边界放在一起进行自定义排序。具体地,我们用 $(\textit{ts}, \textit{op}, \textit{val})$ 表示一个「事件」:

  • $\textit{op}$ 表示该事件的类型。如果 $\textit{op} = 0$,说明该事件表示一个活动的开始;如果 $\textit{op} = 1$,说明该事件表示一个活动的结束。

  • $\textit{ts}$ 表示该事件发生的时间,即活动的开始时间或结束时间。

  • $\textit{val}$ 表示该事件的价值,即对应活动的 $\textit{value}$ 值。

我们将所有的时间按照 $\textit{ts}$ 为第一关键字升序排序,这样我们就能按照时间顺序依次处理每一个事件。当 $\textit{ts}$ 相等时,我们按照 $\textit{op}$ 为第二关键字升序排序,这是因为题目中要求了「第一个活动的结束时间不能等于第二个活动的起始时间」,因此当时间相同时,我们先处理开始的事件,再处理结束的事件。

当排序完成后,我们就可以通过对所有的事件进行一次遍历,从而算出最多两个时间不重叠的活动的最大价值:

  • 当我们遍历到一个结束事件时,我们用 $\textit{val}$ 来更新 $\textit{bestFirst}$,其中 $\textit{bestFirst}$ 表示当前已经结束的所有活动的最大价值。这样做的意义在于,所有已经结束的事件都可以当作第一个活动

  • 当我们遍历到一个开始事件时,我们将该活动当作第二个活动,由于第一个活动的最大价值为 $\textit{bestFirst}$,因此我们用 $\textit{val} + \textit{bestFirst}$ 更新答案即可。

代码

###C++

struct Event {
    // 时间戳
    int ts;
    // op = 0 表示左边界,op = 1 表示右边界
    int op;
    int val;
    Event(int _ts, int _op, int _val): ts(_ts), op(_op), val(_val) {}
    bool operator< (const Event& that) const {
        return tie(ts, op) < tie(that.ts, that.op);
    }
};

class Solution {
public:
    int maxTwoEvents(vector<vector<int>>& events) {
        vector<Event> evs;
        for (const auto& event: events) {
            evs.emplace_back(event[0], 0, event[2]);
            evs.emplace_back(event[1], 1, event[2]);
        }
        sort(evs.begin(), evs.end());
        
        int ans = 0, bestFirst = 0;
        for (const auto& [ts, op, val]: evs) {
            if (op == 0) {
                ans = max(ans, val + bestFirst);
            }
            else {
                bestFirst = max(bestFirst, val);
            }
        }
        return ans;
    }
};

###Python

class Event:
    def __init__(self, ts: int, op: int, val: int):
        self.ts = ts
        self.op = op
        self.val = val
    
    def __lt__(self, other: "Event") -> bool:
        return (self.ts, self.op) < (other.ts, other.op)


class Solution:
    def maxTwoEvents(self, events: List[List[int]]) -> int:
        evs = list()
        for event in events:
            evs.append(Event(event[0], 0, event[2]))
            evs.append(Event(event[1], 1, event[2]))
        evs.sort()

        ans = bestFirst = 0
        for ev in evs:
            if ev.op == 0:
                ans = max(ans, ev.val + bestFirst)
            else:
                bestFirst = max(bestFirst, ev.val)
        
        return ans

###Java

class Solution {
    public int maxTwoEvents(int[][] events) {
        List<Event> evs = new ArrayList<>();
        for (int[] event : events) {
            evs.add(new Event(event[0], 0, event[2]));
            evs.add(new Event(event[1], 1, event[2]));
        }
        Collections.sort(evs);
        int ans = 0, bestFirst = 0;
        for (Event event : evs) {
            if (event.op == 0) {
                ans = Math.max(ans, event.val + bestFirst);
            } else {
                bestFirst = Math.max(bestFirst, event.val);
            }
        }
        return ans;
    }
    
    class Event implements Comparable<Event> {
        int ts;
        int op;
        int val;
        
        Event(int ts, int op, int val) {
            this.ts = ts;
            this.op = op;
            this.val = val;
        }
        
        @Override
        public int compareTo(Event other) {
            if (this.ts != other.ts) {
                return Integer.compare(this.ts, other.ts);
            }
            return Integer.compare(this.op, other.op);
        }
    }
}

###C#

public class Solution {
    public int MaxTwoEvents(int[][] events) {
        List<Event> evs = new List<Event>();
        foreach (var eventArr in events) {
            evs.Add(new Event(eventArr[0], 0, eventArr[2]));
            evs.Add(new Event(eventArr[1], 1, eventArr[2]));
        }
        evs.Sort();
        
        int ans = 0, bestFirst = 0;
        foreach (var ev in evs) {
            if (ev.Op == 0) {
                ans = Math.Max(ans, ev.Val + bestFirst);
            } else {
                bestFirst = Math.Max(bestFirst, ev.Val);
            }
        }
        return ans;
    }
    
    class Event : IComparable<Event> {
        public int Ts { get; set; }
        public int Op { get; set; }
        public int Val { get; set; }
        
        public Event(int ts, int op, int val) {
            Ts = ts;
            Op = op;
            Val = val;
        }
        
        public int CompareTo(Event other) {
            if (Ts != other.Ts) {
                return Ts.CompareTo(other.Ts);
            }
            return Op.CompareTo(other.Op);
        }
    }
}

###Go

func maxTwoEvents(events [][]int) int {
    type Event struct {
        ts  int
        op  int
        val int
    }
    
    evs := make([]Event, 0)
    for _, event := range events {
        evs = append(evs, Event{event[0], 0, event[2]})
        evs = append(evs, Event{event[1], 1, event[2]})
    }
    
    sort.Slice(evs, func(i, j int) bool {
        if evs[i].ts != evs[j].ts {
            return evs[i].ts < evs[j].ts
        }
        return evs[i].op < evs[j].op
    })
    
    ans, bestFirst := 0, 0
    for _, ev := range evs {
        if ev.op == 0 {
            if ev.val + bestFirst > ans {
                ans = ev.val + bestFirst
            }
        } else {
            if ev.val > bestFirst {
                bestFirst = ev.val
            }
        }
    }
    return ans
}

###C

typedef struct {
    int ts;
    int op;
    int val;
} Event;

int compareEvents(const void* a, const void* b) {
    Event* e1 = (Event*)a;
    Event* e2 = (Event*)b;
    if (e1->ts != e2->ts) {
        return e1->ts - e2->ts;
    }
    return e1->op - e2->op;
}

int maxTwoEvents(int** events, int eventsSize, int* eventsColSize) {
    Event* evs = (Event*)malloc(2 * eventsSize * sizeof(Event));
    int idx = 0;
    for (int i = 0; i < eventsSize; i++) {
        evs[idx++] = (Event){events[i][0], 0, events[i][2]};
        evs[idx++] = (Event){events[i][1], 1, events[i][2]};
    }
    qsort(evs, 2 * eventsSize, sizeof(Event), compareEvents);

    int ans = 0, bestFirst = 0;
    for (int i = 0; i < 2 * eventsSize; i++) {
        if (evs[i].op == 0) {
            if (evs[i].val + bestFirst > ans) {
                ans = evs[i].val + bestFirst;
            }
        } else {
            if (evs[i].val > bestFirst) {
                bestFirst = evs[i].val;
            }
        }
    }
    
    free(evs);
    return ans;
}

###JavaScript

var maxTwoEvents = function(events) {
    const evs = [];
    for (const event of events) {
        evs.push({ts: event[0], op: 0, val: event[2]});
        evs.push({ts: event[1], op: 1, val: event[2]});
    }
    
    evs.sort((a, b) => {
        if (a.ts !== b.ts) {
            return a.ts - b.ts;
        }
        return a.op - b.op;
    });
    
    let ans = 0, bestFirst = 0;
    for (const ev of evs) {
        if (ev.op === 0) {
            ans = Math.max(ans, ev.val + bestFirst);
        } else {
            bestFirst = Math.max(bestFirst, ev.val);
        }
    }
    return ans;
};

###TypeScript

function maxTwoEvents(events: number[][]): number {
    interface Event {
        ts: number;
        op: number;
        val: number;
    }
    
    const evs: Event[] = [];
    for (const event of events) {
        evs.push({ts: event[0], op: 0, val: event[2]});
        evs.push({ts: event[1], op: 1, val: event[2]});
    }
    
    evs.sort((a, b) => {
        if (a.ts !== b.ts) {
            return a.ts - b.ts;
        }
        return a.op - b.op;
    });
    
    let ans = 0, bestFirst = 0;
    for (const ev of evs) {
        if (ev.op === 0) {
            ans = Math.max(ans, ev.val + bestFirst);
        } else {
            bestFirst = Math.max(bestFirst, ev.val);
        }
    }
    return ans;
}

###Rust

#[derive(Debug)]
struct Event {
    ts: i32,
    op: i32,
    val: i32,
}

impl Solution {
    pub fn max_two_events(events: Vec<Vec<i32>>) -> i32 {
        let mut evs: Vec<Event> = Vec::new();
        for event in events {
            evs.push(Event { ts: event[0], op: 0, val: event[2] });
            evs.push(Event { ts: event[1], op: 1, val: event[2] });
        }
        
        evs.sort_by(|a, b| {
            if a.ts != b.ts {
                a.ts.cmp(&b.ts)
            } else {
                a.op.cmp(&b.op)
            }
        });
        
        let mut ans = 0;
        let mut best_first = 0;
        for ev in evs {
            if ev.op == 0 {
                ans = ans.max(ev.val + best_first);
            } else {
                best_first = best_first.max(ev.val);
            }
        }
        
        ans
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 是数组 $\textit{events}$ 的长度。

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

排序 + 单调栈二分(Python/Java/C++/Go)

设参加的第二个活动的开始时间为 $\textit{startTime}$,那么第一个活动选哪个最好?

在结束时间小于 $\textit{startTime}$ 的活动中,选择价值最大的活动。

为了方便查找,先把 $\textit{events}$ 按照结束时间从小到大排序。

排序后,对比如下两个活动:

  • 活动一:结束于 $3$ 时刻,价值 $999$。
  • 活动二:结束于 $6$ 时刻,价值 $9$。

活动二的结束时间又晚,价值又小,全方面不如活动一,是垃圾数据,直接忽略。

换句话说,在遍历 $\textit{events}$ 的过程中(注意 $\textit{events}$ 已按照结束时间排序),只在遇到更大价值的活动时,才记录该活动。把这些活动记录到一个栈(列表)中,那么从栈底到栈顶,结束时间是递增的,价值也是递增的,非常适合二分查找。关于二分查找的原理,请看 二分查找 红蓝染色法【基础算法精讲 04】

枚举第二个活动,在单调栈中二分查找结束时间严格小于 $\textit{startTime}$ 的最后一个活动,即为价值最大的第一个活动。如果没找到,那么只能选一个活动。

为了简化判断逻辑,可以在栈底加一个结束时间为 $0$,价值也为 $0$ 的哨兵。

写法一

class Solution:
    def maxTwoEvents(self, events: List[List[int]]) -> int:
        # 按照结束时间排序
        events.sort(key=lambda e: e[1])  

        # 从栈底到栈顶,结束时间递增,价值递增
        st = [(0, 0)]  # 栈底哨兵 
        ans = 0
        for start_time, end_time, value in events:
            # 二分查找最后一个结束时间 < start_time 的活动
            i = bisect_left(st, (start_time,)) - 1
            ans = max(ans, st[i][1] + value)
            # 遇到比栈顶更大的价值,入栈
            if value > st[-1][1]:
                st.append((end_time, value))
        return ans
class Solution {
    public int maxTwoEvents(int[][] events) {
        // 按照结束时间排序
        Arrays.sort(events, (a, b) -> a[1] - b[1]);

        // 从栈底到栈顶,结束时间递增,价值递增
        ArrayList<int[]> st = new ArrayList<>(); // (结束时间, 价值)
        st.add(new int[]{0, 0}); // 栈底哨兵

        int ans = 0;
        for (int[] e : events) {
            int i = search(st, e[0]);
            int value = e[2];
            ans = Math.max(ans, st.get(i)[1] + value);
            // 遇到比栈顶更大的价值,入栈
            if (value > st.getLast()[1]) {
                st.add(new int[]{e[1], value});
            }
        }
        return ans;
    }

    // 返回最后一个满足 st[i][0] < target 的 i
    private int search(List<int[]> st, int target) {
        int left = -1, right = st.size();
        while (left + 1 < right) { // 开区间二分
            int mid = left + (right - left) / 2;
            if (st.get(mid)[0] < target) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }
}
class Solution {
public:
    int maxTwoEvents(vector<vector<int>>& events) {
        // 按照结束时间排序
        ranges::sort(events, {}, [](auto& e) { return e[1]; });

        // 从栈底到栈顶,结束时间递增,价值递增
        vector<pair<int, int>> st = {{0, 0}}; // 栈底哨兵
        int ans = 0;
        for (auto& e : events) {
            int start_time = e[0], value = e[2];
            // 二分查找最后一个结束时间 < start_time 的活动
            auto it = --ranges::lower_bound(st, start_time, {}, &pair<int, int>::first);
            ans = max(ans, it->second + value);
            // 遇到比栈顶更大的价值,入栈
            if (value > st.back().second) {
                st.emplace_back(e[1], value);
            }
        }
        return ans;
    }
};
func maxTwoEvents(events [][]int) (ans int) {
// 按照结束时间排序
slices.SortFunc(events, func(a, b []int) int { return a[1] - b[1] })

// 从栈底到栈顶,结束时间递增,价值递增
type pair struct{ endTime, value int }
st := []pair{{}} // 栈底哨兵
for _, e := range events {
startTime, value := e[0], e[2]
// 二分查找最后一个结束时间 < startTime 的活动
i := sort.Search(len(st), func(i int) bool { return st[i].endTime >= startTime }) - 1
ans = max(ans, st[i].value+value)
// 遇到比栈顶更大的价值,入栈
if value > st[len(st)-1].value {
st = append(st, pair{e[1], value})
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{events}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

写法二:原地

也可以把 $\textit{events}$ 当作栈。

缺点是没法用哨兵。

class Solution:
    def maxTwoEvents(self, events: List[List[int]]) -> int:
        events.sort(key=lambda e: e[1])

        ans = size = 0  # 把 events 当作栈
        for start_time, end_time, value in events:
            i = bisect_left(events, (start_time,), hi=size) - 1
            if i >= 0:
                ans = max(ans, value + events[i][1])
            else:
                ans = max(ans, value)

            if size == 0 or value > events[size - 1][1]:
                events[size] = (end_time, value)
                size += 1
        return ans
class Solution {
    public int maxTwoEvents(int[][] events) {
        Arrays.sort(events, (a, b) -> a[1] - b[1]);

        int ans = 0;
        int size = 0; // 把 events 当作栈
        for (int[] e : events) {
            int i = search(events, size, e[0]);
            int value = e[2];
            if (i >= 0) {
                ans = Math.max(ans, value + events[i][2]);
            } else {
                ans = Math.max(ans, value);
            }

            if (size == 0 || value > events[size - 1][2]) {
                events[size++] = e;
            }
        }
        return ans;
    }

    // 返回最后一个满足 st[i][1] < target 的 i
    private int search(int[][] st, int right, int target) {
        int left = -1;
        while (left + 1 < right) { // 开区间二分
            int mid = left + (right - left) / 2;
            if (st[mid][1] < target) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }
}
class Solution {
public:
    int maxTwoEvents(vector<vector<int>>& events) {
        ranges::sort(events, {}, [](auto& e) { return e[1]; });

        int ans = 0, size = 0; // 把 events 当作栈
        for (auto& e : events) {
            int start_time = e[0], value = e[2];
            auto it = ranges::lower_bound(events.begin(), events.begin() + size, start_time, {}, [](auto& e) { return e[1]; });
            if (it != events.begin()) {
                ans = max(ans, value + (*--it)[2]);
            } else {
                ans = max(ans, value);
            }

            if (size == 0 || value > events[size - 1][2]) {
                events[size++] = e;
            }
        }
        return ans;
    }
};
func maxTwoEvents(events [][]int) (ans int) {
slices.SortFunc(events, func(a, b []int) int { return a[1] - b[1] })

st := events[:0] // 把 events 当作栈
for _, e := range events {
startTime, value := e[0], e[2]
i := sort.Search(len(st), func(i int) bool { return st[i][1] >= startTime }) - 1
if i >= 0 {
ans = max(ans, value+events[i][2])
} else {
ans = max(ans, value)
}
if len(st) == 0 || value > st[len(st)-1][2] {
st = append(st, e)
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{events}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。

专题训练

  1. 单调栈题单。
  2. 动态规划题单的「§7.2 不相交区间」。

分类题单

如何科学刷题?

  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站@灵茶山艾府

从微信公众号&小程序的SDK剖析JSBridge

从微信公众号&小程序的SDK剖析JSBridge

引言

在移动互联网时代,Hybrid应用已成为主流开发模式之一。JSBridge作为连接JavaScript与Native的核心桥梁,让Web页面能够调用原生能力,实现了跨平台开发的完美平衡。微信作为国内最大的超级应用,其公众号JSSDK和小程序架构为我们提供了绝佳的JSBridge实践案例。本文将深入剖析这两套SDK的实现原理,帮助读者理解JSBridge的本质与设计思想。

一、JSBridge核心概念

1.1 什么是JSBridge

JSBridge是JavaScript与Native之间的通信桥梁,它建立了双向消息通道,使得:

  • JavaScript调用Native: Web页面可以调用原生能力(相机、地理位置、支付等)
  • Native调用JavaScript: 原生代码可以向Web页面传递数据或触发事件

1.2 JSBridge通信架构

graph TB
    subgraph WebView层
        A[JavaScript代码]
    end

    subgraph JSBridge层
        B[消息队列]
        C[协议解析器]
    end

    subgraph Native层
        D[原生API Handler]
        E[系统能力]
    end

    A -->|发起调用| B
    B -->|解析协议| C
    C -->|转发请求| D
    D -->|调用能力| E
    E -->|返回结果| D
    D -->|回调| C
    C -->|执行callback| A

    style A fill:#e1f5ff
    style E fill:#fff4e1
    style C fill:#f0f0f0

1.3 通信方式对比

JSBridge主要有三种实现方式:

方式 原理 优点 缺点
URL Schema拦截 通过iframe.src触发特定协议 兼容性好,iOS/Android通用 有URL长度限制,不支持同步返回
注入API Native向WebView注入全局对象 调用简单直接 Android 4.2以下有安全风险
MessageHandler WKWebView的postMessage机制 性能好,安全性高 仅iOS可用

二、微信公众号JSSDK实现原理

2.1 JSSDK架构设计

微信公众号的JSSDK基于WeixinJSBridge封装,提供了更安全和易用的接口。

sequenceDiagram
    participant H5 as H5页面
    participant SDK as wx-JSSDK
    participant Bridge as WeixinJSBridge
    participant Native as 微信客户端

    H5->>SDK: 调用wx.config()
    SDK->>Native: 请求签名验证
    Native-->>SDK: 返回验证结果

    H5->>SDK: 调用wx.chooseImage()
    SDK->>Bridge: invoke('chooseImage', params)
    Bridge->>Native: 转发调用请求
    Native->>Native: 打开相册选择
    Native-->>Bridge: 返回图片数据
    Bridge-->>SDK: 触发回调
    SDK-->>H5: success(res)

2.2 JSSDK初始化流程

JSSDK的初始化需要完成配置验证和ready状态准备:

// 步骤1: 引入JSSDK
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>

// 步骤2: 配置权限验证
wx.config({
  debug: false,
  appId: 'your-app-id',
  timestamp: 1234567890,
  nonceStr: 'random-string',
  signature: 'sha1-signature',
  jsApiList: ['chooseImage', 'uploadImage', 'getLocation']
});

// 步骤3: 监听ready事件
wx.ready(function() {
  // 配置成功后才能调用API
  console.log('JSSDK初始化完成');
});

wx.error(function(res) {
  console.error('配置失败:', res);
});

配置验证流程说明:

  1. 获取签名: 后端通过jsapi_ticket和当前URL生成SHA1签名
  2. 前端配置: 将签名等参数传入wx.config()
  3. 客户端验证: 微信客户端校验签名的合法性
  4. 授权完成: 验证通过后触发ready事件

2.3 WeixinJSBridge底层机制

WeixinJSBridge是微信内部提供的原生接口,不对外公开但可以直接使用:

// 检测WeixinJSBridge是否ready
function onBridgeReady() {
  WeixinJSBridge.invoke(
    'getBrandWCPayRequest',
    {
      appId: 'wx123456',
      timeStamp: '1234567890',
      nonceStr: 'randomstring',
      package: 'prepay_id=xxx',
      signType: 'MD5',
      paySign: 'signature'
    },
    function(res) {
      if (res.err_msg === 'get_brand_wcpay_request:ok') {
        console.log('支付成功');
      }
    }
  );
}

if (typeof WeixinJSBridge === 'undefined') {
  document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else {
  onBridgeReady();
}

WeixinJSBridge与wx JSSDK的关系:

  • WeixinJSBridge: 底层原生接口,直接由微信客户端注入,无需引入外部JS
  • wx JSSDK: 基于WeixinJSBridge的高级封装,提供统一的API规范和安全验证
flowchart LR
    A[H5页面] -->|引入jweixin.js| B[wx JSSDK]
    B -->|封装调用| C[WeixinJSBridge]
    C -->|Native注入| D[微信客户端]
    D -->|系统能力| E[&#34;相机、支付、定位等&#34;]

    style B fill:#07c160
    style C fill:#ff9800
    style D fill:#576b95

2.4 典型API调用示例

以选择图片为例,展示完整的调用链路:

// 封装图片选择功能
function selectImages(count = 9) {
  return new Promise((resolve, reject) => {
    wx.chooseImage({
      count: count,          // 最多选择数量
      sizeType: ['original', 'compressed'],
      sourceType: ['album', 'camera'],
      success: function(res) {
        const localIds = res.localIds; // 返回本地图片ID列表
        resolve(localIds);
      },
      fail: function(err) {
        reject(err);
      }
    });
  });
}

// 使用示例
wx.ready(async function() {
  try {
    const imageIds = await selectImages(5);
    console.log('已选择图片:', imageIds);

    // 继续上传图片
    uploadImages(imageIds);
  } catch (error) {
    console.error('选择失败:', error);
  }
});

function uploadImages(localIds) {
  localIds.forEach(localId => {
    wx.uploadImage({
      localId: localId,
      isShowProgressTips: 1,
      success: function(res) {
        const serverId = res.serverId; // 服务器端图片ID
        // 将serverId发送给后端保存
        console.log('上传成功:', serverId);
      }
    });
  });
}

三、微信小程序双线程架构

3.1 小程序架构设计

微信小程序采用双线程模型,将渲染层与逻辑层完全隔离:

graph TB
    subgraph 渲染层[渲染层 View - WebView]
        A[WXML模板]
        B[WXSS样式]
        C[组件系统]
    end

    subgraph 逻辑层[逻辑层 AppService - JSCore]
        D[JavaScript代码]
        E[小程序API - wx对象]
        F[数据管理]
    end

    subgraph 系统层[Native - 微信客户端]
        G[JSBridge]
        H[网络请求]
        I[文件系统]
        J[设备能力]
    end

    A -.->|数据绑定| F
    C -.->|事件触发| D
    D -->|setData| G
    G -->|更新视图| A
    E -->|调用能力| G
    G -->|转发请求| H
    G -->|转发请求| I
    G -->|转发请求| J

    style 渲染层 fill:#e3f2fd
    style 逻辑层 fill:#f3e5f5
    style 系统层 fill:#fff3e0

架构设计的核心优势:

  1. 安全隔离: 逻辑层无法直接操作DOM,防止XSS攻击
  2. 多WebView支持: 每个页面独立WebView,支持多页面并存
  3. 性能优化: 逻辑层使用JSCore,不加载DOM/BOM,执行更快

3.2 小程序JSBridge通信机制

sequenceDiagram
    participant Logic as 逻辑层<br/>(JSCore)
    participant Bridge as JSBridge
    participant Native as Native层
    participant View as 渲染层<br/>(WebView)

    Note over Logic,View: 场景1: 数据更新
    Logic->>Bridge: setData({key: value})
    Bridge->>Native: 序列化数据
    Native->>View: 传递Virtual DOM diff
    View->>View: 更新页面渲染

    Note over Logic,View: 场景2: 事件响应
    View->>Bridge: bindtap事件触发
    Bridge->>Native: 序列化事件对象
    Native->>Logic: 调用事件处理函数
    Logic->>Logic: 执行业务逻辑

    Note over Logic,View: 场景3: API调用
    Logic->>Bridge: wx.request(options)
    Bridge->>Native: 转发网络请求
    Native->>Native: 发起HTTP请求
    Native-->>Bridge: 返回响应数据
    Bridge-->>Logic: 触发success回调

3.3 数据通信实现

setData是小程序中最核心的通信API,用于逻辑层向渲染层传递数据:

Page({
  data: {
    userInfo: {},
    items: []
  },

  onLoad: function() {
    // 通过setData更新数据,触发视图更新
    this.setData({
      userInfo: {
        name: '张三',
        avatar: 'https://example.com/avatar.jpg'
      },
      items: [1, 2, 3, 4, 5]
    });
  },

  // 优化建议: 只更新变化的字段
  updateUserName: function(newName) {
    this.setData({
      'userInfo.name': newName  // 使用路径语法,减少数据传输
    });
  },

  // 避免频繁setData
  handleScroll: function(e) {
    // 错误示范: 每次滚动都setData
    // this.setData({ scrollTop: e.detail.scrollTop });

    // 正确做法: 节流处理
    clearTimeout(this.scrollTimer);
    this.scrollTimer = setTimeout(() => {
      this.setData({ scrollTop: e.detail.scrollTop });
    }, 100);
  }
});

setData底层流程:

  1. 序列化数据: 将JS对象序列化为JSON字符串
  2. 通过JSBridge发送: Native层接收数据
  3. 传递到渲染层: Native将数据转发到WebView
  4. Virtual DOM Diff: 计算差异并更新视图

3.4 小程序API调用机制

小程序的wx对象是Native注入的JSBridge接口:

// 网络请求示例
function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    wx.request({
      url: `https://api.example.com/user/${userId}`,
      method: 'GET',
      header: {
        'content-type': 'application/json'
      },
      success(res) {
        if (res.statusCode === 200) {
          resolve(res.data);
        } else {
          reject(new Error(`请求失败: ${res.statusCode}`));
        }
      },
      fail(err) {
        reject(err);
      }
    });
  });
}

// 使用async/await优化
async function loadUserInfo() {
  wx.showLoading({ title: '加载中...' });

  try {
    const userData = await fetchUserData(123);
    this.setData({ userInfo: userData });
  } catch (error) {
    wx.showToast({
      title: '加载失败',
      icon: 'none'
    });
  } finally {
    wx.hideLoading();
  }
}

API调用流程图:

flowchart TD
    A[小程序调用 wx.request] --> B{JSBridge检查}
    B -->|参数校验| C[序列化请求参数]
    C --> D[Native接管网络请求]
    D --> E[系统发起HTTP请求]
    E --> F{请求结果}
    F -->|成功| G[回调success函数]
    F -->|失败| H[回调fail函数]
    G --> I[返回数据到逻辑层]
    H --> I
    I --> J[complete函数执行]

    style A fill:#07c160
    style D fill:#ff9800
    style E fill:#2196f3

四、自定义JSBridge实现

4.1 基础实现方案

基于URL Schema拦截实现一个简单的JSBridge:

class JSBridge {
  constructor() {
    this.callbacks = {};
    this.callbackId = 0;

    // 注册全局回调处理函数
    window._handleMessageFromNative = this._handleCallback.bind(this);
  }

  // JavaScript调用Native
  callNative(method, params = {}, callback) {
    const cbId = `cb_${this.callbackId++}`;
    this.callbacks[cbId] = callback;

    const schema = `jsbridge://${method}?params=${encodeURIComponent(
      JSON.stringify(params)
    )}&callbackId=${cbId}`;

    // 创建隐藏iframe触发schema
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = schema;
    document.body.appendChild(iframe);

    setTimeout(() => {
      document.body.removeChild(iframe);
    }, 100);
  }

  // Native回调JavaScript
  _handleCallback(callbackId, result) {
    const callback = this.callbacks[callbackId];
    if (callback) {
      callback(result);
      delete this.callbacks[callbackId];
    }
  }

  // 注册可被Native调用的方法
  registerHandler(name, handler) {
    this[name] = handler;
  }
}

// 使用示例
const bridge = new JSBridge();

// 调用Native方法
bridge.callNative('getLocation', {
  type: 'wgs84'
}, function(location) {
  console.log('位置信息:', location);
});

// 注册供Native调用的方法
bridge.registerHandler('updateTitle', function(title) {
  document.title = title;
});

4.2 Promise风格封装

将回调风格改造为Promise,提升开发体验:

class ModernJSBridge extends JSBridge {
  invoke(method, params = {}) {
    return new Promise((resolve, reject) => {
      this.callNative(method, params, (result) => {
        if (result.code === 0) {
          resolve(result.data);
        } else {
          reject(new Error(result.message));
        }
      });
    });
  }
}

// 现代化使用方式
const bridge = new ModernJSBridge();

async function getUserLocation() {
  try {
    const location = await bridge.invoke('getLocation', {
      type: 'wgs84'
    });
    console.log('经度:', location.longitude);
    console.log('纬度:', location.latitude);
  } catch (error) {
    console.error('获取位置失败:', error.message);
  }
}

4.3 Native端实现(以Android为例)

Android端需要拦截WebView的URL请求并解析协议:

// 这是伪代码示意,用JavaScript语法描述Android的WebViewClient逻辑

class JSBridgeWebViewClient {
  shouldOverrideUrlLoading(view, url) {
    // 拦截自定义协议
    if (url.startsWith('jsbridge://')) {
      this.handleJSBridgeUrl(url);
      return true;  // 拦截处理,不加载URL
    }
    return false;  // 正常加载
  }

  handleJSBridgeUrl(url) {
    // 解析: jsbridge://getLocation?params=xxx&callbackId=cb_1
    const urlObj = new URL(url);
    const method = urlObj.hostname;  // getLocation
    const params = JSON.parse(
      decodeURIComponent(urlObj.searchParams.get('params'))
    );
    const callbackId = urlObj.searchParams.get('callbackId');

    // 调用原生能力
    switch(method) {
      case 'getLocation':
        this.getLocation(params, (location) => {
          // 回调JavaScript
          this.callJS(callbackId, {
            code: 0,
            data: location
          });
        });
        break;
    }
  }

  callJS(callbackId, result) {
    const script = `window._handleMessageFromNative('${callbackId}', ${
      JSON.stringify(result)
    })`;
    webView.evaluateJavascript(script, null);
  }

  getLocation(params, callback) {
    // 调用Android LocationManager获取位置
    // 这里是伪代码,实际需要原生Java/Kotlin实现
    const location = {
      longitude: 116.404,
      latitude: 39.915
    };
    callback(location);
  }
}

五、性能优化与最佳实践

5.1 性能优化要点

graph TB
    A[JSBridge性能优化] --> B[通信优化]
    A --> C[数据优化]
    A --> D[调用优化]
    A --> E[内存管理]

    B --> B1[减少通信频次]
    B --> B2[批量传输数据]
    B --> B3[使用增量更新]
    B --> B4[避免大数据传输]

    C --> C1[JSON序列化优化]
    C --> C2[数据压缩]
    C --> C3[惰性加载]
    C --> C4[缓存机制]

    D --> D1[异步非阻塞]
    D --> D2[超时处理]
    D --> D3[失败重试]
    D --> D4[降级方案]

    E --> E1[及时释放回调]
    E --> E2[避免内存泄漏]
    E --> E3[限制队列长度]

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#f3e5f5
    style D fill:#e8f5e9
    style E fill:#ffe0b2

5.2 最佳实践

1. 合理使用setData(小程序场景):

// 不好的做法
for (let i = 0; i < 100; i++) {
  this.setData({
    [`items[${i}]`]: data[i]
  });  // 100次通信
}

// 好的做法
const updates = {};
for (let i = 0; i < 100; i++) {
  updates[`items[${i}]`] = data[i];
}
this.setData(updates);  // 1次通信

2. 实现超时与错误处理:

class SafeJSBridge extends ModernJSBridge {
  invoke(method, params = {}, timeout = 5000) {
    return Promise.race([
      super.invoke(method, params),
      new Promise((_, reject) => {
        setTimeout(() => {
          reject(new Error(`调用${method}超时`));
        }, timeout);
      })
    ]);
  }
}

// 使用
try {
  const result = await bridge.invoke('slowMethod', {}, 3000);
} catch (error) {
  if (error.message.includes('超时')) {
    console.error('请求超时,请检查网络');
  }
}

3. 权限与安全检查:

// JSSDK安全最佳实践
const secureConfig = {
  // 1. 签名在后端生成,前端不暴露secret
  getSignature: async function(url) {
    const response = await fetch('/api/wechat/signature', {
      method: 'POST',
      body: JSON.stringify({ url })
    });
    return response.json();
  },

  // 2. 动态配置jsApiList,按需授权
  init: async function() {
    const signature = await this.getSignature(location.href);
    wx.config({
      ...signature,
      jsApiList: ['chooseImage']  // 只申请需要的权限
    });
  }
};

六、调试技巧

6.1 调试流程

flowchart LR
    A[开发阶段] --> B{启用debug模式}
    B -->|wx.config debug:true| C[查看vconsole日志]
    B -->|Chrome DevTools| D[断点调试]

    C --> E[检查API调用]
    D --> E

    E --> F{定位问题}
    F -->|签名错误| G[检查后端签名逻辑]
    F -->|API调用失败| H[检查权限配置]
    F -->|通信异常| I[检查JSBridge实现]

    G --> J[修复并重测]
    H --> J
    I --> J

    style B fill:#ff9800
    style F fill:#f44336
    style J fill:#4caf50

6.2 常见问题排查

1. 微信JSSDK签名失败:

// 调试签名问题
wx.config({
  debug: true,  // 开启调试模式
  // ... 其他配置
});

wx.error(function(res) {
  console.error('配置失败详情:', res);
  // 常见错误:
  // invalid signature - 签名错误,检查URL是否一致(不含#hash)
  // invalid url domain - 域名未配置到白名单
});

// 检查点:
// 1. 确保URL不包含hash部分
const url = location.href.split('#')[0];

// 2. 确保timestamp是整数
const timestamp = Math.floor(Date.now() / 1000);

// 3. 确保签名算法正确(SHA1)
// 签名原串: jsapi_ticket=xxx&noncestr=xxx&timestamp=xxx&url=xxx

2. 小程序setData性能问题:

// 开启性能监控
wx.setEnableDebug({
  enableDebug: true
});

// 监控setData性能
const perfObserver = wx.createPerformanceObserver((entries) => {
  entries.getEntries().forEach((entry) => {
    if (entry.entryType === 'render') {
      console.log('渲染耗时:', entry.duration);
    }
  });
});

perfObserver.observe({ entryTypes: ['render', 'script'] });

七、总结

JSBridge作为Hybrid开发的核心技术,通过建立JavaScript与Native的通信桥梁,实现了Web技术与原生能力的完美融合。本文通过剖析微信公众号JSSDK和小程序SDK,深入理解了以下关键点:

  1. 通信机制: URL Schema拦截、API注入、MessageHandler三种主流方式
  2. 架构设计: 微信小程序的双线程模型提供了安全性和性能的最佳平衡
  3. 实现原理: 从JSSDK的签名验证到小程序的setData机制,理解了完整的调用链路
  4. 最佳实践: 性能优化、错误处理、安全防护等工程化经验

掌握JSBridge原理不仅能帮助我们更好地使用微信生态的各种能力,也为构建自己的Hybrid框架提供了坚实的理论基础。在实际项目中,应根据具体场景选择合适的实现方案,并持续关注性能与安全,打造更优质的用户体验。

参考资料

年终总结 - 2025 故事集

📕 如果您刚好点了进来,却不想完整阅读该文章但又想知道它记录了什么。可跳到文末总结

前言

时隔四个月,再执笔即将进入了新的一年 2026 年...

2025 & 2026

时间像往常一样无声息地流动,已近年尾,在过去的 2025 年,三百多天时间里面,发生了很多的事情,或喜,或悲,或静,或闹...此时,灯亮着,窗外偶尔有远处汽车的沙沙声。我在其中,开始回顾并记录撞进心底的瞬间和感受。

你好,世界

还是熟悉的四月份的一天凌晨,老妈跟我在走廊里踱步~

随着清脆的哭声响起,二宝如期而至。过了段时间,护士出来报出母女平安是我们听到的此刻最让人心安的话语。

为什么说是熟悉的四月份,因为老大也是四月份出生的

因为老婆在工作日凌晨分娩,所以我的休陪产的单也先提交了。在收到老婆产后无需我协助事情的话语后,我撤销了陪产单,屁颠屁颠地去上班赚奶粉钱了😄

嗯,从准奶爸到首次喜当爹至今,短短三年时间里面,自己已经是两个小孩的爸爸,真是一个让自己意想不到的速度。

自从当了父母之后,我们更加懂得自己父母的无私且伟大,孩子的天真和无知

相对于第一次喜当爹时候,自己慌张无措,老妈辛苦地忙前忙后,手慌脚乱。有了第一次的经验,我们对于二宝的处理还是挺稳定:

  • 在预产期临近的两三天,我们准备好了大包小包的待产包 -> alway stand by
  • 产后的三天时间,请护工照看老婆和新生儿,老妈在旁边陪同,老爸在家照看大宝
  • 出院后,老婆和二宝直接月子中心坐月子。老妈和我在家照看大宝,周末月子中心看二宝

daughters in nursing room

👆即将出月子中心,大宝和二宝的合影👆

在日常里接力的我们

每天,我们都觉得时间不够用,能留出些许空间和时间来放松,已经很满足😌

老婆来回奔波的工作日

在休完三个多月的产假之后,老婆就去复工了。因为二宝还小,老婆会每天中午都回来哺乳。从小孩三个多月到七个多月,雷打不动,公司和家两头跑。

那一台小电驴,隔三差五就需要去充电。小小电驴,已经超出了它的价值~

好不容易,让二宝断奶了。断奶是件很痛苦的事情,要熬夜,涨奶胸痛等。我还记得在成功断奶后的那天晚上,老婆还特意叫我出去买瓶酒回来庆祝一下✨

beer

👆5%-8% vol 的鸡尾酒👆

虽然二宝断奶了,但是老婆在工作不忙的时候,还是会中午回来看看。用我老婆的话说:有点讨厌,但是又有点舍不得二宝

工作日,爷爷奶奶的时光

老婆跟我,工作日都需要上班,嗯~赚奶粉钱😀

然后,两个宝宝,工作日的时候主要给爷爷和奶奶带。

有时候,两个宝宝都需要奶奶抱,这可苦了奶奶的腰板子了。爷爷更多的时候,是充当了厨师的角色,保证一家人的三餐伙食,嗯~老爸的厨艺真好👍

爷爷奶奶一天下来的流程:早上带娃出去晒太阳,遛娃(主要是让大宝动起来,中午好睡觉);中午喂饭,午休(大宝一般中午休息两个钟,下午三或四点起来);下午洗澡(怕冷着小孩,一般天黑前洗完),喂饭,陪玩;晚上,等老婆和我下班回来,爷爷和奶奶才有空闲的时间。一般这个时候,爷爷就喜欢下楼去周边逛,奶奶就会躺着床上直一下腰板子(有时会跟爷爷下楼逛街)。工作日的时候,如果奶奶晚上没有出去逛街,那么,会在九点多喂完奶给大宝,奶奶会哄大宝睡觉;如果奶奶外出,那么我就会哄大宝睡觉。

mother's birthday

👆奶奶生日的时候,两宝和爷爷奶奶合影👆

休息日,我们的时光

工作日,班上完了;休息日,该带娃了。爷爷奶奶休息日放假,想去哪里就去哪里,放松放松。

休息日带娃,我们的宗旨就是:尽量让娃多动。所以,我们基本都会外出。忙忙碌碌,嗯,我们分享两件事情:

我还记得,某个周末,我们在商场逛了一天,让大宝在商场里面走,她逛得贼开心(这可不,逛得有多累,睡得有多香),推着二宝。中午直接在商场里面解决吃饭的问题,大宝直接在婴儿车上解决了午睡的事情,二宝则是被老婆或者我背在身上睡觉。母婴室没人的时候,我们就会在里面小憩一会。等两宝醒来之后,再逛一下,一天的时间过得慢但是又很快

今年的国庆连着中秋,我们在这个长假期里面,会带他们在小区里面露营(在草坪上铺一垫子),让她们自己玩。大宝走路的年纪,这里走那里走,我得屁颠屁颠跟她后面,从这里把她抱过来那里,从那边把她哄过来这边,真想拿条链子绑着她。相反,二宝就淡定多了,只能在那块布那里爬来爬去,被她妈妈限制着。

Mid-Autumn Festival

👆中秋节当晚,在哄两娃睡着后,老婆跟我在阳台拜月👆

没有惊喜的工位

相对于上一年工作的惊吓,今年的工作可以用没有惊喜来形容。

至于为什么说上一年是惊吓,今年没有惊喜。后面有时间,会出一篇文章来分享下。

简简单单的工位,一水杯,一记事本,一台式电脑,一绿植。屁股一坐,一整天嗖一下就过去了~

在公司,让我活跃起来的,就是中午吃饭的时候。我们的小团体(一安卓,一产品和我)开车去周边吃饭。这段时间,是我们唠嗑的时间,无拘无束,即使我们偶尔会浪费掉午休的时间,但是我还是觉得挺不错的,时间花得值...

工作上糟心的事十根手指可数不过来,触动且温暖了心窝的事情屈指可数。

记得招进来的一个新人,我带了他几天,最后入职短短几天被某人恶心而离职了。他离职的前一天,点了一杯奶茶给我,虽然自己嘴里面说着别客气,但是心里面暖暖的。他才进来短短几天就走人了,自己心里莫名生气:为什么我自己招的人,自己带着熟悉项目后,一转手就被恶心到要离职了???最终他却还温柔地以自我问题作离职的原因。

colleague communication

👆点了份奶茶放我桌面后的对话👆

把明天轻轻放进心里

2026 年悄然将至。在对新的一年有所展望之前,我们先回顾下年终总结 - 2024 故事集中立下的两个 Flags 和完成的情况:

序号 目标 实际 完成率
1 分享文章 20+ 分享文章 18 90%
2 锻炼 30+ 锻炼 32 107%

嗯~ 目标完成率还不赖。

do execise

👆每次锻炼我都会在朋友圈记录,每次耗时 45 分钟左右👆

对于分享文章,一开始就是秉承着记录自己在工作中遇到的一些问题,方便自己和其他人查找的宗旨来进行记录,后面是因为平台搞了奖励而进行的一些创作。而现在,随着 chatgpt, deepseek 等大语言模型的机器人横空出世,浅显的分享和问题的记录都显得鸡肋。所以,在 2026 新的一年内,文章的分享要更加有目的性和实际的意义。2026 年,谁知道会有几篇文章会出炉,也许一篇,也许十篇,也许二十篇,也许零篇。

对于锻炼,这是我长期需要坚持的一件事情,也是最好量化的事情。在新的一年里面,锻炼的次数需 35+

为人父母,为人儿女。我们都有自己的那份责任,2026 年,希望自己更多的时间是回归家庭 - 去听听孩子的欢声笑语,去看看爸妈脸上的笑容,去体验大家聚在一起热热闹闹的氛围 and more

family gathering

👆老爸生日,大姐,二姐大家的娃聚在一起👆

总结

2025 年,简简单单却忙忙碌碌👇:

在生活方面,欢迎二宝加入大家庭。这让我们接下来的一年时间里面,时间安排更加充实紧凑,更感受到当爹妈的不容易,感恩自己的父母在以前那年代含辛茹苦带大了我们三姐弟。在工作方面,没有太多想记录的东西,平平淡淡地打卡上下班。

展望 2026,还是给自己制定了锻炼次数的量化目标。在这个人工智能逐渐成熟的环境下,希望自己能够使用它提升工作效率和帮助自己成长。在 2026 年,自己的重心会放在家庭这边,去感受孩子的成长和家的氛围。

完成于中国广东省广州市

2025 年 12 月 22 日

C# 正则表达式(2):Regex 基础语法与常用 API 全解析

一、IsMatch 入门

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        string input = "2025-12-18";
        string pattern = @"^\d{4}-\d{2}-\d{2}$";

        bool isValid = Regex.IsMatch(input, pattern);
        Console.WriteLine(isValid); // True
    }
}

解析:

  • pattern 是正则表达式,@"..." 是 C# 的逐字字符串字面量。
  • ^$:锚点,表示“从头到尾整串匹配”
  • \d{4}:4 位数字。
  • -:字面量“-”。
  • Regex.IsMatch:看字符串中是不是“满足这个模式”。

二、C# Regex 的 5 个核心方法

System.Text.RegularExpressions.Regex 里,最常用的就是这 5 个方法:

  1. IsMatch
  2. Match
  3. Matches
  4. Replace
  5. Split

三、Regex.IsMatch:最常用的“判断是否匹配”

IsMatch 是表单校验、输入合法性检查中使用频率最高的方法。

bool isEmail = Regex.IsMatch(email, pattern);

示例:

string input = "Order12345";
string pattern = @"\d{3}";

bool has3Digits = Regex.IsMatch(input, pattern); // True

注意点:

  • 默认只要“包含”满足 pattern 的子串,就返回 true,并不要求整个字符串都完全匹配。
  • 如果你想“整个字符串必须符合这个规则”,要在 pattern 外面加上 ^$
// 只允许由 3~5 位数字组成,不允许多一个字符
string pattern = @"^\d{3,5}$";

四、Regex.Match:获取第一个匹配

string text = "My phone is 123-456-7890.";
string pattern = @"\d{3}-\d{3}-\d{4}";

Match match = Regex.Match(text, pattern);
if (match.Success)
{
    Console.WriteLine(match.Value);  // "123-456-7890"
    Console.WriteLine(match.Index);  // 起始索引
    Console.WriteLine(match.Length); // 匹配的长度
}

常用成员:

  • match.Success:是否匹配成功。
  • match.Value:匹配到的字符串。
  • match.Index:匹配在原文本中的起始位置(从 0 开始)。
  • match.Length:长度。

Regex.Match 也有带起始位置、带 RegexOptions 的重载:


五、Regex.Matches:获取所有匹配结果(多个)

string text = "ID: 100, 200, 300";
string pattern = @"\d+";

MatchCollection matches = Regex.Matches(text, pattern);
foreach (Match m in matches)
{
    Console.WriteLine($"{m.Value} at {m.Index}");
}
// 输出:
// 100 at 4
// 200 at 9
// 300 at 14

解析:

  • 返回的是一个 MatchCollection,可以 foreach 遍历。
  • 每个 Match 和前面一样,有 ValueIndexGroups 等属性。

六、Regex.Replace:按模式搜索并替换

Regex.Replace 和字符串的 Replace 很像,但支持模式匹配。

1. 固定字符串替换匹配内容

string input = "2025/12/18";
string pattern = @"/";

string result = Regex.Replace(input, pattern, "-");
Console.WriteLine(result); // "2025-12-18"

这相当于“把所有 / 都换成 -”,和 input.Replace("/", "-") 类似,但 pattern 可以写得更复杂。

2.用捕获组重排内容

string input = "2025-12-18";
string pattern = @"(\d{4})-(\d{2})-(\d{2})";

// 把 yyyy-MM-dd 改成 dd/MM/yyyy
string result = Regex.Replace(input, pattern, "$3/$2/$1");
// result: "18/12/2025"

解析:

这里的 $1$2$3 是捕获组

3. 更高级的 MatchEvaluator 版本

string input = "Price: 100 USD, 200 USD";
string pattern = @"(\d+)\s*USD";

string result = Regex.Replace(input, pattern, m =>
{
    int value = int.Parse(m.Groups[1].Value);
    int converted = (int)(value * 7.2); // 假设汇率
    return $"{converted} CNY";
});

Console.WriteLine(result);
// "Price: 720 CNY, 1440 CNY"

七、Regex.Split:按“模式”切割字符串

可以实现多分隔符的切割

string input = "apple, banana; cherry|date";
string pattern = @"[,;|]\s*"; // 逗号;分号;竖线 + 可选空白

string[] parts = Regex.Split(input, pattern);

foreach (var p in parts)
{
    Console.WriteLine(p);
}
// apple
// banana
// cherry
// date

八、正则基础语法(一):字面字符与转义

1. 字面字符

绝大多数普通字符在正则里就是字面意思:

  • 模式:abc → 匹配文本中出现的 abc
  • 模式:hello → 匹配文本中出现的 hello

2. 特殊字符(元字符)

这些字符在正则中有特殊含义:

  • . ^ $ * + ? ( ) [ ] { } \ |

如果你要匹配其中任意一个“字面意义上的”字符,就要用 \ 转义。

例如:

  • 匹配一个点号 . → 模式 \.
  • 匹配一个星号 * → 模式 \*
  • 匹配一对括号 (abc) → 模式 \( + abc + \)

在 C# 中配合逐字字符串:

string pattern = @"\.";   // 匹配 "."
string pattern2 = @"\*";  // 匹配 "*"

如果不用 @

string pattern = "\\.";   // C# 字符串里写成 "\\." 才表示一个反斜杠+点

实践中几乎所有正则字符串都用 @"",可以少一半反斜杠。


九、正则基础语法(二):预定义字符类 \d / \w / \s

预定义字符类是正则里最常用的工具,它们代表一类字符。

1. \d / \D:数字与非数字

  • \d:digit,匹配 0–9 的任意一位数字,相当于 [0-9]
  • \D:非数字,相当于 [^0-9]

示例:匹配一个或多个数字

string pattern = @"\d+";

2. \w / \W:单词字符与非单词字符

  • \w:word,匹配字母、数字和下划线,相当于 [A-Za-z0-9_]
  • \W:非 \w

示例:匹配“单词”(一串字母数字下划线)

string pattern = @"\w+";

3. \s / \S:空白字符与非空白字符

  • \s:space,匹配空格、制表符、换行等所有空白字符。
  • \S:非空白。

示例:

string pattern = @"\s+"; // 匹配一个或多个空白

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文

GIS 数据转换:使用 GDAL 将 TXT 转换为 Shp 数据

前言

TXT 作为一种文本格式,可以很方便的存储一些简单几何数据。在 GIS 开发中,经常需要进行数据的转换处理,其中常见的便是将 TXT 转换为 Shp 数据进行展示。

本篇教程在之前一系列文章的基础上讲解

如如果你还没有看过,建议从以上内容开始。

1. 开发环境

本文使用如下开发环境,以供参考。

时间:2025年

系统:Windows 11

Python:3.11.7

GDAL:3.11.1

2. 数据准备

TXT(纯文本文件)是一种最基本的文件格式,仅存储无格式的文本数据,适用于各种场景(如数据交换、日志记录、配置文件等)。

如下是全国省会城市人口 TXT 文本结构:

ID,Name,Longitude,Latitude,Population
1,Beijing,116.40,39.90,21712,Shanghai,121.47,31.23,24873,Guangzhou,113.26,23.12,18684,Shenzhen,114.05,22.55,17565,Tianjin,117.20,39.08,13736,Chongqing,106.50,29.53,32057,Chengdu,104.06,30.67,20948,Wuhan,114.30,30.60,11219,Hangzhou,120.15,30.28,119410,Nanjing,118.78,32.04,93111,Xi'an,108.93,34.27,129512,Changsha,112.97,28.20,83913,Zhengzhou,113.62,34.75,126014,Harbin,126.63,45.75,107615,Shenyang,123.43,41.80,83116,Qingdao,120.38,36.07,100717,Dalian,121.62,38.92,74518,Xiamen,118.08,24.48,51619,Ningbo,121.55,29.88,85420,Hefei,117.28,31.86,93721,Fuzhou,119.30,26.08,82922,Jinan,117.00,36.67,92023,Taiyuan,112.55,37.87,53024,Changchun,125.35,43.88,90625,Kunming,102.72,25.04,84626,Nanning,108.37,22.82,87427,Lanzhou,103.82,36.06,43528,Yinchuan,106.27,38.47,28529,Xining,101.77,36.62,26330,Urümqi,87.62,43.82,40531,Lhasa,91.11,29.65,8632,Haikou,110.20,20.05,287

3. 导入依赖

TXT作为一种矢量数据格式,可以使用矢量库OGR进行处理,以实现TXT数据从文本格式转换为Shp格式。其中还涉及坐标定义,所以还需要引入osr模块。

from osgeo import ogr,osr
import os
import csv

4. 数据读取与转换

定义一个方法Txt2Shp(txtPath,shpPath,encoding="UTF-8")用于将TXT数据转换为Shp数据。

"""
说明:将 TXT 文件转换为 Shapfile 文件
参数:
    -txtPath:TXT 文件路径
    -shpPath:Shp 文件路径
    -encoding:TXT 文件编码
"""
def Txt2Shp(txtPath,shpPath,encoding="UTF-8")

在进行TXT数据格式转换之前,需要检查数据路径是否存在。

# 检查文件是否存在
if os.path.exists(txtPath):
    print("TXT 文件存在。")
else:
    print("TXT 文件不存在,请重新选择文件!")
    return

通过GetDriverByName获取Shp数据驱动,并使用os.path.exists方法检查Shp文件是否已经创建,如果存在则将其删除。

# 注册所有驱动
ogr.RegisterAll()

# 添加Shp数据源
shpDriver = ogr.GetDriverByName('ESRI Shapefile')

if os.path.exists(shpPath):
    try:
        shpDriver.DeleteDataSource(shpPath)
        print("文件已删除!")
    except Exception as e:
        print(f"文件删除出错:{e}")
        return False

接着创建Shp数据源和空间参考,数据坐标系这里定义为4326。

# 创建Shp数据源
shpDataSource = shpDriver.CreateDataSource(shpPath)
if shpDataSource is None:
    print("无法创建Shp数据源,请检查文件!")
    return false
# 创建空间参考
spatialReference = osr.SpatialReference()
spatialReference.ImportFromEPSG(4326)

之后通过数据源方法CreateLayer创建Shp图层,使用图层方法CreateField添加属性字段,需要定义属性名称以及属性字段类型。

# 创建图层
shpLayer = shpDataSource.CreateLayer("points",spatialReference,ogr.wkbPoint)

# 添加图层字段
shpLayer.CreateField(ogr.FieldDefn("ID",ogr.OFTString))
shpLayer.CreateField(ogr.FieldDefn("Name",ogr.OFTString))
shpLayer.CreateField(ogr.FieldDefn("Longitude",ogr.OFTReal))
shpLayer.CreateField(ogr.FieldDefn("Latitude",ogr.OFTReal))
shpLayer.CreateField(ogr.FieldDefn("Population",ogr.OFTString))

读取TXT数据并将其转换为Shapefile数据,在打开数据时,根据TXT文件属性,使用逗号分隔符进行读取并跳过表头行数据。之后根据行数据进行属性遍历,将读取的字段值和几何属性写入到要素对象中。

# 读取TXT文件
with open(txtPath,"r",encoding=encoding) as txtFile:
    # 根据逗号分隔符进行读取
    reader = csv.reader(txtFile,delimiter=",")
    # 跳过表头
    header = next(reader)
    # 遍历记录
    for row in reader:
        print(f"要素记录:{row}")
        # 创建要素
        feature = ogr.Feature(shpLayer.GetLayerDefn())

        # 根据图层字段写入属性
        feature.SetField("ID",str(row[0]))
        feature.SetField("Name",str(row[1]))
        feature.SetField("Longitude",float(row[2]))
        feature.SetField("Latitude",float(row[3]))
        feature.SetField("Population",str(row[4]))

        # 创建几何对象
        wkt = f"POINT({float(row[2])} {float(row[3])})"
        pointGeom = ogr.CreateGeometryFromWkt(wkt)

        feature.SetGeometry(pointGeom)

        # 将要素添加到图层
        shpLayer.CreateFeature(feature)
        feature = None

CreateCpgFile2Encode(shpPath,encoding)
# 释放数据资源        
shpDataSource = None

其中CreateCpgFile2Encode方法用于创建字符编码文件,后缀名为.cpg

"""
说明:创建.cpg文件指定字符编码
参数:
    -shpPath:Shp文件路径
    -encoding:Shp文件字符编码
"""
def CreateCpgFile2Encode(shpPath,encoding):
    fileName = os.path.splitext(shpPath)[0]
    cpgFile = fileName + ".cpg"

    with open(cpgFile,"w",encoding=encoding) as f:
        f.write(encoding)
        print(f"成功创建编码文件: {cpgFile}")

程序成功转换数据显示如下:

使用ArcMap打开显示结果如下:

从 v5 到 v6:这次 Ant Design 升级真的香

2025 年 11 月底,Ant Design 正式发布了 v6 版本。

回顾过去,从 v3 到 v4 的断崖式升级,到 v5 引入 CSS-in-JS 带来的心智负担和性能压力,很多前端同学一提到“升级”就条件反射般护住发际线。但这一次,Ant Design 团队明显听到了社区的呼声。

v6 没有为了“创新”而搞大刀阔斧的重构,而是聚焦于解决长期痛点提升开发体验平滑迁移。本文结合一线业务开发中的真实场景,聊聊 v6 的核心变化,以及这次升级到底值不值得升。

样式覆盖不再是“玄学”

你一定深有体会:设计师要求改 Select 下拉框背景色、调整 Modal 头部内边距,或者给 Table 的某个单元格加特殊样式。在 v5 及之前,你只能打开控制台,一层层扒 DOM 结构,找到类似 .ant-select-selector 的 class,然后用 :global!important 暴力覆盖。一旦组件库内部 DOM 微调,你的样式就崩了。

全量 DOM 语义化 + 细粒度 classNames / styles API
v6 对所有组件进行了 DOM 语义化改造(如用 <header><main> 等代替无意义的 <div>),更重要的是引入了复数形式的 classNamesstyles 属性,让你直接通过语义化的 key 来定制关键区域。

// v6 写法:精准、安全、健壮
<Modal
  title="业务配置"
  open={true}
  classNames={{
    header: 'my-modal-header',
    body: 'my-modal-body',
    footer: 'my-modal-footer',
    mask: 'glass-blur-mask', // 甚至能直接控制遮罩
    content: 'my-modal-content',
  }}
  styles={{
    header: { borderBottom: '1px solid #eee', padding: '16px 24px' },
    body: { padding: '24px' },
  }}
>
  <p>内容区域...</p>
</Modal>

v5 vs v6 对比(Modal 头部样式定制)

// v5(hack 写法,易崩)
import { global } from 'antd'; // 或直接写 less
:global(.ant-modal-header) {
  border-bottom: 1px solid #eee !important;
}

v6 技术价值

  • 不再依赖内部 class 名:官方承诺这些 key(如 header、body)的存在,即使未来 DOM 结构变化,你的样式依然有效。
  • 支持动态样式styles 属性接受对象,方便结合主题或 props 动态生成。

原生 CSS 变量全面回归

v5 的 CSS-in-JS 方案虽然解决了按需加载和动态主题,但在大型后台系统里,运行时生成样式的 JS 开销仍然明显,尤其在低端设备上切换主题或路由时容易掉帧、闪烁。

v6 的解法:零运行时(Zero-runtime)CSS 变量模式
彻底抛弃 CSS-in-JS,默认使用原生 CSS Variables(Custom Properties)。

  • 体积更小:CSS 文件显著减小(官方称部分场景下减少 30%+)。
  • 响应更快:主题切换只需修改 CSS 变量值,浏览器原生处理,毫秒级生效,无需重新生成哈希类名。
  • 暗黑模式友好:直接通过 --antd-color-primary 等变量实现全局主题切换。

这对需要支持多品牌色、暗黑模式的 SaaS 平台来说,是巨大的性能红利。

高频场景官方接管

瀑布流布局、Drawer 拖拽调整大小、InputNumber 加减按钮等,都是业务中常见需求,但之前往往需要引入第三方库或自己手写,增加维护成本和打包体积。

v6 的解法:新增实用组件 & 交互优化

  • Masonry 瀑布流(内置)
import { Masonry } from 'antd';

<Masonry columns={{ xs: 1, sm: 2, md: 3, lg: 4 }} gutter={16}>
  {items.map(item => (
    <Card key={item.id} cover={<img src={item.cover} />} {...item} />
  ))}
</Masonry>
  • Drawer 支持拖拽:原生支持拖拽改变宽度,无需自己写 resize 逻辑。
  • InputNumber 支持 spinner 模式:加减按钮直接在输入框两侧,像购物车那样。
  • 其他:Tooltip 支持平移(panning)、弹层默认支持模糊蒙层(blur mask)等交互优化。

这些补齐了业务高频场景,减少了“自己造轮子”的痛苦。

升级建议:这次真的“平滑”吗?

v6 迁移关键事实

  • React 版本要求:必须升级到 React 18+(不再支持 React 17 及以下)。
  • 破坏性变更:部分 API 被废弃(如 borderedvariantheadStylestyles.header 等),v7 将彻底移除。
  • 兼容性:v5 项目绝大多数业务逻辑代码无需改动,但若大量使用了深层 hack 样式,可能需要调整。
  • 推荐工具:官方提供 Codemod 迁移脚本,可自动化处理大部分废弃 API。

建议

  1. 新项目:直接上 v6,享受更好的性能、体验和未来维护性。
  2. v5 项目:先在 dev 分支尝试升级。无大量 hack 样式的话,成本很低。
  3. v4 及更老项目:跨度较大,建议先逐步迁移到 v5,再升 v6;或在新模块中使用 v6(配合微前端或包隔离)。
  4. 升级前检查
    • 确认 React ≥ 18
    • 运行官方 Codemod
    • 验证目标浏览器支持 CSS 变量(IE 彻底不支持)

总结

Ant Design v6 是一次**“返璞归真”**的升级。它把控制权还给开发者(语义化 API),用现代浏览器特性解决性能问题(零运行时 CSS 变量),并补齐了业务高频组件。

升级核心收益

  • 更少的 hack 代码,更健壮的样式
  • 显著的性能提升(主题切换、渲染速度)
  • 官方接管高频业务组件,减少第三方依赖
  • 平滑迁移路径,真正降低了“升级火葬场”的风险

对于业务开发者来说,这意味着:更少的加班、更快的页面、更早下班

参考链接

祥源文旅:实际控制人俞发祥因涉嫌犯罪被采取刑事强制措施

36氪获悉,祥源文旅公告,公司实际控制人俞发祥因涉嫌犯罪被绍兴市公安局采取刑事强制措施,案件正在调查过程中。截至公告披露日,公司未收到有关机关要求公司协助调查的通知,公司控制权未发生变化。俞发祥未在公司担任任何职务,公司生产经营一切正常,上述事项不会对公司正常生产经营产生重大影响。

热门中概股美股盘前集体走强,拼多多涨超2%

36氪获悉,热门中概股美股盘前集体走强,截至发稿,拼多多涨超2%,小鹏汽车涨0.76%,蔚来涨0.6%,百度涨0.46%,哔哩哔哩涨0.28%,京东涨0.24%,阿里巴巴涨0.23%,理想汽车跌0.06%。

美股大型科技股盘前普涨,美光科技涨近4%

36氪获悉,美股大型科技股盘前普涨,截至发稿,美光科技涨近4%,英伟达、特斯拉、英特尔涨超1%,谷歌涨0.66%,奈飞涨0.56%,亚马逊涨0.48%,Meta涨0.45%,微软涨0.32%,苹果跌0.16%。

启境首款车开启大规模路测

12月22日,启境首款车开启大规模路测。据了解,该车定位猎装轿跑,将搭载华为乾崑智能汽车解决方案。目前,该车已进入全国多环境路测阶段,预计将在零下30度的极寒、40度以上的高温以及高海拔等多重场景中完成全方位严苛测试,覆盖极寒冰面、高温湿热、高原等环境,重点验证底盘操控、智驾安全、座舱舒适、车身防腐、碰撞安全及NVH静谧性等全方位性能。
❌