likes
comments
collection
share

ReactNative实现静态资源服务器加载本机资源

作者站长头像
站长
· 阅读数 22

前言

最近有个需要,需要把3D场景放在移动端进行渲染,但是3D场景通常都比较大,直接用webview 加载线上的3D场景耗时比较久,原先移动端同事用UniApp开发的,但是ios系统 uni-app无法fetch本地资源,因为对uni-app不熟,所以就尝试用RN来实现加载本地资源的需求。另外提一句,我们3D场景主要使用Babylonjs进行开发,所以方案选择时候会优先考虑支持Babylonjs

方案选择

经过调研,大概有3种方案

  1. 最简单的一种就是将静态资源放到移动端的静态资源目录下面,然后直接作用webview进行加载,路径大概为file:///xxxxx/xxxx,但实测ios无法支持在这样的路径下请求到本地的资源,因此就放弃了
  2. BabylonReactNative是Babylonjs官方提供的ReactNative库,给了开发人员在原生系统开发功能的接口,但是跑官方demo的时候,就mac系统能成功运行,Ubuntu和window都运行失败,各种编辑错误。而且,使用这种方式需要改目前封装的3D引擎,所以进入待定方案
  3. 目前采用的方案,就是在手机端开启一个静态资源服务,然后通过webview 加载http://localhost:3000/xx/xx这样的路径,目前看来是最符合的

环境准备

因为需要支持iOS系统,所以优先使用mac系统进行开发。因为所用到的库只能在Linux系统上才能编译成功,所以如果使用Window开发,需要安装wsl子系统,然后在子系统上安装安卓环境

根据ReactNative官方提供的教程,安装好环境,初始化一个RN项目

安装项目所需的依赖

npm i react-native-static-server react-native-fs react-native-webview

简单介绍下这3个库

  • react-native-static-server 这个是在移动端开启静态服务器的库,但是不更新了,但是能用.@dr.pogodin/react-native-static-server这个虽然在更新,但编译各种问题,暂时不推荐使用,但在安卓的教程中用了这个库,还没去改掉!!!
  • react-native-fs 这个库主要用来下载远程资源到本地
  • react-native-webview

安装完毕 如果是ios,需要重新pod install

本教程主要是实现Demo,所以目标仅仅只是demo能跑,并符合预期结构,就行 下面把ios和Android 分开进行

项目的目录结构

ReactNative实现静态资源服务器加载本机资源

assets/webroot 为静态资源目录

Android

可能需要参考

  1. 给Gradle设置Proxy
  2. 在WSL2安装Android Studio

设置静态资源目录

android > app > build.gradle

android {
    sourceSets { 
        main { assets.srcDirs = [''../../assets/'] } 
    }

Demo

ReactNative实现静态资源服务器加载本机资源

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 */

import React, { useEffect, useRef, useState } from 'react';
import type { PropsWithChildren } from 'react';
import {
  SafeAreaView,
  ScrollView,
  StatusBar,
  StyleSheet,
  Text,
  useColorScheme,
  View,
  Alert,
  Button,
  Linking,
  Platform,
  Image,
} from 'react-native';

import {
  Colors
} from 'react-native/Libraries/NewAppScreen';

import RNFS from 'react-native-fs';

import Server, {
  STATES,
  extractBundledAssets,
} from '@dr.pogodin/react-native-static-server';

import { WebView } from 'react-native-webview';

export const formUrl = 'https://static.runoob.com/images/demo/demo1.jpg';
export const downloadDest = `${RNFS.DocumentDirectoryPath}/webroot/demo.jpg`;

/*下载文件*/
export function downloadFile(downloadDest: string, formUrl: string) {
  // On Android, use "RNFS.DocumentDirectoryPath" (MainBundlePath is not defined)

  // 图片
  // const downloadDest = `${RNFS.MainBundlePath}/${((Math.random() * 1000) | 0)}.jpg`;
  // const formUrl = 'http://img.kaiyanapp.com/c7b46c492261a7c19fa880802afe93b3.png?imageMogr2/quality/60/format/jpg';

  // 文件
  // const downloadDest = `${RNFS.MainBundlePath}/${((Math.random() * 1000) | 0)}.zip`;
  // const formUrl = 'http://files.cnblogs.com/zhuqil/UIWebViewDemo.zip';

  // 视频
  // const downloadDest = `${RNFS.MainBundlePath}/${((Math.random() * 1000) | 0)}.mp4`;
  // http://gslb.miaopai.com/stream/SnY~bbkqbi2uLEBMXHxGqnNKqyiG9ub8.mp4?vend=miaopai&
  // https://gslb.miaopai.com/stream/BNaEYOL-tEwSrAiYBnPDR03dDlFavoWD.mp4?vend=miaopai&
  // const formUrl = 'https://gslb.miaopai.com/stream/9Q5ADAp2v5NHtQIeQT7t461VkNPxvC2T.mp4?vend=miaopai&';

  // http://wvoice.spriteapp.cn/voice/2015/0902/55e6fc6e4f7b9.mp3
  // const formUrl = 'http://wvoice.spriteapp.cn/voice/2015/0818/55d2248309b09.mp3';

  const options = {
    fromUrl: formUrl,
    toFile: downloadDest,
    background: true,
    begin: res => {
      console.log('begin', res);
      console.log('contentLength:', res.contentLength / 1024 / 1024, 'M');
    },
    progress: res => {
      let pro = res.bytesWritten / res.contentLength;
      console.log('pro: ', pro);
    },
  };
  try {
    const ret = RNFS.downloadFile(options);
    ret.promise
      .then(res => {
        console.log('success', res);

        console.log('file://' + downloadDest);
      })
      .catch(err => {
        console.log('err', err);
      });
  } catch (e) {
    console.log(e);
  }
}

function App(): JSX.Element {
  const isDarkMode = useColorScheme() === 'dark';
  // Once the server is ready, the origin will be set and opened by WebView.
  const [origin, setOrigin] = useState<string>('');

  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
  };

  useEffect(() => {
   //获取对应的静态资源目录
    const fileDir: string = Platform.select({
      android: `${RNFS.DocumentDirectoryPath}/webroot`,
      ios: `${RNFS.MainBundlePath}/webroot`,
      windows: `${RNFS.MainBundlePath}\\webroot`,
      default: '',
    });

    // In our example, `server` is reset to null when the component is unmount,
    // thus signalling that server init sequence below should be aborted, if it
    // is still underway.
    let server: null | Server = new Server({ fileDir, stopInBackground: true });

    (async () => {
      // On Android we should extract web server assets from the application
      // package, and in many cases it is enough to do it only on the first app
      // installation and subsequent updates. In our example we'll compare
      // the content of "version" asset file with its extracted version,
      // if it exist, to deside whether we need to re-extract these assets.
      if (Platform.OS === 'android') {
        let extract = true;
        try {
          const versionD = await RNFS.readFile(`${fileDir}/version`, 'utf8');
          const versionA = await RNFS.readFileAssets('webroot/version', 'utf8');
          if (versionA === versionD) {
            extract = false;
          } else {
            await RNFS.unlink(fileDir);
          }
        } catch {
          // A legit error happens here if assets have not been extracted
          // before, no need to react on such error, just extract assets.
        }
        if (extract) {
          console.log('Extracting web server assets...');
          // await extractBundledAssets(fileDir, 'webroot');
        }
      }

      server?.addStateListener((newState) => {
        // Depending on your use case, you may want to use such callback
        // to implement a logic which prevents other pieces of your app from
        // sending any requests to the server when it is inactive.

        // Here `newState` equals to a numeric state constant,
        // and `STATES[newState]` equals to its human-readable name,
        // because `STATES` contains both forward and backward mapping
        // between state names and corresponding numeric values.
        console.log(`New server state is "${STATES[newState]}"`);
      });
      const res = await server?.start();

      if (res && server) {
        setOrigin(res);
      }
    })();
    return () => {
      (async () => {
        // In our example, here is no need to wait until the shutdown completes.
        server?.stop();

        server = null;
        setOrigin('');
      })();
    };
  }, []);

  const webView = useRef<WebView>(null);

  console.log(origin);

  return (
    <View style={styles.webview}>
      <View style={{ height: 50, display: "flex" }}>
        <Button title='下载文件' onPress={() => downloadFile(downloadDest, formUrl)}></Button>
        {
          origin && <Image
            style={styles.tinyLogo}
            source={{ uri: origin + "/demo.jpg" }}
          />
        }
      </View>
      <WebView
        style={{ flex: 1 }}
        cacheMode="LOAD_NO_CACHE"
        // This way we can receive messages sent by the WebView content.
        onMessage={(event) => {
          const message = event.nativeEvent.data;
          Alert.alert('Got a message from the WebView content', message);
        }}
        // This way selected links displayed inside this WebView can be opened
        // in a separate system browser, instead of the WebView itself.
        // BEWARE: Currently, it does not seem working on Windows,
        // the onShouldStartLoadWithRequest() method just is not triggered
        // there when links inside WebView are pressed. However, it is worth
        // to re-test, troubleshoot, and probably fix. It works fine both
        // Android and iOS.
        onShouldStartLoadWithRequest={(request) => {
          const load = request.url.startsWith(origin);
          if (!load) {
            Linking.openURL(request.url);
          }
          return load;
        }}
        ref={webView}
        source={{ uri: origin }}
      />
    </View >
  );
}

const styles = StyleSheet.create({
  webview: {
    borderColor: 'black',
    borderWidth: 1,
    flex: 1,
    marginTop: 12,
  },
  tinyLogo: {
    width: 50,
    height: 50,
  },
});

export default App;


ios

设置静态资源目录

打开Xcode,把静态目录asset 拖到Xcode指定位置,具体参考 ReactNative实现静态资源服务器加载本机资源

Demo

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 */

import React, {useEffect, useRef, useState} from 'react';
import type {PropsWithChildren} from 'react';
import {
  SafeAreaView,
  ScrollView,
  StatusBar,
  StyleSheet,
  Text,
  useColorScheme,
  View,
  Alert,
  Button,
  Linking,
  Platform,
  Image,
} from 'react-native';

import {Colors} from 'react-native/Libraries/NewAppScreen';

import RNFS from 'react-native-fs';

import Server from 'react-native-static-server';

import {WebView} from 'react-native-webview';

export const formUrl = 'https://static.runoob.com/images/demo/demo1.jpg';
// export const downloadDest = `${RNFS.DocumentDirectoryPath}/webroot/demo.jpg`;
export const downloadDest = `${RNFS.MainBundlePath}/webroot/demo.jpg`;

/*下载文件*/
export function downloadFile(downloadDest: string, formUrl: string) {
  // On Android, use "RNFS.DocumentDirectoryPath" (MainBundlePath is not defined)

  // 图片
  // const downloadDest = `${RNFS.MainBundlePath}/${((Math.random() * 1000) | 0)}.jpg`;
  // const formUrl = 'http://img.kaiyanapp.com/c7b46c492261a7c19fa880802afe93b3.png?imageMogr2/quality/60/format/jpg';

  // 文件
  // const downloadDest = `${RNFS.MainBundlePath}/${((Math.random() * 1000) | 0)}.zip`;
  // const formUrl = 'http://files.cnblogs.com/zhuqil/UIWebViewDemo.zip';

  // 视频
  // const downloadDest = `${RNFS.MainBundlePath}/${((Math.random() * 1000) | 0)}.mp4`;
  // http://gslb.miaopai.com/stream/SnY~bbkqbi2uLEBMXHxGqnNKqyiG9ub8.mp4?vend=miaopai&
  // https://gslb.miaopai.com/stream/BNaEYOL-tEwSrAiYBnPDR03dDlFavoWD.mp4?vend=miaopai&
  // const formUrl = 'https://gslb.miaopai.com/stream/9Q5ADAp2v5NHtQIeQT7t461VkNPxvC2T.mp4?vend=miaopai&';

  // http://wvoice.spriteapp.cn/voice/2015/0902/55e6fc6e4f7b9.mp3
  // const formUrl = 'http://wvoice.spriteapp.cn/voice/2015/0818/55d2248309b09.mp3';

  const options = {
    fromUrl: formUrl,
    toFile: downloadDest,
    background: true,
    begin: res => {
      console.log('begin', res);
      console.log('contentLength:', res.contentLength / 1024 / 1024, 'M');
    },
    progress: res => {
      let pro = res.bytesWritten / res.contentLength;
      console.log('pro: ', pro);
    },
  };
  try {
    const ret = RNFS.downloadFile(options);
    ret.promise
      .then(res => {
        console.log('success', res);

        console.log('file://' + downloadDest);
      })
      .catch(err => {
        console.log('err', err);
      });
  } catch (e) {
    console.log(e);
  }
}

function App(): JSX.Element {
  const isDarkMode = useColorScheme() === 'dark';
  // Once the server is ready, the origin will be set and opened by WebView.
  const [origin, setOrigin] = useState<string>('');

  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
  };

  useEffect(() => {
    const fileDir: string = Platform.select({
      android: `${RNFS.DocumentDirectoryPath}/webroot`,
      ios: `${RNFS.MainBundlePath}/webroot`,
      windows: `${RNFS.MainBundlePath}\\webroot`,
      default: '',
    });

    // In our example, `server` is reset to null when the component is unmount,
    // thus signalling that server init sequence below should be aborted, if it
    // is still underway.
    let server: null | Server = new Server(0, fileDir, {
      localOnly: true,
      keepAlive: true,
    });
    console.log('fileDir: ', fileDir);

    (async () => {
      // On Android we should extract web server assets from the application
      // package, and in many cases it is enough to do it only on the first app
      // installation and subsequent updates. In our example we'll compare
      // the content of "version" asset file with its extracted version,
      // if it exist, to deside whether we need to re-extract these assets.
      if (Platform.OS === 'android') {
        let extract = true;
        try {
          const versionD = await RNFS.readFile(`${fileDir}/version`, 'utf8');
          const versionA = await RNFS.readFileAssets('webroot/version', 'utf8');
          if (versionA === versionD) {
            extract = false;
          } else {
            await RNFS.unlink(fileDir);
          }
        } catch {
          // A legit error happens here if assets have not been extracted
          // before, no need to react on such error, just extract assets.
        }
        if (extract) {
          console.log('Extracting web server assets...');
          // await extractBundledAssets(fileDir, 'webroot');
        }
      }

      const res = await server?.start();
      console.log('res: ', res);

      if (res && server) {
        console.log('re2rs: ', res);
        setOrigin(res);
      }
    })();
    return () => {
      (async () => {
        // In our example, here is no need to wait until the shutdown completes.
        server?.stop();

        server = null;
        setOrigin('');
      })();
    };
  }, []);

  const webView = useRef<WebView>(null);

  return (
    <View style={styles.webview}>
      <View style={{height: 50, display: 'flex'}}>
        <Button
          title="下载文件"
          onPress={() => downloadFile(downloadDest, formUrl)}></Button>
        {origin && (
          <Image style={styles.tinyLogo} source={{uri: origin + '/demo.jpg'}} />
        )}
      </View>
      <WebView
        style={{flex: 1}}
        originWhitelist={['*']}
        cacheMode="LOAD_NO_CACHE"
        // This way we can receive messages sent by the WebView content.
        onMessage={event => {
          const message = event.nativeEvent.data;
          Alert.alert('Got a message from the WebView content', message);
        }}
        // This way selected links displayed inside this WebView can be opened
        // in a separate system browser, instead of the WebView itself.
        // BEWARE: Currently, it does not seem working on Windows,
        // the onShouldStartLoadWithRequest() method just is not triggered
        // there when links inside WebView are pressed. However, it is worth
        // to re-test, troubleshoot, and probably fix. It works fine both
        // Android and iOS.
        onShouldStartLoadWithRequest={request => {
          // const load = request.url.startsWith(origin);

          // console.log('request.navigationType: ', request.navigationType);
          // console.log('request.url: ', request.url);

          // if (!load) {
          //   Linking.openURL(request.url);
          // }
          // return true;
          //这里和安卓不同,为了防止ios打开新网页
          const isExternalLink =
            Platform.OS === 'ios' ? request.navigationType === 'click' : true;
          console.log('isExternalLink: ', isExternalLink);
          if (request.url.slice(0, 4) === 'http' && isExternalLink) {
            Linking.canOpenURL(request.url).then(supported => {
              console.log('supported: ', supported);
              if (supported) {
                Linking.openURL(request.url);
              }
            });
            return false;
          }
          return true;
        }}
        useWebView2
        ref={webView}
        // source={!!origin ? {uri: origin} : undefined}
        source={{
          uri: 'https://hc3d.histron.cn/share.html?k=fa2b5d05-24fa-4f81-979c-4f6d90f07294',
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  webview: {
    borderColor: 'black',
    borderWidth: 1,
    flex: 1,
    marginTop: 12,
  },
  tinyLogo: {
    width: 50,
    height: 50,
  },
});

export default App;

项目地址

github ios和android 实现还没整合,目前

  • master 为安卓demo
  • dev 分支为ios demo

环境信息

Ubuntu

System:
    OS: Linux 5.15 Ubuntu 20.04.3 LTS (Focal Fossa)
    CPU: (12) x64 Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz
    Memory: 9.79 GB / 15.58 GB
    Shell: 5.0.17 - /bin/bash
  Binaries:
    Node: 18.12.1 - ~/.nvm/versions/node/v18.12.1/bin/node
    Yarn: 1.22.19 - /mnt/d/Program Files/nodejs/yarn
    npm: 8.19.2 - ~/.nvm/versions/node/v18.12.1/bin/npm
    Watchman: 20230222.123454.0 - /usr/local/bin/watchman
  SDKs:
    Android SDK: Not Found
  IDEs:
    Android Studio: AI-221.6008.13.2211.9619390
  Languages:
    Java: 11.0.18 - /usr/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 18.2.0 => 18.2.0 
    react-native: 0.71.4 => 0.71.4 
  npmGlobalPackages:
    *react-native*: Not Found

ios

System:
    OS: macOS 13.2.1
    CPU: (8) arm64 Apple M1
    Memory: 117.09 MB / 8.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 18.15.0 - ~/.nvm/versions/node/v18.15.0/bin/node
    Yarn: 1.22.10 - /usr/local/bin/yarn
    npm: 9.5.0 - ~/.nvm/versions/node/v18.15.0/bin/npm
    Watchman: 2023.03.13.00 - /opt/homebrew/bin/watchman
  Managers:
    CocoaPods: 1.12.0 - /usr/local/bin/pod
  SDKs:
    iOS SDK:
      Platforms: DriverKit 22.2, iOS 16.2, macOS 13.1, tvOS 16.1, watchOS 9.1
    Android SDK: Not Found
  IDEs:
    Android Studio: Not Found
    Xcode: 14.2/14C18 - /usr/bin/xcodebuild
  Languages:
    Java: Not Found
  npmPackages:
    @react-native-community/cli: Not Found
    react: 18.2.0 => 18.2.0 
    react-native: 0.71.4 => 0.71.4 
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found

总结

本教程主要实现了在android和ios系统上打开静态资源服务器,请求本地资源以及获取远程资源到静态目录功能, 仅为功能可行性参考,实际开发可能有出入。 最后,祝大家生活愉快~

转载自:https://juejin.cn/post/7212603037666705469
评论
请登录