Expo 开发 React Native 入门

2,433 阅读5分钟

之前公司有个 RN0.54 的老项目,开发体验真是一言难尽,安卓 studio 都下载不到了,各种初始化配置让人头皮发麻。前段时间接手了一个新的 PDA 项目,要开发一个 app,这不机会就来了么,新技术尝尝鲜。(可能也没那么新了)。。。

一、Expo 的好处是什么?

Expo 是一个工具套件,专为简化 React Native 应用的开发、测试和部署而设计。它提供了开箱即用的开发环境,开发者无需配置 Android Studio 或 Xcode,即可快速上手。通过 Expo Go 应用,你可以在真实设备上实时预览和测试代码变化,支持热重载和快速刷新,支持线上打包,大大提高了开发效率。

Expo 提供了丰富的内置功能和跨平台 API,如摄像头、文件系统、推送通知等,帮助开发者轻松构建功能齐全的 iOS 和 Android 应用。它的托管服务(如云构建、OTA 更新)简化了应用的构建和发布流程,无需复杂的本地工具链。

对于不熟悉原生开发的前端开发者,Expo 屏蔽了复杂的原生代码细节,使得开发过程更加顺畅。它特别适合快速原型开发和 MVP 项目,帮助团队快速验证想法并迭代产品。甚至创建出来的初始项目压根就没android 和 ios 的目录,就一个 app,直接无脑堆代码就好了。

此外,Expo 的社区活跃,提供了详尽的文档和支持,确保开发者在遇到问题时能够迅速找到解决方案。对于需要跨平台支持的中小型项目,Expo 是一个高效且灵活的选择。

二、安装依赖

我用的 node 20 环境,首先安装全局脚手架。

1、用淘宝源试下,先切源吧
nrm use taobao

2、没下载的话先 install 一个 20
nvm use 20  

node -v
npm -v

npm i -g  eas-cli expo-cli

eas是打包用的,expo-cli 就是我们的全局 cli,开发用的。

三、手机设备下载 Expo Go app

expo.dev/go

image.png 貌似要翻墙?

手机打开可以看到这个界面

image.png

四、创建项目并启动

 npx create-expo-app app

启动项目

npm run start

image.png

有多个指令可以运行下试试。

mac如果遇到如下报错可以 装一个watchman

image.png

start 后会出现一个二维码

image.png

五、真机调试

用手机打开Expo go,使用Scan QR code扫二维码

image.png

image.png

修改代码重新保存,会热更新

easy~

内嵌一个 webview 试试

打包 apk 后,要用 https 的地址,否则可能打不开。下面会说解决办法。

image.png

也是可以正常打开的。

也可以run andiord 或者 start 进行本地模拟器调试

image.png

六、webview 通讯

我就以安卓举例了,ios 没试。

通过

webViewRef?.current?.injectJavaScript(`
      window.postMessage(${JSON.stringify({
        message: JSON.stringify({
          ip,
          port,
          androidId: Application.getAndroidId(),
        }),
      })}, '*');
    `);

在onLoad的时候将数据从 RN 发送给 webview

  const webViewRef = useRef<WebView | null>(null);
  
  const sendMessageToWebView = async () => {
    webViewRef?.current?.injectJavaScript(`
      window.postMessage(${JSON.stringify({
        message: JSON.stringify({
          ip,
          port,
          androidId: Application.getAndroidId(),
        }),
      })}, '*');
    `);
  };

  const uri = ip?.includes("https")
    ? `https://${ip}:${port}`
    : `http://${ip}:${port}`;

  return (
    <SafeAreaProvider>
      <WebView
        style={{
          flex: 1,
        }}
        // style={styles.container}
        originWhitelist={["*"]}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        mixedContentMode="always"
        source={{ uri: uri }}
        // source={{ uri: `${ip}` }}
        ref={webViewRef}
        onLoad={() => sendMessageToWebView()}
        // injectedJavaScript={''}
        onMessage={(event) => {
          try {
            // 传过来的 data 肯定为字符串,可以跟 web 端约定好交互的数据格式
            const data = JSON.parse(event.nativeEvent.data);
            console.log("data", data);
          } catch (error) {
            // handle parse error
          }
        }}
      />
    </SafeAreaProvider>
  );

webview 接收

 const handleMessage = (event) => {
    const data = event.data;
    console.log("Received message from RN:", data);
    setWebViewData(JSON.parse(data?.message || "{}"));
  };

  useEffect(() => {
    console.log("home 页面");
    window.addEventListener("message", handleMessage);
    document.addEventListener("message", handleMessage);
    return () => {
      window.removeEventListener("message", handleMessage);
      document.addEventListener("message", handleMessage);
    };
  }, []);

image.png

webview 发送给 RN

  window?.ReactNativeWebView.postMessage("message from webview");

RN onMessage接收

   <SafeAreaProvider>
      <WebView
        style={{
          flex: 1,
        }}
        // style={styles.container}
        originWhitelist={["*"]}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        mixedContentMode="always"
        source={{ uri: uri }}
        // source={{ uri: `${ip}` }}
        ref={webViewRef}
        onLoad={() => sendMessageToWebView()}
        // injectedJavaScript={''}
        onMessage={(event) => {
          try {
            // 传过来的 data 肯定为字符串,可以跟 web 端约定好交互的数据格式
            const data = JSON.parse(event.nativeEvent.data);
            console.log("data", data);
          } catch (error) {
            // handle parse error
          }
        }}
      />
    </SafeAreaProvider>

七、webview支持 http

到这一步就开始麻烦了,常规的 expo 就搞不了了,要从托管managed工作流中退出到裸漏bare工作流。

1、eject一下,可能比较慢

expo eject

2、在 android/app/src/main/AndroidManifest.xml 文件

application 里添加 android:usesCleartextTraffic="true"

我目前的 application 这一行


  <application 
  android:name=".MainApplication" 
  android:label="@string/app_name" 
  android:icon="@mipmap/ic_launcher" 
  android:roundIcon="@mipmap/ic_launcher_round" 
  android:allowBackup="true" 
  android:theme="@style/AppTheme" 
  android:supportsRtl="true"  
  android:usesCleartextTraffic="true">

八、修改 app 名称、图标、和首屏图

eject 之后在修改 app.json里的配置文件和图片啥的貌似就不生效了。 要去 android 的目录里去修改了。

替换这里的图片,适配不同分辨率。

image.png

app 名称在 string.xml 里改一下

image.png

android/app/build.gradle 里的versionCode和versionName大包前也修改了下,每次累加一下

image.png

为了防止不生效。。。我是同步修改了 app.json

image.png

九、线上打包

1、先到expo官网注册一下账号,expo.dev/

image.png

2、然后本地登录expo账号,输入:

eas login

3、登录后,输入以下代码,让其自动生成eas.json配置文件。

eas build --platform android

4、全点是就行,没啥讲究

image.png

eas.json 文件

image.png

5、改造下 eas.json文件,不然打出来的不是 apk 文件。

        "release": {
            "android": {
                "buildType": "apk"
            }
        }

image.png 6、运行打包命令

eas build --platform android --profile release

因为要排队打包,一般要十几分钟,慢的话不太清楚,大家慢慢等待即可。

打包好后,下载安装即可。

我这个搞了 11 分钟。

image.png 但是这玩意啥也不弄的话,构建的体积有点大,后续再研究下,应该包含了太多不相关的东西。

image.png

本地打包没试,感兴趣的自己尝试下吧。哈哈哈哈哈~~~

十、彩蛋

开发完 h5 之后发现项目现场的 pda 是个老古董,安卓 9 的版本。。。。Android System WebView 是 66.0.3359.158 然后 webview套进去直接白屏,vconsole 还打印不出来报错。人都麻了。。。

这里推荐一个调试方式:

Android

chrome

chorme下载资源需要翻墙,有时候调试会出现白屏或提示404,可以使用edge来调试

  1. 手机连接电脑,在手机上打开USB调试,并允许
  2. 电脑打开chrome浏览器,在地址栏输入:

chrome://inspect/

  1. 界面会显示设备和页面列表,选择需要调试的页面
edge
  1. 手机连接电脑,在手机上打开USB调试,并允许
  2. 电脑打开edge浏览器,在地址栏输入:

edge://inspect/

  1. 界面会显示设备和页面列表,选择需要调试的页面

设备打开 usb调试模式之后就可以在电脑上定位到具体的报错信息,果然是语法不兼容的问题。新项目用的 vite4 进行的打包,最后通过 babel 的低版本转换和 vite 的@vitejs/plugin-legacy做了向下兼容。还是没能处理完。最后把仅剩的那个报错语法直接删了(笑死🤣)。