NextJS App 改造原生 App 的完美指南
May 2, 2024
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:
npm i @capacitor/core
npm i -D @capacitor/cli
npx cap init
npm i @capacitor/android @capacitor/ios
修改配置文件 capacitor.config.js :
import { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.ygeeker.geekits",
appName: "Geekits",
webDir: "out",
server: {
androidScheme: "https",
errorPath: "/500.html",
},
};
export default config;
如果你的 Next APP 一直是纯静态导出,那么恭喜你,大功告成。运行此命令可将你的 Next App 编译为静态文件:
next export
接着将编译出的代码同步到 Capacitor:
npx cap sync
大功告成!
但如果你的 Next App 使用了混合构建模式(例如使用了服务端功能),你可能无法直接执行 next export。你需要将一些依赖服务器的逻辑移动到客户端处理。例如:
- 客户端使用 device API 获取 locale(而不是 Next 的 Locale API)
- 客户端使用 hook 获取数据(而不是从 getServerProps 等方法)
从客户端获取 Locale 的示例如下:
async function getDeviceLanguage() {
let { value } = await Device.getLanguageCode();
if (value === "en") {
value = "en-US";
}
if (value === "zh") {
value = "zh";
}
return value;
}
function MainApp({ Component, pageProps }: AppProps) {
// The auto-detected locale from NextJS
// If user has no preferred locale, use auto
const [preferredLocale, setPreferredLocale] = useState(pageProps.locale);
useEffect(() => {
const readLocaleConfig = async () => {
// https://en.wikipedia.org/wiki/IETF_language_tag
let preferredSet = localStorage.getItem("locale");
if (preferredSet) {
if (preferredSet === "auto" && !isWeb()) {
setPreferredLocale(await getDeviceLanguage());
} else if (preferredSet !== "auto") {
// If user has spicified the language
setPreferredLocale(preferredSet);
}
}
};
readLocaleConfig();
}, []);
}
Step 2 - 添加屏幕安全区域
至此我们已经拥有一个基本的原生 App,但仍有很多小问题需要适配。
不同于浏览器环境,移动设备异形屏会影响显示,例如刘海和打孔。为了避免出血,可以在全局 CSS 文件加如下全局变量:
html {
--ion-safe-area-top: env(safe-area-inset-top);
--ion-safe-area-bottom: env(safe-area-inset-bottom);
--ion-safe-area-left: env(safe-area-inset-left);
--ion-safe-area-right: env(safe-area-inset-right);
}
然后在需要预留 Bleeding Area 的地方使用该属性,例如 Appbar(头部):
padding-top: var(--ion-safe-area-top),
Step 3 - 运行时条件处理
尽管 Capacitor 提供了检测运行平台的 API,但在 Next 静态导出的过程中,其API总是会检测为浏览器环境。
例如,有时候我们希望某些 React 组件仅在原生平台中出现,在导出时就需要告知 Next 我们是否在为原生平台打包。
为了在 Next 导出过程中检测指定打包平台,我们可以手动添加一个环境变量指定导出目标。方便起见,你可以在 package.json 中写入以下脚本:
"scripts": {
"build": "next build",
"build:cap": "CAPACITOR_BUILD=true next build && CAPACITOR_BUILD=true next export && npx cap sync",
"dev:cap": "CAPACITOR_BUILD=true next",
},
接着封装一个方法检测打包平台:
const isCapacitor = () => process.env.CAPACITOR_BUILD === "true";
Step 4 - 添加原生图标
Capacitor 并不会为你生成各种尺寸的原生 App 图标,你需要自己手动制作替换。
你只需准备一张 1024 * 1024 的图标原图,接着你可以利用这个工具一键生成 iOS 和 Android 平台的图标。
将下载下来的文件替换项目中的文件即可。
P.S. 此工具无法生成 Adaptive Icon,你需要自己制作前景和背景。
后续流程
Capacitor 的功能远不止本文所写,你可以从官方文档探索更多可能。