Skip to main content

游牧周记第41期

· 15 min read
Suhe
This site owner

日常

阳台种菜

九层塔、冰菜、生菜、羽衣甘蓝又割了一茬。

正好在B站看到一个视频,关于吃蔬菜的危害(我是一名生物专业的学生,这就是为什么我不吃蔬菜),其实是有道理的,我懂。 当然他还有关于不吃水果等的视频。

关注

HTX Studio

虽然知道何同学很厉害,但他们在国内发布的视频我都看得不多(感觉比影视飓风强点不多)。 本周突然在Youtube看到了他的英文版,而且发音不太像AI(如果是真的,那他的English is quite good.。 现在有117万订阅,每个视频10w+播放,这是只有6个长视频的情况下,BTW,我全部看完了。 几乎所有的评论都在感叹其制作质量和内容是降维打击。 可能老外们不会注意到这些优秀的团队都来自杭州。

开发

Expo SDK 54后发生的问题

发现ThemeProvider似乎不能动态的调整了,也就是value不能变化。 还在找原因。

CLI 型 AI 开发工具

装了Claude Code和Gemini Cli,下一步还有Codex,总得用到的,先看看吧。 CC由于封号原因,先用了国内某代理,价格比官方贵一倍,还随时抽风。 Gemini Cli用Google Account的API Key连接auth,每次用之前把终端proxy调到位。 Codex还没开始,因为必须注册pro账号,还是等代理吧。 哎国内开发就是找罪受。

React Native Dimensions函数

  • 问题:react native中, Dimensions.get('screen')和 Dimensions.get('window')获取的数据有何不同?
  • 回答(by Kimi):

在 React Native 中,Dimensions.get('screen')Dimensions.get('window') 都可以获取设备的尺寸信息,但它们返回的数据含义不同,适用于不同的场景。


✅ 一句话区别:

  • Dimensions.get('window'):获取的是应用窗口的尺寸(不包括状态栏、导航栏等系统 UI)。

  • Dimensions.get('screen'):获取的是整个屏幕的尺寸(包括状态栏、导航栏等系统 UI)。


📌 举个例子(以竖屏 Android 手机为例):

表格

复制

属性windowscreen
width应用可用宽度屏幕总宽度
height应用可用高度(不含状态栏/导航栏)屏幕总高度(含状态栏/导航栏)

⚠️ 注意事项:

  • iOS 上,windowscreen 通常是一样的(除非有状态栏隐藏或特殊布局)。

  • Android 上,两者经常不同,尤其是有虚拟导航栏的设备。

  • 如果你在做全屏布局适配刘海屏,建议优先使用 window,因为它反映的是应用实际可用区域


✅ 最佳实践:

  • Dimensions.get('window') 来布局页面内容。

  • Dimensions.get('screen') 来调试或获取设备总尺寸(很少用于布局)。


🔍 示例代码:

JavaScript

复制

import { Dimensions } from 'react-native';

const window = Dimensions.get('window');
const screen = Dimensions.get('screen');

console.log('Window:', window);
console.log('Screen:', screen);

✅ 总结一句话:

window 是你能用的, screen 是你能看的。布局用 window,调试看 screen

React Native组合样式的性能问题

由于我的个人代码最佳styling实践中,大量采用了style=[...]方式的应用,以保持一定程度的灵活性,并希望至少做到超过或与tailwindcss(NativeWind)一致的性能。

但与单一style参数比,组合会多大程度上影响性能开销呢? 问了grok。

react native 代码:


const styles=StyleSheet.create({
a: { margin: 16},
b:{ padding: 16},
c:{ margin: 16, padding: 16 }})

...

<View style={[styles.a, styles.b]}> ...

<View style={styles.c}> ...

这两种写法,性能上差距大吗?如果[...]中组合数增加呢?

在 React Native 中,使用 StyleSheet.create 定义样式并通过 style 属性应用样式时,[styles.a, styles.b]styles.c 这两种写法的性能差异需要从样式合并和渲染的机制来分析。以下是详细解答:

1. 两种写法的本质

  • [styles.a, styles.b]:这是将多个样式对象组合成一个数组,React Native 会在运行时将这些样式合并为一个单一的样式对象。合并过程会按照数组中样式的顺序,后面样式的属性会覆盖前面样式的同名属性(如果有冲突)。
  • styles.c:这是一个单一的样式对象,React Native 直接应用这个样式,没有合并过程。

2. 性能差异分析

a. 单一样式对象(styles.c

  • 优点:React Native 直接使用 styles.c 中的样式定义,无需额外的合并操作。样式解析和应用过程更直接,理论上性能开销最小。
  • 适用场景:当样式固定且无需动态组合时,单一样式对象是性能最佳的选择。

b. 样式数组([styles.a, styles.b]

  • 合并开销:React Native 在运行时会对样式数组进行合并,生成一个最终的样式对象。这个合并过程涉及对象属性的复制和覆盖,会有一定的性能开销。
  • 开销大小:对于 [styles.a, styles.b] 这样只有两个样式的数组,合并开销非常小,在大多数情况下几乎可以忽略。React Native 的样式系统经过优化,处理少量样式的合并性能影响微乎其微。
  • 动态性和灵活性:样式数组允许动态组合样式(例如根据条件添加或移除样式),这在需要动态调整 UI 的场景下非常有用。

c. 组合数增加的影响

  • 如果样式数组中的组合数增加(例如 [styles.a, styles.b, styles.d, styles.e]),合并开销会略微增加,因为需要处理更多的样式对象。
  • 性能影响
    • 合并多个样式对象的时间复杂度大致是线性的,取决于样式对象的属性数量。每个样式对象的属性越多,合并过程的开销越大。
    • 在实际应用中,除非样式数组非常长(例如几十个样式对象)或样式对象包含大量属性(例如几十个属性),否则性能影响通常不会明显。
    • React Native 的样式系统是高效的,合并操作在 JavaScript 层执行,且最终样式会被缓存,减少重复计算。

d. 缓存与优化

  • React Native 的 StyleSheet.create 创建的样式对象会被缓存,样式 ID 会被传递到原生层,避免每次都重新解析样式。
  • 对于样式数组,React Native 会在第一次合并时生成一个新的样式 ID,之后的渲染会复用这个 ID,因此重复渲染的性能开销较小。
  • 如果样式数组的内容是动态变化的(例如根据状态或 props 动态添加样式),每次变化都会触发新的合并操作,可能会增加少量开销。

3. 实际性能差距

  • 少量样式组合(例如 [styles.a, styles.b]):性能差距可以忽略不计。现代设备的 JavaScript 引擎和 React Native 的优化使得这种场景下的合并开销非常小。
  • 大量样式组合(例如超过 10 个样式对象):性能开销会略有增加,但除非在高频渲染的场景(如 FlatList 的每一项都动态组合大量样式)或低性能设备上,否则用户难以感知到差异。
  • 单一样式对象styles.c):始终是最优选择,因为它避免了合并步骤,但灵活性较低。

4. 建议与最佳实践

  • 优先使用单一样式对象:如果样式是固定的,尽量将所有样式定义在一个对象中(如 styles.c),避免不必要的合并开销。
  • 合理使用样式数组:当需要动态组合样式(例如根据条件应用不同样式)时,使用样式数组是合理的选择。尽量控制数组长度,避免不必要地拆分样式。
  • 优化动态样式
    • 如果动态样式频繁变化,可以使用 useMemo 缓存样式数组,减少重复计算。例如:
      const combinedStyles = useMemo(() => [styles.a, condition ? styles.b : styles.d], [condition]);
    • 避免在高频渲染组件(如 FlatList 或 SectionList 的渲染项)中使用过于复杂的动态样式组合。
  • 测试性能:在低端设备或复杂场景下,使用 React Native 的性能监控工具(如 Performance MonitorReact DevTools)测试实际性能影响。

5. 结论

  • [styles.a, styles.b]styles.c 的性能差距在大多数情况下微乎其微,特别是在样式数组较短(2-3 个样式)时。
  • 如果样式数组中的组合数显著增加(例如 10 个以上样式对象),合并开销会略有增加,但通常不会对用户体验产生明显影响。
  • 为追求最佳性能,优先使用单一样式对象(如 styles.c),但在需要动态性和灵活性的场景下,合理使用样式数组并配合优化手段(如 useMemo)是可接受的。

感觉和我想的差不多。 如果和tailwindcss的方式对比呢?

在 React Native 中使用 NativeWind(Tailwind CSS 的 React Native 实现)时,多个样式组合的写法(如 className="m-4 p-4 bg-blue-500") 与原生 React Native 的 StyleSheet.create 样式组合(如 [styles.a, styles.b]styles.c)在性能上的处理机制有相似之处,但也有一些关键差异。以下是详细分析,针对 NativeWind 的样式组合与性能处理,并与 React Native 原生样式系统进行对比。

1. NativeWind 的样式组合机制

NativeWind 将 Tailwind CSS 的类名(如 m-4 p-4)转换为 React Native 的样式对象。它在构建时或运行时将类名映射到对应的样式属性,生成类似于 StyleSheet.create 的样式对象,最终传递给 React Native 的原生渲染层。

  • 样式组合的处理

    • 当你写 className="m-4 p-4 bg-blue-500",NativeWind 会解析这些类名,生成一个合并后的样式对象,类似于:
      {
      margin: 16,
      padding: 16,
      backgroundColor: '#3B82F6'
      }
    • 多个类名会被合并为单一的样式对象,类似于 React Native 的 [styles.a, styles.b] 合并过程。
    • NativeWind 的样式解析通常在 JavaScript 层完成,解析后的样式对象会被缓存(通过 StyleSheet.create 或类似机制),并传递到原生层。
  • 与 React Native 原生样式的对比

    • 相似之处:NativeWind 的样式最终会被转换为 React Native 的样式对象,性能上依赖于 React Native 的样式系统。因此,合并多个样式的开销与 [styles.a, styles.b] 的合并过程类似。
    • 不同之处:NativeWind 需要额外的类名解析步骤(将字符串类名映射到样式对象),这在首次解析时会引入少量开销。React Native 的 StyleSheet.create 直接使用预定义的 JavaScript 对象,没有解析步骤。

2. 性能分析

a. NativeWind 的性能开销

  • 类名解析
    • NativeWind 在首次遇到某个类名组合(如 m-4 p-4)时,会解析这些类名并生成对应的样式对象。这个过程涉及字符串解析和映射,可能比直接使用 StyleSheet.create 多一些开销。
    • 一旦样式对象生成,NativeWind 会缓存结果(类似于 StyleSheet.create 的缓存机制),后续使用相同的类名组合时会复用缓存,避免重复解析。
  • 合并开销
    • 当使用多个类名(如 className="m-4 p-4 bg-blue-500 text-white"),NativeWind 会将这些类名对应的样式合并为一个样式对象。合并过程与 [styles.a, styles.b] 类似,性能开销取决于类名数量和样式属性的复杂性。
    • 对于少量类名(例如 2-5 个),合并开销微乎其微,接近于原生样式的数组合并。
  • 运行时动态性
    • 如果类名是动态生成的(例如 className={condition ? 'm-4' : 'm-2'}),NativeWind 需要在每次类名变化时重新解析和合并样式,可能增加少量运行时开销。
    • NativeWind 的 AOT(Ahead-of-Time)编译模式(如果启用)可以在构建时预生成样式,减少运行时解析开销。

b. 与 React Native 原生样式的性能对比

  • 单一样式对象(如 styles.c vs className="m-4 p-4"):
    • 原生 styles.c 直接使用预定义的样式对象,无需解析,性能略优。
    • NativeWind 的 className="m-4 p-4" 需要解析类名,首次渲染时有轻微额外开销,但缓存后性能接近原生。
  • 多个样式组合(如 [styles.a, styles.b] vs className="m-4 p-4"):
    • 原生样式的 [styles.a, styles.b] 合并是简单的对象合并,性能开销小。
    • NativeWind 的多个类名合并涉及类名解析和对象合并,首次解析的开销略高于原生样式,但缓存后差异不大。
  • 大量样式组合(如 className="m-4 p-4 bg-blue-500 text-white flex-1 rounded-lg ..."):
    • 如果类名数量增加,NativeWind 的解析和合并开销会略微增加,但由于缓存机制,实际影响通常不明显。
    • 原生样式的数组合并(如 [styles.a, styles.b, styles.c, ...])在样式对象数量多时也有类似开销,性能差异不大。

c. NativeWind 的性能优化

  • 缓存机制:NativeWind 会缓存解析后的样式对象,类似于 StyleSheet.create。相同的类名组合(如 m-4 p-4)在后续渲染中会复用缓存,减少重复解析。
  • AOT 编译:NativeWind 提供 AOT 编译(通过 @nativewind/vite-plugin 等工具),在构建时预生成样式,消除运行时解析开销。这种模式下,性能接近原生 StyleSheet.create
  • Tree Shaking:NativeWind 支持 Tree Shaking,移除未使用的样式定义,减少打包体积和运行时开销。
  • 静态类名:当类名是静态的(不动态变化),NativeWind 的性能接近原生样式,因为解析和合并的结果会被缓存。

3. NativeWind 是否有更好的性能处理?

  • 与原生样式相比
    • 原生 StyleSheet.create 在单一样式对象(如 styles.c)的情况下性能最优,因为没有解析步骤。
    • NativeWind 在首次解析类名时有额外开销,但通过缓存和 AOT 编译,性能可以接近原生样式,尤其是在静态类名场景下。
  • NativeWind 的优势
    • 开发效率:Tailwind 的类名写法更简洁,减少手动编写样式对象的工作量,适合快速迭代。
    • 一致性:与 Web 开发中的 Tailwind CSS 保持一致,便于跨平台开发。
    • 动态性:通过动态类名(如 className={condition ? 'm-4' : 'm-2'}),可以更方便地实现条件样式,代码更简洁。
  • NativeWind 的劣势
    • 首次解析类名的开销略高于原生样式,尤其在动态类名频繁变化时。
    • 如果滥用大量动态类名(例如在高频渲染的列表中),可能导致性能下降。

4. 性能影响与类名数量

  • 少量类名(如 className="m-4 p-4"):性能与 [styles.a, styles.b] 几乎无差异,解析和合并开销可以忽略。
  • 大量类名(如 className="m-4 p-4 bg-blue-500 text-white flex-1 rounded-lg shadow-md ..."):
    • 解析和合并的开销略有增加,但由于缓存机制,影响通常不明显。
    • 如果类名动态变化频繁(例如在 FlatList 的每一项中动态生成类名),可能导致性能瓶颈,建议优化动态逻辑。
  • 与原生样式的对比:当组合数增加时,NativeWind 的解析开销可能略高于原生样式的数组合并,但实际影响取决于具体场景(如设备性能、渲染频率)。

5. 建议与最佳实践

  • 优先静态类名:尽量使用静态类名(如 className="m-4 p-4"),利用 NativeWind 的缓存机制,性能接近原生样式。
  • 启用 AOT 编译:在生产环境中使用 NativeWind 的 AOT 编译模式,预生成样式,消除运行时解析开销。
  • 优化动态类名
    • 使用 useMemo 缓存动态类名,减少重复解析。例如:
      const className = useMemo(() => condition ? 'm-4 p-4' : 'm-2 p-2', [condition]);
    • 避免在高频渲染组件(如 FlatList 项)中使用复杂动态类名。
  • 控制类名数量:尽量精简类名组合,避免不必要的冗余样式(例如用 p-4 替代 pt-4 pr-4 pb-4 pl-4)。
  • 性能测试:在低端设备或复杂场景下,使用 React Native 的性能工具(如 Performance MonitorReact DevTools)测试 NativeWind 的实际性能影响。
  • 与原生样式混合使用:在性能敏感的场景下,可以结合 StyleSheet.create 和 NativeWind。例如,使用 NativeWind 处理布局样式,使用 StyleSheet.create 处理复杂动画或高频更新的样式。

6. 结论

  • 性能差异:NativeWind 的多个样式组合(如 className="m-4 p-4") 与 React Native 的 [styles.a, styles.b] 在性能上道理相似,都有合并开销。NativeWind 因类名解析有轻微额外开销,但通过缓存和 AOT 编译,性能可以接近原生样式。
  • 大量样式组合:当类名数量增加时,NativeWind 的解析和合并开销略高于原生样式的数组合并,但实际影响通常不明显,除非在高频动态渲染场景下。
  • 更好的性能处理:NativeWind 通过 AOT 编译和缓存机制优化性能,在静态场景下表现接近原生样式。动态类名场景下需注意优化(如使用 useMemo)。
  • 选择建议
    • 如果追求极致性能且样式固定,使用 StyleSheet.create 的单一样式对象(如 styles.c)是最佳选择。
    • 如果需要开发效率和动态性,NativeWind 是一个优秀选择,性能差距在大多数场景下可以接受。

AI的回答考虑到了动态变化问题,大家都不会太好,当然那会有其他的处理方式,大不了为了性能暂不用任何组合而是多个style切换。

游牧周记第40期

· 3 min read
Suhe
This site owner

开发

nextjs的google ads问题

我的nextjs网站,似乎在集成ads时都一直有问题。 原来的代码:

<Script async src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${process.env.NEXT_PUBLIC_ADSENSE_ID}`} crossOrigin="anonymous" strategy="afterInteractive" />

AD不会显示,并有warning:

adsbygoogle.js?client=undefined:225 AdSense head tag doesn't support data-nscript attribute.

按照ChatGpt说法:

问题就出在 Next.js 的 <Script> 组件:它会额外加上 data-nscript 之类的属性,而 Google AdSense 的脚本是 严格要求只能原样引入 的。

✅ 正确做法

直接用原生 <script>,不要用 next/script

// app/layout.tsx 或者 _document.tsx <head> <script async src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${process.env.NEXT_PUBLIC_ADSENSE_ID}`} crossOrigin="anonymous" ></script> </head>


❗️另外注意

  1. process.env.NEXT_PUBLIC_ADSENSE_ID 必须在 .env 文件里定义,例如:

    NEXT_PUBLIC_ADSENSE_ID=ca-pub-1234567890123456

    并且要以 NEXT_PUBLIC_ 开头,否则 Next.js 不会暴露到前端。

  2. Google AdSense 要求 必须在 <head> 内加载,不要放到 <body>

当然我还是硬编码ads id方便一些。

关注

江边学社

佛学频道,介绍里有一句:“特别感谢@闲者知无涯-吴魏老师,他是本频道一切内容的缘起;”。 这个信息量大,我才知道自己听了很久,随时被封的闲者知无涯叫做吴魏。 另外闲者知无涯在youtube的频道多年未更新了,关注也就几百人。 而这位有2万+,每个视频点击也是数千到数万。 音频完全来自闲者知无涯,配了动画和图,很不错。 看了几集,回顾当时听的情况,感觉真的可以理解他为啥被封多次了,什么“永远健康”之类的😂。

影视

星期三season2

第4-8集出来了,原来当爆米花片看的,没想到越来越惊艳,后面剧情和演员表现都非常厉害。难得的不落俗套的娱乐佳片。 小狼女灵魂互换那集真是太棒了,隐身妹也没那么刻板和工具人了。 我就觉得乌鸦女面熟,果然是久违的Lady Gaga。

异形地球

各方面评价都很高,我觉得还行。 Fox的片,暴力方面不打折扣。 刚看了第5集,有那味了,愚蠢的人类啊。 异形不是只有一种,他们之间有竞争,神仙(妖怪)打架,眼球怪的压迫力超过JJ怪了。

游牧周记第39期

· 2 min read
Suhe
This site owner

开发

expo的scrollView只能触摸边缘滑动

expo app中的scrollview,发现只有触摸边缘才能滑动,而中间等部分无反应,什么原因?参考代码:


<ScrollView
ref={scrollViewRef}
style={s.flex} contentContainerStyle={s.p}>
<Text style={tx.para_inline}>
{response}
</Text>
</ScrollView>

AI给出一个方法,为 <Text> 添加pointerEvents="none"。 实际上无效。

最后我发现原来是ScrollView的style问题,去掉s.flex就好了,只需要contentContainerStyle。

后来发现还是无效。😓

新版本学易发布

其实只是4.0.+小版本升级,但改变了首页问卦按钮,加入了一个新界面:指引,也就是向导。

算是多年来界面和操作难得的更新了。

和一个湖南妹子推荐App,不懂易经只想算卦的前提下,我自以为已经很直观的首页还是让人一头雾水,必须要有个更加傻瓜的方法引导用户一步步操作。

推广

去图书馆和ChatGPT沟通了一上午,希望得到点app推广的建议。 答案都很好,但没有超出我想象的部分。 总的来说就是要持续不断地制作发布有一定价值和吸引力的作品(图、文、视频)。 后来请AI帮我做了新头像,用于数字游牧类视频,关于app推广的单独用app logo,并单独开了新的youtube频道。

美剧

广告狂人

久仰大名多年就是一直错过的神剧,无意之中在B站看了一段就停不下来了。 没错B站非官方发布的啥都有,除了两个限制:露点的要打码,TV版放不了(我的TCL)。

男主就是所有男人心目中希望自己成熟后的样子,原来今年看的苹果好剧"掩耳盗邻"就是他啊。

游牧周记第38期

· 3 min read
Suhe
This site owner

关注

最近听(看)了不少哲学方面的内容,颇有种信息量巨大,需要整理提炼的冲动,我看来应该做笔记,写点啥了。

大问题

哲学。喉咙不清,可能有轻度炎症。 但内容极好,选题精彩,节奏紧凑。 B站

徒梦的学习笔记

哲学对话,非视频! 作为背景听太棒了,台湾的。以2人对话方式展开。 Bilibili 后来发现内容是从其他语音平台搬运的,仅B站就还有类似频道。 原频道还没有找。

# PowerfulJRE

没啥好说的,Joe Rogan的访谈记录, 2000万+订阅。 Youtube 用来练英语听力不错。 B站的思维黑洞Lab,只有2k多粉,收集了大量的这类谈话节目,也不错看。

我的视频

App快速开发上架挑战

7天能完成一个新App的开发和上架吗? 我的实际挑战纪录片B站

阳台种菜

昆明降雨大降温,结果冰菜长得最好。

开发

expo-router和unistyle的冲突

我第一次使用unistyle,这时已经是v3+了,网上和AI掌握的很多资料失效过时。

核心在于执行顺序,要保证最早执行unistyle的configure,只有这个办法了:

// app.json中
"main": "index.ts"

// 新建index.ts(根目录)
import "./src/styles/unistyles"; // 初始化 unistyles 必须最先执行
import "expo-router/entry"; // 启动 Expo Router

不然再怎么配置也没有用。 官网说明

App开发的这些边距尺寸概念

下面这张图能帮你一眼看懂它们各自「量」的是哪一块:

┌────────────────────────────────────────┐
│ Status Bar ← statusBar.height = 20 │
├────────────────────────────────────────┤
│ │
│ APP CONTENT │
│ │
├────────────────────────────────────────┤
│ Navigation Bar │
│ (虚拟三大键/手势指示条) │
│ ← navigationBar.height = 44 │
└────────────────────────────────────────┘
  • insets
    告诉你「内容区域」距离屏幕四周安全距离是多少。
    例如:

    insets = {
    top: 28, // 避开刘海 + Status Bar
    bottom: 40, // 避开 Navigation Bar
    left: 0,
    right: 0,
    ime: 0 // 键盘弹起时为键盘高度
    }

    所以:
    insets.top 通常 ≈ Status Bar 高度 + 刘海高度
    insets.bottom 通常 ≈ Navigation Bar 高度 + 底部圆角高度

  • statusBar
    只量系统状态栏本身(电量、信号、时间那一栏)的 绝对尺寸
    在 iOS 刘海屏和 Android 状态栏高度不同,但 不会 把刘海额外高度算进来。

  • navigationBar
    只量虚拟导航栏(三大键或手势提示条)的 绝对尺寸
    如果设备启用了全面屏手势,这个高度可能是 0。


一句话总结

名称描述
insets内容可安全放置的区域 ↔️ 你需要用它来做自适应布局
statusBar系统状态栏的精确尺寸
navigationBar虚拟导航栏的精确尺寸

布局时 优先用 insets,只有当你要精确知道状态栏或导航栏本身多高时才去看 statusBar / navigationBar

游牧周记第37期

· 11 min read
Suhe
This site owner

美剧

安道尔2

第一季就很好看,个人觉得是星战宇宙中超过曼达洛人的存在,其实也就这2部我看得下去。 这一季开局一般,到了后半截开始起飞,非常有后劲。

异形.地球

看了2集,蛮好的。 太空船和外星生物这次没有掉到纽约或西方主流大城市,轮到泰国了(新暹罗,应该指曼谷吧)。

星期三2

这调调还在。 说实话前段时间看了些演技极差的烂片后,发现这部片子不管怎么扯,所有演员的演技在线。

听小机 Listen Bot

没错,这就是我花了1周开发上架的App。 我的app网站 8月13日凌晨终于审核完成。 主体开发就2天,然后第三天进入IOS发布审核。 play市场1天完成。 appStore审核了4天,所以总计7天。

一周上架一个可以赚钱的app?

  1. idea酝酿时间不能算在内,其中最主要的不是核心功能思路,而是落实有没有成熟的同类产品。
  2. 从动手写第一个代码开始算起,这时候遇到第一个问题,包名怎么取?虽然是个英文名,甚至是简写而已,但影响到写代码过程中的思路和体验,最好基本确定,不要coding过程中一直想改,影响心情。
  3. 名称用了4个钟头才定下来,其中90%都是在开启上网功能的ChatGpt上查重和想点子,最后出门溜达一圈自己想出了一个,回家用AI落实没啥太成熟的同行应用,还被夸了一道,就定了。中英文一起。
  4. 这是一个和语音识别有关的应用,AI从我描述的核心功能出发,设计了技术栈总结,结果发现ChatGPT建议的好多是第三方包和服务,反而是国产Kimi推荐了一个靠设备自身识别功能的,非常令人兴奋。
  5. 这时已经是晚上,用新的包名在macbook上建了一个expo最新版(sdk都54+了),就累不动睡了。
  6. 第二天坐公交去五华区图书馆,从10点干到13点,终于把核心的语音识别功能调试成功,高高兴兴回家去,准备开着大屏幕接着写。
  7. 然后就是轻车熟路的AI LLM服务接入,唯一问题是什么时候切入,发现是个非常细节的问题,调试到自己一头雾水的时候,已经凌晨了,还是睡了。
  8. 已经算第三天了,想着继续降低一点体验感要求,就图个能尽快提交市场。首先简化了AI介入为手动,然后UI部分完全套用之前用熟了的自创useStyle框架(各位看我教程都明白),快速搞了个基本能用的版本。
  9. 这时候想到必须今晚提交,那还得有logo(icon)啊,开启免费版ChatGPT,说了想法,结果生成一个还不错的小人,就是手有点怪,让他改,改更怪了,运气好在5张的今日免费额度用完前搞定了。
  10. 基本文案也用AI弄了,当然自己加工时间也不短,差不多耗了2个小时。中英日文版,没错,及时如此紧张的时间,我还是用了多语言。先写简体中文的json,然后在IDE中AI生成其他版本。顺便说一句,这次IDE用回了VSCode,Cursor把人整郁闷了这次放弃。总之AI没有花一分钱。
  11. 然后截图,准备上线。这时候才发现问题来了,Eas网络抽风,死活都没法在线打包,本地也卡得要死,有时试上十多次才能进入正常运行环节,等到地老天荒才能打成一个包。这个过程中也没闲着继续调试App功能,不断发现bug和可以优化的地方,同时请AI检查逻辑错误。就这么边调试边打包,都第n个build了,发现天已经黑了。
  12. 说明一下,现在还没试过任何android的命令,不是说要完全放弃安卓,而是以苹果为目标,抓住快速变现人群才是关键,iOS版上线成功,才有做其他市场的心情。
  13. 这天真累啊,RevenueCat的配置也快速搞定了,虽然只是iOS,这可是我最关心的功能,虽然做了无数次,还是有点担心它抽风。事实证明还好,集成得很快。
  14. App store上架资料准备期间,国内ICP备案的申请也在进行,这时候发现已经是周五凌晨,我理论上已经到第4天了。3天完成的计划被打破。ICP备案如果不加入安卓,以后要补充可能麻烦,于是在基本没调试过android版的情况下,先打包apk,这个漫长的调试啊,整完已经3点am了。累死,睡了。
  15. 第4天,基本都在折腾打包和上架提交,这期间解锁了GitHub Action对接Eas的新技能,打包效率大幅提升,直到Eas鸡贼的免费额度用完,不过iOS版也基本可用了,终于提交了AppStore,发现第一次提交不需要ICP备案也没问题,即使后面出了问题只影响国内市场发布,等等也无所谓。秉承来都来了的原则,把Google Play市场的资料也整了。第一次运行了android版,算是意外的顺利,这次开发过程中比较幸运的一件事,没花多少时间修改安卓UI就ok了。看来安卓要先上线,唯一的问题是我考虑安卓打包签名用本地文件,不想依赖云服务,于是也只能本地打包,那个速度不比eas的ios本地打包快多少。过程中不断发现bug和优化点,打了n个包以后提交测试,然后推进正式版,居然实现了android先上线。这时候又到子夜了,今天结束。
  16. 第5天周末,发现iOS还在待审核状态,ICP备案阿里云方面的工作和工信部那边完成了,管局还没开工,估计要等周一了。今天状态稍微放松点了,不断测试调试,iOS等审核不敢发新版,android连续提交2个版本,包括RevenueCat的集成,Google这边挺折腾的。今晚不用熬夜了,下午同学通知聚会吃菌子,然后就骑单车出门了,算是锻炼一把。
  17. 截至目前iOS还在待审核状态,我个人的经验证明3天上架不太可能,除非只是Google Play,加上iOS的话4-5天算是极限了。至于国内市场,本人可不敢想,作为个人开发者基本放弃了。整个过程中代码时间大约只占一半,Eas打包意外的烦人,大约浪费了我1/4的时间,还有1/4的时间是文案、图片和国内ICP备案等琐事,包括想名字等。

下次看是否出一个审核过程的要点说明。

Expo开发

一个新app从开发到上架,再次归纳

  1. ideas
  2. AI确认名称和技术栈
  3. coding和测试,包括GitHub的代码管理
  4. 集成RevenueCat,如果有其他第三方服务包括广告等,继续
  5. 真机测试,先集中力量搞iOS,毕竟是付费主力
  6. Eas打包测试,配置GitHub Actions触发打包,这一步也可以更靠前到3
  7. AppStore id和证书的申请
  8. AppStore新建App,AI辅助完善多语言文字、icon,搞点截图(注意大小,还有iPad13寸版本)
  9. 这时候如果有国内市场,就要考虑ICP认证了,这件事涉及到Android最好一起做
  10. 不管Android版本测试与否,先打包apk,为了ICP
  11. 阿里云去申请ICP备案,前面一些工作是为了提取信息
  12. 备案要几天,这时候继续提交AppStore,先testFlight,但也不要太细,差不多就提交
  13. 测试Android,差不多了就打包提交GooglePlay,先申请,截图和文字可以先用iOS的
  14. 第一次aab打包,之前建立新的keystore文件签名(下方有详细介绍)
  15. 提交GooglePlay,也是测试一下,发布正式版

新android项目的本地签名

云端的选择有两个,在GooglePlay或Eas服务中设置。

不想依赖云服务,所以最终选择了自己建立keystore文件。 这里发现以前项目的keystore不能复用,不然GooglePlay不批准,也就是说一个app要对应一个不同的签名。

macOs上这样做:

# 确认安装了keytool
keytool -version

# 如果没有,安装java,没错,这玩意是java自带的
brew install openjdk

然后生成keystore文件:

keytool -genkeypair \
-v \
-storetype JKS \
-keystore my-release-key.keystore \
-alias my-key-alias \
-keyalg RSA \
-keysize 2048 \
-validity 10000

参数含义
-keystorekeystore 文件名(会生成在当前目录)
-alias你给密钥取的别名(之后构建要用)
-keyalg加密算法(Android 推荐 RSA)
-keysize密钥长度(2048 够用)
-validity有效期(单位天,10000 天≈27 年)
执行上面命令后,会提示你输入:
  • keystore 密码(必须记住)
  • 姓名、组织、城市、省份、国家代码
  • key 密码(建议和 keystore 密码相同,方便记忆)

生成后可以验证一下:

keytool -list -v -keystore my-release-key.keystore

会提示输入密码,然后显示:

  • SHA1 / SHA256 证书指纹
  • 证书有效期

比较容易犯的错误之一是把alias搞错了,可以这样找回:

keytool -list -v -keystore path/to/my-release-key.keystore

然后你的expo项目中eas.json:

 
{
"build": {
"apk": {
"android": {
"buildType": "apk"
},
"credentialsSource": "local"
}
}
}

local就是指本地的加密配置文件: 建立 credentials.json(不要传到 Git,前面的key store文件也是)

{
"android": {
"keystore": {
"keystorePath": "./my-release-key.keystore",
"keystorePassword": "你的_keystore_密码",
"keyAlias": "my-key-alias",
"keyPassword": "你的_key_密码"
}
}
}

最后打包:

eas build --platform android --profile apk

tailwindcss v4+的theme怎么搞?

查遍全网,只有这个靠谱!AI全部懵了

tailwindcss中global.css的三个层次概念

我遇到的问题:

@layer base {
a {
text-decoration: none;
}
}

这段代码无效,但把内容从@layer base中提到最外层,就ok了。

AI解读原因:

Tailwind CSS 在编译时会@layer base 里的样式权重降到低于 base 层之外的普通 CSS,而浏览器对 <a> 的 UA 样式(text-decoration: underline)又属于「user-agent stylesheet」,权重很低,所以放在最外层能覆盖,放进 @layer base 反而被 UA 样式顶掉。

这里涉及一个Tailwind 的三层概念。 ──────────────

  1. base
    浏览器 UA 样式 + 你的 @layer base {} 代码 → 权重最低(0-0-0)。
    典型用途:重置 <a><h1> 等标签默认样式。

  2. components
    @layer components {} → 权重 0-0-1。
    典型用途:你写的 .btn.card 等工具类。

  3. utilities
    .text-red-500 等原子类 → 权重 0-1-0,足够高,不会被 base/components 顶掉

──────────────

为什么放在 @layer base 里有时“无效”?

• 浏览器 UA 对 <a>text-decoration: underline 权重也是 0-0-0,
但「后出现的规则」会覆盖「先出现的规则」。
• Tailwind 把 base 层放在 最前面,所以
base → UA → 你的最外层 CSS,最终 UA 样式反而在最后生效。
• 因此需要:

  • 把样式写在 Tailwind 之后(最外层),或

  • !important / :where() 提升/降低权重。

什么时候该用 @layer base

• 想整理所有「全局标签重置」到一处,而不是散落各处。
• 不介意权重 0-0-0,或愿意配合 !important / :where()
• 方便 Tailwind 在 PurgeCSS 时知道「这段代码属于 base 层,不要误删」。

简记
@layer base ≠ 高权重,只是一个「分组 + 排序」指令。

那最初的问题这样做,定义就生效了:

@layer base {
a {
text-decoration: none !important;
}
}

游牧周记第36期

· 8 min read
Suhe
This site owner

游戏

Diablo IV的卡死问题

2025之后,就卡顿极其严重,问了无数AI,查了无数资料均无效果。 后来还是Gemini 2.5Pro给出了稍微有用的答案。 去掉一个配置文件就好了。 LocalPrefs.txt,说是在Diablo IV的文档目录下,我建议搜一下。 其实就是彻底把游戏配置归零,然后进入游戏提醒重新配置,你也可以直接跳过。 然后真的就顺了很多,虽然和几年前刚刚安装时比有时候还有点卡。 原理就是作为多年老用户,遗留了很多老的配置在配置文件中,互相可能有冲突了。

Gemini越来越神了,会否成为真正的AI一哥?

expo开发

iOS app如何去掉Apple Pay功能设置

我注意到remove的是com.apple.developer.in-app-payments merchant.com.your.merchant.id 貌似内购,而不是apple pay啊


🧠 结论先讲:

com.apple.developer.in-app-payments 是 Apple Pay(实体支付),不是内购(IAP)!


✅ 区分这两个容易混淆的权限

能力名称键名(Key)功能用途说明
🛒 In-App Purchase (IAP)com.apple.InAppPurchase(隐含于系统)订阅、金币、虚拟物品App Store 管理,Xcode 和 profile 默认支持
💳 Apple Pay(实体支付)com.apple.developer.in-app-payments实体商品支付、Apple Wallet 支付需要配置商户 ID,如:merchant.com.xxx

🔍 你看到的这个字段:

<key>com.apple.developer.in-app-payments</key>
<array>
<string>merchant.com.your.merchant.id</string>
</array>

正是 Apple Pay 的专属字段,它告诉 Xcode:

这个 App 打算使用 Apple Pay,接入商户 merchant.com.your.merchant.id


❗️它不是 IAP(内购)

  • IAP 并不需要你手动加任何 entitlement

  • 只要你的 App ID 开启了 In-App Purchase 能力,并在 App Store Connect 配置商品,就可以正常使用;

  • Apple Pay 才需要这个 entitlement,并配合后台商户配置使用。


✅ 你应该怎么做?

  • ✅ 如果你只使用 IAP(订阅、金币),删除这个字段是完全正确的

  • ✅ 同时在 Apple Developer Portal → App ID → Capabilities 中关闭 Apple Pay;

  • ✅ 用 eas credentials 删除旧 profile,重新生成干净的 App Store profile;

  • ✅ 在 eas build 时加入 --clear-cache,清除旧的 entitlements 残留。


✅ 为什么这个字段出现在 prebuild 后?

Expo 的模板可能默认为支持 Apple Pay 的插件(如 expo-payments-stripe)预留了这个能力。如果你:

  • 用了 expo-dev-client

  • 或者启用了其他支付相关插件(即使没用 Apple Pay)

就有可能在 prebuild 时被注入。

suhe注:不认同这点,以前没出现过,原因还是不详。


✅ 最后确认(只使用 IAP 的最小需求)

项目是否需要 Apple Pay
expo-in-app-purchases❌ 不需要
react-native-iap❌ 不需要
App Store Connect 商品配置❌ 不需要 Apple Pay,仅需要开启 IAP 能力
merchant.com.xxx✅ 仅 Apple Pay 需要,IAP 完全不需要

GitHub结合Eas打包的工作流程

一个人开发项目,而且希望主分支 main 继续承担日常开发任务,同时只在需要 EAS 构建时创建 PR + 贴 label 触发远程打包。这种需求非常清晰,也非常实用,我们可以为你设计一套轻量、流畅、零冗余的 Git 使用流程,专为“单人开发 + Expo GitHub App 构建”场景优化。


✅ 你的目标简化版

需求实现方式
日常开发就在 main 分支上继续直接用 main 写代码,无需每次都建分支
需要远程构建时触发 PR + Expo 构建快速从 main 创建一个构建分支,发 PR,贴 Label,完成后自动删除

🧭 推荐工作流设计(为你量身定制)

✅ Step 1:日常开发保持在 main

你继续在 main 分支做日常开发、调试、测试,无需变动流程。

git checkout main
# 编辑代码...
git commit -am "修复组件显示 bug"
git push origin main

✅ Step 2:需要构建时,创建一个构建专用分支

你只在需要构建的时候,从当前的 main 快速拉个构建用分支(不做额外开发):

git checkout -b build/eas-android-20250803
git push origin build/eas-android-20250803

📌 命名建议:build/eas-平台-日期,比如 build/eas-android-20250803


✅ Step 3:GitHub 上发起 PR(无需修改任何代码)
  1. 打开 GitHub,会自动提示你:

    Compare & pull request
  2. 点击进入 PR 页面。

  3. 在右侧添加 Label,例如:

    eas-build-android:prod_android
  4. 不需要写代码 diff,不需要添加 Reviewer,你自己点就好。


✅ Step 4:Expo GitHub App 识别 Label 并构建
  • 构建过程会在 PR → Checks 里展示;

  • Expo Dashboard 会显示构建进度和下载链接;

  • 你可以继续用电脑干别的事,等它构建完成。


✅ Step 5:构建完成后,合并 PR 并清理分支

如果你希望主分支也保留这次构建记录:

# 在 GitHub PR 页面点击 “Merge Pull Request”

然后回到本地:

git checkout main
git pull origin main
git branch -d build/eas-android-20250803
git push origin --delete build/eas-android-20250803

也可以 不合并,直接关闭 PR 然后删除分支也没关系 —— 因为你本来就在 main 写代码,构建用 PR 只是工具人 😎。


✅ 自动化版本(可选 Shell 脚本)

如果你想一条命令就完成构建流程(不用记这么多命令),你可以用如下脚本:

#!/bin/bash

DATE=$(date +%Y%m%d)
BRANCH=build/eas-android-$DATE

# 确保当前在 main
git checkout main
git pull

# 创建构建分支并推送
git checkout -b $BRANCH
git push origin $BRANCH

echo ""
echo "✅ 分支 $BRANCH 已创建并推送"
echo "👉 请访问 GitHub 页面创建 PR 并添加 label: eas-build-android:prod_android"

保存为 eas-build.sh,加权限:

chmod +x eas-build.sh

然后以后只需运行:

./eas-build.sh

就自动完成创建分支 + 推送,剩下你只需在 GitHub 上贴个 label。


✅ 为什么这个流程适合你(一人开发者)?
特性优点
main 分支持续开发不被强迫换分支、不打断手感
构建过程隔离不会把构建 commit 和构建控制信息混进 main
构建结束即可删PR 不需要合并,临时存在就好
label 构建精准控制不用担心误触发、不用写复杂 CI 文件
GitHub 自动记录构建日志PR 页面和 Expo Dashboard 保留构建历史,可审计

🧩 可选优化项(建议长期使用时加上)
  • .github/PULL_REQUEST_TEMPLATE.md:自动提示自己加 label;

  • .gitignore:确保构建相关的缓存文件不被提交;

  • eas.json 里 profile 明确配置好平台和镜像,便于稳定复用。


✅ 总结

你可以用这个结构保持开发流畅、构建可控:

开发场景操作
正常写代码就在 main
想打包构建main 拉个 build/eas-xxx 分支 → 发 PR → 贴 label
构建完成Merge 或 Close PR,然后删掉构建分支
重复这个流程所有构建 PR 都是干净、独立、不会干扰你 main 的

Eas云端打包pnpm项目的问题

报错情况:

eas打包expo app项目报错:Running "pnpm install --frozen-lockfile" in /Users/expo/workingdir/build/ directory ERROR  packages field missing or empty For help, run: pnpm help install pnpm install --frozen-lockfile exited with non-zero code: 1

之前没有发现过,但近期出现,可能两个原因:

  1. pnpm/eas/expo版本更新;
  2. eas云端服务是gitHub Action触发的; 之前的一个项目遇到过,差点忘了,解决方法就是在pnpm-workspace.yaml中加入:
# 告诉 pnpm 在哪些文件夹里寻找项目
packages:
# 如果您的 Expo 项目就在根目录,并且没有其他包,可以这样写:
- '.'

App 切换到后台的Expo优雅处理方式

首先做一个通用勾子。

// useAppStatePaulseTask.tsx

import { useEffect, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';

export function useAppStatePauseTask(onPause: () => void) {
const appState = useRef(AppState.currentState);

useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
if (appState.current.match(/active/) && nextAppState.match(/inactive|background/)) {
onPause(); // App 进入后台时触发
}
appState.current = nextAppState;
});

return () => subscription.remove();
}, [onPause]);

}

在页面调用的例子:


// 在 App 切换到后台(如按了 Home 键、切换到其他 App)时停止某些正在进行的任务

useAppStatePauseTask(() => {
handleAiStop(); // 中止 AI
ExpoSpeechRecognitionModule.stop(); // 停止语音识别
});

新作品

没错,我要开发新的app了,灵感来自于到中国的一个澳洲朋友。 和AI有关,和语言有关,先透露到这里。 总之先去AppStore注册,还有RevenueCat把付款机制做了,图标啥的AI赶紧整一个。 对了还有ICP备案啥的。 一切从简从速。

游牧周记第35期

· 5 min read
Suhe
This site owner

龙树谅从不失手

代码

我的App终于更新

学易从3.X 到 4.X时代了,上次大版本升级不知是3年还是几年前的事了? 这次筹划准备了半年吧,中间还在外旅游了几个月,回来后又被Diablo耽搁几周,实际整合代码的时间约3周,最累是发布前几天。 主要变动都在UI上,终于把几张封面图换了。

expo router的android奇异事件

半透明层覆盖,用animation: 'fade_from_bottom', 解决。 AI说:有时动画也会造成灰色背景干扰。可尝试这样的方法,反正不用default。

expo sdk53后android奇异事件

问题:

expo router项目从52升级到53后,代码没变,但android上出现之前正常的顶部显示贴边了,穿过了状态栏。这种现象原因及解法?

Grok回答:

问题原因

在将 Expo Router 项目从 SDK 52 升级到 SDK 53 后,Android 上出现顶部内容贴边、穿过状态栏的问题,主要原因是 Expo SDK 53 引入了对 Android **边到边(Edge-to-Edge)**显示的支持。这一新特性使得应用的内容默认会延伸到状态栏和导航栏下方,而不像 SDK 52 及之前版本那样默认避免与系统栏重叠。以下是具体原因:

  1. 边到边显示的引入
    • SDK 53 默认启用了 Android 的边到边布局,应用内容会绘制在状态栏和导航栏下方,导致顶部内容(如头部标题或导航栏)可能与状态栏重叠。
    • 在 Android SDK 34 及以下版本的设备上,这一问题尤为明显,因为边到边显示的适配可能不完全兼容旧版 Android 系统。
  2. 状态栏透明性(Translucent Status Bar)
    • 如果你在代码中使用了 expo-status-bar 且设置了 translucent 属性为 true,状态栏会变为透明,允许内容绘制在其下方,从而导致重叠问题。
  3. SafeAreaView 或安全区域处理未更新
    • 如果你的应用未正确使用 SafeAreaView 或 react-native-safe-area-context 来处理状态栏的安全区域,升级后内容可能会直接贴边,忽略状态栏的高度。
  4. Expo Router 布局问题
    • Expo Router 的根布局可能在 SDK 53 中默认添加了一个白色背景(或其他默认样式),当启用边到边显示时,内容会填充整个屏幕,可能导致视觉上的重叠问题。
  5. 新架构(New Architecture)的影响
    • SDK 53 默认启用了 React Native 的新架构(New Architecture),这可能影响某些组件(如 StatusBar 或导航相关组件)的行为,尤其是在与边到边显示交互时。

我采用的解决方法:

  • 在根布局文件(通常是 app/_layout.tsx)或相关屏幕组件中,使用 SafeAreaView 包裹内容。
  • 示例代码: ·
import { SafeAreaView } from 'react-native-safe-area-context';
import { View, Text } from 'react-native';
import { Stack } from 'expo-router';

export default function Layout() {
return (
<SafeAreaView style={{ flex: 1 }}>
<Stack />
</SafeAreaView>
);
}

expo模糊效果的问题

本来是另外一个问题引出的。 我用expo-image组件的blurhash来做模糊效果背景图,ios良好,Android问题很多,出现黑色穿透背景等。 突然想起expo-blur的BlurView在android上也是试验性的,多少年了似乎也没有正式支持。 果然ios才是亲儿子,哦不对,expo是ios的乖儿子(自认的?)。 问了ChatGPT,它推荐一个第三方react-native-blur组件,我看两个月前还在更新,star数只有20。问一下AI他们的区别是什么? 回答如下:

iOS 上两者都可以用;但 Android 上 expo-blur 的“模糊”其实是假的,性能差、bug 多,@sbaiahmed1/react-native-blur 才是真正能用、性能好、效果稳定的模糊组件。

如果你愿意配置原生或使用 EAS Build,强烈推荐切换到 @sbaiahmed1/react-native-blur

要么我还是用图片文件算了?

种植

继续跟踪羽衣甘蓝、冰菜、罗勒和其他,还有生菜也长出来了,移栽。 阳台不够大真麻烦啊。

看片

几个月前看到开头那些“正确角色”就被劝退的《杀戮人机》。现在片荒,又拿出来看,硬着头皮撑过前面半截,大脑麻木了一阵就进入状态了。 总的来说很苹果味,轻松还带点悬疑。 亚马逊的《基地》都第三季了,看预告似乎第一反派“骡”出现了,要进入剧情高潮。但我从第一季开始就抑制不住对这片子的恶心,即使跳着看也很少坚持看完一集。作为小说粉的我,实在受不了这个改变和表演的风格。如果是50-70年代科幻间谍风反而我会喜欢。 亚马逊的魔戒前传很多人不喜欢,我还觉得不错,但这部...真就算了。 《黑袍纠察队》是他们不可超越的神作。

《研究院》《倒计时》都看了第一集,感觉节奏好,吸引力还行,但没有追剧冲动。

游牧周记第34期

· 10 min read
Suhe
This site owner

阳台种植

冰菜

没想到冰菜是这么容易发芽的。 但是因为种子实在太小(和粉尘差不多,传说中的芥子是否也如此?),播散时无法间隔,现在想间苗移栽难度很大。

其他苗的移栽也有点操作困难,我是否需要学一下,或者买点工具?

其他

似乎是罗勒和生菜,还有个薄荷我不记得种在哪里了。

开发

关于zustand的调用方法

我的问题是这样:


expo项目中,zustand定义了一组全局状态如下:
type TheStates = {
step: Steps
auto: boolean
result?: ChangingYaoNumber[]
coinsToPay: number
activeSubscriberPayCoins?: boolean
updateStates: (setting: Partial<TheStates>) => void
}

export const useDivinationStates = create<TheStates>()(
set => ({
step: Steps.ASK,
auto: true,
result: undefined,
coinsToPay: C.MIN_DIVINATION_COINS,
activeSubscriberPayCoins: false,
updateStates: statesData => set((state) => ({ ...state, ...statesData }))
})
)
...
UI页面在调用其中多个状态的时候,一般采用以下2种方式:
1. 一句话:
const { auto, step } = useTheStates()
这种方式似乎会造成当其他状态变化,也会重复渲染?
2. 分开写
const auto = useTheState(state=>state.auto)
const step = useTheState(state=>state.auto)
这种方式可以精准管理每个状态,避免重复渲染,但太麻烦。
请简单分析以上方法优缺点,直接输出结果。

另外为了结合两者优点可否这样写:
const { auto, step } = useDivinationStates(state => ({
auto: state.auto,
step: state.step,
}))
这个写法是否会造成组件无限重复渲染?简单分析并直接给结论。

实际上最后那个方式真的造成了重复渲染,我一般用2,但这个问题只是测试一下AI,同时想搞清此问题原理。

各个AI说法不一。

kimi k2

  1. 两种写法的优缺点
  • 写法1:
    优点:最简洁。
    缺点:只要 store 中任何字段变化(如 coinsToPay、result…),组件都会重新渲染,哪怕只用到了 auto、step。

  • 写法2: 优点:每个字段独立订阅,仅当该字段变化时才渲染。
    缺点:代码啰嗦,字段多时需要写很多行。

  1. 你给出的写法

不会导致无限渲染。
Zustand 会对 selector 返回的对象做浅比较。只要 auto、step 的值没变,返回的新对象会被判定为“相等”,不会触发额外渲染。因此它同时保留了写法1的简洁和写法2的按需订阅效果,推荐使用。

豆包

第一个问题都差不多结论,关于第二个问题如下(后面也是这样):

不会导致无限重渲染。这种写法通过自定义选择器只提取所需状态,只有当 auto 或 step 变化时才会触发更新。但需注意确保选择器返回值的引用稳定性(例如避免内联函数导致的浅比较失效)。

最后提到了一个稳定性问题。

Qwen 3

代码模式

分析:

  • 这种方法通过选择器函数来提取所需的状态,类似于方法2中的精确控制,但又保持了一定程度上的简洁性。
  • 它只会对指定的状态进行浅比较,只有这些状态发生变化时才会触发组件的重新渲染。

结论:

这种写法不会造成组件无限重复渲染。只要选择器返回的对象中包含的状态发生变化,组件才会重新渲染,否则不会。因此,这种方法能够有效地结合两种方法的优点,既保证了代码的简洁性,又避免了不必要的重新渲染。

Grok 3

分析:

  • 这种写法使用 Zustand 的选择器(selector)功能,通过 useDivinationStates 的回调函数返回一个对象 auto, step,仅订阅 auto 和 step 两个状态。
  • Zustand 的选择器会比较返回对象的字段(auto 和 step),只有当这些字段的值发生变化时,组件才会重新渲染。
  • 不会导致无限重复渲染,因为 Zustand 的选择器是基于值的浅比较(shallow comparison),只要 auto 和 step 的值不变化,组件就不会重新渲染。
  • 这种方式结合了方式1的简洁性和方式2的精准订阅优势,避免了无关状态变化导致的重复渲染。

结论:

  • 新写法 不会造成无限重复渲染。
  • 它是最佳实践,兼具简洁性和性能优化,仅当 auto 或 step 变化时触发渲染,适合大多数场景。推荐使用。

完全是肯定的,对了最早推荐这样写的也是它。

Gemini 2.5 Pro

先Thinking了一阵子

这个写法的意图很好,希望结合前两者的优点。但它存在一个严重的问题。

  • 分析:

    1. 组件每次渲染(或因父组件渲染而重新渲染)时,选择器函数 state => ({ auto: state.auto, step: state.step }) 都会被重新执行。
    2. 这个函数每次执行都会在内存中创建一个全新的对象 { auto: ..., step: ... }
    3. Zustand 在接收到 store 的更新后,会比较选择器上一次返回的值和这一次返回的值是否相等,以决定是否要触发组件渲染。
    4. 默认的比较方式是严格相等 (Object.is,类似于 ===)。由于每次返回的都是一个新对象,它们的内存地址不同,所以 oldObject === newObject 的结果永远是 false
    5. 因此,只要 store 中任何一个状态发生变化(即使不是 autostep),这个 hook 就会认为它所订阅的值发生了变化(因为对象引用变了),从而触发组件重新渲染。
    6. 组件的重新渲染又会再次执行选择器,创建一个新对象... 如果这个组件内部有触发 store 更新的逻辑,就会陷入无限重复渲染的死循环。
  • 结论:
    是的,这种写法会造成不必要的重复渲染,并且在特定情况下可能导致无限循环。 它不仅没有解决方法一的性能问题,反而因为每次都创建新对象而可能表现更差。绝对应该避免这种写法。

第一个提到无限循环的,而且要求绝对避免,态度坚决!!!

ChatGPT 4.1

  • 结论不会造成组件无限重复渲染
  • 原因:Zustand 会对比 selector 返回的对象(此处是 {auto, step}),只有当对象内容变化(即 autostep 变化)时才会触发渲染。不会因为无关状态变化而导致渲染,也不会死循环。 ...
  • 推荐用第三种写法:useDivinationStates(state => ({ auto: state.auto, step: state.step })),既简洁又精准。
  • 不会导致无限渲染,可以放心使用。

GPT居然没分析出来,令我有点意外。

Claude-Sonnet-4

结论:会造成无限重复渲染

原因:
每次组件渲染时,state => ({ auto: state.auto, step: state.step }) 都会创建一个新的对象。由于对象引用不同,zustand认为状态发生了变化,触发重渲染,形成无限循环。

正确的解决方案:

使用 shallow 比较或自定义比较函数:

javascript

复制

import { shallow } from 'zustand/shallow'

const { auto, step } = useDivinationStates(
state => ({ auto: state.auto, step: state.step }),
shallow
)

或者使用 useShallow hook(zustand v4.4+):

javascript

复制

import { useShallow } from 'zustand/react/shallow'

const { auto, step } = useDivinationStates(
useShallow(state => ({ auto: state.auto, step: state.step }))
)

这样既保持了代码简洁,又避免了不必要的重渲染。

第二个发现问题的,并且还给出了建议的写法,要试试!!!

最后我补充测试了Grok 4

这个写法使用了zustand的selector机制,返回一个自定义对象,只订阅auto和step的状态。zustand会使用浅比较(shallow equality)检查selector返回的对象是否变化:

  • 如果auto或step变化,对象内容变化,组件会重新渲染(预期行为)。
  • 如果其他状态(如result)变化,但auto和step不变,selector返回的对象相同,不会触发渲染。
  • 它不会修改store状态,只读取,因此不会导致渲染循环。无限渲染通常源于渲染过程中直接修改状态(如在useEffect中无条件setState),而这里仅是读取。

结论
不会造成组件无限重复渲染。这个写法有效结合了两者的优点:简洁(一行解构多个状态)且高效(只在选定状态变化时渲染),是推荐的优化方式。

马斯克的大力出奇迹,还是没能正确分析!!!

果然代码领域Claude还是老大,Gemini 2.5 Pro也非常令人惊喜!ChatGPT有点拉了我没想到。 国产还需努力。

新版react-native-picker的奇葩问题

android不说了,本来就不用它。 ios上,不管传什么值,永远报错:

Error: Exception in HostFunction: TypeError: expected dynamic type 'string', but had type 'double'

用尽无数方法(AI也用了无数)没法解决。 最后降级,目前版本是"@react-native-picker/picker": "2.11.1",参考expo官方资料降到"2.7.5"后ios正常,但android build出错;只好重新安装:

npx expo install @react-native-picker/picker

结果这次两边都ok了,版本还是:"@react-native-picker/picker": "2.11.1"。 不知这次能稳定多久? 结果第二天编译又出错了,貌似还是缓存问题。 突发奇想,我再升级到最新版呢,于是再装(2.11.1),一切正常了,仿佛没事发生。

reanimated旋转动画哪家AI强

我是指AI编程领域。 对我是要做一个硬币旋转的效果。 其实几个月前我就在设计了,所有AI都问遍了,多多少少都有问题,且无法解决。 截至上次Claude和GPT的还算能用,android完全ok,IOS模拟器上不行,显示效果Bug无法解决。

今天采用Gemini 2.5pro完美解决,虽然过程中也遇到一两次同样错误,但很快就修复搞定了。 其实知道现在我也不知怎么搞定的。😅

Gemini还给出了很优雅的代码分离方式。

JS随机数小问题

actionFn: () => Math.floor(Math.random() * 6) + 1, // 生成1到6的随机数!Math.random() 的取值区间是 左闭右开:

Math.random() 的取值区间是 左闭右开
[0, 1),即 0 ≤ r < 1

因此
Math.random() * 6 的区间是
[0, 6),即 0 ≤ r·6 < 6

Math.floor 只会向下取整,最大只能取到 5(因为 5.999… 向下取整也是 5)。
所以 Math.floor(Math.random() * 6) 的结果范围是 0–5绝不会出现 6

组件设计的styles小tip

在React Native中,style属性通常用于定义组件的样式。关于style?: StyleProp<ViewStyle>;style?: ViewStyle;的区别和前者的好处,主要体现在类型安全和灵活性上。

  1. 类型安全StyleProp<ViewStyle>实际上是一个可以接受多种类型的联合类型,不仅仅包括直接的样式对象(即ViewStyle),还包括数组形式的样式(用于组合多个样式对象)以及nullundefined。这意味着使用StyleProp<ViewStyle>可以更准确地描述style属性能够接受的所有合法输入类型,提供更好的类型检查和错误提示。

  2. 灵活性:通过使用StyleProp<ViewStyle>,你不仅可以传递一个样式对象,还可以传递一个样式的数组。这种方式允许你以一种更加灵活的方式组合不同的样式规则,例如覆盖默认样式或合并多个样式源。这对于动态调整UI或根据状态应用不同样式非常有用。

  3. 支持其他样式类型:虽然这里的讨论集中在ViewStyle上,但StyleProp也可以处理其他类型的样式,如TextStyleImageStyle,增加了代码的复用性和可读性。

综上所述,使用style?: StyleProp<ViewStyle>;相比于style?: ViewStyle;提供了更高的灵活性和更强的类型安全性,使得代码更加健壮和易于维护。

游牧周记第33期

· 5 min read
Suhe
This site owner

日常

种植

夏天播种,本来就容易发芽,昆明也没那么热,总的来说合适。 家里阳台太小,确实很不方便。 另一个大点的被封了,通风不畅,关键是阳光不足。 目前要点:

  • 种实用的植物,能吃的:羽衣甘蓝(非常容易发芽),生菜(还没出芽),罗勒(好像长出来了),冰菜(今年新种,还没动静),薄荷(没动静)。
  • 特别的植物:山乌龟(本想做点造型,让其爬水管,但方向难控制,长得乱了,要修剪),一些水培的酸角。同学处挖来的缅茄,即树番茄,叶子超大,就是不长高,据说以后是颗大树。
  • 之前得植物:柠檬如此难以长大,折腾我几年了,死了又活,没有结果;前几天网购了一颗新品种花叶柠檬(泰国柠檬),替代之前死了那颗,这次来自浙江,包装和配土都很好,希望能活。
  • 买了100多L营养土,分10袋运来,家里都没地方摆;有点像打碎点的松针土,混合了一些其他物质,国产品牌(浙江的); 领悟: 人闲下来就是要有个自己的院子。

图书馆

回昆明后第一次去图书馆,选择周围热闹,有公交的五华区图书馆。 有点失望:人多桌椅少,电源插座等设施不方便,寄存柜不是满的就是坏的,二楼阅览室有个能用的。 自习室是少数适合用电脑的地方,但上方的光线直射下来,热得躲不开。 二楼虽然有点不晒的桌椅,但根本没机会抢到位置。

关注

PanSci 泛科学

我关注多年的Youtube频道,主播非常有意思,我是说说话风格,这种用梗和幽默是华语媒体中比较Funny的,和大陆哪些都不一样。

开发

学易app新版本

4.0+已经开发了半年,但都在玩一直没有实际进步。 3.12只更新了一次,主要是上次ICP备案问题,被迫改api网址的事情。 回昆明后,花了很多时间在Diablo IV,好不容易静下来写代码。 这次版本更新,有以下开发要点:

  • 不做大的新功能引入,更在意原有功能的清理和优化;
  • 原有style、多语言、iap和广告等基础通用(可复用)部分,认真进行性能和代码风格优化;
  • 之前的各种组件性能调优;
  • 大量进行细节部分的体验提升,主要是动画效果;
  • 界面有所改变,但大结构基本保持,主要是提升可读性,UI更多在字体、颜色等方便参考Apple自身美学规范;

AI的使用经验:

  • 不付费,Claude唯一值得,但太贵;
  • 不在IDE做大量开发,只做代码补全、插错等辅助工作;
  • 新建组件,提前在免费LLM中做功课,精心编写要求,然后poe中用Claude写核心代码,再到ChatGPT进一步优化;
  • Gemini和Grok辅助;
  • 完成的代码和之前的一些代码,发给ChatGPT,让其进行分析、评估、查错和优化;提出要点,而不是全部自动改写,这一招非常受用;
  • 现在想做的是想让AI尽量帮我处理多语言资源文件的自动翻译和生成,结果还挺不顺。

免费字体

一直在避免加装第三方字体包,原因有三:

  • 版权问题
  • 系统资源占用
  • 系统自动的已经很美观 现在为啥想到呢?因为在设计按钮类动作文字的风格,以前link就用蓝色(和Apple一样,现在也是),其他功能点击则选择橙色,有点怪,发现Apple自己是选用的较粗字体来做区别,其实和正文、标题等不易区分,我也不想再用跳色,想用字体来区分一下。 https://fonts.google.com/ 这里有很多免费字体(包括一些中文的,以及无数其他语言,但同时简繁兼容的还没有),且还有个@expo-google-fonts,适合expo开发。

Expo/ReactNative技巧

  • Pressable的hitSlop 我之前发现,按钮太小,经常手指难以触发。 UI设计要求注意最小触控区域,但不代表视觉按钮尺寸,如按钮大小32pt,但触控区域要求44pt,则可以用这种方式:
<Pressable
onPress={handlePress}
hitSlop={6} // 在每边加 6pt,总触控区域为 32+12=44
style={styles.circleButton}
>
<Ionicons name="add" size={20} color="#000" />
</Pressable>

const styles = StyleSheet.create({
circleButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F2F2F7',
justifyContent: 'center',
alignItems: 'center',
},
});

expo router Android tabbar导航奇葩问题

这个问题我问了AI:

expo router项目,在useRouter.push(某些路由)时,在android中,打开的页面同时,不时会有一个覆盖的(透明或半透明)层水平出现,覆盖住整个页面,出发点是一个app/(tabs)中的页面,目标在app下的各个tsx,如果没有在app的子目录中,此现象不会出现,如果在子目录中,就会出现。ios没有这个现象,请分析,并给出解决办法?

这次ChatGPT没能搞定,反而是Grok 3找到办法了。

结合 transparentModal 和自定义样式,确保页面全屏显示并覆盖 Tab Bar。

只要将原来的目标子目录presentation从'modal'改成'transparentModal'即可。 然后发现其实是在页面下方有个底色层,这样改虽然消失了,但整体也变得透明了。 要么就在android相应页面container中加上backgroundColor,结果也不行,还是透明的。

再检查发现,问题出在tabs设置的tabBarBackground上,去掉就没这个问题;而且这个TabBarBackground是特别设计的,用到了BlurView和绝对定位,这个在Android本来就不稳,先去掉了,现在至少正常。

游牧周记第32期

· 2 min read
Suhe
This site owner

Coding

react-native-skia

关注

  • Reactiive Youtube又一个讲ReactNative的,更新不多,关注量不大。 我主要看他的动画讲解,包括最近在接触的skia。

  • Dan’s React Native Lab Youtube另一个ReactNative创作者,关注更少,但主题很实用。

  • 渤海小吏 B站378万关注,百大之一。有些视频播放量近500万。 主打西游记和中国古代历史。 历史部分我还没看,毕竟B站太多。 西游记讲解信息量大,片子长,当然重点是角度特别,细讲原著细节和解密分析,然后还有长篇大论的做人做事道理(潜规则)。 作为背景听不错。

日常

丙烯画

Acrylic画,工具买全了,开始实践。 比我想象的好上手,比小时候水粉水彩的痛苦经历体验好很多,当年咋没这个。 覆盖力强是一方面,另外画布比纸张确实好多了。

阳台种植

去年种的菜发芽的只有生菜和羽衣甘蓝,都吃了。 今年准备还是按照沙拉目标做,羽衣甘蓝播种第三天就发芽了(现在是7月),然后生菜和冰菜的种子在路上。 没地方搞土,只好买了120L的国产大牌子营养土,价格是美乐棵的一半多点(也不算便宜)。