阅读视图
国家统计局:推动CPI温和回升的有利因素在累积
速卖通:2025年速卖通消费电子行业下的储能电池类目海外托管增长近10倍
恒指午间休盘跌0.99%,恒生科技指数跌1.15%
深圳水贝金饰克价达到1200元
半日主力资金加仓电力设备股,抛售计算机股
鸿蒙组件封装手把手教程:重复代码拜拜,效率UP UP
做鸿蒙ArkUI开发的兄弟姐妹们,是不是总被重复代码折磨?登录页的确认按钮、购物页的结算按钮,样式一模一样还要写两遍;图片加文字的布局,换个页面又得重新拼一遍——改样式时逐个文件找,维护起来头都大了!其实只要学会组件封装,把重复代码“打包”起来,下次直接拿过来用,效率直接翻倍,还方便团队协作。今天就照着华为官方文档,手把手教你三种核心封装方式,新手也能轻松拿捏!
一、先搞懂:组件封装到底好在哪?
简单说,封装就是把相同或相似的UI样式、布局、逻辑“装起来”,核心好处就三个:
- 少写重复代码:写一次能用N次,不用复制粘贴
- 维护超方便:要改样式/逻辑,只改封装组件,所有用到的地方自动同步
- 团队不吵架:统一组件风格,不用纠结“你写的按钮和我不一样”
鸿蒙里最常用的封装场景就三种:只复用样式、复用样式+布局+逻辑、批量管理多个组件,咱们一个个说清楚。
二、第一种:组件公共样式封装(只复用样式)
适用场景
如果只是多个组件要共用一套样式,比如所有确认按钮都是胶囊形、一样的大小和颜色,就用这种方式。比如登录页的“登录”按钮和购物页的“结算”按钮,功能不同但样式一致,直接封装样式就行。
核心思路
用系统提供的AttributeModifier接口,把公共样式写在一个类里,之后哪个组件要用,直接套用这个类就行。
手把手操作
第一步:封装公共样式类
先创建一个类,实现AttributeModifier接口,把按钮的默认态、按压态样式都写进去:
// 封装按钮的公共样式
export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
// 默认是普通按钮,后续可修改
private buttonType: ButtonType = ButtonType.Normal;
// 设置按钮类型(比如胶囊形、普通形)
type(type: ButtonType): MyButtonModifier {
this.buttonType = type;
return this;
}
// 按钮默认状态的样式
applyNormalAttribute(instance: ButtonAttribute): void {
instance.type(this.buttonType); // 按钮类型
instance.width(200); // 宽度
instance.height(50); // 高度
instance.fontSize(20); // 字体大小
instance.fontColor('#0A59F7'); // 字体颜色
instance.backgroundColor('#0D000000'); // 背景色
}
// 按钮按压状态的样式
applyPressedAttribute(instance: ButtonAttribute): void {
instance.fontColor('#0A59F7');
instance.backgroundColor('#26000000'); // 按压时背景色加深
}
}
第二步:使用封装好的样式
在需要的页面里,创建样式实例,通过attributeModifier()方法应用到按钮上:
@Entry
@Component
struct AttributeStylePage {
// 创建样式实例,设置为胶囊形按钮
modifier = new MyButtonModifier().type(ButtonType.Capsule);
build() {
NavDestination() {
Column() {
// 直接套用封装好的样式
Button('确认按钮')
.attributeModifier(this.modifier)
}
.margin({ top: $r('app.float.margin_top') })
.width('100%')
.height('100%')
}
.title(getResourceString($r('app.string.common_style_extract'), this))
}
}
小提醒
- 这种样式只能用在系统组件上(比如Button、Image),自定义组件暂时不支持
- 样式类可以跨文件导出,整个项目都能复用,还能灵活改参数(比如改按钮类型、颜色)
三、第二种:自定义组件封装(样式+布局+逻辑一起复用)
适用场景
如果不仅要复用样式,连布局和逻辑都要复用,比如商品卡片(图片+名称+价格固定布局)、个人信息项(图标+文字+箭头),就适合封装成自定义组件。而且还能让使用方灵活修改部分属性,比如图片大小、文字内容。
核心思路
用@Component装饰器把组件“打包”,不变的部分(比如图片和文字的排列方式)写在内部,可变的部分(比如图片地址、大小)用参数暴露出去,让使用方按需配置。
手把手操作
咱们以“图片+文字”的组合组件为例:
第一步:先封装子组件的样式(可选)
分别给Image和Text组件写样式类,方便后续修改:
// 图片组件的样式封装
export class CustomImageModifier implements AttributeModifier<ImageAttribute> {
private imageWidth: Length = 0;
private imageHeight: Length = 0;
// 初始化图片大小
constructor(width: Length, height: Length) {
this.imageWidth = width;
this.imageHeight = height;
}
// 提供修改宽高的方法
width(width: Length) {
this.imageWidth = width;
return this;
}
height(height: Length) {
this.imageHeight = height;
return this;
}
// 应用图片样式
applyNormalAttribute(instance: ImageAttribute): void {
instance.width(this.imageWidth);
instance.height(this.imageHeight);
instance.borderRadius($r('app.float.border_radius')); // 圆角
}
}
// 文字组件的样式封装
export class CustomTextModifier implements AttributeModifier<TextAttribute> {
applyNormalAttribute(instance: TextAttribute): void {
instance.fontSize($r('app.float.font_size_l')); // 字体大小
}
}
第二步:封装自定义组件
把图片和文字的布局、点击事件都封装进去,暴露可变参数:
@Component
export struct CustomImageText {
// 图片样式(默认100x100)
@Prop imageAttribute: AttributeModifier<ImageAttribute> = new CustomImageModifier(100, 100);
// 文字样式(默认样式)
@Prop textAttribute: AttributeModifier<TextAttribute> = new CustomTextModifier();
// 图片资源(必须由使用方传入)
@Prop imageSrc: PixelMap | ResourceStr | DrawableDescriptor;
// 文字内容(必须由使用方传入)
@Prop text: string;
// 点击事件(可选,使用方按需传入)
onClickEvent?: () => void;
build() {
// 固定布局:图片和文字纵向排列
Column({ space: 12 }) {
Image(this.imageSrc)
.attributeModifier(this.imageAttribute)
Text(this.text)
.attributeModifier(this.textAttribute)
}
// 点击事件触发
.onClick(() => {
if (this.onClickEvent !== undefined) {
this.onClickEvent();
}
})
}
}
第三步:使用自定义组件
按需传入图片资源、文字、样式和点击事件:
@Component
struct CommonComponent {
// 自定义图片大小为330x330
imageAttribute: CustomImageModifier = new CustomImageModifier(330, 330);
build() {
NavDestination() {
Column() {
CustomImageText({
imageAttribute: this.imageAttribute, // 传入自定义图片大小
imageSrc: $r('app.media.image'), // 图片资源
text: 'Scenery', // 文字内容
onClickEvent: () => {
// 点击组件显示提示
this.getUIContext().getPromptAction().showToast({ message: 'Clicked' })
}
})
}
.margin({ top: $r('app.float.margin_top') })
.width('100%')
.height('100%')
}
.title(getResourceString($r('app.string.common'), this))
}
}
四、第三种:组件工厂类封装(批量管理多个组件)
适用场景
如果项目里有很多零散组件(比如单选框、复选框、输入框),想统一管理,让业务团队通过组件名直接获取使用,就用这种方式。比如传入“Radio”拿单选框,传入“Checkbox”拿复选框,不用逐个导入。
核心思路
用@Builder装饰器定义组件模板,再用wrapBuilder函数包装,存入一个Map(键是组件名,值是组件对象),最后导出这个“组件工厂”,使用方按名字取就行。
手把手操作
第一步:定义组件模板
用@Builder写全局的组件模板(比如单选框、复选框):
// 单选框组件模板
@Builder
function myRadio() {
Text($r('app.string.radio'))
.width('100%')
.fontColor($r('sys.color.mask_secondary'))
// 男选项
Row() {
Radio({ value: '1', group: 'radioGroup' })
.margin({ right: $r('app.float.margin_right') })
Text('man')
}
.width('100%')
// 女选项
Row() {
Radio({ value: '0', group: 'radioGroup' })
.margin({ right: $r('app.float.margin_right') })
Text('woman')
}
.width('100%')
}
// 复选框组件模板
@Builder
function myCheckBox() {
Text($r('app.string.checkbox'))
.width('100%')
.fontColor($r('sys.color.mask_secondary'))
// 全选
Row() {
CheckboxGroup({ group: 'checkboxGroup' })
.checkboxShape(CheckBoxShape.ROUNDED_SQUARE)
Text('all')
.margin({ left: $r('app.float.margin_right') })
}
.width('100%')
// 选项1
Row() {
Checkbox({ name: '1', group: 'checkboxGroup' })
.shape(CheckBoxShape.ROUNDED_SQUARE)
.margin({ right: $r('app.float.margin_right') })
Text('text1')
}
.width('100%')
// 选项2
Row() {
Checkbox({ name: '0', group: 'checkboxGroup' })
.shape(CheckBoxShape.ROUNDED_SQUARE)
.margin({ right: $r('app.float.margin_right') })
Text('text2')
}
.width('100%')
}
第二步:创建组件工厂
把组件模板包装后存入Map,再导出工厂:
// 定义组件工厂Map,键是组件名,值是组件对象
let factoryMap: Map<string, object> = new Map();
// 把组件存入工厂(用wrapBuilder包装@Builder方法)
factoryMap.set('Radio', wrapBuilder(myRadio));
factoryMap.set('Checkbox', wrapBuilder(myCheckBox));
// 导出工厂,供外部使用
export { factoryMap };
第三步:使用工厂里的组件
导入工厂,按组件名获取并渲染:
// 导入组件工厂(路径要按实际项目调整)
import { factoryMap } from '../view/FactoryMap';
@Component
struct ComponentFactory {
build() {
NavDestination() {
Column({ space: 12 }) {
// 按名字获取单选框组件并渲染
(factoryMap.get('Radio') as WrappedBuilder<[]>).builder();
// 按名字获取复选框组件并渲染
(factoryMap.get('Checkbox') as WrappedBuilder<[]>).builder();
}
.width('100%')
.padding($r('app.float.padding'))
}
.title(getResourceString($r('app.string.factory'), this))
}
}
小提醒
- 只有全局的
@Builder方法才能用wrapBuilder包装 - 从工厂拿的组件,只能在
struct的build方法里使用
五、封装后常见问题:直接抄作业就行!
1. 怎么调用子组件里的方法?
三种实用方法,按需选:
方法一:用Controller类(推荐)
定义一个控制器,子组件把方法“交”给控制器,父组件通过控制器调用:
// 定义控制器
export class Controller {
action = () => {}; // 用来存子组件的方法
}
// 子组件
@Component
export struct ChildComponent {
@State bgColor: ResourceColor = Color.White;
controller: Controller | undefined = undefined;
// 子组件的方法:切换背景色
private switchColor = () => {
this.bgColor = this.bgColor === Color.White ? Color.Red : Color.White;
}
// 组件初始化时,把方法赋值给控制器
aboutToAppear(): void {
if (this.controller) {
this.controller.action = this.switchColor;
}
}
build() {
Column() {
Text('Child Component')
}.backgroundColor(this.bgColor).borderWidth(1)
}
}
// 父组件
@Entry
@Component
struct Index {
private childRef = new Controller(); // 创建控制器实例
build() {
Column() {
// 把控制器传给子组件
ChildComponent({ controller: this.childRef })
Button('切换子组件颜色')
.onClick(() => {
this.childRef.action(); // 调用子组件方法
})
.margin({ top: 16 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
}
方法二:用@Watch监听
父组件改状态变量,子组件监听变量变化,触发方法:
// 子组件
@Component
export struct ChildComponent {
@State bgColor: ResourceColor = Color.White;
// 监听checkFlag变量变化
@Link @Watch('switchColor') checkFlag: boolean;
// 变量变化时触发的方法
private switchColor() {
this.bgColor = this.checkFlag ? Color.Red : Color.White;
}
build() {
Column() {
Text('Child Component')
}.backgroundColor(this.bgColor).borderWidth(1)
}
}
// 父组件
@Entry
@Component
struct Index {
@State childCheckFlag: boolean = false;
build() {
Column() {
ChildComponent({ checkFlag: this.childCheckFlag })
Button('切换颜色')
.onClick(() => {
this.childCheckFlag = !this.childCheckFlag; // 改变量
})
.margin({ top: 16 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
}
方法三:用Emitter事件通信
子组件监听事件,父组件发送事件,触发子组件方法:
// 子组件
@Component
export struct ChildComponent {
// 定义事件ID
public static readonly EVENT_ID_SWITCH_COLOR = 'SWITCH_COLOR';
@State bgColor: ResourceColor = Color.White;
private switchColor = () => {
this.bgColor = this.bgColor === Color.White ? Color.Red : Color.White;
}
// 组件初始化时监听事件
aboutToAppear(): void {
emitter.on(ChildComponent.EVENT_ID_SWITCH_COLOR, this.switchColor);
}
// 组件销毁时取消监听
aboutToDisappear(): void {
emitter.off(ChildComponent.EVENT_ID_SWITCH_COLOR, this.switchColor);
}
build() {
Column() {
Text('Child Component')
}.backgroundColor(this.bgColor).borderWidth(1)
}
}
// 父组件
@Entry
@Component
struct Index {
build() {
Column() {
ChildComponent()
Button('切换颜色')
.onClick(() => {
emitter.emit(ChildComponent.EVENT_ID_SWITCH_COLOR); // 发送事件
})
.margin({ top: 16 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
}
2. 怎么调用父组件里的方法?
超简单!子组件留个回调参数,父组件把自己的方法传进去:
// 子组件
@Component
export struct ChildComponent {
call = () => {}; // 回调参数,用来存父组件的方法
build() {
Column() {
Button('调用父组件方法')
.onClick(() => {
this.call(); // 触发父组件方法
})
}
}
}
// 父组件
@Entry
@Component
struct Index {
// 父组件的方法
parentAction() {
try {
this.getUIContext().getPromptAction().showToast({ message: 'Parent Action' });
} catch (error) {
let err = error as BusinessError;
hilog.warn(0x000, 'testTag', `showToast failed, code=${err.code}, message=${err.message}`);
}
}
build() {
Column() {
// 把父组件方法传给子组件
ChildComponent({ call: this.parentAction })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
}
3. 怎么实现“插槽”(可变UI部分)?
用@BuilderParam!子组件留个位置,父组件按需传入UI内容:
// 子组件
@Component
export struct ChildComponent {
// 默认空UI
@Builder
customBuilder() {}
// 暴露给父组件的UI参数
@BuilderParam customBuilderParam: () => void = this.customBuilder;
build() {
Column() {
Text('子组件固定内容')
this.customBuilderParam(); // 父组件传入的可变UI
}
}
}
// 父组件
@Entry
@Component
struct Index {
// 父组件定义的可变UI
@Builder
componentBuilder() {
Text(`父组件传入的UI`)
}
build() {
Column() {
// 传入可变UI
ChildComponent() {
this.componentBuilder();
}
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
}
4. 怎么传递组件数组,实现循环渲染?
先把组件包装成全局@Builder,再用wrapBuilder封装成数组,最后用ForEach循环:
// 1. 定义全局组件模板
@Builder
function itemBuilder(text: string) {
Text(text)
.width('100%')
.padding(10)
.borderWidth(1)
}
// 2. 封装成组件数组
const componentArray = [
wrapBuilder(itemBuilder, '项目1'),
wrapBuilder(itemBuilder, '项目2'),
wrapBuilder(itemBuilder, '项目3')
];
// 3. 循环渲染
@Component
struct ForEachComponent {
build() {
Column() {
ForEach(componentArray, (item) => {
item.builder(); // 渲染每个组件
})
}
}
}
总结
组件封装的核心就是“提取重复,暴露可变”:
- 只复用样式:用
AttributeModifier - 复用样式+布局+逻辑:用
@Component写自定义组件 - 批量管理组件:用组件工厂(Map+wrapBuilder)
掌握这三招,再也不用写重复代码,项目维护起来也省心。遇到调用方法、插槽这些问题,直接抄上面的作业就行~ 赶紧把你项目里的重复组件封装起来试试吧!
今年广州力争实现项目投资超3800亿元
在AI时代下,技术人应该脱离再等等思维
为什么“再等等”是毒药?
你对自己说“再等等”,这听起来像很谨慎,但本质上是--延迟面对现实。
再等等都真正含义是:
让自己再多一个不发布的理由
不是你还差一点。
而是你给自己一个永远合理的缓冲区。
完善,永远没有终点
你永远可以说:
-
功能还不够全
-
体验还能再顺一点
-
架构还能再稳一点
所以问题从来不是:
什么时候够好?
而是:
你有没有一个必须发布的时间点?
一个你必须接受的判断句
没有发布时间点的项目,本质上是不打算发布。
不是你不想,而是你在结构上允许自己一直逃避。
可售性优先原则(核心思想)
什么是“可售性”?
一句话:有人愿意为“当前状态”付钱,而不是为你脑海里的未来版本。
这里要忽略三件事:
1️⃣ 潜力
2️⃣ 愿景
3️⃣ 长期规划
这些东西都不重要。
可售性我们只关心一个问题
现在有没有愿意掏钱?
不是等你做完,不是等你优化,不是等你“再完善一点”。
而是现在。
可售性 vs 完整性
其实很多技术人就是分不清这个概念,我们用两个概念的承诺方式做对比。
完整性的承诺是:
1️⃣ 我会把它做好
2️⃣ 我会把边角补齐
3️⃣ 我会对所有的情况负责
它带来的感受是:
👉 安心。
可售性的承诺是:
1️⃣ 我现在就解决一个具体问题
2️⃣ 不完美,但有用
3️⃣ 今天就能交付
它带来的感受是:
👉 清醒。
请记住,完整性让你安心,可售性让你清醒。一个让你舒服,一个让你面对现实。
耿乐再创业项目“青初于蓝”已完成两轮融资
A股三大指数午间休盘涨跌不一,大连圣亚涨停
把「搜打撤」玩成模拟经营,我在《逃离鸭科夫》实现「财务自由」
敢不敢挑战用Cocos3.8复刻曾经很火的割绳子游戏?
引言
哈喽大家好,我是亿元程序员,最近有小伙伴私信笔者:
亿哥,不知道你有没有玩过上面那款
16年前火遍全网的割绳子游戏。我最近在做游戏时,需要做类似这个游戏里面的绳子效果,不知道怎么实现!
你最近不是在更新热门小游戏实战合集吗!
敢不敢挑战用Cocos3.8复刻一个?
好家伙,这款游戏笔者最熟悉不过了,那时候苹果4才刚出来没多久,这样的触屏物理游戏可以算得上人手一个。
关于割绳子的物理效果,我记得之前参与论坛投稿活动时,参考某个砍树游戏出过一期:
翻了一下,看到了一些扎心的评论:
言归正传,本期带大家一起来看看,如何在Cocos游戏开发中,如何实现不像棍子的绳子效果,并且实战做一个曾经很火的割绳子游戏。
本文源工程可在文末获取,小伙伴们自行前往。
回顾一下
想要在Cocos游戏开发中,实现一条带有物理效果的绳子,可以使用我们系统自带的距离关节Distance Joint。
距离关节会将关节两端的刚体约束在一个最大范围内。超出该范围时,刚体的运动会互相影响。
既然如此,为什么之前做的绳子像棍子?
关节太少
因为之前做的demo,钩子和物品之间,仅仅使用了一个距离关节,所以很难模拟出来绳子柔软的效果。
那么不像棍子的绳子怎么做?
增加足够多的关节
首先制作一节5*20的绳子,属性组件如下:
理论上只要添加足够多的关节,就会越来越接近柔软的绳子效果,因此我们可以通过代码去控制生成足够多的关节,一节连一节。
效果如下,不过绳子看起来并不是连续的,关节因为重力效果,会被拉开一段距离。
这个问题怎么解决?
画线辅助
为了解决受重力效果导致的绳结断开的问题,我们可以通过Graphics组件逐点进行画线,代码如下。
效果如下,但是看起来还是有点问题,绳子连接处不太和谐,不像一根绳子,反而像哼哼哈嘿的双截棍:
因此我们需要把绳子画得更加平滑一点。
平滑曲线
想要让曲线更加平滑,通常可以采用二次贝塞尔曲线连接相邻点,首先我们先要把所有的点位收集起来:
核心的绘制方法quadraticCurveTo:
这样看起来就合理一点了:
通过上述流程,我们就能成功实现一根不像棍子的绳子了。
接下来我们完成割绳子游戏的剩余部分。
割绳子游戏实战
有了上面的绳子基础,我们想要实现一个完整的割绳子游戏就易如反掌了。
1. 割绳子
既然是割绳子游戏,割当然是也是重要的一环,割绳子最主要判断手指移动的轨迹与实际上哪一段关节相交。
判断相交的核心方法如下:
2. 切割效果
为了增加游戏效果,我们可以在屏幕上画线时,增加一段拖尾效果。
效果如下:
3. 其他游戏元素
除去绳子相关的部分,剩下就是割绳子游戏的其他元素,包括:
-
钩子: 主要起到固定绳子的作用,需要添加刚体组件,
Type设置为Static。 -
甜甜圈: 需要添加碰撞组件、管理脚本以及刚体组件,需要开启
Enabled Contact Listener,在对应的管理脚本进行处理碰撞事件。如下碰撞到星星和目标点时进行处理。
-
星星: 添加碰撞组件和管理脚本,增加一些简单的动画。
旋转和碰撞放大效果。
-
目标点: 目标点和上面差不多,主要是用来判断是否到达目标点,以及添加一些到达动画。
-
其他: 随着游戏关卡地不断增加,会有越来越多的其他游戏元素,笔者只实现了以上基础的部分,感兴趣的小伙伴们可以自行扩展。
4. 效果演示
结语
割绳子游戏真的是回忆杀,勾起笔者的无限回忆,等我退休了,一定要完完整整复刻一个,一边复刻一边玩。
小伙伴们,你们玩过这款游戏吗?那时候的你们进入游戏行业了吗?
本文实战完整源码已集成到亿元Cocos小游戏实战合集,内含体验链接。
我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。
AD:笔者线上的小游戏《打螺丝闯关》《贪吃蛇掌机经典》《重力迷宫球》《填色之旅》《方块掌机经典》大家可以自行点击搜索体验。
实不相瞒,想要个赞和爱心!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!
推荐文章:
胡润发布中国人工智能50强,寒武纪以6300亿元价值居首
马蜂窝与梵净山达成战略合作
2025年新建商品房销售8.39万亿
1月19日,国家统计局公布《2025年全国房地产市场基本情况》和《2025年12月份70个大中城市商品住宅销售价格变动情况》,数据显示,2025年,新建商品房销售面积88101万平方米,比上年下降8.7%;新建商品房销售额83937亿元,下降12.6%;12月份,一线城市新建商品住宅销售价格同比下降1.7%,降幅比上月扩大0.5个百分点,二、三线城市新建商品住宅销售价格同比分别下降2.5%和3.7%;12月份,一线城市二手住宅销售价格同比下降7.0%,二、三线城市二手住宅销售价格同比均下降6.0%。市场继续呈现筑底趋势。
01 商品房库存持续下降
2025年,全国房地产开发投资82788亿元,比上年下降17.2%;其中,住宅投资63514亿元,下降16.3%。
在销售端,2025年新建商品房销售面积88101万平方米,比上年下降8.7%;其中住宅销售面积下降9.2%,至73299平方米。新建商品房销售额83937亿元,下降12.6%;其中住宅销售额下降13.0%。
图源:国家统计局
作为重要的未来预测性指标,2025年房屋新开工面积58770万平方米,下降20.4%。其中,住宅新开工面积42984万平方米,下降19.8%。这意味着未来可售的新房面积持续减少,这一点也成为楼市库存下降的主要原因之一。
2025年末,商品房待售面积76632万平方米,比上年末增长1.6%,比11月末回落1.0个百分点。其中,住宅待售面积同比增长2.8%,但比今年2月末的库存高点下降了2938万平方米,至约4亿平方米。
目前新房市场的库存面积和新开工面积总和只比年销售面积略高。
02 一线市场止跌回暖信号增强
在价格方面,根据统计局数据,12月份,一线城市新建商品住宅销售价格环比下降0.3%,降幅比上月收窄0.1个百分点。其中,上海上涨0.2%,北京、广州和深圳分别下降0.4%、0.6%和0.5%。
二线城市新建商品住宅销售价格环比下降0.4%,降幅扩大0.1个百分点。三线城市新建商品住宅销售价格环比下降0.4%,降幅与上月相同。
图源:国家统计局
二手房市场,12月份,一线城市二手住宅销售价格环比下降0.9%,降幅比上月收窄0.2个百分点。其中,北京、上海、广州和深圳分别下降1.3%、0.6%、1.0%和0.6%。二、三线城市二手住宅销售价格环比均下降0.7%,降幅均扩大0.1个百分点。
58安居客研究院院长张波分析认为,一线市场止跌回暖信号增强。一线城市新建商品住宅环比降幅收窄 0.1 个百分点,其中上海新房环比上涨 0.2%、同比大涨 4.8%,成为一线城市中唯一实现同比、环比双涨的城市,144㎡以上大户型的热销印证了改善型需求的坚实韧性。二手房市场同样呈现改善迹象,一线城市二手住宅环比降幅收窄 0.2 个百分点,上海二手房价环比降幅收窄至 0.6%,需求端观望情绪正在逐步消解。
值得关注的是,今年一月市场止跌信号已经显现。张波表示,近期全国面的政策出台较为密集,贷款利率下调、税费优惠等政策直接降低购房门槛,公积金贷款利率和商贷利率都已达到历史低点。从安居客线上数据来看,政策对用户的拉动作用明显,1月前两周,用户主动发起微聊(用户和经纪人线上文字沟通)对数周环比增长 1.8%,月同比大幅增长 8.6%。发起微聊的用户数周环比增长 1.3%,月同比增长 7.5%。用户发起留电意愿持续增强,留电用户数周环比增长 1.0%,月同比增长 7.1%,但整体来看,用户从线上浏览转向主动咨询的情况不断增多,尤其是多子女家庭、新市民等政策重点支持群体的咨询量增长更为突出,表明市场观望情绪正逐步缓解,买家议价空间收窄,市场定价趋于理性。
美光预计前所未有的存储芯片短缺将持续到2026年之后
市场监管总局:2025年发布国家计量技术规范213项
重构第三天,我把项目里 500 个 any 全部换成了具体的 Interface,然后项目崩了😭
开始在重构旧项目的最近一个月,我每天打开项目代码,心情都像是在上坟😖。
这个项目是5年前的老代码,说是用了 TypeScript,但含 any 量高达 80%。
User 是 any,Response 是 any,连 window 也是 (window as any)。
每次写业务,我都得猜这个 res.data.list 到底是个数组,还是个 null,还是后端心情不好传回来的一个空字符串。
作为一个有代码洁癖的资深前端,我忍不了😡。
每周五下午,趁着业务需求的空窗期,决定搞个大扫除。
目标是:干掉核心模块里的所有 any,还 TypeScript 一个明确的类型。
过程是 - 爽是爽了
那个周末我是在多巴胺的海洋里度过的。
我对着接口文档,把那些面目可憎的 any 一个个替换成了极其优雅的 Interface。
Refactor 前:
// 屎山代码
function renderUser(user: any) {
const name = user.info.name; // 这里的 info 可能是 undefined,但 TS 不报错
const age = user.age.toFixed(2); // 这里的 age 可能是 string,TS 也不报错
return `User: ${name}, Age: ${age}`;
}
Refactor 后:
// 优雅代码
interface UserInfo {
name: string;
avatar?: string;
}
interface User {
id: number;
info: UserInfo; // 必须存在
age: number; // 必须是数字
}
function renderUser(user: User) {
// 此时 IDE 智能提示全出来了,简直丝滑
return `User: ${user.info.name}, Age: ${user.age.toFixed(2)}`;
}
看着 VS Code 里那一个个红色的波浪线被我修好,看着 TS 编译通过的绿色对勾,我感觉自己就是代码界的上帝🥱。我甚至顺手把以前代码里那些丑陋的像 if (user && user.info) 防御性判断给删了—— 因为 Interface 定义里写了,info 是必选属性,不可能为空!
周一早上,我信心满满地提了 Merge Request,顺便把代码推上了测试环境。
我心想:兄弟们,以后你们调接口都有智能提示了😁。
事故发生了:P1 级白屏,我在群里被 @ 成了筛子
上午 10 点的时候,测试环境崩了。
上午 10 点半,技术总监冲到我工位:你动核心模块了?怎么列表页全白了?控制台全是报错!
我一愣:不可能啊,我本地编译全通过了,TS 类型检查也是完美的。
打开控制台一看,我傻眼了。满屏红字:
Uncaught TypeError: Cannot read properties of undefined (reading 'name')Uncaught TypeError: user.age.toFixed is not a function
怎么会?
我明明定义了 interface User,里面写死了 info 必须存在,age 必须是 number 啊!
真相是,原来TypeScript 是最大的骗子😡
经过两个小时的狼狈排查,我终于明白了那个让所有 TS 新手都会摔坑的真理:
TypeScript 的类型,在运行时(Runtime)就是个屁!
后端的嘴,骗人的鬼
Interface 定义里,我信了后端的文档,写了 age: number。
但实际上,老数据里有几百条数据,age 存的是字符串 "18"😡。
以前用 any 的时候,JS 隐式转换还能跑(或者之前压根没调 toFixed)。
现在我为了规范,加了 .toFixed(2),因为 TS 告诉我它是数字。
结果运行时,浏览器拿着字符串 "18" 去调 toFixed,直接炸穿😥。
必选属性莫名其妙的消失
Interface 里我写了 info: UserInfo(必选)。
于是我自信地删掉了 if (user.info) 的判空逻辑。
结果后端返回的数据里,因为历史脏数据,真的有几个用户的 info 是 null😡。
TS 编译时很开心,浏览器运行时直接抛出 Cannot read properties of null。
我把编译时的类型安全,误当作了运行时的数据安全。
我以为我在写 Java,其实我还在写 JavaScript。TS 编译成 JS 后,那些漂亮的 Interface 全都被擦除得干干净净,裸奔的数据依然是那个烂样子。
反思反思反思:这 500 个 any,原来是护身符😥
看着回滚后的代码,那个丑陋的 any 重新回到了屏幕上,我竟然感到了一丝安全感。
这次事故给了我三个血淋淋的教训:
别太迷信文档,要信数据。
后端的 Swagger 文档写写而已,你真信了 Required,上线就得背锅。
TS 是协定,Zod 才是严格的。
如果你真的想保证数据类型安全,光写 Interface 没用(那只是给 IDE 看的)。你得上运行时校验库(比如 Zod 或 Runtypes)。
// 这才是真安全
const UserSchema = z.object({
age: z.number(), // 运行时如果拿到 string,这里直接抛错,而不是等到业务逻辑里炸开
});
防御性编程永远不要删 !!!
不管 TS 告诉你这个字段多一定存在,只要数据源来自网络(API),老老实实写 Optional Chaining (user?.info?.name)。
最后的结局
那天下午,我默默地把自己写的那些 interface 改成了原样:
interface User {
// 认怂了,全部改成可选 + 联合类型😒
age?: number | string;
info?: UserInfo | null;
}
虽然代码里又要加回那些烦人的 if 判断,虽然类型提示没那么完美了,但至少——它不崩了😒。
如果你也想重构项目里的 any,听哥一句劝:
除非你上了 Zod 做运行时校验,否则那个丑陋的 any,可能是你项目里最后一道防线。
分享完毕🙌