
1. 项目概述从“技能”到“可复用的开发资产”在移动应用开发领域尤其是使用 React Native 这类跨平台框架时我们常常面临一个经典困境如何在不同的项目间高效复用那些经过验证的、复杂的 UI 组件或功能模块你可能会把代码从一个项目复制到另一个项目然后花大量时间调整依赖、修复因环境差异导致的 bug。或者你维护着一个私有的 Git 子模块或 NPM 包但版本管理、文档同步和团队协作又成了新的负担。今天要聊的expo/skills正是 Expo 官方为解决这一痛点而推出的一个实验性、但极具启发性的解决方案。它不是一个成熟的产品更像是一个官方发布的“最佳实践”原型或概念验证。简单来说Skills 旨在将 React Native/Expo 应用中的功能模块如“用户认证”、“支付集成”、“实时聊天”封装成独立的、可插拔的“技能包”。你可以把它想象成一个超级组件库但不止于 UI它包含了实现一个完整功能所需的前端组件、后端逻辑通过 Serverless Functions、状态管理、甚至图标和文档并且能够通过一个命令像安装 App 一样“安装”到你的 Expo 项目中。这个项目的核心价值在于标准化与自动化。它试图定义一种协议让功能模块的集成从“手工复制粘贴”升级为“一键式配置”极大地提升了开发效率降低了维护成本并为构建可组合的、模块化应用生态铺平了道路。对于独立开发者、初创团队或是大型企业中需要快速搭建原型或标准化内部工具的场景expo/skills所代表的思路具有很高的参考价值。2. 核心设计理念与架构拆解2.1 什么是“Skill”超越组件的功能单元在expo/skills的语境下一个 “Skill” 远不止是一个 React 组件。它是一个自包含的功能单元至少包含以下几个部分前端组件 (UI Logic): 提供 React Native 视图组件、自定义 Hooks、上下文Context等用于在应用内呈现交互界面和处理客户端逻辑。后端函数 (Serverless Functions): 通过 Expo 的eas.json配置将相关的 API 路由或 Serverless 函数定义打包。当 Skill 被安装时这些函数可以自动部署到 Expo 的应用服务后端。配置与元数据: 一个skill.json清单文件定义了 Skill 的名称、版本、依赖、所需的权限如相机、地理位置、环境变量模板等。开发资源: 可能包括图标素材、字体、或本地化文件。文档与示例: 内嵌的使用说明或链接到外部文档。例如一个“用户画像User ProfileSkill”可能包含一个可编辑的个人信息表单组件、一个头像上传裁剪组件、用于保存和读取用户数据的自定义 Hook、以及处理图片上传和数据库更新的 Serverless 函数。2.2 架构解析如何实现“即插即用”expo/skills的实现依赖于 Expo 生态的几项核心技术其架构可以理解为一次精心设计的“工程整合”基于 Expo Application Services (EAS): EAS 是 Expo 的云服务套件其中 EAS Build云构建和 EAS Submit云提交已广为人知。Skills 巧妙地利用了 EAS 的“配置变量Environment Variables”和“预览Previews”或“更新Updates”的工作流。Skill 的配置和代码可以通过 EAS 的通道进行分发和管理。与 Expo Dev Client 深度集成: Expo 开发客户端允许你在真机上加载本地开发中的项目。Skills 利用这一点实现了动态加载。当你运行npx expo install skills/xxx概念命令时CLI 工具会修改你的项目配置如app.json注入 Skill 的模块引用并可能通过 EAS 拉取预构建的更新包在开发客户端上即时生效。清单文件驱动 (skill.json): 这是 Skill 的“身份证”和“说明书”。它采用 JSON 格式声明了该 Skill 的所有属性和要求。CLI 工具通过解析这个文件自动完成以下工作检查并安装 NPM 依赖。向项目的app.json添加所需的插件Plugins配置例如expo-camera插件。创建环境变量模板文件如.env.skill-xxx提示开发者填入必要的 API 密钥等信息。注册后端函数路由到项目的 EAS 配置中。模块联邦Module Federation思想的实践: 虽然实现方式不同但 Skills 的理念与 Webpack 5 的模块联邦异曲同工都旨在实现应用间的运行时模块共享。在 React Native 环境下它通过 Expo 的更新系统和原生模块链接Linking机制模拟了动态加载独立代码包的能力。注意expo/skills仓库本身更多是一个规范和示例的展示。它包含了创建 Skill 的模板、skill.json的规范定义、以及一个示例 Skill如“相机”。实际的“安装”和“管理”CLI 工具可能尚未完全开源或处于早期实验阶段但其设计思路是完整且公开的。3. 创建一个自定义 Skill从零到一的实践指南理解概念后最直接的学习方式就是动手创建一个。下面我将以构建一个“简易天气显示 Skill”为例拆解全过程。这个 Skill 包含一个显示当前城市天气的组件并调用一个开源的天气 API。3.1 环境准备与项目初始化首先你需要一个标准的 Expo 项目作为 Skill 的开发容器。建议使用 TypeScript 以获得更好的类型提示。# 使用 Expo CLI 初始化一个新项目作为你的 Skill 开发项目 npx create-expo-app weather-skill-demo --template blank-typescript cd weather-skill-demo接下来你需要参考expo/skills仓库中的模板结构。虽然官方可能没有提供一键生成器但我们可以模仿其核心结构创建目录weather-skill-demo/ ├── src/ │ ├── components/ │ │ └── WeatherWidget.tsx # 我们的天气组件 │ ├── hooks/ │ │ └── useWeather.ts # 获取天气数据的自定义 Hook │ └── index.ts # Skill 的主出口文件 ├── functions/ # 后端函数可选本例放在前端直接调用 ├── assets/ # 图标等静态资源 ├── skill.json # Skill 清单文件核心 ├── package.json └── app.json3.2 编写核心功能代码1. 创建自定义 Hook (useWeather.ts):这个 Hook 负责数据获取。为了简化我们直接在客户端调用公共 API。在实际的 Skill 中复杂逻辑应放在后端函数中以隐藏 API 密钥。// src/hooks/useWeather.ts import { useState, useEffect } from react; import * as Location from expo-location; const OPENWEATHER_API_KEY YOUR_API_KEY; // 应通过环境变量注入 const OPENWEATHER_URL https://api.openweathermap.org/data/2.5/weather; export interface WeatherData { temp: number; description: string; icon: string; city: string; } export const useWeather (city?: string) { const [weather, setWeather] useStateWeatherData | null(null); const [loading, setLoading] useState(true); const [error, setError] useStatestring | null(null); useEffect(() { const fetchWeather async () { try { let targetCity city; if (!targetCity) { // 请求位置权限并获取当前城市 let { status } await Location.requestForegroundPermissionsAsync(); if (status ! granted) { setError(Permission to access location was denied); setLoading(false); return; } let location await Location.getCurrentPositionAsync({}); // 这里简化处理实际应用中应进行逆地理编码 targetCity Shanghai; // 示例 } const response await fetch( ${OPENWEATHER_URL}?q${targetCity}appid${OPENWEATHER_API_KEY}unitsmetric ); const data await response.json(); if (data.cod 200) { setWeather({ temp: data.main.temp, description: data.weather[0].description, icon: https://openweathermap.org/img/wn/${data.weather[0].icon}.png, city: data.name, }); } else { setError(City not found); } } catch (err) { setError(Failed to fetch weather data); console.error(err); } finally { setLoading(false); } }; fetchWeather(); }, [city]); return { weather, loading, error }; };2. 创建展示组件 (WeatherWidget.tsx):这是一个简单的展示组件使用上一步的 Hook。// src/components/WeatherWidget.tsx import React from react; import { View, Text, Image, ActivityIndicator, StyleSheet } from react-native; import { useWeather } from ../hooks/useWeather; interface WeatherWidgetProps { city?: string; } export const WeatherWidget: React.FCWeatherWidgetProps ({ city }) { const { weather, loading, error } useWeather(city); if (loading) { return ( View style{styles.container} ActivityIndicator sizesmall color#000 / Text style{styles.text}Loading weather.../Text /View ); } if (error || !weather) { return ( View style{styles.container} Text style{[styles.text, styles.error]}{error || Failed to load weather}/Text /View ); } return ( View style{styles.container} Text style{styles.city}{weather.city}/Text Image source{{ uri: weather.icon }} style{styles.icon} / Text style{styles.temp}{Math.round(weather.temp)}°C/Text Text style{styles.desc}{weather.description}/Text /View ); }; const styles StyleSheet.create({ container: { alignItems: center, padding: 16, backgroundColor: #e3f2fd, borderRadius: 12, minWidth: 150, }, city: { fontSize: 18, fontWeight: bold, marginBottom: 4 }, icon: { width: 50, height: 50 }, temp: { fontSize: 32, fontWeight: 600, marginVertical: 4 }, desc: { fontSize: 14, color: #666, textTransform: capitalize }, text: { fontSize: 14, color: #666 }, error: { color: #d32f2f }, });3. 创建主出口文件 (index.ts):这是 Skill 被其他项目引入时的入口。// src/index.ts export { WeatherWidget } from ./components/WeatherWidget; export { useWeather } from ./hooks/useWeather; // 可以导出类型 export type { WeatherData } from ./hooks/useWeather;3.3 定义 Skill 清单文件 (skill.json)这是整个 Skill 的灵魂。它描述了 Skill 的元数据、依赖和集成要求。{ name: weather-display, version: 1.0.0, displayName: Weather Display, description: A simple skill to display current weather for a location., author: Your Name, license: MIT, dependencies: { // 声明 Skill 运行所需的 npm 包 expo-location: ~16.0.0 }, expoConfig: { // 声明需要添加到宿主 app.json 的插件配置 plugins: [ [ expo-location, { locationAlwaysAndWhenInUsePermission: Allow $(PRODUCT_NAME) to use your location. } ] ] }, env: [ // 声明 Skill 需要的环境变量CLI 会提示宿主项目配置 { key: OPENWEATHER_API_KEY, description: Your OpenWeatherMap API key, required: true } ], assets: [ // 声明需要复制的静态资源路径 assets/weather-icons/* ] }3.4 配置 Package.json 与构建在package.json中你需要正确设置main入口并考虑如何打包。对于简单的 Skill可以直接将src目录作为源码分发。更复杂的 Skill 可能需要构建步骤如使用tsc或babel编译成lib目录。{ name: your-scope/weather-skill, version: 1.0.0, main: src/index.ts, // 或 lib/index.js scripts: { build: tsc, prepublishOnly: npm run build }, files: [ src, // 或 lib skill.json, assets ], peerDependencies: { react: *, react-native: *, expo: * }, dependencies: { expo-location: ~16.0.0 }, devDependencies: { typescript: ^5.0.0, types/react: ~18.2.0 } }实操心得在开发 Skill 时务必保持依赖声明的一致性。skill.json中的dependencies、package.json中的dependencies/peerDependencies以及代码中实际import的模块必须匹配。否则在安装到宿主项目时极易出现依赖冲突或缺失。一个建议是将运行时必需的、非全局的包放在dependencies而将react、react-native、expo这类宿主项目肯定有的放在peerDependencies。4. 在宿主项目中“安装”与使用 Skill由于完整的官方 CLI 工具可能尚未成熟我们可以模拟其核心流程手动将 Skill 集成到一个新的 Expo 项目中。这能帮助我们深刻理解 Skills 机制背后的原理。4.1 手动集成步骤假设我们有一个宿主应用项目MyWeatherApp。拷贝 Skill 源码将weather-skill-demo/src目录拷贝到宿主项目的某个位置例如skills/weather-display/。安装依赖根据skill.json中的dependencies在宿主项目根目录执行npx expo install expo-location合并配置手动将skill.json中expoConfig.plugins的内容合并到宿主项目的app.json或app.config.js中的plugins数组里。确保权限描述文案合适。配置环境变量在宿主项目的根目录创建或修改.env文件添加OPENWEATHER_API_KEYyour_real_api_key。并在代码中通过process.env.EXPO_PUBLIC_OPENWEATHER_API_KEYExpo 推荐方式或expo-constants来读取。导入并使用组件在宿主应用的屏幕组件中直接导入 Skill 的组件。// MyWeatherApp/app/index.tsx import { WeatherWidget } from ../skills/weather-display/src/components/WeatherWidget; import { useWeather } from ../skills/weather-display/src/hooks/useWeather; export default function HomeScreen() { // 你也可以直接使用 Hook const { weather } useWeather(Beijing); return ( View TextCurrent Weather in App:/Text WeatherWidget cityLondon / {weather TextBeijing Temp: {weather.temp}°C/Text} /View ); }4.2 自动化集成的理想流程在expo/skills的理想模型中上述手动步骤应由一个 CLI 命令自动完成# 假设的命令 npx expo skill add your-scope/weather-skill这个命令会从 NPM 仓库或指定 Git 地址拉取 Skill 包。读取skill.json。自动安装声明的 NPM 依赖 (expo-location)。自动修改app.json添加插件配置。在项目根目录生成一个.env.skill-weather-display模板文件提示用户填写OPENWEATHER_API_KEY。可能通过 EAS 更新机制将 Skill 的 UI 代码动态加载到开发客户端。注意事项自动修改项目配置文件 (app.json) 存在一定风险。在实现自动化工具时务必做好备份和冲突检测。例如如果宿主项目已经配置了expo-location插件则需要智能合并而非覆盖。5. 深入解析Skill 的依赖管理与冲突解决这是构建健壮 Skill 系统最复杂的一环。当多个 Skill 以及宿主项目本身都声明了对同一个库的不同版本时如何解决5.1 Expo 的解决方案探析根据 Expo 生态的现有工具和skills项目的思路其依赖管理可能倾向于以下策略Peer Dependencies 为主Skill 将其核心功能依赖如expo-location,react-native-maps声明为peerDependencies并指定一个较宽的兼容版本范围如^16.0.0。这表示“我需要这个包但希望宿主项目来提供它。”CLI 的依赖协调当运行expo skill add时CLI 会检查所有已安装 Skill 和宿主项目的peerDependencies尝试计算出一个能满足所有要求的版本。然后它会在宿主项目的package.json中安装或更新该依赖到协调后的版本。冲突检测与提示如果版本要求无法协调例如 Skill A 需要^16.0.0Skill B 需要^17.0.0CLI 应中止安装并给出明确的错误信息提示开发者需要手动解决冲突可能通过联系 Skill 维护者更新版本或寻找替代 Skill。私有依赖打包对于极少数仅被该 Skill 使用、且与宿主或其他 Skill 几乎不可能冲突的第三方库可以打包进 Skill 自身的构建产物中。但这需要谨慎因为可能增加 Bundle 体积且如果两个 Skill 打包了同一个库的不同版本在原生层面仍可能引发不可预知的问题。5.2 给 Skill 开发者的建议最小化依赖只引入绝对必要的依赖。优先使用 Expo SDK 和 React Native 内置 API。宽泛的版本范围在peerDependencies中使用^兼容次版本或~兼容补丁版本来指定范围给予宿主项目更大的灵活性。提供替代方案或降级路径在文档中说明如果你的 Skill 需要较新的库版本而宿主项目无法升级是否有可选的、功能降级的配置方式。彻底测试在多个不同 Expo SDK 版本的项目中测试你的 Skill确保其兼容性。6. 进阶应用Skill 与后端函数 (Serverless Functions) 的联动一个真正强大的 Skill 往往需要后端逻辑支持。Expo 通过EAS Functions基于 Supabase Functions提供了 Serverless 解决方案。Skill 可以无缝集成这一点。6.1 在 Skill 中定义后端函数在 Skill 项目根目录创建functions/文件夹里面存放你的 Serverless 函数。例如我们可以把调用 OpenWeather API 的逻辑移到后端以保护 API 密钥。weather-skill-demo/ ├── functions/ │ └── get-weather/ │ ├── index.ts # 函数主逻辑 │ └── package.json # 函数特定依赖functions/get-weather/index.ts:// 这是一个使用 Node.js 运行时环境的 Serverless 函数 import { serve } from https://deno.land/std0.168.0/http/server.ts; import { createClient } from https://esm.sh/supabase/supabase-js2; const OPENWEATHER_API_KEY Deno.env.get(OPENWEATHER_API_KEY); serve(async (req) { const { city } await req.json(); if (!city) { return new Response(JSON.stringify({ error: City is required }), { status: 400 }); } try { const response await fetch( https://api.openweathermap.org/data/2.5/weather?q${city}appid${OPENWEATHER_API_KEY}unitsmetric ); const data await response.json(); return new Response(JSON.stringify(data), { headers: { Content-Type: application/json }, }); } catch (error) { return new Response(JSON.stringify({ error: Failed to fetch weather }), { status: 500 }); } });6.2 在 Skill 清单中声明函数在skill.json中增加一个functions字段描述这个函数及其配置需求。{ ... // 其他原有字段 functions: [ { name: get-weather, path: ./functions/get-weather, env: [OPENWEATHER_API_KEY] // 声明函数需要的环境变量 } ] }6.3 宿主项目的集成与调用当这个 Skill 被安装时理想的 CLI 工具会将functions/get-weather目录复制到宿主项目的functions/目录下或一个特定的skills-functions/目录。在宿主的eas.json配置中注册这个函数的路由。将OPENWEATHER_API_KEY加入到 EAS 项目的环境变量配置中。在前端 Skill 组件中useWeatherHook 的调用就需要改为指向这个部署后的函数端点// 修改后的 useWeather Hook 部分代码 const response await fetch(/api/functions/get-weather, { // 假设路由为 /api/functions/* method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ city: targetCity }), });实操心得将敏感逻辑和 API 密钥移至后端函数是生产环境应用的最佳实践。Skills 模式鼓励了这种架构分离。在开发 Skill 时应优先考虑将数据获取、第三方服务集成、数据库操作等逻辑设计为可部署的后端函数使 Skill 成为一个真正完整的“全栈功能模块”。7. 常见问题、排查技巧与生态展望7.1 开发与调试中的常见问题Skill 组件在宿主项目中不显示或报错检查依赖首先确认宿主项目是否安装了 Skill 声明的所有peerDependencies和dependencies。运行npx expo doctor检查健康状态。检查配置合并确认skill.json中的expoConfig尤其是插件和权限已正确合并到宿主的app.json中。有时需要重启开发服务器或清理缓存 (npx expo start -c)。检查导入路径如果是手动集成确保导入路径正确。如果是通过理论上的 CLI 安装检查 node_modules 中 Skill 包的内容是否完整。环境变量未生效Expo 的环境变量需要以EXPO_PUBLIC_为前缀才能在客户端代码中访问。确保在 Skill 的skill.json的env字段中声明时或宿主项目配置时遵循了这个规则。在 EAS 构建或部署函数时需要在 EAS 项目控制台或通过eas secret:create命令设置环境变量。原生模块链接问题如果 Skill 包含了需要原生代码的库如某些react-native-*库在安装 Skill 后可能需要重新运行npx pod-install(iOS) 或重新构建原生项目。Skills 的理想状态是能通过 Expo 的自动链接Autolinking和开发客户端的动态更新来避免这一步但对于深度集成的原生模块可能仍需手动处理。7.2 对现有工作流的提升与挑战提升效率飞跃对于常见功能用户系统、支付、社交分享、数据图表从“寻找库 - 阅读文档 - 集成 - 调试”变为“一键安装 - 配置密钥 - 直接使用”。质量与一致性官方或社区维护的优质 Skills 经过了更多项目的验证比自行实现的代码往往更稳定、安全。关注点分离开发者可以更专注于业务逻辑而非重复搭建基础设施。挑战生态培育需要吸引大量开发者创建和维护高质量的 Skills形成良性生态。版本管理与升级当 Skill 有安全更新或功能迭代时如何平滑地推送到所有使用它的宿主项目性能与体积引入多个 Skills 是否会显著增加应用启动时间和 Bundle 体积需要良好的按需加载和代码分割策略。调试复杂性当问题出现在一个集成的 Skill 内部时调试链路可能更长需要更完善的错误追踪和日志系统。7.3 个人实践中的体会与建议在我尝试借鉴expo/skills思路封装内部工具的过程中有几点深刻体会首先清单文件 (skill.json) 的设计至关重要。它不仅是配置更是契约。除了已有的字段考虑加入compatibility字段明确声明支持的 Expo SDK 版本范围、React Native 版本范围能提前避免大量集成问题。其次文档即代码。一个优秀的 Skill 应该包含充足的 JSDoc 注释并最好能利用 TypeScript 生成 API 文档。同时在 Skill 项目根目录提供一个example文件夹里面是一个最小化的 Expo 示例应用展示 Skill 的所有用法。这对于使用者来说比任何文字说明都直观。最后从小处着手。不要一开始就试图创建一个大而全的“用户管理系统 Skill”。可以从一个功能清晰、边界明确的组件开始比如一个“评分组件 Skill”或“图片选择器 Skill”。验证了整个流程开发、打包、集成、使用后再逐步增加复杂度。expo/skills项目虽然目前可能更像一个“思想实验”但它清晰地指出了未来跨平台应用开发的一个进化方向功能模块化、集成自动化、生态标准化。即使你不直接使用这个实验性项目将其理念应用到你自己的项目管理和团队协作中设计内部的“微前端”或“模块协议”也能带来可观的效率提升。