普通视图
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
React/Vue 代理配置全攻略:Vite 与 Webpack 实战指南
回顾计算属性的缓存与监听的触发返回结果
《闭包、RAG与AI面试官:一个前端程序员的奇幻LangChain之旅》
JavaScript中的迭代器和生成器
2.2 Node的模块实现
我用 NestJS + Vue3 + Prisma + PostgreSQL 打造了一个企业级 sass 多租户平台
Three.js 入门指南:揭开 3D 网页的魔法面纱
Next.js第十五章(Image)
nest.js / hono.js 一起学!日志功能/统一返回格式/错误处理
PinK(Cocos4.0?)生成飞机大战,抢先体验全流程!
你以为 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 设计铁律
🔥 如果你只能记住一段话,请记住这里
- Props 是组件的“对外接口”,不是随便传的变量
- 解构 Props,是在声明组件的能力边界
- 默认值,决定组件的“基础体验”
- 类型校验,让组件自带说明书
- children,让组件从“可用”变成“好用”
八、写在最后
当你真正理解 Props 之后,你会发现:
- React 不只是 UI 库
- 它在教你如何设计 API
- 如何让别人“用得爽”
Props 写得好不好,决定了一个人 React 水平的上限。
从微信公众号&小程序的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);
});
配置验证流程说明:
- 获取签名: 后端通过jsapi_ticket和当前URL生成SHA1签名
- 前端配置: 将签名等参数传入wx.config()
- 客户端验证: 微信客户端校验签名的合法性
- 授权完成: 验证通过后触发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["相机、支付、定位等"]
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
架构设计的核心优势:
- 安全隔离: 逻辑层无法直接操作DOM,防止XSS攻击
- 多WebView支持: 每个页面独立WebView,支持多页面并存
- 性能优化: 逻辑层使用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底层流程:
- 序列化数据: 将JS对象序列化为JSON字符串
- 通过JSBridge发送: Native层接收数据
- 传递到渲染层: Native将数据转发到WebView
- 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×tamp=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,深入理解了以下关键点:
- 通信机制: URL Schema拦截、API注入、MessageHandler三种主流方式
- 架构设计: 微信小程序的双线程模型提供了安全性和性能的最佳平衡
- 实现原理: 从JSSDK的签名验证到小程序的setData机制,理解了完整的调用链路
- 最佳实践: 性能优化、错误处理、安全防护等工程化经验
掌握JSBridge原理不仅能帮助我们更好地使用微信生态的各种能力,也为构建自己的Hybrid框架提供了坚实的理论基础。在实际项目中,应根据具体场景选择合适的实现方案,并持续关注性能与安全,打造更优质的用户体验。
参考资料
年终总结 - 2025 故事集
📕 如果您刚好点了进来,却不想完整阅读该文章但又想知道它记录了什么。可跳到文末总结。
前言
时隔四个月,再执笔即将进入了新的一年 2026 年...
![]()
时间像往常一样无声息地流动,已近年尾,在过去的 2025 年,三百多天时间里面,发生了很多的事情,或喜,或悲,或静,或闹...此时,灯亮着,窗外偶尔有远处汽车的沙沙声。我在其中,开始回顾并记录撞进心底的瞬间和感受。
你好,世界
还是熟悉的四月份的一天凌晨,老妈跟我在走廊里踱步~
随着清脆的哭声响起,二宝如期而至。过了段时间,护士出来报出母女平安是我们听到的此刻最让人心安的话语。
为什么说是熟悉的四月份,因为老大也是四月份出生的
因为老婆在工作日凌晨分娩,所以我的休陪产的单也先提交了。在收到老婆产后无需我协助事情的话语后,我撤销了陪产单,屁颠屁颠地去上班赚奶粉钱了😄
嗯,从准奶爸到首次喜当爹至今,短短三年时间里面,自己已经是两个小孩的爸爸,真是一个让自己意想不到的速度。
自从当了父母之后,我们更加懂得自己父母的无私且伟大,孩子的天真和无知。
相对于第一次喜当爹时候,自己慌张无措,老妈辛苦地忙前忙后,手慌脚乱。有了第一次的经验,我们对于二宝的处理还是挺稳定:
- 在预产期临近的两三天,我们准备好了大包小包的待产包 -> alway stand by
- 产后的三天时间,请护工照看老婆和新生儿,老妈在旁边陪同,老爸在家照看大宝
- 出院后,老婆和二宝直接月子中心坐月子。老妈和我在家照看大宝,周末月子中心看二宝
![]()
👆即将出月子中心,大宝和二宝的合影👆
在日常里接力的我们
每天,我们都觉得时间不够用,能留出些许空间和时间来放松,已经很满足😌
老婆来回奔波的工作日
在休完三个多月的产假之后,老婆就去复工了。因为二宝还小,老婆会每天中午都回来哺乳。从小孩三个多月到七个多月,雷打不动,公司和家两头跑。
那一台小电驴,隔三差五就需要去充电。小小电驴,已经超出了它的价值~
好不容易,让二宝断奶了。断奶是件很痛苦的事情,要熬夜,涨奶胸痛等。我还记得在成功断奶后的那天晚上,老婆还特意叫我出去买瓶酒回来庆祝一下✨
![]()
👆5%-8% vol 的鸡尾酒👆
虽然二宝断奶了,但是老婆在工作不忙的时候,还是会中午回来看看。用我老婆的话说:有点讨厌,但是又有点舍不得二宝。
工作日,爷爷奶奶的时光
老婆跟我,工作日都需要上班,嗯~赚奶粉钱😀
然后,两个宝宝,工作日的时候主要给爷爷和奶奶带。
有时候,两个宝宝都需要奶奶抱,这可苦了奶奶的腰板子了。爷爷更多的时候,是充当了厨师的角色,保证一家人的三餐伙食,嗯~老爸的厨艺真好👍
爷爷奶奶一天下来的流程:早上带娃出去晒太阳,遛娃(主要是让大宝动起来,中午好睡觉);中午喂饭,午休(大宝一般中午休息两个钟,下午三或四点起来);下午洗澡(怕冷着小孩,一般天黑前洗完),喂饭,陪玩;晚上,等老婆和我下班回来,爷爷和奶奶才有空闲的时间。一般这个时候,爷爷就喜欢下楼去周边逛,奶奶就会躺着床上直一下腰板子(有时会跟爷爷下楼逛街)。工作日的时候,如果奶奶晚上没有出去逛街,那么,会在九点多喂完奶给大宝,奶奶会哄大宝睡觉;如果奶奶外出,那么我就会哄大宝睡觉。
![]()
👆奶奶生日的时候,两宝和爷爷奶奶合影👆
休息日,我们的时光
工作日,班上完了;休息日,该带娃了。爷爷奶奶休息日放假,想去哪里就去哪里,放松放松。
休息日带娃,我们的宗旨就是:尽量让娃多动。所以,我们基本都会外出。忙忙碌碌,嗯,我们分享两件事情:
我还记得,某个周末,我们在商场逛了一天,让大宝在商场里面走,她逛得贼开心(这可不,逛得有多累,睡得有多香),推着二宝。中午直接在商场里面解决吃饭的问题,大宝直接在婴儿车上解决了午睡的事情,二宝则是被老婆或者我背在身上睡觉。母婴室没人的时候,我们就会在里面小憩一会。等两宝醒来之后,再逛一下,一天的时间过得慢但是又很快。
今年的国庆连着中秋,我们在这个长假期里面,会带他们在小区里面露营(在草坪上铺一垫子),让她们自己玩。大宝走路的年纪,这里走那里走,我得屁颠屁颠跟她后面,从这里把她抱过来那里,从那边把她哄过来这边,真想拿条链子绑着她。相反,二宝就淡定多了,只能在那块布那里爬来爬去,被她妈妈限制着。
![]()
👆中秋节当晚,在哄两娃睡着后,老婆跟我在阳台拜月👆
没有惊喜的工位
相对于上一年工作的惊吓,今年的工作可以用没有惊喜来形容。
至于为什么说上一年是惊吓,今年没有惊喜。后面有时间,会出一篇文章来分享下。
简简单单的工位,一水杯,一记事本,一台式电脑,一绿植。屁股一坐,一整天嗖一下就过去了~
在公司,让我活跃起来的,就是中午吃饭的时候。我们的小团体(一安卓,一产品和我)开车去周边吃饭。这段时间,是我们唠嗑的时间,无拘无束,即使我们偶尔会浪费掉午休的时间,但是我还是觉得挺不错的,时间花得值...
工作上糟心的事十根手指可数不过来,触动且温暖了心窝的事情屈指可数。
记得招进来的一个新人,我带了他几天,最后入职短短几天被某人恶心而离职了。他离职的前一天,点了一杯奶茶给我,虽然自己嘴里面说着别客气,但是心里面暖暖的。他才进来短短几天就走人了,自己心里莫名生气:为什么我自己招的人,自己带着熟悉项目后,一转手就被恶心到要离职了???最终他却还温柔地以自我问题作离职的原因。
![]()
👆点了份奶茶放我桌面后的对话👆
把明天轻轻放进心里
2026 年悄然将至。在对新的一年有所展望之前,我们先回顾下年终总结 - 2024 故事集中立下的两个 Flags 和完成的情况:
| 序号 | 目标 | 实际 | 完成率 |
|---|---|---|---|
| 1 | 分享文章 20+ 篇 | 分享文章 18 篇 | 90% |
| 2 | 锻炼 30+ 次 | 锻炼 32 次 | 107% |
嗯~ 目标完成率还不赖。
![]()
👆每次锻炼我都会在朋友圈记录,每次耗时 45 分钟左右👆
对于分享文章,一开始就是秉承着记录自己在工作中遇到的一些问题,方便自己和其他人查找的宗旨来进行记录,后面是因为平台搞了奖励而进行的一些创作。而现在,随着 chatgpt, deepseek 等大语言模型的机器人横空出世,浅显的分享和问题的记录都显得鸡肋。所以,在 2026 新的一年内,文章的分享要更加有目的性和实际的意义。2026 年,谁知道会有几篇文章会出炉,也许一篇,也许十篇,也许二十篇,也许零篇。
对于锻炼,这是我长期需要坚持的一件事情,也是最好量化的事情。在新的一年里面,锻炼的次数需 35+ 。
为人父母,为人儿女。我们都有自己的那份责任,2026 年,希望自己更多的时间是回归家庭 - 去听听孩子的欢声笑语,去看看爸妈脸上的笑容,去体验大家聚在一起热热闹闹的氛围 and more
![]()
👆老爸生日,大姐,二姐大家的娃聚在一起👆
总结
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 个方法:
IsMatchMatchMatchesReplaceSplit
三、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和前面一样,有Value、Index、Groups等属性。
六、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,2171万
2,Shanghai,121.47,31.23,2487万
3,Guangzhou,113.26,23.12,1868万
4,Shenzhen,114.05,22.55,1756万
5,Tianjin,117.20,39.08,1373万
6,Chongqing,106.50,29.53,3205万
7,Chengdu,104.06,30.67,2094万
8,Wuhan,114.30,30.60,1121万
9,Hangzhou,120.15,30.28,1194万
10,Nanjing,118.78,32.04,931万
11,Xi'an,108.93,34.27,1295万
12,Changsha,112.97,28.20,839万
13,Zhengzhou,113.62,34.75,1260万
14,Harbin,126.63,45.75,1076万
15,Shenyang,123.43,41.80,831万
16,Qingdao,120.38,36.07,1007万
17,Dalian,121.62,38.92,745万
18,Xiamen,118.08,24.48,516万
19,Ningbo,121.55,29.88,854万
20,Hefei,117.28,31.86,937万
21,Fuzhou,119.30,26.08,829万
22,Jinan,117.00,36.67,920万
23,Taiyuan,112.55,37.87,530万
24,Changchun,125.35,43.88,906万
25,Kunming,102.72,25.04,846万
26,Nanning,108.37,22.82,874万
27,Lanzhou,103.82,36.06,435万
28,Yinchuan,106.27,38.47,285万
29,Xining,101.77,36.62,263万
30,Urümqi,87.62,43.82,405万
31,Lhasa,91.11,29.65,86万
32,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>),更重要的是引入了复数形式的 classNames 和 styles 属性,让你直接通过语义化的 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 被废弃(如
bordered→variant、headStyle→styles.header等),v7 将彻底移除。 - 兼容性:v5 项目绝大多数业务逻辑代码无需改动,但若大量使用了深层 hack 样式,可能需要调整。
- 推荐工具:官方提供 Codemod 迁移脚本,可自动化处理大部分废弃 API。
建议
- 新项目:直接上 v6,享受更好的性能、体验和未来维护性。
- v5 项目:先在 dev 分支尝试升级。无大量 hack 样式的话,成本很低。
- v4 及更老项目:跨度较大,建议先逐步迁移到 v5,再升 v6;或在新模块中使用 v6(配合微前端或包隔离)。
-
升级前检查:
- 确认 React ≥ 18
- 运行官方 Codemod
- 验证目标浏览器支持 CSS 变量(IE 彻底不支持)
总结
Ant Design v6 是一次**“返璞归真”**的升级。它把控制权还给开发者(语义化 API),用现代浏览器特性解决性能问题(零运行时 CSS 变量),并补齐了业务高频组件。
升级核心收益
- 更少的 hack 代码,更健壮的样式
- 显著的性能提升(主题切换、渲染速度)
- 官方接管高频业务组件,减少第三方依赖
- 平滑迁移路径,真正降低了“升级火葬场”的风险
对于业务开发者来说,这意味着:更少的加班、更快的页面、更早下班。
参考链接
- Ant Design v6 迁移指南(中文) / English
- 语义化更新说明
- 更新日志 (Changelog)(中文) / English