Skip 开源:从“卖工具”到“卖信任”的豪赌 - 肘子的 Swift 周报 #120
Skip Tools 日前宣布全面免费并开源核心引擎 skipstone。这意味着 Skip 彻底改变了经营方式:从“卖产品”转向“卖服务+社区赞助”。这次变化,既有对之前商业模式执行不佳而被迫调整的无奈,也体现了 Skip 团队的果敢——在当前 AI 盛行、开发工具格局固化的背景下,主动求变,力求突破。
Skip Tools 日前宣布全面免费并开源核心引擎 skipstone。这意味着 Skip 彻底改变了经营方式:从“卖产品”转向“卖服务+社区赞助”。这次变化,既有对之前商业模式执行不佳而被迫调整的无奈,也体现了 Skip 团队的果敢——在当前 AI 盛行、开发工具格局固化的背景下,主动求变,力求突破。
![]()
当奥迪决定给 A5 Coupe 画上句号,奔驰把 C 级和 E 级的轿跑合二为一变成 CLE 时,大家似乎都默认了双门轿跑正走在消亡的路上。研发成本高,受众越来越窄,双门轿跑在效率至上的时代显得有些格格不入。
但宝马不这么想。
![]()
▲ 新一代 4 系假想图,图片来自:SugarDesign
宝马研发主管 Joachim Post 最近明确了新一代 4 系的开发计划。这则消息让内燃机的拥趸们松了一口气,因为宝马不只要留住 4 系,还要保住 V8 和 V12 这种大排量内燃机。他在接受 Autocar 采访时直言:
4 系对我们来说是一款非常重要的车,它更具运动感,而宝马作为一个运动品牌,在性能方面有着清晰的传承。它(4 系)在未来也将扮演重要角色。
Joachim 表示,下一代 4 系会沿着两条完全不同的技术路径演进。燃油版本继续在升级后的 CLAR 平台上迭代,纯电版的 i4 则会直接用上全新的 Neue Klasse 架构。
![]()
▲ i4 假想图,图片来自:SugarDesign
这种分工避开了折中的产品定义。燃油车型可以继续深挖内燃机的潜力,纯电车型能享受到原生平台带来的新体验。
Joachim Post 还在采访中反复提到了协同效应,只要 3 系和 X3 这些走量的主力还在更新,通过平台协同开发出来的 4 系就不需要背负太沉重的研发包袱。
开发衍生车型的成本并没有想象中那么高。每当我们开发 3 系或 X3 时,我们都会同步考虑到 4 系或 X4。我们通过这种协同效应,让衍生车型能非常容易地基于基准车型构建出来。
![]()
▲新 M3 假想图,图片来自:Carscoops
这种共享并不只是共用几个零件那么简单,它涉及到整条生产线的自动化排产以及通用的底层电气架构。
在竞品撤退的空白地带,宝马保留了一道宽厚的护城河。
具体到产品,经过了多个世代的打磨,CLAR 平台已经进化到了极其成熟的状态,它最大的优势在于那套纵置引擎的布局灵活性。不管是搭载 48V 轻混系统的四缸发动机,还是作为 M Power 灵魂所在的 S58 六缸引擎,CLAR 都能在不改变白车身核心结构的前提下,提供稳定的物理支撑和散热空间。
![]()
▲ 新一代 M3,图片来自:BMW
至于 Neue Klasse 架构下的纯电版 i4,按照目前披露的技术细节,它很有可能会搭载 108kWh 的高能量密度电池组,配合 800V 高压平台和宝马自研的第六代圆柱电池技术,整车的续航里程将指向 800 公里大关。
对于宝马来说,采用双平台策略不失为一个稳妥的选择——在欧洲和中国这种电动化相对成熟的市场,宝马必须拿出一套全新的技术标准去硬碰硬。而在北美这种对大排量内燃机依然有强烈路径依赖的地盘,CLAR 平台下的直列六缸发动机依然是最好的金字招牌。
至于大家最担心的欧 7 排放标准,宝马也有它的处理方式。
![]()
▲新 X7 假想图,图片来自:Carscoops
之前业内普遍认为这道门槛会成为大排量引擎的终结者,迫使厂家去砸数以亿计的欧元开发复杂的混动系统。但宝马工程团队发现,通过对排气系统和催化装置进行深度的细节优化,现有的 V8 甚至劳斯莱斯那台 V12 发动机,在低投入的前提下就能达成合规。
Joachim Post 对此非常有信心:「这也是我们的巨大优势。」
![]()
▲新一代 M3,图片来自:Kolesa
宝马战略布局的灵活性在市场端体现得更明显。当奥迪彻底放弃双门轿跑、奔驰选择精简合并产品线时,宝马成为了极少数依然愿意在该细分市场维持多维度布局的品牌。
宝马并没有急着给自己贴上全电化的标签,也没把自己困在内燃机的旧梦里。它利用成熟的平台效应,把 4 系列变成了一套灵活的组合。它既可以是搭载尖端三电技术的先锋,也可以是满足硬核玩家胃口的传统选择。
这让宝马在应对全球不同市场的转型阵痛时,拥有了更宽的缓冲余地。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
![]()
2026 年,smart 将会发布两款新车:smart 首款掀背轿车 #6 EHD,将于 2026 年上半年正式发布;smart #2 原型概念车,将在 2026 年 4 月北京车展进行全球首秀,smart #2 是 smart 重返双门双座小车市场的标志性车型,由梅赛德斯-奔驰设计团队设计,基于 smart 专为超紧凑车型打造的全新 ECA 架构生产,预计该车型会在 2026 年下半年发布。
不过 2026 年,smart 发布的第一款车是 #3 Keith Haring 艺术特别版,这款车是 smart 和艺术家 Keith Haring 合作推出的限量车型,上市限时售价 18.49 万元,中国限量首发 99 台。
三则新闻交织在一起,呈现出了一个复杂的 smart,一个和以前大不一样的 smart,同时又是一个和以前一样的 smart。
![]()
在 smart 发布 #3 Keith Haring 艺术特别版之前,这家主打有趣好玩的品牌,已经进行了 56 次联名,#3 Keith Haring 艺术特别版也就成为了 smart 旗下第 57 款联名车型。在此之前,与之联名的品牌包括迪士尼,山地车先锋品牌 Trek,潮流品牌 AAPE,高性能改装品牌 BRABUS,经典咖啡品牌 Peet’s Coffee 等等。
甚至,smart 品牌的出现,也是联名的结果。smart 品牌的真正起源可以追溯到 1972 年。当时,戴姆勒公司为了应对城市地区日益严重的交通拥堵和空气污染,提出了小型城市汽车的概念,不久之后,小型城市汽车的概念车从图纸变成了现实。
![]()
▲ smart Fortwo 的前身车型奔驰 NAFA
接着到了 1981 年,戴姆勒做出了一款名叫 NAFA (德语 Nahverkehrsfahrzeug 的缩写,意为短途汽车)的车型。它的优点当然是车身小巧,排放低,但是这款车没法满足戴姆勒的安全标准。直到十多年之后的 1994 年,小型汽车的安全技术终于过关,这一年戴姆勒和最大的手表集团斯沃琪(Swatch)合作,共同创立了 smart 品牌,完整含义是(swatch mercedes ART),接着就是 1997 年的法兰克福车展,smart 展出了首款概念车,然后量产车型在 1998 年投入生产。
从历史因素来看,推出两门两座车型,以及联名车型,就是最 smart 的行为。
![]()
从官方发布的预告图来看,smart #2 的定位类似于 smart 历史上的第一款车型 smart Fortwo 微型车 ,整车设计较为圆润,车头紧凑呈下溜造型,尾部配备小型扰流板设计。不久前 smart 预热这款微型车的时候解释说:
27 年前,首款 smart fortwo 车型风靡全球,成为品牌乃至整个行业的标志性车型。我们推出的城市轿车深受全球超过两百万用户的喜爱,成为城市出行解决方案和城市文化的重要象征。这款全新纯电动车型,作为超紧凑型 A 级车,将融合温暖科技,在初代 smart fortwo 经典车型的核心特质基础上进行迭代升级。这标志着 smart 品牌重返其所定义和擅长的细分市场。
2025 年的中国汽车市场,大车好卖,小车也好卖,诸如乐道 L90 ,蔚来 ES8,岚图泰山和极氪 9X 这样的三排六座全尺寸 SUV 是市场关注的焦点,销量情况持续向好,但销冠却是另外一位低调的选手:吉利星愿,一款起售价 6.88 万元,车长 4 米出头的微型车。
当然,无论吉利星愿还是五菱宏光 MINI EV,车身不管怎么小,还是要保证两排四座或者五座的,而 smart #2 的两座车型虽然会被划分到小车范畴,但目标受众完全不一样。前者可能更偏向于三四线城市的代步车型,主打一个入门和性价比,而 smart #2 的售价很可能和性价比无关,同时主要目标受众大概是一二线城市的都市丽人。
不过作为一个天生就国际化的品牌,smart 的两座小车在道路狭窄停车困难的欧洲也有不错的市场前景。
至于 #3 Keith Haring 艺术特别版就无需赘言,smart 的基本操作,看图就行。
![]()
![]()
![]()
![]()
当下的 smart 和二十多年前的 smart 在股权和产品已经变化颇多,外部市场环境上也已经完全不同。对于从 2022 年开始新生,由奔驰设计,吉利智造的新 smart 来说,当下中国汽车市场的竞争烈度前所未有,这个有趣的品牌如何与那些在智能,科技,性价比力量十足的品牌竞争,是需要一些巧劲儿的。
显然,#3 Keith Haring 艺术特别版和即将推出的 smart #2 就是那个巧劲儿,也是 smart 的底色,不管怎么变,这个品牌必须是有趣的,好玩的,与众不同的。
实际上,联名车型的发布,还有新车技术的透露,都发生在 smart times「灵感大赏」活动上,在这个密友(车主)年度活动上,smart 公布了中文品牌主张「天生爱不同」,与英文品牌主张「Open your mind」呼应。
![]()
smart 越来越大,这已经是不争的事实,尤其是 smart #5 的尺寸来到了 4705×1920×1705mm,轴距为 2900mm,和奥迪 Q5L 的尺寸相差不多,这完全和 smart 一贯的「小」不相关了。
smart 首款掀背轿车 #6 EHD 也不会小,据透露,这款车的尺寸大概介于领克 07 和领克 10 之间,同时内饰会具有明显的奔驰风格。smart #6 EHD 将会搭载最新一代雷神电混 2.0 技术,综合续航超 1800 公里,整车车长近 5 米(4906×1922×1508mm,轴距 2926mm),采用掀背+运动尾翼设计。
![]()
鉴于这款车已经到了工信部申报阶段,所以其外观等信息已经不是秘密。
总结起来,这款车在 smart 体系内的突破在于:
![]()
如果说 smart #2 作为两座车型,希望吸引独立的都市丽人的话,那么 smart #6 EHD 显然就想吸引家庭用户。
这个逻辑和领克一步步从领克 01 和领克 03 的驾趣取向车型发展到领克 08 和领克 900 这类家庭取向车型类似,任何一个年轻有趣的品牌,都会面临用户成熟用户成家立业的的挑战,适时推出更大的车型,同时保留驾驶趣味和品牌特色的车型是必经之路。
smart #6 EHD 就是这个品牌交出答案。
有趣的是,smart 在 2026 年的两款重磅新车,恰好组成了 20#2#6,一款致敬历史经典新生,一款不破不立面向未来,这就是当下 smart 的情况:它和以前不一样了,但和以前还是一样。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
发布日期 2023 年 8 月 3 日
isexe 是跨平台检查文件是否为「可执行文件」的专用工具包,核心解决「Windows 和 Unix 系统判断可执行文件的规则完全不同」的问题。
unix系统根据文件权限判断;window系统根据文件扩展名判断。
isexe-3.1.1/src/index.ts
import * as posix from './posix.js' // 导入 POSIX 系统(Linux/macOS 等)的实现
import * as win32 from './win32.js' // 导入 Windows 系统的实现
export * from './options.js' // 导出配置选项类型(如 IsexeOptions)
export { win32, posix } // 允许直接访问特定平台的实现
const platform = process.env._ISEXE_TEST_PLATFORM_ || process.platform
const impl = platform === 'win32' ? win32 : posix
/**
* Determine whether a path is executable on the current platform.
*/
export const isexe = impl.isexe
/**
* Synchronously determine whether a path is executable on the
* current platform.
*/
export const sync = impl.sync
isexe-3.1.1/src/posix.ts
const isexe = async (
path: string, // 要检查的文件路径(比如 "/usr/bin/node" 或 "C:\\node.exe")
options: IsexeOptions = {} // 配置项,默认空对象
): Promise<boolean> => {
const { ignoreErrors = false } = options
try {
// await stat(path):获取文件状态
// checkStat(statResult, options):判断是否可执行
return checkStat(await stat(path), options)
} catch (e) {
// 把错误转为 Node.js 标准错误类型(带错误码)
const er = e as NodeJS.ErrnoException
if (ignoreErrors || er.code === 'EACCES') return false
throw er // 非预期错误,向上抛出
}
}
import { Stats, statSync } from 'fs'
import { stat } from 'fs/promises'
const checkStat = (stat: Stats, options: IsexeOptions) =>
stat.isFile() && checkMode(stat, options)
const checkMode = (
// 文件的 Stats 对象(通常由 fs.stat 或 fs.lstat 获取)
// 包含文件的权限位(mode)、所有者 ID(uid)、所属组 ID(gid)等元数据。
stat: Stats,
// 配置对象,允许自定义用户 ID(uid)、组 ID(gid)、用户所属组列表(groups),默认使用当前进程的用户信息。
options: IsexeOptions
) => {
// 1、获取用户与组信息
// 当前用户的 ID(优先使用 options.uid,否则调用 process.getuid() 获取当前进程的用户 ID)。
const myUid = options.uid ?? process.getuid?.()
// 当前用户所属的组 ID 列表(优先使用 options.groups,否则调用 process.getgroups() 获取)。
const myGroups = options.groups ?? process.getgroups?.() ?? []
// 当前用户的主组 ID(优先使用 options.gid,否则调用 process.getgid(),或从 myGroups 取第一个组 ID)。
const myGid = options.gid ?? process.getgid?.() ?? myGroups[0]
// 若无法获取 myUid 或 myGid,抛出错误(权限判断依赖这些信息)
if (myUid === undefined || myGid === undefined) {
throw new Error('cannot get uid or gid')
}
// 2、构建用户所属组集合
const groups = new Set([myGid, ...myGroups])
// 3、解析文件权限位与归属信息
const mod = stat.mode // 文件的权限位(整数,如 0o755 表示 rwxr-xr-x)
const uid = stat.uid // 文件所有者的用户 ID
const gid = stat.gid // 文件所属组的组 ID
// 4、定义权限位掩码
// 八进制 100 → 十进制 64 → 对应所有者的执行权限位(x)
const u = parseInt('100', 8)
// 八进制 010 → 十进制 8 → 对应所属组的执行权限位(x)
const g = parseInt('010', 8)
// 八进制 001 → 十进制 1 → 对应其他用户的执行权限位(x)
const o = parseInt('001', 8)
// 所有者和所属组的执行权限位掩码(64 | 8 = 72)
const ug = u | g
// 5、权限判断逻辑
return !!(
mod & o || // 1. 其他用户有执行权限
(mod & g && groups.has(gid)) || // 2. 所属组有执行权限,且当前用户属于该组
(mod & u && uid === myUid) || // 3. 所有者有执行权限,且当前用户是所有者
(mod & ug && myUid === 0) // 4. 所有者或组有执行权限,且当前用户是 root(UID=0)
)
}
mod (权限位) :Unix 系统中用 9 位二进制表示文件权限(分为所有者、所属组、其他用户三类,每类 3 位,分别控制读 r、写 w、执行 x 权限)。例如 0o755 对应二进制 111 101 101,表示:
u):可读、可写、可执行(rwx)。g):可读、可执行(r-x)。o):可读、可执行(r-x)。isexe-3.1.1/src/posix.ts
const sync = (
path: string,
options: IsexeOptions = {}
): boolean => {
const { ignoreErrors = false } = options
try {
return checkStat(statSync(path), options)
} catch (e) {
const er = e as NodeJS.ErrnoException
if (ignoreErrors || er.code === 'EACCES') return false
throw er
}
}
import { Stats, statSync } from 'fs'
import { stat } from 'fs/promises'
isexe-3.1.1/src/win32.ts
const isexe = async (
path: string,
options: IsexeOptions = {}
): Promise<boolean> => {
const { ignoreErrors = false } = options
try {
return checkStat(await stat(path), path, options)
} catch (e) {
const er = e as NodeJS.ErrnoException
if (ignoreErrors || er.code === 'EACCES') return false
throw er
}
}
import { Stats, statSync } from 'fs'
import { stat } from 'fs/promises'
const checkStat = (stat: Stats, path: string, options: IsexeOptions) =>
stat.isFile() && checkPathExt(path, options)
isexe-3.1.1/src/win32.ts
const checkPathExt = (path: string, options: IsexeOptions) => {
// 获取可执行扩展名列表
const { pathExt = process.env.PATHEXT || '' } = options
const peSplit = pathExt.split(';')
// 特殊情况处理:空扩展名
// 空扩展名通常表示 “任何文件都视为可执行”,这是一种特殊配置
if (peSplit.indexOf('') !== -1) {
return true
}
// 检查文件扩展名是否匹配
for (let i = 0; i < peSplit.length; i++) {
// 转小写:避免大小写问题(比如.EXE和.exe视为同一个)
const p = peSplit[i].toLowerCase()
// 截取文件路径的最后N个字符(N是当前扩展名p的长度),也转小写
const ext = path.substring(path.length - p.length).toLowerCase()
// 匹配条件:扩展名非空 + 文件扩展名和列表中的扩展名完全一致
if (p && ext === p) {
return true
}
}
return false
}
isexe-3.1.1/src/win32.ts
const sync = (
path: string,
options: IsexeOptions = {}
): boolean => {
const { ignoreErrors = false } = options
try {
return checkStat(statSync(path), path, options)
} catch (e) {
const er = e as NodeJS.ErrnoException
if (ignoreErrors || er.code === 'EACCES') return false
throw er
}
}
Styled Components 是一个 CSS-in-JS 库,让你可以在 JavaScript/TypeScript 代码中编写样式,并将样式与组件紧密结合。
1. CSS-in-JS
// 传统方式
const styles = StyleSheet.create({
container: { padding: 16 }
});
// Styled Components 方式
const Container = styled.View`
padding: 16px;
`;
2. 自动样式隔离 每个 styled component 都有唯一的 class 名,避免样式冲突:
const Button = styled.TouchableOpacity`...`;
// 生成类似:.Button-asdf1234 { ... }
3. 主题支持 内置主题系统,轻松实现深色/浅色主题:
const Title = styled.Text`
color: ${props => props.theme.colors.text};
`;
4. 动态样式 基于 props 动态改变样式:
const Button = styled.TouchableOpacity<{ variant: 'primary' | 'secondary' }>`
background-color: ${props =>
props.variant === 'primary' ? '#007AFF' : '#5856D6'};
`;
| 特性 | StyleSheet | Styled Components |
|---|---|---|
| 样式隔离 | ❌ 需要手动管理 | ✅ 自动隔离 |
| 主题支持 | ❌ 需要额外配置 | ✅ 内置支持 |
| 动态样式 | ⚠️ 条件语句复杂 | ✅ 简洁直观 |
| TypeScript | ✅ 支持 | ✅ 完整类型推断 |
| 样式复用 | ⚠️ 需要手动合并 | ✅ 继承机制 |
| 组件封装 | ❌ 样式和组件分离 | ✅ 样式与组件一体 |
# 安装 styled-components
yarn add styled-components
# 安装类型定义和 Babel 插件
yarn add -D @types/styled-components babel-plugin-styled-components
依赖说明:
styled-components: 核心库@types/styled-components: TypeScript 类型定义babel-plugin-styled-components: 优化开发体验和性能编辑 babel.config.js:
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
// ... 其他插件
[
'babel-plugin-styled-components',
{
displayName: true, // 开发模式下显示组件名
meaninglessFileNames: ["index", "styles"],
pure: true, // 移除不必要的辅助代码
},
]
],
};
配置说明:
displayName: true - 开发时在 React DevTools 中显示组件名称meaninglessFileNames - 忽略这些文件名,不生成 class 名pure: true - 启用 tree-shaking 优化创建 app/types/styled-components-native.d.ts:
import 'styled-components/native';
declare module 'styled-components/native' {
// 主题模式类型
type ThemeModeType = 'dark' | 'light';
// 间距类型
type SpacingType = {
xs: number;
sm: number;
md: number;
lg: number;
xl: number;
xxl: number;
screenPadding: number;
cardPadding: number;
inputPadding: number;
negSm: number;
negMd: number;
negLg: number;
};
// 字体类型
type FontSizeType = {
xs: number;
sm: number;
base: number;
lg: number;
xl: number;
xxl: number;
xxxl: number;
};
type FontWeightType = {
regular: number;
medium: number;
semibold: number;
bold: number;
};
type TypographyType = {
fontSize: FontSizeType;
fontWeight: FontWeightType;
};
// 颜色类型
type ColorsType = {
primary: string;
secondary: string;
background: string;
text: string;
textWhite: string;
success: string;
warning: string;
error: string;
info: string;
border: string;
overlay: string;
transparent: string;
};
// 主题接口
export interface DefaultTheme {
mode: ThemeModeType;
colors: ColorsType;
spacing: SpacingType;
typography: TypographyType;
}
}
更新 babel.config.js 和 tsconfig.json 中的别名配置:
babel.config.js:
module.exports = {
plugins: [
[
'module-resolver',
{
root: ['./app'],
alias: {
'@': './app',
'@providers': './app/providers',
// ... 其他别名
},
},
],
],
};
tsconfig.json:
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@providers": ["app/providers"],
"@providers/*": ["app/providers/*"]
}
}
}
创建以下文件结构:
app/styles/theme/
├── custom/
│ ├── spacing.ts # 间距系统
│ └── typography.ts # 字体系统
├── dark/
│ └── index.ts # 深色主题颜色
├── light/
│ └── index.ts # 浅色主题颜色
└── index.tsx # 主题生成器
app/styles/theme/custom/spacing.ts:
export const spacing = {
// 基础间距(4px 基准)
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
// 特殊间距
screenPadding: 16,
cardPadding: 16,
inputPadding: 12,
// 负间距
negSm: -8,
negMd: -16,
negLg: -24,
} as const;
export type Spacing = typeof spacing;
app/styles/theme/custom/typography.ts:
export const typography = {
fontSize: {
xs: 12,
sm: 14,
base: 16,
lg: 18,
xl: 20,
xxl: 24,
xxxl: 32,
},
fontWeight: {
regular: 400,
medium: 500,
semibold: 600,
bold: 700,
},
} as const;
export type Typography = typeof typography;
app/styles/theme/light/index.ts:
import { ColorsType } from "styled-components/native";
const colors: ColorsType = {
primary: '#007AFF',
secondary: '#5856D6',
background: '#FFFFFF',
text: '#000000',
textWhite: '#FFFFFF',
success: '#34C759',
warning: '#FF9500',
error: '#FF3B30',
info: '#5AC8FA',
border: '#C6C6C8',
overlay: 'rgba(0, 0, 0, 0.5)',
transparent: 'transparent'
};
export { colors };
app/styles/theme/dark/index.ts:
import { ColorsType } from "styled-components/native";
const colors: ColorsType = {
primary: '#0A84FF',
secondary: '#5E5CE6',
background: '#121212',
text: '#FFFFFF',
textWhite: '#FFFFFF',
success: '#32D74B',
warning: '#FF9F0A',
error: '#FF453A',
info: '#64D2FF',
border: '#3A3A3C',
overlay: 'rgba(0, 0, 0, 0.7)',
transparent: 'transparent'
};
export { colors };
app/styles/theme/index.tsx:
import { DefaultTheme, ThemeModeType } from 'styled-components/native';
import { colors as darkColor } from './dark';
import { colors as lightColor } from './light';
import { spacing } from './custom/spacing';
import { typography } from './custom/typography';
const getTheme: (type: ThemeModeType) => DefaultTheme = type => {
const theme = type === 'dark' ? darkColor : lightColor;
return {
mode: type,
spacing,
typography,
colors: theme,
};
};
export { getTheme };
app/providers/ThemeProvider/index.tsx:
import { getTheme } from '@/styles';
import { createContext, PropsWithChildren, useCallback, useState } from 'react';
import { useColorScheme } from 'react-native';
import {
DefaultTheme,
ThemeModeType,
ThemeProvider as StyledThemeProvider,
} from 'styled-components/native';
// Context 类型定义
type ContextProps = {
mode: ThemeModeType;
theme: DefaultTheme;
toggleTheme: () => void;
};
// 默认主题
const defaultTheme: ContextProps = {
mode: 'light',
theme: getTheme('light'),
toggleTheme: () => {},
};
// 创建 Context
export const ThemeContext = createContext<ContextProps>(defaultTheme);
// ThemeProvider 组件
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const isDarkMode = useColorScheme() === 'dark';
const [mode, setMode] = useState<ThemeModeType>(isDarkMode ? 'dark' : 'light');
// 切换主题函数
const toggleTheme = useCallback(() => {
setMode(prev => (prev === 'light' ? 'dark' : 'light'));
}, []);
const theme = getTheme(mode);
return (
<ThemeContext.Provider value={{ mode, theme, toggleTheme }}>
<StyledThemeProvider theme={theme}>
{children}
</StyledThemeProvider>
</ThemeContext.Provider>
);
};
app/providers/index.ts:
export { ThemeContext, ThemeProvider } from './ThemeProvider';
app/styles/index.ts:
// 主题 Design Tokens
export * from './theme';
// 通用样式
export * from './common';
创建一个测试组件 app/index.tsx:
import styled from 'styled-components/native';
import { ThemeProvider, ThemeContext } from '@providers';
import { useContext } from 'react';
const Container = styled.View`
padding: ${props => props.theme.spacing.md}px;
background-color: ${props => props.theme.colors.background};
`;
const Title = styled.Text`
font-size: ${props => props.theme.typography.fontSize.xl}px;
font-weight: ${props => props.theme.typography.fontWeight.bold};
color: ${props => props.theme.colors.text};
`;
const Button = styled.TouchableOpacity`
background-color: ${props => props.theme.colors.primary};
padding: ${props => props.theme.spacing.md}px;
border-radius: 8px;
margin-top: ${props => props.theme.spacing.md}px;
`;
const ButtonText = styled.Text`
color: ${props => props.theme.colors.textWhite};
text-align: center;
`;
function App() {
return (
<ThemeProvider>
<AppContent />
</ThemeProvider>
);
}
function AppContent() {
const { toggleTheme, mode } = useContext(ThemeContext);
return (
<Container>
<Title>Styled Components 配置成功!</Title>
<Title>当前主题: {mode}</Title>
<Button onPress={toggleTheme}>
<ButtonText>切换主题</ButtonText>
</Button>
</Container>
);
}
export default App;
配置完成后,必须重新构建应用:
# 清理缓存并重启
yarn start --reset-cache
# 或者重新构建
# iOS
yarn ios
# Android
yarn android
styled-components
@types/styled-components
babel-plugin-styled-components
babel.config.js
@providers)问题:props.theme 报类型错误
解决:
app/types/styled-components-native.d.ts 文件存在DefaultTheme 接口定义了所有需要的字段问题:点击切换主题,样式不变
检查:
ThemeProvider 内部?props.theme.colors.xxx 而不是硬编码颜色值?解决:
yarn start --reset-cache
babel.config.js 语法解决:
babel.config.js 和 tsconfig.json 别名配置'./app/providers'