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

前言

最近有个需要,需要把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 作者:ZoeLee

(0)
上一篇 2023年3月21日 下午4:11
下一篇 2023年3月21日 下午4:21

相关推荐

发表回复

登录后才能评论