跳到主要内容

游牧周记第47期

· 阅读需 12 分钟
Suhe
This site owner

标签

NeocloudCoreweave, Nebius

影视

流人 season 5

这一季还是那么好看,而且变成轻松喜剧了,毕竟上一季有点沉重,算是喘口气的作品。

开发

expo+RevenueCat的促销码问题

用RevenueCat一年半以上了,没想到过测试促销码吗? 我现在才发现这个问题是因为有老用户提出。 后来找遍网络都没人提及,官方似乎也很模糊(回去再看看docs)。 AI多数含混不清,只有kimi板上钉钉地说没有解决方案。 太奇葩了,这么牛的平台居然不支持。 其实兑换成功后,dashboard上确实看到了这笔交易,说明rc能正常读取的,只是app端如何解决似乎无办法? 以前我用的https://github.com/hyochan/react-native-iap也非常简单就搞定了啊,几乎无感的设计,于是我又去看了看,这个组件还在不断更新中,可能哪天我要回归?

附上kimi的回答,我现在非常欣赏它。

把 docs.revenuecat.com 全站翻了一遍,结论可以一句话先说完:
RevenueCat 2025 年依旧「看不到」任何通过 App Store 手动兑换的 Promo Code(一次性内购那 100 个码)。
官方只支持「订阅类 Offer Codes」,而它们跟 Promo Code 完全是两套东西。下面把散落在各文档/社区帖里的关键信息按话题汇总,给你一份「官方拼图」。


1. Promo Code(消费类)在 RevenueCat 里的真实表现

  • 交易会被 当成原价购买 写进 Charts & CustomerInfo,价格、take-home 比例都不变,没有任何字段标记它是 promo
  • 仪表盘里 offer_code = null 就表明 不是 Offer Code;Promo Code 不会出现在这里。
  • 苹果/Google 的 API 不会把「用了 promo code」这件事告诉 RevenueCat,所以 SDK、webhook、ETL 全程都 无法区分
  • 官方在 iOS Subscription Offers 文档里直接把「In-App Purchase Promo Codes」列为 ⚠️ Not recommended,理由:
    – 只能 100 个/6 个月
    – 不能商用
    – Revenue 不准
    – 不支持 presentCodeRedemptionSheet

2. 订阅侧 Offer Codes ≠ Promo Code

  • Offer Codes 必须先在 App Store Connect 建「订阅优惠」,再在 RevenueCat Dashboard 绑定同一个 Subscription Price ID
    消费类 IAP 没有 Price ID,走不通这套流程
  • 用户用 Offer Code 兑换后,RevenueCat 会把首笔交易记成 $0offer_code 字段会写进 CustomerInfo & webhook;续费按正常价格追踪——这些福利只对订阅生效

3. 社区反复出现的「我该怎么追踪 Promo Code 用户?」

  • 官方员工统一答复:
    追踪不了
    – 真想归因,只能自己维护「码-用户」映射,然后 Granted Entitlements 发奖。
  • 有人尝试用 App Store Connect 里看到的「Subscriber ID」去撞 RevenueCat original_app_user_id,发现 两边 ID 对不上,完全无法关联。

4. 官方给出的可行替代方案

方案是否支持消费类RevenueCat 能否识别备注
① 原生 Promo Code100/6 个月,Revenue 不准,不推荐
② Offer Codes❌(仅订阅)需订阅产品 + Price ID
③ Granted Entitlements自建码系统,调用 /promotional API 发奖
④ 后端直接解锁内容不走商店,不走 RevenueCat,需自记账

5. 结论与行动指南

  1. 如果你做的是一次性内购(金币、解锁关卡等),放弃用苹果原生 Promo Code 做归因——RevenueCat 2025 年依旧拿不到任何标志位。
  2. 想让 RevenueCat 图表、CustomerInfo、webhook 都能区分「促销用户」,只能自建兑换系统
    – 前端输入码 → 后端验证 → 调用 RevenueCat Granted Entitlements API 发权益;
    – 权益事件会带 store=PROMOTIONAL,方便后续分析。
  3. 若坚持要用原生 Promo Code,也请务必在后台 手动维护「已发码名单」,用户进 App 后通过 syncPurchases() 把交易拉下来,再靠自己映射发奖,RevenueCat 侧仍显示原价

再看perplexity的回答:

Revenurecat 2025最新版确实支持苹果的Promo Codes(优惠码)功能,特别是通过Apple的Offer Codes(优惠码)实现定制化的促销和试用。根据RevenueCat的官方文档,Promo Codes可以在App Store Connect中配置,用户可以通过这些优惠码获得折扣或试用期,支持在应用内直接兑换 。revenuecat

支持情况

  • 优惠码配置:可以在App Store Connect中预先创建,并通过RevenueCat的Customer Center自动展示和应用 。revenuecat+1

  • 兑换方式:用户可以通过苹果提供的兑换页面(包括在应用内弹出兑换页面或跳转到App Store)完成Promo Code的兑换 。revenuecat

  • 内购设计:建议在设计时,利用Apple的Offer Codes实现针对特定用户群或优惠的个性化促销,确保符合苹果的政策,并可以通过RevenueCat自动管理订阅状态。revenuecat+1

设计建议

  • 促销码的申请:在苹果商店后台通过App Store Connect配置优惠码(Offer Codes)。

  • 应用内兑换流程

    • 通过Purchases.shared.presentCodeRedemptionSheet()弹出兑换界面(建议在用户操作需要兑换时调用),

    • 或者跳转到苹果的兑换链接(https://apps.apple.com/redeem?)让用户在Safari或App Store应用中完成兑换。revenuecat

  • 配合RevenueCat自动匹配:兑换成功后,RevenueCat会自动检测到新交易,并同步用户的订阅状态。revenuecat

关于主要消费类内购

  • 如果是订阅类的内购,建议在设置时使用苹果的Offer Codes形成优惠策略,结合RevenueCat的自动订阅管理,确保优惠和促销体验流畅。

  • 设计过程中,应确保Promo Codes的创建、兑换流程符合苹果政策,避免因违规操作被拒 。revenuecat

总体而言,Revenurecat 2025支持苹果Promo Codes,设计上需在苹果后台配置优惠码,在App中引导用户兑换,利用RevenueCat自动同步订阅信息,实现无缝体验。


总而言之,revenuecat好好做它的订阅简化工作就行,普通的消费类产品,就用react-native-iap去玩。下一个版本我准备这样改。

Offer code 新尝试

react-native-iap(现在还是用expo-iap)专门处理消费类产品的offer code(对于iOS就是promo codes),其他的仍然用revenueCat。

现在发现一个现象,真机测试时,expo-iap的监听,要等约20秒才能获得充值完成信息。 如果此时报错“无法连接/重试”,多等等似乎还是会有监听到,但关闭CodeRedemptionSheet似乎就不行了。 如果不用内置CodeRedemptionSheet,在appStore充值等于打水漂(Gemini说是因为:App Store 的沙盒测试环境和真实的 App Store 是完全隔离的,有道理)。

testFlight也没有什么改善。

最后的方法是直接审批上线测试,发现一切似乎正常了,从兑换完成到监听成功,约有5秒延迟,但没有丢失的现象。

以上均指iOS,GooglePlay的促销代码至今没有用过,反正我有自己的代码销售体系(见之前creem.io的周记)。

Claude Code的国内用法

不只是修改配置,调用国内大模型。 还要在大模型中方便选择和切换。 于是有了开源项目:claude-code-router. (这个作者正在找寻杭州的的AI Agent相关开发工作。) 这个项目除了自己cli外,还有个内置的web ui。 但第一次要自己写config.json这一点有些折腾人,一个字也不能写错,文档不是太清楚。

到这里还没完,README中提到:

Now you can use models such as GLM-4.5Kimi-K2Qwen3-Coder-480B-A35B, and DeepSeek v3.1 for free through the iFlow Platform.
You can use the ccr ui command to directly import the iflow template in the UI. It’s worth noting that iFlow limits each user to a concurrency of 1, which means you’ll need to route background requests to other models.
If you’d like a better experience, you can try iFlow CLI.

于是引出了iFlow这个平台(及其cli工具),上面有很多大模型,好像都是免费,我原本准备用siliconFlow的,发现很多收费不便宜(而且都是流量计费,不比国外的便宜)。

为什么能这么做?我问了Kimi:

claude-code-router 是一个「零配置」反向代理 + 动态路由网关,让你把 Claude Code(只认 Anthropic 格式)无缝对接到 任意 OpenAI/Anthropic 兼容端点,并且支持:

  • 多模型负载均衡

  • 自动重试 / 降级

  • 免费额度聚合(核心卖点)

一句话:iflow 把「免费」当拉新手段,且机房资源自建,烧得起;对路由器作者来说,免费池越深,项目吸引力越大,所以文档里 80% 示例都以 iflow 为默认上游。

风险与注意

  1. 免费池随时可能缩紧
    iflow 用户协议留了一条「平台有权调整免费额度」,路由器作者已做「自动感知下线」,但仍建议关键业务留 1 个付费 key 做兜底。

  2. 数据合规
    iflow 目前未披露等保/SOC 报告,金融、医疗、政府项目慎用。

  3. 速率突发
    免费节点不保证 SLA,大促/新模型开放日容易出现 30-60 s 的排队,路由器会降级,但体感仍比直连慢。


结论速记

  • claude-code-router = 免费额度聚合器 + 智能降级网关

  • iflow.cn 之所以被力推,是因为「模型多、额度大、不限速、续命玩法多」

  • 个人/原型阶段,用路由器+iflow 基本可以实现「0 元跑 Claude Code」;生产环境务必加至少 1 个付费上游做兜底。

kimi所说的这些免费额度和原因等,我在iflow官网均为见到。 但确实也没找到额度限制和实际使用的收费情况,只有个token记录页面,对一个expo app老项目init一下生成CLAUDE.md就用了差不多60k tokens。

最后提一下,Silicon Flow官方也支持快速配置集成进Claude Code的,这里

供应商转换器(Transformers)

在配置上面的集成国产大模型过程中,遇到的概念。 大体如下:

在 CCR(claude-code-router)UI 里看到的「供应商转换器」其实就是一个请求/响应格式转换器
它的作用只有一句话:
把 Claude Code 发出的“Claude 原生格式”请求,转成目标厂商能识别的格式;再把厂商回包转成 Claude 能解析的格式。


  • Claude Code 本体只认 Anthropic 的 /v1/messages 接口格式(message create 那套)。

  • 市面上绝大多数中转站或国内平台只提供 OpenAI 格式/v1/chat/completions)或 Gemini 原生格式/v1beta/models/…)。

  • 如果不做转换,直接转发就会 404 或报 “model not found”。

因此 CCR 内置了 3 种现成的「供应商转换器」:

  1. Anthropic
    几乎不改包,仅替换 key、base_url,适用于官方 Claude 或已经“原生兼容 CC”的渠道。

  2. OpenRouter / OpenAI
    claude 的 message create 请求转成 OpenAI 的 chat completions 格式;回包再把 choices[0].message.content 映射回 content 数组。

  3. Gemini
    把请求转成 Gemini 的 generateContent 格式,并处理候选回复、安全过滤等字段。


在 UI 里怎么选

  • 如果你的地址以 /v1/chat/completions 结尾 → 选 OpenRouter(或 OpenAI)。

  • 如果以 /v1beta/models/ 结尾 → 选 Gemini

  • 如果平台声明“已支持 Claude Code 原生接口” → 选 Anthropic

我遇到的全是OpenAI格式。

哪些模型支持webSearch

CCR的配置中会有专门webSearch设置,但哪些模型是支持的呢? 在Silicon Flow中有时提到网页浏览、搜索等字样,但不清晰。 kimi这样回答的,但我现在非常不信任它:

截至 2025-10 月,官方自带“联网搜索(webSearch)”能力、且国内开发者可直接调用的模型只有下面几类,其余都需要自己在网关层再包一层 Bing/Google 搜索 API 才能算“支持”。

  1. OpenAI 官方

    • gpt-4.1

    • gpt-4o
      其余(o3 / o3-mini / o4-mini 等)均不支持 web-search 工具,会直接报错 Hosted tool 'web_search_preview' is not supported

  2. Google Gemini 系列

    • gemini-2.5-pro

    • gemini-2.5-flash
      在 CCR 里只要把 webSearch 路由指到这两个模型即可生效,官方已内置实时搜索插件

  3. 百度智能云千帆

    • ERNIE-X1-Turbo-32K
      通过入参 web_search=true 开启联网,搜索源为百度自家索引
  4. 智谱 GLM-4-Flash-250414
    官方文档写明“实时网页检索”能力,且模型本身免费,但搜索端有 QPS/额度限制

  5. Ollama 生态
    0.4.x 版本后提供统一的 Web-Search API,凡是跑在 Ollama 上的模型(qwen2.5、llama3.3、gemma2 等)只要上下文够长(≥32 k)都可“一键”获得联网能力;不过搜索服务需要自己在后台配 Bing/Google Key


一句话总结
真正“原生”就带联网搜索、且国内直接能调用的:
gpt-4.1 / gpt-4o、gemini-2.5-pro/flash、ERNIE-X1-Turbo、GLM-4-Flash-250414,以及 Ollama 0.4+ 上的所有模型(需自配搜索 Key)。
除此之外的“国产大模型”要想联网,都得自己再包一层搜索 API。

游牧周记第46期

· 阅读需 6 分钟
Suhe
This site owner

标签Tags

F1美国站

开发

学易app参加评奖

Expo App Awards 又有一个新的app点子,去expo网站才发现在搞评奖,就报名一下。

creem付款开发小记

没法用stripe,就只能找creem了。 开发挺简单,就是文档太差。

live/test模式的设计不好好搞清楚,还有点容易乱。

首先是虚拟物品购买(非订阅)

跳转结果页面

做充值码ok。

前端主动查询

app用,没法子,只能这样,而且手动。

webhook

可惜不能调用app内部链接。 不然怎么要用前面的前端自主查询。

Jwt的库 jose

GPT推荐。

next/router还是next/navigation

AI回答: 在 Next.js 13+(App Router) 下,正确的是:

import { useRouter } from 'next/navigation'

原因:

  1. next/routerPages Router(旧版路由)的 hook,只在 pages/ 目录下使用。

  2. next/navigationApp Router 的新 hook,适用于 app/ 目录的 'use client' 组件。

  3. 两者 API 有差异:

    • next/navigationuseRouter() 只有少量方法:push(), replace(), refresh(), back()

    • next/router 提供更多事件和状态,但不适合 App Router。

结论

  • 你的项目是基于 App Router(app 目录),应该使用:
import { useRouter } from 'next/navigation'
  • 如果你在 pages 目录下开发旧式页面,才用 next/router

localStorage和cookie的选择

目前涉及多个网站共用user登录中心的问题,差不多就是SSO。 AI分析大概这样:

策略存储位置子域可否访问安全性推荐程度
localStorage 保存 JWT❌ 不共享⚠️ 可被 JS 访问,容易被 XSS 攻击不推荐跨域用
cookie(HttpOnly + Secure✅ 可通过设置 Domain=.suheapp.org 共享✅ 高✅ 推荐
URL 参数传递 JWT(?token=xxx临时共享⚠️ 容易被泄露⚠️ 仅限回调临时用途
所以cookie是最好的?

nextjs的奇葩问题

代码如下:

'use client'



import { useEffect, useState } from 'react'

import { PUBLIC_ENV } from '@/lib/env/env.client'



export default function HomePage() {

const [user, setUser] = useState<{ sub: string; email?: string } | null>(null)

console.log('app user ---', user)

const [loading, setLoading] = useState(true)



useEffect(() => {

const fetchUser = async () => {

try {

const res = await fetch('/api/verify-jwt')

console.log('res ---', res)

const data = await res.json()

if (res.ok && data) setUser(data)

} catch (err) {

console.error(err)

} finally {

setLoading(false)

}

}



fetchUser()

}, [])



if (loading) return <div className="p-4 text-center">Loading...</div>



return (

<div className="p-4 text-center">

{user ? (

<>

<h1 className="text-2xl font-bold mb-4">Welcome, {user.email || user.sub}</h1>

<a

href={`${PUBLIC_ENV.USER_WEB_URL}/sign-out?redirect_url=${encodeURIComponent(window.location.origin)}`}

className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"

>

Sign Out

</a>

</>

) : (

<a

href={`${PUBLIC_ENV.USER_WEB_URL}/sign-in?redirect_url=${encodeURIComponent(window.location.origin)}`}

className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"

>

Sign In

</a>

)}

</div>

)

}

很简单的一个useEffect渲染。 然而console.log('res ---', res)永远不执行,console.log('app user ---', user)永远是null,但实际UI上已经显示获得的user信息,为啥?

ChatGPT发现了它的强项,解释得很精彩,但也很令人困惑:

非常棒的观察 👍
这其实是一个 React + Next.js hydration(同构渲染) 的“错觉问题”,不是你的逻辑错。
下面我来帮你完整解释发生了什么 👇


🧠 现象总结

  • 页面 UI ✅ 能正确显示出用户信息(说明 setUser() 确实生效了)。

  • 但你在控制台看到:

    • console.log('res ---', res) 没出现;

    • console.log('app user ---', user) 永远是 null

    • ✅ 但渲染结果里 user 有值!

听起来很矛盾,对吧?其实完全合理。


🧩 真相:Hydration(服务端渲染 + 客户端挂载)

在 Next.js 的 "use client" 页面中:

  1. 初始渲染

    • Next.js 先在服务器(Node.js)端执行一遍组件(SSR),生成静态 HTML。

    • 这时 React 并没有运行 useEffect(),因为它只在浏览器端运行。
      所以:

      console.log('app user ---', user)

      在 SSR 阶段输出一次(user 还是 null)。

  2. 客户端挂载阶段(Hydration)

    • 浏览器端 React 接管 HTML(Hydration),

    • 再执行 useEffect()

    • 这时发出 fetch 请求并调用 setUser(data)

  3. 问题是:

    • 控制台的 console.log('app user ---', user) 是在初次渲染阶段打印的(只执行一次);

    • setUser 会触发组件重新渲染,但不会重新执行 effect 或之前的 console.log

    • 所以控制台仍然显示那行 user: null,但 React 实际状态早已更新。


✅ 验证方法

把日志加在渲染体里,而不是外层:

return (
<div className="p-4 text-center">
{console.log('render user:', user)}
{user ? (
<h1>Welcome, {user.sub}</h1>
) : (
<div>Please sign in</div>
)}
</div>
)

这次你会看到:

render user: null
render user: { sub: 'abc123' }

说明状态确实更新,UI 正常响应,只是之前的 console.log 没触发重新打印。


✅ 总结一句话

你看到的“console.log 不执行 / user 永远是 null”,
其实是因为 React 的渲染分两阶段(SSR + Hydration),日志只打印在初次渲染阶段。
UI 已经拿到正确的 state 并重新渲染,这是 React 正常行为。


好久没做网站了,受不了啊!

nextjs图片资源放哪里问题

研究一个官方案例:https://github.com/vercel/next.js/tree/canary/examples/with-supabase 发现favicon.ico, og-image.png等图片都在app目录下,我问kimi为啥不放在public中,它的回答:

现状

  • app/icon.ico app/opengraph-image.png 直接放 app/ 目录——这是 Next.js 13 新约定
    放在 app 根或任意段里的 icon / opengraph-image / twitter-image 会被 自动优化为响应式、带哈希的文件,并输出到 .next/static/media/无需手写 <link rel="icon">支持暗色/亮色双图标

  • 普通装饰性 PNG/JPG(按钮背景、装饰图)目前仓库里几乎没有;如果出现,仍推荐丢进 public/,然后 /images/xx.png 引用,保持缓存与哈希策略简单。

规则速记

文件类型放哪里原因
favicon.ico / apple-icon.png / opengraph-image.pngapp/ 根或同级段自动优化、自动生成 <link>、支持双主题
页面里手写的 <img src="..."> 装饰图public/images/xx.png不走构建优化,纯静态,CDN 缓存友好
组件里 import Logo from './logo.png'与组件同级Webpack 会加哈希,缓存击穿风险低

结论:示例把“品牌图标/OG 图”放 app/故意用新特性,不是错误;其他纯装饰静态资源仍应归 public

游牧周记第45期

· 阅读需 6 分钟
Suhe
This site owner

创业

海外收款有个人新渠道

在一个微信公众号看到的。 creem.io 据说是:

目前支持个人注册的海外支付工具只有 Creem

据说可以用支付宝收款,注意不是用户支付宝付款哈,不然爽了(那就不是为了出海,而是可以兼顾国内用户,替代stripe了那是)。

准备花点时间把yi.suhe.space的支付和用户功能集成起来(和app结合?怕审核不过,那就只有开发网页版的了...)。

payout账号的审核,网上说有点看运气,也比较容易踩坑,要事先把网站啥的准备到位,我直接试了申请,第二天成功过审,12小时左右吧,也有人说几分钟就可以的,但是如果不过,可能要等一个月。

截至周五,第一个虚拟商品,学易app的卦金正式测试通过,集成安卓app上线了。

日常

该自己缴医保了

之前的周记说过这个事,现在终于要自己缴纳了。 离职至今2年了。 我的医保(安宁)已经处于暂停参保状态(小程序查的) 电话和安宁医保确认,失业保险金停了。 然后联系官渡医保局,只需要20日前“云南医保”小程序办理灵活就业登记,下个月开始在“电子税务局”小程序缴费即可(云南医保小程序也有入口)。 选择单建统筹(之前提过)。 然后提交就没反应了,又打电话发现提出去了,等人工审核。 之后要求每月1-10日内缴费(不能多缴,可以选择方式),无提醒,必须自己记住。

目前的阳台种菜

昆明连续阴雨降温,周五终于出了太阳。 发现最重要的是光照问题,其次是土壤肥力。 冰菜为例,长得最好的是外阳台,但不怎么晒的部位。 泰国花叶柠檬开花了,买的时候就有好多小果子。 生菜和芝麻菜盆移到室内阳台,但还是光相对好的地方。 这些老是倒伏的我赚到书房阳台(暴晒)放一段时间看能掰回来不。

开发

axios的proxy问题

提交creem请求,有时会出现404错误,BAD REQUEST啥的,很难查出,后来发现似乎和客户端有vpn proxy有关,果然关掉就好了。

AI介绍说axios可以绕过proxy,只需要参数配置如下:

xios.post({
url: 'http://example.com',
proxy: false // 关键:禁用 axios 的代理支持,强制直连(仅在 Node 中有效)
});

但原则上“没有用。浏览器受其自身代理设置限制,JS 无法绕过。”,搞不明白了。 我在测试似乎有效,当然只是在服务端,可能没有浏览器限制的问题。

关于ngrok反向代理服务

creem.io的webhook测试过程中提到。我请ChatGPT介绍一下:


🧩 什么是 ngrok

ngrok 是一个广泛使用的 反向代理与内网穿透服务,可将本地运行的服务(如本地开发的 Web 应用、API、Webhook 接口等)通过一个安全的公网 URL 暴露到互联网。它常用于开发、测试、演示或临时部署场景中。


⚙️ 工作原理

ngrok 的核心原理是 反向代理隧道(reverse proxy tunnel)

  1. 开发者在本地运行一个 ngrok 客户端(ngrok 命令行工具)。

  2. 客户端与 ngrok 云端服务器建立一条 加密的持久连接(TLS隧道)

  3. ngrok 云端服务器分配一个公网访问地址(如 https://abcd1234.ngrok.io)。

  4. 当外部用户访问该地址时,请求会通过隧道转发到本地运行的服务端口(如 localhost:8080)。

这样就实现了从公网到本地的安全访问,而无需修改防火墙或路由器设置。


🚀 基本用法

安装 ngrok 后,可以通过简单命令启动隧道,例如:

ngrok http 8080

执行后,ngrok 会显示一个公网 URL,如:

Forwarding  https://abcd1234.ngrok.io -> http://localhost:8080

你可以直接通过这个地址访问本地的 Web 应用。

常见应用场景包括:

  • 调试第三方 Webhook(如微信、GitHub、Stripe 回调)

  • 本地项目演示或测试

  • 临时搭建小型网站或API服务

  • 远程访问本地管理面板或数据库界面(需谨慎安全配置)


☁️ 支持平台

ngrok 支持多种平台,包括:

  • 操作系统:Windows、macOS、Linux

  • 容器与云环境:Docker、Kubernetes、AWS、GCP 等

  • 编程接口:提供 REST API、Web 控制台以及可编程 SDK

此外,官方提供 ngrok agentngrok cloud edge 等企业级组件,可用于更复杂的部署与集成。


💰 收费与版本

ngrok 提供 免费版与付费版

版本特点
免费版随机生成的临时子域名,支持基本 HTTP/HTTPS 隧道
Pro 版可自定义子域名(如 myapp.ngrok.io),支持 TCP 隧道、更多并发连接
Business / Enterprise支持自有域名、访问控制、流量分析、团队协作与 SLA 保证

付费计划一般按月或按年订阅,可在官网 https://ngrok.com 查询最新价格与功能详情。


🔐 总结

ngrok 是一个高效、安全、跨平台的反向代理工具,通过加密隧道让开发者能快速将本地服务暴露到公网。它大大简化了测试与集成过程,是现代 Web 开发与云原型设计中不可或缺的辅助工具。


creem的开发问题

文档只有英文,且不易读,资料不全似的。 提供了一个案例,半年没更新,算是给点文档补充吧。 目前仍然不支持多语言。 提供了SDK,我之前没看到,因为文档案例中多是axios,我还专门安装了axios。 测试过程还是有些疑惑,在dashboard可以设置成test mode,但不知会不会同时影响正式系统。 也不知道在正式mode(live)时,test的东西还可以测试不? 还需要继续研究。 下一步准备专门做一个web store项目。

游牧周记第44期

· 阅读需 2 分钟
Suhe
This site owner

标签

sora2

k签证

AI

Sora 2

这玩意号称会颠覆tiktok,我的ios账号是美区,可找到安装,然后vpn地址必须调整到非hk ip,接下来就是邀请码,找了好久,最后加入一个微信群,终于有可以用的了。 做了第一个视频:猫哥谈福报。 邀请码:VEMCRE

智谱的Coding模型

这几天都在说高端大气的Claude 4.5,但同时跟随另一个号称差不了多少的智谱# GLM-4.6,有一个DeepSeek? 模型是开源(当然普通电脑就算了),网页端免费,api收费但便宜。 我看了一下开发方是:北京智谱华章科技股份有限公司

关注

人民公园说AI

这帮家伙有海外华人或国内从业者,虽然没有请过任何重量级嘉宾,谈话风格也不够高大上,但当背景听还是挺放松的,抓热点很快,有热情,算是言之有物吧。 youtube

硅谷101

一个高品质的频道,内容当然就是美国财经创新领域的东西,女主落落大方,个人感觉比已经解决破音问题的小Lin好多了。 探讨较深,有很多嘉宾的参与,有时候较长,他们也有播客。 youtube

兴趣

F1直播哪里看

前几天突然决定培养一下F1赛车的兴趣,新加坡已经开赛,接下来阿布扎比,我该找个直播来源。 结果发现国内很难。 首先CCTV 5 (+)只播正式赛,且不稳定(也就是随时可能被其他比赛和内容替代)。 据说广东的五星体育和上海那边有些本地台会转播,但没上星,其他地方看不到。 然后是收费的腾讯体育等,我也没法在新买的电视上看,毕竟只接了机顶盒和apple TV。 当然F1 app等也要注册收费。 后来发现youtube有直播,👌,但是没有中文解说的(英文、日文都有),算是一个小缺憾吧。

游牧周记第43期

· 阅读需 5 分钟
Suhe
This site owner

日常

阳台种植

新买了一些东西:

  • 2颗来自德宏的树莓苗,一颗黑色一颗红色,但只是苗,我分不出,分别用两个盆栽上,看来品质还行。
  • 一袋(10斤)云南森林腐叶土,纯黑色,希望可以弥补有些植物酸性不足和肥少的问题(以前买的营养土似乎太寡淡了...)。
  • 苦楝油和皂液,混装加水后用,但最近似乎没发现红蜘蛛和虫害了。

Diablo IV s10

最近这个游戏打得有点频繁了,s9一直玩亡灵,但没啥突破,感觉冲层无力,最大的成就是最后几天得到一个4星的堕狱头盔,但赛季也结束了。 9月24日,s10上线,当天又以熟悉的亡灵试玩,一口气冲到55。现在大家都说新赛季德鲁伊最好玩,于是第二天改小德,2天打到凌晨,60(153)级,早中期的拍拍装配齐,感觉攻击力确实可以,但会暴毙,最大的问题是我们玩惯了纯招单手逛街的人,觉得有点腰酸背痛了,等凑齐装备,下次改渡鸦流派,看会否轻松点。

出行

高铁去老挝计划

原计划国庆假期后触发,后来又有点急想再提早两天。 办签证有2个方法,在网上电子签或到昆明的领事馆办。

领事馆据说最近热门到要排队,要实现拍摄合适的2寸白底照片,带上身份证等一堆证件,估计300多块钱/人手续费。 那是不是电子签好点呢?我开始也这么想,于是上了https://laoevisa.gov.la,填写了一些内容,上传以前的证件照和护照照片,每人收费36usd(看似便宜很多),正要提交,突然想起一个问题:如果填写有误,可以修改吗,会不会退钱?没地方问就问了GPT,结果是否的,听说照片要求6个月以内拍的,一些文字细节也不能出错,我真的担心可能过不了,那就打水飘了。 考虑良久,还是下楼找个小店拍照了,多年前去泰国也是这家拍的签证照。 原计划第二天就去领事馆,后来突然想改晚点去(比如11月),主要还是因为今年出游较多,有点倦怠了,琅勃拉邦一个地方对我的吸引力有限(真有趣的反而是高铁),旅行目标还不明确,就再等等吧。

开发

Unistyles的theme切换太懒了怎么办

上周解决了整体theme切换的问题,这周发现复杂点的app,部分页面和组件不更新,特别是tabs中和list中,ChatGPT给了个直接又简单的方案。如下:

问题其实是 react-native-unistyles v3+ 在 Expo SDK 54 里切换主题时,部分组件没有重新刷新。
这是因为 UnistylesRuntime.setTheme() 并不会强制触发整个 React 树的 re-render,尤其是被缓存的 screen / VirtualizedList item。


1. 为什么会发生

  • Tabs 场景:React Navigation 默认会缓存 tab screen,不会因为 theme 改变而自动 unmount/remount。

  • FlatList 场景:子项渲染由 renderItem 控制,如果 item 的 key 和 props 没变,React 不会 re-render。

  • UnistylesRuntime 的限制:调用 setTheme 实际只是更新内部 runtime theme,只有订阅 hook(useStyles, useTheme)的地方才会触发 re-render。


2. 常见坑

  1. 你用了 UnistylesRuntime.setAdaptiveThemes(true/false),但如果之前的组件挂载时拿到的 theme 是“老的”,它不会自动更新。

  2. React Navigation tab 屏幕缓存(默认 lazyunmountOnBlur: false),不会重新构建。

  3. FlatList 的 item 如果不依赖 theme 作为 extraData,不会刷新。


3. 解决方案

✅ 方法 A:确保 theme 改变能触发 re-render

const theme = useTheme(); // Unistyles hook

<FlatList
data={data}
renderItem={renderItem}
extraData={theme.name} // 强制当 theme.name 变化时刷新
/>

✅ 方法 B:给 Navigation 强制刷新

React Navigation 可以利用 key 来强制重新挂载:

<Tab.Navigator
key={theme.name} // 当 theme 变化时,强制整个 Tab.Navigator 重建
>
...
</Tab.Navigator>

或者在 _layout.tsx 根节点:

const theme = useTheme();

return (
<Stack key={theme.name}>
{/* theme 改变时整个 Stack 重新渲染 */}
</Stack>
);

✅ 方法 C:避免滥用 setAdaptiveThemes

  • 如果只想手动切换主题,用 UnistylesRuntime.setTheme(...) 就够了,不需要先关再开 adaptive。

  • adaptive 主要给 系统 theme 跟随 用。


4. 推荐写法(_layout.tsx)

useEffect(() => {
devLog('[Root Layout] new theme setting:', settingTheme)

if (settingTheme === 'system') {
UnistylesRuntime.setAdaptiveThemes(true)
} else {
UnistylesRuntime.setAdaptiveThemes(false)
UnistylesRuntime.setTheme(settingTheme as keyof UnistylesThemes)
}
}, [settingTheme])

然后在 Navigation 根加:

const theme = useTheme();

return (
<Stack key={theme.name}>
{/* screens */}
</Stack>
)

👉 总结:

  • FlatList 要用 extraData={theme}

  • Navigation 要用 key={theme.name} 来强制刷新缓存的 screen。

  • 不要频繁调用 setAdaptiveThemes,只在系统跟随模式时启用。


巧妙使用key参数,高! 控制不用太细,在根_layout.tsx或相应的总体导航组件加入即可。 我个人在(tabs)_layout.tsx加入。

skia组件在expo SDK 54后无法响应

升级expo到SDK 54后,发现skia绘制的组件,在外包裹上pressable或touchable,均无法响应点按。ChatGPT没有找到佐证材料,自己编了些理由,但居然得到了正确的方法。

它这样说的:

这个问题其实不是你代码本身的锅,而是 **Expo SDK 54 升级后 react-native-skia的事件穿透机制变了**。简单说就是: Skia 画布默认会「吃掉」触摸事件,导致外层的Pressable/Touchable 都接收不到点击。

✅ 解决方案

1. 禁用 Canvas 的触摸拦截

Canvas 加上:

<Canvas style={{ flex: 1 }} pointerEvents="none"> {/* 绘制 */} </Canvas>

这样 Canvas 就不会「吃掉」点击事件,父级 Pressable 就能响应。

于是就ok了。

游牧周记第42期

· 阅读需 6 分钟
Suhe
This site owner

标签

kirk, 西贝

出行

淡季高铁去大理

上午8:44有一趟昆明站出发到保山的复兴号。 2小时到大理站,一等座不到200,相比自驾需要4小时,且光过路费160来说,确实更有吸引力,代价就是放弃了深度游和周边游,巍山和剑川怕是不去了。

住宿

最后关头才决定不开车的,但带车位的酒店已经订了,就试试嘛,第一次没有住古镇。 在网红的兴盛桥头。 各种折扣打完158(有完整大窗户可以看点风景,否则可以再便宜20)。 各方面都不错,智能化和卫生都好,再加点钱据说有更适合睡眠的床品。 22平米。 但总的来说,大理即使淡季,酒店价格都不算低的,尤其是对于民宿酒店如此密集的大理而言。 旁边有公交站,可以到古城或火车站。 我休整一下就下楼搭旅游快线去古城了(5元,半小时一班车)。

跟团环洱海

就算在整个云南,我也是第一次跟团,纯粹是闲的无聊又好奇,而且太便宜了。 古城拉团的是白族本地人,就聊起来,然后50块钱参加了个团(其他人至少80),导游是白族妹,颜值可以、热情大方,也不算太庸俗。出发时快4点,一圈回来9点过(我是回到下关)。 n个S湾之一骑单车,然后喜洲古镇看田、买冰箱贴、吃冰棒冰牛奶,这两个环节体验还行,后面的双廊、看孔雀等等,都挺无聊,到海东那些什么圣托里尼都完全黑了,大家急着回去,于是也没安排了。

这几天洱海开鱼,据说晚上鱼灯好看,其实沿路看就那样,可能还是需要角度。

NCC数字游民社区

石碓(duì, duī)村上坡到顶,就是2023年我们第一次离职出行来住的客栈上方,有个跑马场,旁边小路进去,挺隐秘的院子。爬坡有点累,对宅男宅女有好处。 上午10点去了一次,只有个小伙子,下楼到会议室(工作室?)开电脑看片。我问了一下说其他人都没起床或者没出门。让我加了小程序,聊了一下。

中午1点过又来看,多了一个男人在逗猫。都懒洋洋的,不吭气不打招呼(我除非看到妹子或艺术家气质的人,一般也不主动打招呼😅)。 老板不知道哪里人,盘了这个院子搞社区,现在人在黄山,还有昆山、三亚,共4个点。 相当于这边没有管理人和总的组织人,不过我看了黑板墙壁通告,还不时有活动开展的。 上午和小伙聊天,他说这边一共20多人在住着,多数住一个月就走(纯体验),有程序员和设计师等,租房都是在小程序下单,单人间2500/月,还有双人间。 我看了一下小程序,注册时要写明自己的定位(程序员、艺术家等等)、星座和MBTI模型等等,有一些远程接单的活,还有个纽约的票务创业项目,找React Native合作,每月2W-4W(cny),不知为啥我一定兴趣都没。

相关视频在B站

心得

  • 大理还是有魅力的,昆明人觉得在这边看天看云居然都挺感动,难道地州的云更美?(似乎比昆明更加柔和一点)
  • 最好的交通工具还是电单车,古城没有通用平台共享的,只能租,还要再租点还,除非住宿一体,否则不方便。如果有人能把大理古城租车平台统一掉就好了。
  • 没有交通工具,靠腿的话还是很累。
  • 古城吃的肯定不如下关市区,即使耙肉饵丝也是,就算为了吃也应该留在下关。不过云南总的来说大理除了生皮和海稍鱼,似乎也没啥特别。
  • 没有共享单车(除了公园内和专门开辟的区域外),这也可以理解,不然更混乱难管了,坡多路复杂也不好骑。

应用

Perplexity当新闻头条看

好久没有使用Perplexity了,因为其搜索功能大多数模型都有。 其app经常推新闻和一些信息,今天看了一下感觉还行,当然显然是有个人化训练的,对比一下和头条网易的区别吧。

小画家

https://jspaint.app/ 哈哈,居然把这种老物挖出来了,都怪macos的预览编辑图片多年不长进,没有背景改色功能(油漆桶),reddit查到6年前的帖子提到这个网址,居然还在还能用。 都是因为iOS的液体玻璃后,icon要改一改。

想法

技术资料网

想把expo.dev这种有开源资源的技术docs搬运,并翻译成中文,再部署。 发现大批量翻译是比较麻烦且有成本的,一般会用到python调api。 后来发现这样做的网站太多了,如expo中文等,大不了更新慢点。 文档网站还是原创或冷门的好点。

开发

expo的ThemeProvider放在外层才行

原来的代码:



<SubscriptionProvider>

<SQLiteProvider databaseName={C.DATABASE_FILE_NAME} onInit={migrateDbIfNeeded} useSuspense>

<GestureHandlerRootView>

<ThemeProvider value={NavTheme}>

<Stack>

<Stack.Screen name="index" />

<Stack.Screen name="settings" options={{ presentation: 'modal' }} />

<Stack.Screen name="iap" options={{ presentation: 'modal', headerShown: C.isAndroid, headerTransparent: C.isAndroid, title: '' }} />

<Stack.Screen name="ai-token-bill" options={{ presentation: 'modal' }} />

<Stack.Screen name="history" options={{ presentation: 'modal', headerShown: false }} />

<Stack.Screen name="+not-found" />

</Stack>

</ThemeProvider>

</GestureHandlerRootView>

<StatusBar style={isDark ? 'light' : 'dark'} />

</SQLiteProvider>

</SubscriptionProvider>


无论NavTheme如何变化,都不会改变整体UI风格的风格。 Claude Code也检查不出原因,给了一堆解法完全无效。

然而问题在这里,修改ThemeProvider包裹范围即可,实在不想一个个试,就包在最外层,然后一切恢复正常。

估计是中间哪一层限制了刷新导致的,就这个项目而言,我发现是SQLiteProvider导致的,估计和里面的Suspend啥的有关。对了把<StatusBar style={isDark ? 'light' : 'dark'} />也移到外面。

最后是:

<SubscriptionProvider>

<ThemeProvider value={NavTheme}>

<SQLiteProvider databaseName={C.DATABASE_FILE_NAME} onInit={migrateDbIfNeeded} useSuspense>

<GestureHandlerRootView>

<Stack>

<Stack.Screen name="index" />

<Stack.Screen name="settings" options={{ presentation: 'modal' }} />

<Stack.Screen name="iap" options={{ presentation: 'modal', headerShown: C.isAndroid, headerTransparent: C.isAndroid, title: '' }} />

<Stack.Screen name="ai-token-bill" options={{ presentation: 'modal' }} />

<Stack.Screen name="history" options={{ presentation: 'modal', headerShown: false }} />

<Stack.Screen name="+not-found" />

</Stack>

</GestureHandlerRootView>

</SQLiteProvider>

<StatusBar style={isDark ? 'light' : 'dark'} />

</ThemeProvider>

</SubscriptionProvider>

游牧周记第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