--- date: "2023-08-27 14:20:20" --- # 在 react-native-webview 中使用打包後的 JS 程式碼:使用 vite 整合進 Expo 的開發流程 ## 睽違七年的 React Native 上個月回來寫 [React-native](https://reactnative.dev/),想做點自己的小玩具玩。距離上次寫 React-native 已經是七年前的 [kaif.io](https://github.com/Yukaii/kaif-ios) rn app 了,歲月如梭啊,在這期間,我竟然連**一次**都沒有想在手機上跑自己程式的想法,看來果然是被蘋果生態系禁錮太久了 XD ~~(歡迎來到水果監獄)~~ 為了要在 React-native 上使用 [codemirror](https://codemirror.net/) 這個網頁用的程式碼編輯器,我需要使用 [react-native-webview](https://github.com/react-native-webview/react-native-webview),而且為了實作可互動的界面,我同樣需要在 webview 中使用 React.js 要如何實作呢?我們先從 [react-native-webview](https://github.com/react-native-webview/react-native-webview) 的 API 開始看起。 ## react-native-webview 的 source API 在 react-native-webview 提供兩種 API 可以執行 JavaScript 程式碼: 1. [`source` Attribute](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#reference): 可以給網址或是 HTML 原始碼 2. [injected JavaScript 系列](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#injectedjavascript):直接給 inline 的 JavaScript 程式碼,會在不同的生命週期事件執行 因為我想要讓我的 webview 能夠離線執行,所以勢必要把 JavaScript 和 HTML 一起打包進 React Native App 裡面。 翻了翻其他使用 webview 客製的案例,以 [`react-native-webview-leaflet`](https://github.com/reggie3/react-native-webview-leaflet)來說,就是直接把 [build 完的 html 原始碼](https://github.com/reggie3/react-native-webview-leaflet/tree/master/WebViewLeaflet/assets) commit 進 repo 裡,作為一位已經被**現代網頁開發工具**慣壞了的工程師,這種行為當然不可以發生!而且要怎麼 Hot Reload? 值得一題,我同時還使用了 [Expo](https://expo.io/) 這套 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`): ```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`) ```javascript 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 為了跨平臺辣~~) ```javascript 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 內容: ```javascript 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`: ```jsx import { WebView } from "react-native-webview"; export default function WebviewComponent () { // ... const webviewHTML = useWebviewHTML(); // ... return <WebView source={{ html: webviewHTML }} /> } ``` 只要 Assets 檔案有改,Reload app 的話 html 也會重新載入,超級舒適!! ## 就這樣了 難得的技術小筆記。其實最近弄 [Blast Launcher](https://github.com/BlastLauncher/blast) 比較多,方才打開草稿記錄夾,發現這篇也躺了快一個月,趕忙在還沒忘記的時候記錄一下。 關於流程其實也有改進的地方,比如複製檔案那邊,應該要讓 vite 直接輸出到 `assets` 資料夾就好,這樣 incremental build 也會有更舒服的體驗,不過當時沒有足夠的時間研究出來,就算是留下一個改進的伏筆了 (=ↀωↀ=)✧