在 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 程式碼:
source
Attribute: 可以給網址或是 HTML 原始碼- 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 也會有更舒服的體驗,不過當時沒有足夠的時間研究出來,就算是留下一個改進的伏筆了 (=ↀωↀ=)✧