github.com/Yukaii
Daily Oops

Daily Oops!

https://yukai.dev/blog/2023/08/27/react-native-webview-js-bundling-inline-with-vite在 react-native-webview 中使用打包後的 JS 程式碼:使用 vite 整合進 Expo 的開發流程https://fed.brid.gy/
August 27, 2023

在 react-native-webview 中使用打包後的 JS 程式碼:使用 vite 整合進 Expo 的開發流程

睽違七年的 React Native

上個月回來寫 React-native,想做點自己的小玩具玩。距離上次寫 React-native 已經是七年前的 kaif.io rn app 了,歲月如梭啊,在這期間,我竟然連一次都沒有想在手機上跑自己程式的想法,看來果然是被蘋果生態系禁錮太久了 XD (歡迎來到水果監獄)

為了要在 React-native 上使用 codemirror 這個網頁用的程式碼編輯器,我需要使用 react-native-webview,而且為了實作可互動的界面,我同樣需要在 webview 中使用 React.js

要如何實作呢?我們先從 react-native-webview 的 API 開始看起。

react-native-webview 的 source API

在 react-native-webview 提供兩種 API 可以執行 JavaScript 程式碼:

  1. source Attribute: 可以給網址或是 HTML 原始碼
  2. injected JavaScript 系列:直接給 inline 的 JavaScript 程式碼,會在不同的生命週期事件執行

因為我想要讓我的 webview 能夠離線執行,所以勢必要把 JavaScript 和 HTML 一起打包進 React Native App 裡面。

翻了翻其他使用 webview 客製的案例,以 react-native-webview-leaflet來說,就是直接把 build 完的 html 原始碼 commit 進 repo 裡,作為一位已經被現代網頁開發工具慣壞了的工程師,這種行為當然不可以發生!而且要怎麼 Hot Reload?

值得一題,我同時還使用了 Expo 這套 React Native 開發框架。Expo 把許多 React Native 的原生 Library 都包進框架裡面(包含 react-native-webview),帶來最無縫最容易最絲滑順暢的 React Native 開發體驗,要怎麼把自定的 WebView JavaScript Bundling 和 Expo 無縫整合呢?

vite and vite-plugin-singlefile, 以及一點點的 gluing scripts

因為一些大人的因素,完整的程式碼還沒開源,我這邊就貼部分的程式碼出來。

首先先準備好 webview 要用的 html (/webivew/index.html):

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0,user-scalable=no"
/>
<title>CodeMirror</title>
</head>
<body>
<div id="app"></div>
<script src="./index.ts" type="module"></script>
</body>
</html>

以及對應的 vite 設定檔(/vite.config.js)

import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({
plugins: [viteSingleFile()],
build: {
target: "ios13",
rollupOptions: {
input: ["./webview/index.html"],
output: {
manualChunks: undefined,
},
},
},
});

此時每次 vite build 後會輸出到 /dist/webview/index.html,我再寫了一個簡單的腳本,複製 index.html 到 react-native 的 /assets 目錄去(欸對就這麼簡單還要寫 JavaScript 幹嘛 XDD 為了跨平臺辣

const fs = require("fs");
const path = require("path");
const distPath = path.join(__dirname, "../dist");
const webviewPath = path.join(distPath, "webview");
const indexPath = path.join(webviewPath, "index.html");
const outputPath = path.join(__dirname, "../assets/webviews/index.html");
if (!fs.existsSync(distPath) || !fs.existsSync(indexPath)) {
console.log("No dist folder or index.html found, skipping updateHTML");
process.exit(0);
}
fs.copyFileSync(indexPath, outputPath);

然後寫一個 react hook 去讀 HTML 內容:

import useSWR from "swr";
import * as FileSystem from "expo-file-system";
import { useAssets } from "expo-asset";
export const useWebviewHTML = () => {
const [assets] = useAssets([require("../assets/webviews/index.html")]);
const webviewAssetUri = assets?.[0]?.localUri;
const { data: webviewHTML } = useSWR(
() => (webviewAssetUri ? "webview" : null),
async () => {
return await FileSystem.readAsStringAsync(webviewAssetUri!);
},
);
return webviewHTML;
};
export default useWebviewHTML;

最後丟給 react-native-webview

import { WebView } from "react-native-webview";
export default function WebviewComponent () {
// ...
const webviewHTML = useWebviewHTML();
// ...
return <WebView
source={{ html: webviewHTML }}
/>
}

只要 Assets 檔案有改,Reload app 的話 html 也會重新載入,超級舒適!!

就這樣了

難得的技術小筆記。其實最近弄 Blast Launcher 比較多,方才打開草稿記錄夾,發現這篇也躺了快一個月,趕忙在還沒忘記的時候記錄一下。

關於流程其實也有改進的地方,比如複製檔案那邊,應該要讓 vite 直接輸出到 assets 資料夾就好,這樣 incremental build 也會有更舒服的體驗,不過當時沒有足夠的時間研究出來,就算是留下一個改進的伏筆了 (=ↀωↀ=)✧

https://fed.brid.gy/
本篇文章驕傲的使用 HackMD 發佈
Yukai Huanghttps://yukai.dev

Hi

This is Yukai Huang's personal website.

Here you can read my recent posts, play with my side projects before, or get to know me more.

安久吧!

https://fed.brid.gy/