阅读视图

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

90 后正在掌管中国 AI,凭实力活成了「爽文」主角

清华姚班、普林斯顿博士、前 OpenAI 核心成员、27 岁、首席 AI 科学家……当这些标签堆砌在一个人身上时,你很难不感受到一种来自智商层面的压迫感。

但就是这种类似的标签组合,正在中国 AI 赛道上频繁出现。

置身于 2026 年回望,在中国 AI 过去三年的狂飙突进中,昔日叱咤风云的互联网大佬似乎已退居幕后;取而代之站在舞台中央,是以梁文锋、姚顺雨、林俊旸、杨植麟为代表的 80 后乃至 90 后。

他们之中,有人仰望星空,有人极致务实,甚至无意遵循上一代巨头立下的江湖规矩,却殊途同归地站在了 AGI 的大门口。

与此同时,他们既有硬刚海外 AI 巨头的硬气,也不乏算力捉襟见肘时的窘迫。风浪极大,舵手极年轻,但这种反差,恰恰将是 2026 年中国 AI 最真实的切面:

姚顺雨(1998 年):腾讯 CEO/总裁办公室首席 AI 科学家
林俊旸(1993 年):通义千问系列模型研发主导者、阿里 P10
杨植麟(1992 年):月之暗面创始人
罗福莉(1995 年): 小米 MiMo 大模型负责人
梁文锋(1985 年):DeepSeek 创始人
闫俊杰(1989 年):MiniMax 创始人
张鹏(1980 年):智谱 AI 创始人
张祥雨(1990 年):阶跃星辰首席科学家
……

80/90 后,站在了 AGI 的大门口

风险投资公司 Antler 曾公布一项数据:2024 年,AI 独角兽创始人的平均年龄已经降到了 29 岁,而其他行业还在 34 岁徘徊。甚至像 Mercor 这样的百亿美元估值公司,三位创始人今年才刚满 22 岁。

视线转回国内,这种「低龄化」情况屡见不鲜。大象虽然转身慢,但巨头们做了一个极其聪明的决定:既然打不过年轻人,那就把年轻人变成自己人。

比如,腾讯选择了 27 岁的姚顺雨。

1998 年出生,清华姚班,普林斯顿博士,前 OpenAI 核心成员,专注于「让 AI 像人一样思考和行动」的课题。他提出的 ReAct 框架和(Tree of Thoughts)决策框架,被业内奉为 AI Agent 的教科书级理论。

2025 年,腾讯直接将其招致麾下,任命为「CEO/总裁办公室首席 AI 科学家」的头衔,也将 AI Infra 和大模型研究的重任交给这位 95 后。

在最近的 AGI-Next 峰会上,我们也能看出姚顺雨对于 ToC 和 ToB 的大模型路径均有深刻见解:他指出 ToC 要依赖「上下文」提供情绪价值,而 ToB 比拼「生产力」,企业只愿为最强模型付费 。

他强调在 ToC 侧充分利用微信等生态的海量上下文,在 ToB 侧发挥大公司真实场景和数据优势,以此破局 AI 应用。这不仅仅是技术路线的选择,更是在为腾讯庞大的社交生态寻找 AI 时代的出口。

同样打破大厂晋升纪录的,还有阿里的林俊旸。

1993 年的北大才子,在阿里达摩院一路狂飙,2025 年,32 岁的他成了阿里史上最年轻的 P10。

他曾参与研发阿里达摩院的千亿参数多模态模型 M6,主导通义千问系列模型研发,涵盖多模态、模型优化训练、代码生成、数学推理等领域。

作为通义千问(Qwen)开源策略的一手推动者,他不仅在全球开发者社区撕开了一道口子,更让阿里在激烈的模型竞赛中稳住了身位。

如果说巨头内部的年轻化是由于生存焦虑,那么 AI 创业战场上的厮杀,则更多了几分个人英雄主义的色彩。月之暗面的杨植麟大概是这个群体里最像拿到爽文男主剧本的那一个。

1992 年出生,清华学霸,卡内基梅隆博士,2019 年提出 XLNet 模型的作者之一,这位理工男有着特有的浪漫,所创立的公司命名为「月之暗面」,源自 Pink Floyd 的专辑。在多次公开场合的露面中,他也热衷于讨论 AGI 理念,在信奉 Scaling Law 的道路上无比坚决。

而在光谱的另一端,梁文锋则是另一种画风。

虽然同样都是名牌大学出身,本科就读于浙江大学电子信息工程专业,硕士继续在浙大信息与通信工程专业深造,但背靠量化私募巨头幻方量化,DeepSeek 自带一股「带资进组」的硬气。

去年,幻方量化以 56.6% 的平均回报率跻身中国百亿级量化基金业绩榜第二,仅次于宁波灵均投资 70% 多的收益率,也为 DeepSeek 的发展填上了更充足的弹药。

从 2024 年 5 月的 DeepSeek-V2 开始,他们持续迭代并开源了 236B→671B 的 DeepSeekMoE 系列,在国内最早系统地把「细粒度专家分割 + 共享专家隔离 + 无辅助损失负载均衡」等想法写进论文、放出了完整权重。

此外,得益于在 AI Infra 等方面的持续优化, DeepSeek 硬是靠着这种在螺蛳壳里做道场的本事,用有限的算力打造了 DeepSeek V3 和 R1 这样极具破坏力的产品。

当 DeepSeek 以极低的 API 价格血洗市场时,所有的 AI 模型创业者都感到了一阵寒意。

甚至大模型 Xiaomi MiMo 的负责人罗福莉同样出自 DeepSeek,2022 年加入幻方量化,负责深度学习策略建模;也是 MoE 大模型 DeepSeek-V2 的核心开发者之一 。去年 11 月 12 日,她正式官宣加入小米,负责推进小米 AGI 方向的研发落地。

Leonis AI 100 报告显示,大多数 AI 创始人在创业时只有 20 多岁。Antler 的数据更惊人:AI 公司从成立到成为独角兽,平均只需要 4.7 年,比其他行业快了整整两年。

MiniMax 和智谱就是这种「中国速度」的代表。

技术的尽头是商业化,1 月 9 日,MiniMax 在港股敲钟,市值一度冲破千亿,从创立到上市只用了 4 年。MiniMax 共计员工 385 人,但团队平均年龄 29 岁,人均 95 后,385 个人里 73.8% 是研发,三分之一有海外背景。

创始人闫俊杰在 2015 年毕业于中科院自动化所获博士,此后在清华计算机系做博士后研究,曾任商汤科技副总裁、研究院副院长兼智慧城市 CTO。

除了闫俊杰,MiniMax 还有多位年轻核心人物:贠烨祎(31 岁,COO,前商汤战略负责人)、赵鹏宇(29 岁,大语言模型负责人)和周彧聪(32 岁,联合创始人兼视觉多模态负责人)等等。

以及清华系的智谱 AI,唐杰教授坐镇,80 后 CEO 张鹏冲锋,是学术圈与年轻团队结合的典型代表。这家公司在 2026 年初抢了 MiniMax 的风头,成为全球「大模型第一股」,市值一度逼近 800 亿港元。

还有前微软全球副总裁姜大昕创立的阶跃星辰,核心团队全是 90 后,首席科学家张祥雨作为 ResNet 残差网络论文的作者之一蜚声学术界,同样也是 90 后 AI 新星,其论文总引用数超过 30 万次。

无论互联网巨头内部,还是新兴独角兽企业中,35 岁以下的技术领袖已经全面崛起。他们正走上核心岗位,掌握着关键决策和研发方向,成为中国 AI 大模型竞赛的主力军。

名校+竞赛+顶级机构历练,成了中国 AI 领军人物的三件套

上一代互联网巨头的创始人多为 60 后、70 后,对比之下,这一波 AI 浪潮的领导者整体年龄提前了至少 20 岁。

究其原因,AI 是一个知识迭代极快的新兴领域,需要颠覆性思维和快速学习能力,天然青睐年轻头脑。

Transformer 架构提出不到 10 年,ChatGPT 爆发才 3 年。在 LLM(大语言模型)这个领域,大家的起跑线其实相差无几。甚至年轻人因为没有「思维包袱」,反而更容易接受 Scaling Law 的暴力美学。

此外,教育的伏笔其实早已埋下,中国在 2000 年前后启动的大批英才教育计划(如清华「姚班」等)如今开始结出硕果。

姚顺雨获 NOI 银牌,杨植麟获信息学奥赛省一等奖。这些经历培养了他们优秀的算法和编码能力,也让他们更早接触高水平的计算机科学训练。

凭借在竞赛中脱颖而出,他们当中也有不少人保送进入清华、北大等名校计算机相关专业 。

从学历背景来看,姚顺雨、杨植麟、张鹏均为清华系,闫俊杰在清华做博士后。其他人也多来自北大、浙大、上交大、北航等一流院校。

这些名校背景帮助他们打下了扎实的理论基础和视野,当 AI 迎来创业风口,这批人顺理成章地成为主角。

更重要的是周期的更替。上一轮移动互联网的红利已经见顶。大模型的范式转移,为年轻人提供了一个巨大的超车机会。资本也乐于寻找新故事,这让年轻创业者获得了超越资历的资源支持。

当然,一些专家学者指出,90 后、95 后创业者相较前辈更敢想敢干、也更强调自我价值实现。

比如在最近的 AGI-Next 峰会上,唐杰教授评价说:90 后、00 后更具冒险精神,要为敢冒险的年轻一代提供更好的创新环境 。相应地,这一代创业者很多不满足于在大厂按部就班升职,而是渴望掌舵核心项目或自主创业。

唐杰、杨强、杨植麟、林俊旸和刚回国的姚顺雨坐一起都聊了啥? – 知乎
这种内在驱动力,使他们在机会来临时果断走向台前,同时年轻一代普遍国际化视野更开阔,不少人有海外留学或工作经历(姚顺雨、杨植麟等),更能直接对标 OpenAI、DeepMind 等全球标杆,怀抱实现「中国版 OpenAI」的使命感。

基于此,中国 AI 创业者年轻化并非偶然:既有 AI 技术迭代特性的必然要求,又有教育体系和环境支持的厚积薄发,更有年轻一代勇于冒险、不拘一格的精神使然。

国际化技术视野、大规模工程实践经验、顶尖人脉资源,顶级机构的背书也都为他们独当一面打下坚实基础。

然而,年轻并非免死金牌,现实也远比外界想象更为粗砺。

一方面,OpenAI 与 Google 在模型推理能力与多模态交互上构筑的技术壁垒,仍在以月为单位不断抬升;

另一方面,如何在算力资源并不富裕的客观条件下,通过架构创新与工程优化,榨干每一块芯片的极限性能,成了摆在每一位年轻舵手面前必须直面的技术大考。

而在技术之外,商业模式的模糊性依然悬在头顶。尽管应用层百花齐放,但在高企的训练与推理成本面前,如何找到可持续的造血机制,依然是全行业都在摸着石头过河的难题。
风浪确实大,但这恰恰是年轻人的主场。他们还有时间去试错,去推倒重来。

在 AI 这个方兴未艾、快速迭代的领域,既没有论资排辈的规则,也没有巨人的肩膀可以依靠。但这反倒成了一种自由——不用循规蹈矩,不用致敬前浪,他们走到哪,哪里就是路。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


UniApp 的 rpx是什么,跟rem比呢?

吃透 UniApp 的 rpx:一套代码适配多端的核心秘诀

在 UniApp 开发中,“适配” 永远是绕不开的话题。从手机到平板,从 iOS 到 Android,不同设备的屏幕尺寸和像素密度千差万别,如何让界面元素在所有设备上都显示得恰到好处?rpx(responsive pixel)就是 UniApp 为我们准备的 “适配神器”,今天就带你彻底搞懂 rpx 的使用逻辑,以及它和 px、rem 的核心区别。

一、先搞懂:rpx 到底是什么?

rpx 是 UniApp(基于微信小程序)推出的响应式像素单位,核心设计目标是 “一键适配多端”,它的底层逻辑非常简单:

UniApp 规定,所有设备的屏幕宽度都被固定为750rpx(无论实际物理宽度是多少)。比如:

  • iPhone6/7/8(物理宽度 375px):750rpx = 375px → 1rpx = 0.5px
  • iPhone6/7/8 Plus(物理宽度 414px):750rpx = 414px → 1rpx ≈ 0.552px
  • 安卓 1080p 屏幕(物理宽度 360px):750rpx = 360px → 1rpx = 0.48px

简单说,rpx 会根据设备屏幕宽度自动换算,你只需要按 750px 的设计稿直接写数值(设计稿 100px = 代码 100rpx),无需手动计算适配比例。

1.1 rpx 的基础使用

在 UniApp 中,rpx 可以直接用在所有支持样式的地方(style 属性、css/scss 文件),用法和 px 完全一致:

<template>
  <view class="container">
    <!-- 行内样式使用rpx -->
    <view style="width: 375rpx; height: 100rpx; background: #409eff;">
      占屏幕宽度50%的盒子
    </view>
    <!-- 类样式使用rpx -->
    <view class="btn">按钮</view>
  </view>
</template>

<style scoped>
.container {
  padding: 20rpx;
}
.btn {
  width: 200rpx;
  height: 80rpx;
  line-height: 80rpx;
  font-size: 28rpx; /* 字体也推荐用rpx */
  background: #67c23a;
  color: white;
  text-align: center;
  border-radius: 40rpx;
}
</style>

1.2 rpx 的使用注意事项

  1. 不要用于字体的极端场景:虽然字体可以用 rpx,但如果设计稿要求 “不同屏幕字体大小变化幅度小”,建议搭配upx(UniApp 扩展单位,和 rpx 逻辑一致)或动态计算。
  2. 避免嵌套过深的百分比 + rpx 混合:比如父元素用 rpx,子元素用百分比,容易出现适配偏差。
  3. 多端适配特殊处理:在 App 端(尤其是平板),如果需要更精细的适配,可以通过uni.getSystemInfoSync()获取屏幕宽度,手动调整 rpx 换算比例。

二、正面刚:rpx vs px vs rem

为了更清晰理解 rpx 的优势,我们把这三个最常用的单位放在一起对比:

特性 rpx(响应式像素) px(物理像素) rem(根元素像素)
核心逻辑 基于屏幕宽度 750 等分 固定像素,与设备无关 基于根节点(html)字体大小
适配性 自动适配多端,无需计算 固定尺寸,适配性差 需手动设置根字体大小适配
使用场景 UniApp / 小程序多端开发 固定尺寸元素(如 1px 边框) H5/web 端适配
计算复杂度 0(直接用设计稿数值) 高(需按不同屏幕换算) 中(需配置根字体)
跨端支持 UniApp 全端支持 所有端支持 H5 支持,小程序 / App 需兼容

2.1 实战对比:同一个按钮的三种写法

需求:设计稿 750px 宽度,按钮宽度 200px,适配不同屏幕。
① rpx 写法(推荐)
.btn {
  width: 200rpx; /* 直接用设计稿数值,自动适配 */
  height: 80rpx;
}
② px 写法(适配性差)
/* 仅在375px宽度屏幕显示正常,其他屏幕会变形 */
.btn {
  width: 200px;
  height: 80px;
}

/* 如需适配,需手动媒体查询 */
@media screen and (width: 414px) {
  .btn {
    width: 224px; /* 200*(414/375) */
    height: 89.6px;
  }
}
③ rem 写法(UniApp 中需兼容)
/* 第一步:设置根字体大小(以375px屏幕为基准) */
html {
  font-size: 37.5px; /* 375/10,方便计算 */
}

/* 第二步:按钮样式 */
.btn {
  width: 5.333rem; /* 200/37.5 */
  height: 2.133rem; /* 80/37.5 */
}

/* 第三步:媒体查询适配其他屏幕 */
@media screen and (width: 414px) {
  html {
    font-size: 41.4px;
  }
}

从上面的对比能明显看出:rpx 写法最简洁,无需额外配置和计算,是 UniApp 开发的最优解。

2.2 什么时候不用 rpx?

rpx 虽好,但不是万能的,这两种场景建议换用其他单位:

  1. 1px 边框:rpx 无法精准表示 1px(比如在 2 倍屏上,1rpx=1px,3 倍屏上 1rpx≈1.5px),此时用 px 更合适。
  2. H5 端极致适配:如果 UniApp 项目主要面向 H5,且需要和现有 web 项目的 rem 适配逻辑统一,可选用 rem。
  3. 固定比例的图形:比如圆形头像(宽高比 1:1),可以用 rpx + 百分比组合,或直接用 vw/vh(UniApp H5 端支持)。

三、避坑指南:rpx 使用的常见问题

问题 1:设计稿不是 750px 宽度怎么办?

换算公式:目标rpx = 设计稿像素值 * 750 / 设计稿宽度。比如设计稿宽度 375px,按钮宽度 100px → 100*750/375=200rpx。

问题 2:App 端 rpx 适配偏差?

原因:部分安卓设备的屏幕密度计算差异。解决:通过uni.getSystemInfoSync()获取screenWidth,手动调整样式:

运行

``` javascript
onLoad() {
  const { screenWidth } = uni.getSystemInfoSync();
  // 计算实际rpx比例
  this.rpxRatio = 750 / screenWidth;
  // 动态设置样式
  this.btnWidth = 200 / this.rpxRatio;
}
```

问题 3:小程序端 rpx 和 px 混用导致布局错乱?

解决:统一单位体系,优先用 rpx,仅在特殊场景(如 1px 边框)用 px。

总结

  1. rpx 是 UniApp 多端适配的核心单位:基于 750px 屏幕宽度等分,无需手动换算,直接复用设计稿数值。
  2. rpx vs px vs rem:rpx 适配效率最高(自动),px 适配性最差(固定),rem 需手动配置(适合 H5)。
  3. 使用原则:UniApp 开发优先用 rpx,仅在 1px 边框、H5 兼容等特殊场景换用 px/rem。

掌握 rpx 的核心逻辑,你就能摆脱 “适配焦虑”,真正实现 UniApp“一套代码,多端运行” 的核心优势。与其在不同设备的适配中反复调试,不如从一开始就选对单位,让开发效率翻倍

微信小程序Canvas海报生成组件的完整实现方案

微信小程序Canvas海报生成组件的完整实现方案

前言

在微信小程序开发中,海报生成是一个常见的需求场景,比如邀请海报、分享海报、活动推广等。虽然需求看似简单,但实际开发中会遇到很多技术难点:Canvas 绘制、图片处理、权限管理、2倍图适配等。

本文将详细介绍如何从零开始开发一个高度可配置的微信小程序海报生成组件 wxapp-poster,深入解析核心实现细节和技术难点。

一、需求分析与技术选型

1.1 需求分析

在开始开发之前,我们需要明确组件的核心需求:

  • 功能需求:支持背景图、二维码、文字的自定义配置
  • 样式需求:支持颜色、字体、间距等所有样式参数的自定义
  • 交互需求:支持预览、保存到相册
  • 性能需求:使用 2 倍图提升清晰度,避免模糊
  • 兼容性需求:适配不同屏幕尺寸,处理权限问题

1.2 技术选型

为什么选择 Canvas?

微信小程序中实现图片合成主要有两种方案:

  1. 服务端生成:需要后端支持,增加服务器压力,实时性差
  2. 客户端 Canvas 绘制:实时生成,用户体验好,无需服务器支持

我们选择 Canvas 方案,因为:

  • 实时生成,用户体验好
  • 不依赖后端服务
  • 可以充分利用小程序原生能力

为什么使用 Component 而不是 Page?

组件化设计可以让代码更易复用和维护,符合小程序的最佳实践。

二、架构设计

2.1 组件结构

components/poster/
├── poster.js      # 组件逻辑
├── poster.wxml    # 组件模板
├── poster.wxss    # 组件样式
├── poster.json    # 组件配置
└── package.json   # npm 包配置

2.2 设计思路

组件采用双视图设计

  1. 预览视图:使用 WXML + WXSS 实现,用于用户预览
  2. Canvas 视图:隐藏的 Canvas,用于实际绘制和生成图片

这种设计的优势:

  • 预览视图可以实时响应样式变化
  • Canvas 在后台绘制,不影响用户体验
  • 最终保存的是 Canvas 生成的图片,质量更高

三、核心实现详解

3.1 Canvas 初始化与 2 倍图处理

为什么需要 2 倍图?

在移动端,为了在高分辨率屏幕上显示清晰,需要使用 2 倍图。Canvas 的默认分辨率较低,直接绘制会导致图片模糊。

实现代码:

initCanvas() {
  let that = this;
  wx.getSystemInfo({
    success: (res) => {
      const { imageRatio, whiteAreaHeight } = that.properties;
      // 使用2倍canvas提升清晰度
      const canvasWidth = res.screenWidth * 2; // 2倍图宽度
      // 根据配置的图片比例计算高度
      const imageAreaHeight = canvasWidth * imageRatio.height / imageRatio.width;
      // 白色区域高度转换为px(2倍图)
      const whiteAreaHeightPx = whiteAreaHeight * canvasWidth / res.screenHeight * 2;
      const canvasHeight = imageAreaHeight + whiteAreaHeightPx;
      
      that.setData({
        screenHeight: res.screenHeight,
        photoWidth: canvasWidth, // canvas实际宽度(2倍图)
        photoHeight: canvasHeight, // canvas实际高度(2倍图)
      });
      // 初始化完成后绘制
      that.draw();
    }
  });
}

关键技术点:

  1. 2 倍图计算canvasWidth = screenWidth * 2
  2. 比例计算:根据配置的 imageRatio 计算实际高度
  3. rpx 转 px:小程序使用 rpx 单位,需要转换为 px
    • 转换公式:px = rpx * screenWidth / 750
    • 2倍图转换:px = rpx * screenWidth * 2 / 750

3.2 Canvas 绘制流程

绘制流程分为以下几个步骤:

draw() {
  // 1. 获取图片信息
  wx.getImageInfo({
    src: backgroundImage,
    success: (imageRes) => {
      let ctx = wx.createCanvasContext('canvasPoster', that);
      
      // 2. 绘制背景
      ctx.setFillStyle(canvasBackgroundColor);
      ctx.fillRect(0, 0, canvasWidth, canvasHeight);
      
      // 3. 绘制背景图片
      ctx.drawImage(backgroundImage, 0, 0, canvasWidth, imageAreaHeight);
      
      // 4. 绘制白色区域
      ctx.setFillStyle(whiteAreaBackgroundColor);
      ctx.fillRect(0, imageAreaHeight, canvasWidth, whiteAreaHeightPx);
      
      // 5. 绘制文字
      // ... 文字绘制逻辑
      
      // 6. 绘制二维码(异步)
      if (qrImage) {
        wx.getImageInfo({
          src: qrImage,
          success: (qrRes) => {
            ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize);
            that.drawCanvas(ctx, canvasWidth, canvasHeight);
          }
        });
      }
    }
  });
}

3.3 文字绘制与垂直居中

文字绘制是组件中最复杂的部分,需要处理:

  • rpx 到 px 的转换
  • 字体大小的 2 倍图适配
  • 垂直居中对齐
  • 行间距控制

实现代码:

// rpx 转 px 的转换系数
// 原逻辑:28rpx -> 36 * canvasWidth / 750
// 转换系数:主文本 36/28 ≈ 1.286, 次文本 32/24 ≈ 1.333
const primaryTextRatio = 36 / 28;
const secondaryTextRatio = 32 / 24;
const fontSize1 = primaryTextSize * primaryTextRatio * canvasWidth / 750;
const fontSize2 = secondaryTextSize * secondaryTextRatio * canvasWidth / 750;

// 计算白色区域中心点
const whiteAreaStartY = imageAreaHeight;
const whiteAreaCenterY = whiteAreaStartY + whiteAreaHeightPx / 2;

// 计算行间距
const lineSpacing = fontSize2 * lineSpacingRatio;
// 计算两行文字的总高度
const totalTextHeight = fontSize1 + lineSpacing + fontSize2;

// 计算第一行文字的y坐标(垂直居中)
// fillText的y坐标是基线位置,所以需要加上字体大小
const firstLineY = whiteAreaCenterY - totalTextHeight / 2 + fontSize1;
// 计算第二行文字的y坐标
const secondLineY = firstLineY + fontSize1 + lineSpacing;

// 绘制文字
ctx.setFontSize(fontSize1);
ctx.setFillStyle(primaryTextColor);
ctx.setTextAlign('left');
ctx.fillText(primaryText, leftPaddingPx, firstLineY);

关键技术点:

  1. 基线对齐fillText 的 y 坐标是文字基线位置,不是顶部,需要加上字体大小的一半才能实现视觉居中
  2. 行间距计算:使用相对于字体大小的比例,保证不同字体大小下间距协调
  3. 垂直居中算法
    中心点Y = 区域起始Y + 区域高度 / 2
    第一行Y = 中心点Y - 总高度 / 2 + 字体大小
    第二行Y = 第一行Y + 字体大小 + 行间距
    

3.4 二维码绘制与定位

二维码需要:

  • 根据配置的比例计算大小
  • 右对齐
  • 垂直居中
if (qrImage) {
  wx.getImageInfo({
    src: qrImage,
    success: (qrRes) => {
      // 计算二维码尺寸(正方形,根据配置的比例)
      const qrSize = whiteAreaHeightPx * qrSizeRatio;
      // 计算x坐标:距离右边
      const rightPaddingPx = rightPadding * canvasWidth / 750;
      const qrX = canvasWidth - rightPaddingPx - qrSize;
      // 计算y坐标:垂直居中
      const qrY = whiteAreaCenterY - qrSize / 2;
      
      // 绘制二维码(正方形)
      ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize);
      
      // 绘制所有内容
      that.drawCanvas(ctx, canvasWidth, canvasHeight);
    }
  });
}

3.5 Canvas 转图片

绘制完成后,需要将 Canvas 转换为图片文件:

drawCanvas(ctx, canvasWidth, canvasHeight) {
  let that = this;
  ctx.draw(true, () => {
    // 因为安卓机兼容问题, 所以方法要延迟
    setTimeout(() => {
      wx.canvasToTempFilePath({
        canvasId: 'canvasPoster',
        x: 0,
        y: 0,
        width: canvasWidth,
        height: canvasHeight,
        destWidth: canvasWidth,  // 保持2倍图分辨率
        destHeight: canvasHeight,
        success: res => {
          let path = res.tempFilePath;
          that.setData({
            tempImagePath: path
          });
          // 触发绘制完成事件
          that.triggerEvent('drawcomplete', { tempImagePath: path });
        },
        fail: (err) => {
          console.error('生成临时图片失败:', err);
          that.triggerEvent('error', { err });
        }
      }, that); // 新版小程序必须传this
    }, 200);
  });
}

关键技术点:

  1. 延迟处理:Android 设备上需要延迟执行,确保 Canvas 绘制完成
  2. 分辨率保持destWidthdestHeight 设置为 2 倍图尺寸,保持高清
  3. this 传递:新版小程序 API 必须传递组件实例

四、权限处理机制

保存图片到相册需要处理相册权限,这是小程序开发中的常见难点。

4.1 权限状态

微信小程序的权限有三种状态:

  1. 未授权:用户未操作过
  2. 已授权:用户已同意
  3. 已拒绝:用户已拒绝,需要引导到设置页面

4.2 实现代码

saveImage() {
  if (!this.data.tempImagePath) {
    wx.showToast({
      title: loadingText,
      icon: 'none'
    });
    return;
  }

  // 检查授权
  wx.getSetting({
    success: (res) => {
      if (res.authSetting['scope.writePhotosAlbum']) {
        // 已授权,直接保存
        this.doSaveImage();
      } else if (res.authSetting['scope.writePhotosAlbum'] === false) {
        // 已拒绝授权,引导用户开启
        wx.showModal({
          title: permissionModalTitle,
          content: permissionModalContent,
          showCancel: true,
          confirmText: permissionModalConfirmText,
          success: (modalRes) => {
            if (modalRes.confirm) {
              wx.openSetting(); // 打开设置页面
            }
          }
        });
      } else {
        // 未授权,请求授权
        wx.authorize({
          scope: 'scope.writePhotosAlbum',
          success: () => {
            this.doSaveImage();
          },
          fail: () => {
            wx.showToast({
              title: needPermissionText,
              icon: 'none'
            });
          }
        });
      }
    }
  });
}

处理流程:

用户点击保存
    ↓
检查权限状态
    ↓
┌─────────────────┬──────────────┬──────────────┐
│   已授权        │   已拒绝      │   未授权      │
│   直接保存      │   引导设置    │   请求授权    │
└─────────────────┴──────────────┴──────────────┘

五、组件化设计

5.1 属性设计

组件采用高度可配置的设计,所有样式参数都可通过属性配置:

properties: {
  // 内容相关
  backgroundImage: { type: String, value: '' },
  qrImage: { type: String, value: '' },
  primaryText: { type: String, value: '邀请您一起加入POPO' },
  secondaryText: { type: String, value: '长按二维码识别' },
  
  // Canvas相关
  canvasBackgroundColor: { type: String, value: '#7e57c2' },
  canvasZoom: { type: Number, value: 40 },
  imageRatio: { type: Object, value: { width: 750, height: 1050 } },
  
  // 颜色配置
  whiteAreaBackgroundColor: { type: String, value: '#ffffff' },
  primaryTextColor: { type: String, value: '#000000' },
  secondaryTextColor: { type: String, value: '#9C9C9C' },
  
  // 字体配置
  primaryTextSize: { type: Number, value: 28 },
  secondaryTextSize: { type: Number, value: 24 },
  lineSpacingRatio: { type: Number, value: 0.3 },
  
  // ... 更多配置
}

5.2 事件设计

组件通过事件与父组件通信:

// 绘制完成事件
that.triggerEvent('drawcomplete', { tempImagePath: path });

// 保存成功事件
this.triggerEvent('savesuccess', { tempImagePath: this.data.tempImagePath });

// 保存失败事件
this.triggerEvent('saveerror', { err, message });

// 错误事件
that.triggerEvent('error', { err });

5.3 方法暴露

组件暴露 saveImage 方法供外部调用:

// 在页面中调用
const poster = this.selectComponent('#poster');
poster.saveImage();

六、样式与布局

6.1 Canvas 隐藏

Canvas 需要隐藏,但保持绘制能力。使用 zoomposition 实现:

<canvas 
  class="poster__canvas" 
  canvas-id="canvasPoster" 
  style="width:{{photoWidth}}px;height:{{photoHeight}}px;zoom:{{canvasZoom}}%">
</canvas>
.poster__canvas {
  position: absolute;
  left: 99999rpx;  /* 移出屏幕 */
  top: 0rpx;
}

6.2 预览视图

预览视图使用常规的 WXML + WXSS 实现,实时响应样式变化:

<view class="poster__image-container">
  <image class="poster__image-container-image" src="{{backgroundImage}}"></image>
  <view class="poster__white-area">
    <view class="poster__white-area-left">
      <view class="poster__text poster__text--primary">{{primaryText}}</view>
      <view class="poster__text poster__text--secondary">{{secondaryText}}</view>
    </view>
    <view class="poster__white-area-right">
      <image class="poster__qr-image" src="{{qrImage}}" />
    </view>
  </view>
</view>

七、性能优化

7.1 图片加载优化

  • 使用 wx.getImageInfo 预加载图片,确保绘制时图片已加载完成
  • 异步加载二维码,避免阻塞主流程

7.2 Canvas 绘制优化

  • 使用 2 倍图提升清晰度,避免模糊
  • 延迟执行 canvasToTempFilePath,确保 Android 设备兼容性

7.3 内存管理

  • 及时清理临时文件路径
  • 避免重复绘制,只在必要时触发

八、常见问题与解决方案

8.1 Canvas 模糊问题

问题:生成的图片模糊

解决方案:使用 2 倍图

const canvasWidth = res.screenWidth * 2; // 2倍图

8.2 Android 设备兼容性

问题:Android 设备上 canvasToTempFilePath 执行失败

解决方案:延迟执行

setTimeout(() => {
  wx.canvasToTempFilePath({...}, that);
}, 200);

8.3 文字垂直居中

问题:文字无法垂直居中

解决方案:考虑基线对齐

// fillText的y坐标是基线位置
const firstLineY = whiteAreaCenterY - totalTextHeight / 2 + fontSize1;

8.4 权限处理

问题:用户拒绝权限后无法再次请求

解决方案:引导用户到设置页面

if (res.authSetting['scope.writePhotosAlbum'] === false) {
  wx.showModal({
    confirmText: '去设置',
    success: (modalRes) => {
      if (modalRes.confirm) {
        wx.openSetting();
      }
    }
  });
}

九、发布 npm 包

9.1 package.json 配置

{
  "name": "wxapp-poster",
  "version": "1.0.2",
  "description": "微信小程序海报生成组件",
  "main": "poster.js",
  "miniprogram": ".",
  "files": [
    "poster.js",
    "poster.wxml",
    "poster.wxss",
    "poster.json",
    "README.md"
  ]
}

9.2 发布流程

# 1. 登录 npm
npm login

# 2. 发布
npm publish

十、总结

本文详细介绍了微信小程序海报生成组件的开发实践,包括:

  1. 技术选型:选择 Canvas + Component 方案
  2. 2倍图处理:提升图片清晰度
  3. 文字绘制:处理 rpx 转换、垂直居中、行间距
  4. 权限管理:完善的权限处理机制
  5. 组件化设计:高度可配置、低耦合
  6. 性能优化:图片预加载、延迟执行等

通过这个组件的开发,我们不仅解决了海报生成的需求,还积累了很多小程序开发的经验。希望这篇文章能帮助到正在开发类似功能的开发者。

相关链接


如果这篇文章对你有帮助,欢迎 Star ⭐ 和 Fork,也欢迎提出 Issue 和 PR!

❌