第一次参与开源记录
发表于 2026 年 9 月 14 日 · 次浏览
记录一下第一个被合并的开源 PR。
在这之前我对参与开源的理解挺模糊的。总觉得有的很水,就是去别人的文档里改改错别字刷个绿格子。想正常参与开源门槛很高,我也觉得自己一个大二升大三的学生,平时写业务还不算特别熟练,开源离我挺远的。
后来做这个既有简历的要求,想把简历完善一下,加一个开源经历会比较好看,另外也想体验一下参加这种大型项目维护的感觉。
之前跟维护者私聊了一下,我提出了想参与维护这个项目,维护者很开心,他说入门可以拿 AI 去扫扫仓库里的问题,提提 issue,他会亲自看然后合 PR。
前几天ai额度没了,天才程序员陨落了,这两天重置了周额度,我就把仓库拉到本地用ai一顿扫,结果真找到一个会影响真机样式的运行时问题。经过自己学习之后,又经过多次验证,确保问题确实存在,随后我在本地写了测试用例,改完代码提了 PR。作者随后在我的分支上追加了性能优化,最后合入发版。第一次开源就这样完成了。
项目简介
平时在浏览器里写网页用 Tailwindcss很舒畅。中括号、斜杠、冒号,现代浏览器都能正常解析。
<div class="w-[100px] bg-red-500/80 hover:bg-blue-500 2xl:p-4"></div>但如果把这套写法放到微信小程序里,微信开发者工具当场就会报错,因为小程序的 WXSS 样式编译器有硬性限制。
中括号、斜杠和冒号会被直接判定成语法错误。选择器不能以纯数字开头,2xl:p-4 这种名字放进 WXSS 里就会报错。另外 Tailwind 默认生成的全局样式重置里包含星号通配符,小程序里同样没有这个东西。
weapp-tailwindcss 做的事情是在编译阶段把这些微信处理不了的符号换掉。比如把 hover:p-4 改成 hover_cp-4,把数字开头的 2xl:p-4 前面加一个下划线变成 _2xl_cp-4。它在改写 WXSS 选择器的同时,把 WXML 标签上的类名也改成相同的名字,这样两边就能对上。
具体问题
编译阶段的静态类名转义处理得挺好,但在运行时动态合并类名的地方出了问题。
平时写组件经常会用类似 twMerge 的工具函数做样式合并,让后传入的样式覆盖掉默认样式。
<view className={twMerge('p-1 2xl:p-2', className)} />如果外部传入 2xl:p-4,理应把前面的 2xl:p-2 覆盖掉。
这个包的工作机制是先把你传进去的小程序类名还原成 Tailwind 语法,交给官方 tailwind-merge 处理覆盖关系,再把结果重新转义成小程序的格式。
底层负责转义的库是 @weapp-core/escape。这个库内部假定每次只处理单个类名,遇到数字开头的类名就在最前面补一个下划线。
上层的 packages-runtime/runtime/src/create-runtime.ts 在调用时,把整条带有空格的字符串直接传给了转义和还原函数。
const normalized = shouldUnescape(rawInput) ? transformers.unescape(rawInput) : rawInput
const merged = fn(preparedValue)
const escaped = transformers.escape(restored)这样传参会引发两个问题。
第一个问题是样式覆盖失败。如果代码里传入的是编译后的小程序类名 twMerge('_2xl_cp-2 _2xl_cp-4'),函数只看了整串文本的最开头,把第一个下划线剥了,中间的第二个完全没动。传给合并器时变成了 '2xl:p-2 _2xl:p-4'。合并器判定这是两个不同的修饰符,把两个类名都保留了下来,覆盖失效。
第二个问题是样式在真机上完全不生效。如果传入普通类名 twMerge('p-1 2xl:p-2 2xl:p-4'),合并完得到 'p-1 2xl:p-4',再交给转义函数。转义函数只看第一个字符,发现是字母 p,判定这串文本不需要补下划线。最后输出就会变成 'p-1 2xl_cp-4'。
WXML 节点上的类名变成了没有下划线的 2xl_cp-4,样式表里生成的规则却是带下划线的 ._2xl_cp-4。两个名字差了一个下划线,微信开发者工具里不报错,但写在大屏上的 2xl 样式在真机上根本出不来。
单元测试复现
要证实这个问题,最直接的办法是在项目的单测目录里写断言。
把项目拉到本地装好依赖后,在 packages-runtime/merge/test/ 目录下加一个测试文件。
import { describe, expect, it } from 'vitest'
import { twMerge } from '../src'
describe('2xl variant escaping issue in miniprogram', () => {
it('转义类名输入 2xl 冲突应消解', () => {
expect(twMerge('_2xl_cp-2 _2xl_cp-4')).toBe('_2xl_cp-4')
})
it('原始类名输入非首位 2xl 类须保留下划线', () => {
expect(twMerge('p-1 2xl:p-2 2xl:p-4')).toBe('p-1 _2xl_cp-4')
})
})在终端里执行测试命令。
corepack pnpm --filter @weapp-tailwindcss/merge test终端立刻报了两个错误。
FAIL test/verify-2xl.test.ts > 转义类名输入 2xl 冲突应消解
AssertionError: expected '_2xl_cp-2 _2xl_cp-4' to be '_2xl_cp-4'
FAIL test/verify-2xl.test.ts > 原始类名输入非首位 2xl 类须保留下划线
AssertionError: expected 'p-1 2xl_cp-4' to be 'p-1 _2xl_cp-4'两个断言都挂掉了,说明问题确实存在。
修改代码,提 PR
底层库每次只能吃单个类名,上层就不能整串塞进去。在转义和还原之前,先按空白字符把字符串切开,让每个单词单独走一遍转义,最后再拼起来。
我在 packages-runtime/runtime/src/create-runtime.ts 里加了一段分词逻辑。
function transformTokens(value: string, transformFn: (token: string) => string): string {
if (!value) return value
if (!value.includes(' ')) return transformFn(value)
return value.split(/\s+/).filter(Boolean).map(transformFn).join(' ')
}改完之后把刚才写的断言并入官方已有的 twMerge.test.ts。重新跑测试,23 个用例全部通过。
随后我在 GitHub 提了 Issue # 1198,把两组场景、复现代码和错误输出贴上去。接着切分支提交代码,按照仓库的要求在 .changeset/ 目录下加上改动说明,发起 PR # 1199。
维护者跟进改动,合PR
提完 PR 时已经比较晚了,我本来以为要等几天。结果作者 ice breaker 半夜在线。他看了 PR 后直接拉取了我的分支,在上面追加了两个提交。一个帮我把代码结构规整了一下,另一个做了一次性能优化,用字符查找替换了正则表达式,减少运行时的开销,并在 Issue 里留了很详细的回复,确认了问题原因并说明已经跟进处理。
云端测试跑通之后,PR # 1199 被合入主分支,关联的 Issue 随之关闭。随后触发了官方的发版 PR # 1201,新版本被打包推上了 npm。
以前总觉得参与大项目门槛很高。现在看来,只要从真实问题出发,哪怕是一个下划线缺失导致的选择器失配,只要把复现证据给足,维护者都会认真对待。
纪念一下第一次开源经历。其实自己起步也很晚了,比不上那些很早就活跃在GitHub里面四处开源的技术哥。慢慢来吧,希望以后能做出更多有用的贡献。