跳到主要内容

游牧周记第41期

· 阅读需 15 分钟
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 分钟
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 分钟
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 分钟
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 分钟
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 分钟
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 分钟
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 分钟
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 分钟
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 分钟
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的国产大牌子营养土,价格是美乐棵的一半多点(也不算便宜)。