Geekits 是我开发了五年多的开源项目,也是我维护最久的开源项目。在经过不断的迭代之后,在 Next 技术栈稳定了下来。
本文将介绍我如何把一个纯 Web App 改造为优雅的原生 App,让这个产品支持多终端运行。
其实改造要做的工作不仅仅是把 App 放入 Webview,而是要移除原生app不需要的功能,用原生 API 替换某些 JS 实现的功能,样式的适配等等。
为什么要做原生 App
我个人并不完全赞同“Web 才是未来”的说法。但我很喜欢 Local first 的 web app,所以即使在开发 Web App 时,我都尽力让 Geekits 离线可用。
而迁移到原生让 local first 的实现更加方便,资源加载更快,大的数据也无需联网加载(例如成语查询功能)。
不仅如此,可操作的窗口尺寸也更大了,不用担心标签页会被不小心关掉,响应速度也更快。
最后,系统提供了更多浏览器不便获取(或者版本受限)的信息,例如加速度传感器,电池信息等。
Step 1 - 安装配置 Capacitor
得益于 Capacitor 不错的 DX,一切顺利的话,你只需要几分钟即可得到一个“能运行的”原生版本。
首先,安装 Capacitor:
1npm i @capacitor/core 2npm i -D @capacitor/cli 3npx cap init 4npm i @capacitor/android @capacitor/ios
修改配置文件 capacitor.config.js :
1import { CapacitorConfig } from "@capacitor/cli"; 2 3const config: CapacitorConfig = { 4 appId: "com.ygeeker.geekits", 5 appName: "Geekits", 6 webDir: "out", 7 server: { 8 androidScheme: "https", 9 errorPath: "/500.html", 10 }, 11}; 12 13export default config;
如果你的 Next APP 一直是纯静态导出,那么恭喜你,大功告成。运行此命令可将你的 Next App 编译为静态文件:
1next export
接着将编译出的代码同步到 Capacitor:
1npx cap sync
大功告成!
但如果你的 Next App 使用了混合构建模式(例如使用了服务端功能),你可能无法直接执行 next export。你需要将一些依赖服务器的逻辑移动到客户端处理。例如:
- 客户端使用 device API 获取 locale(而不是 Next 的 Locale API)
- 客户端使用 hook 获取数据(而不是从 getServerProps 等方法)
从客户端获取 Locale 的示例如下:
1async function getDeviceLanguage() { 2 let { value } = await Device.getLanguageCode(); 3 4 if (value === "en") { 5 value = "en-US"; 6 } 7 if (value === "zh") { 8 value = "zh"; 9 } 10 11 return value; 12} 13 14function MainApp({ Component, pageProps }: AppProps) { 15 // The auto-detected locale from NextJS 16 // If user has no preferred locale, use auto 17 const [preferredLocale, setPreferredLocale] = useState(pageProps.locale); 18 19 useEffect(() => { 20 const readLocaleConfig = async () => { 21 // https://en.wikipedia.org/wiki/IETF_language_tag 22 let preferredSet = localStorage.getItem("locale"); 23 if (preferredSet) { 24 if (preferredSet === "auto" && !isWeb()) { 25 setPreferredLocale(await getDeviceLanguage()); 26 } else if (preferredSet !== "auto") { 27 // If user has spicified the language 28 setPreferredLocale(preferredSet); 29 } 30 } 31 }; 32 33 readLocaleConfig(); 34 }, []); 35 36}
Step 2 - 添加屏幕安全区域
至此我们已经拥有一个基本的原生 App,但仍有很多小问题需要适配。
不同于浏览器环境,移动设备异形屏会影响显示,例如刘海和打孔。为了避免出血,可以在全局 CSS 文件加如下全局变量:
1html { 2 --ion-safe-area-top: env(safe-area-inset-top); 3 --ion-safe-area-bottom: env(safe-area-inset-bottom); 4 --ion-safe-area-left: env(safe-area-inset-left); 5 --ion-safe-area-right: env(safe-area-inset-right); 6}
然后在需要预留 Bleeding Area 的地方使用该属性,例如 Appbar(头部):
1padding-top: var(--ion-safe-area-top),
Step 3 - 运行时条件处理
尽管 Capacitor 提供了检测运行平台的 API,但在 Next 静态导出的过程中,其API总是会检测为浏览器环境。
例如,有时候我们希望某些 React 组件仅在原生平台中出现,在导出时就需要告知 Next 我们是否在为原生平台打包。
为了在 Next 导出过程中检测指定打包平台,我们可以手动添加一个环境变量指定导出目标。方便起见,你可以在 package.json 中写入以下脚本:
1"scripts": { 2 "build": "next build", 3 "build:cap": "CAPACITOR_BUILD=true next build && CAPACITOR_BUILD=true next export && npx cap sync", 4 "dev:cap": "CAPACITOR_BUILD=true next", 5 },
接着封装一个方法检测打包平台:
1const isCapacitor = () => process.env.CAPACITOR_BUILD === "true";
Step 4 - 添加原生图标
Capacitor 并不会为你生成各种尺寸的原生 App 图标,你需要自己手动制作替换。
你只需准备一张 1024 * 1024 的图标原图,接着你可以利用这个工具一键生成 iOS 和 Android 平台的图标。
将下载下来的文件替换项目中的文件即可。
P.S. 此工具无法生成 Adaptive Icon,你需要自己制作前景和背景。
后续流程
Capacitor 的功能远不止本文所写,你可以从官方文档探索更多可能。