Skip to main content

游牧周记第46期

· 6 min read
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 min read
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 min read
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 min read
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 min read
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 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;
}
}