反应本机(世博会)加载降价文件

react native (expo) load markdown files

我在将降价文件 (.md) 加载到我的 React Native(非分离的 expo 项目)中时遇到了一些问题。

找到了这个很棒的包,可以让我渲染它。但无法弄清楚如何将本地 .md 文件作为字符串加载。

import react from 'react';
import {PureComponent} from 'react-native';
import Markdown from 'react-native-markdown-renderer';

const copy = `# h1 Heading 8-)

| Option | Description |
| ------ | ----------- |
| data   | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext    | extension to be used for dest files. |
`;

export default class Page extends PureComponent {

  static propTypes = {};
  static defaultProps = {};

  render() {
    return (
        <Markdown>{copy}</Markdown>
    );
  }
}

顺便说一句:我试过谷歌搜索,但无法获得有用的建议

https://forums.expo.io/t/loading-non-media-assets-markdown/522/2?u=norfeldtconsulting

我在 SO 上尝试了 reactjs 的建议答案,但问题似乎是它只接受 .js.json 文件

据我所知,这不能在世博会内完成。我在我的手机上使用 react-native 和 运行 它进行开发。

react-native 使用 Metro 作为默认的打包器,它也有类似的问题。您必须改用 haul 捆绑器。

npm install --save-dev haul

npx haul init

npx haul start --platform android

在单独的终端中 运行 react-native run-android。这将使用 haul 而不是 metro 来捆绑文件。

要添加 markdown 文件,请安装 raw-loader 并编辑 haul.config.js 文件。 raw-loader 将任何文件作为字符串导入。

将您的 haul.config.js 自定义为如下所示:

import { createWebpackConfig } from "haul";
export default {
 webpack: env => {
  const config = createWebpackConfig({
    entry: './index.js',
  })(env);
  config.module.rules.push({
      test: /\.md$/,
      use: 'raw-loader'
   })
  return config;
 }
};

现在可以使用const example = require('./example.md')

导入markdown文件

Haul 支持 webpack 配置,因此您可以添加任何您想要的自定义 babel 转换。

我不知道具体问题出在哪里,但是我在项目中添加了 html 个文件,我想它会非常相似。

在您的 app.json 中,尝试添加这些字段:

"assetBundlePatterns": [
  "assets/**",
],
"packagerOpts": {
  "assetExts": ["md"]
},

packagerOpts 使得独立版将捆绑 .md 文件。我想你已经有一个资产文件夹,但为了以防万一你没有,你将需要一个。

然后,在 AppLoading 上,可能不需要使用 Asset.loadAsync 加载资产,但最好排除这种情况。查看 documentation 如何使用它。

导入文件时,您可能需要三种方式,具体取决于环境。我将从我的 Medium article:

中复制这段摘录

In the simulator, you can access any file in the project. Thus, source={require(./pathToFile.html)} works. However, when you build a standalone, it doesn’t work quite in the same way. I mean, at least for android it doesn’t. The android webView doesn’t recognise asset:/// uris for some reason. You have to get the file:/// path. Thankfully, that is very easy. The assets are bundled inside file:///android_asset (Careful, don’t write assets), and Expo.Asset.fromModule(require(‘./pathToFile.html')).localUri returns asset:///nameOfFile.html. But that’s not all. For the first few times, this uri will be correct. However, after a while, it changes into another file scheme, and can’t be accessed in the same way. Instead, you’ll have to access the localUri directly. Thus, the complete solution is:

/* Outside of return */
const { localUri } = Expo.Asset.fromModule(require('./pathToFile.html'));
/* On the webView */
source={
  Platform.OS === ‘android’
  ? {
    uri: localUri.includes('ExponentAsset')
      ? localUri
      : ‘file:///android_asset/’ + localUri.substr(9),
  }
  : require(‘./pathToFile.html’)
}

(A constant part of the uri is ExponentAsset, that’s why I chose to check if that was part of it)

这应该可以解决您的问题。如果没有,请评论出了什么问题,我会尽力帮助您。干杯!

感谢@Filipe 的回复,我得到了一些指导并得到了一个适合您需要的工作示例。

就我而言,我在 assets/markdown/ 文件夹中有一个 .md 文件,该文件名为 test-1.md

诀窍是获取文件的本地 url,然后使用 fetch API 获取其内容作为 string.

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Markdown from 'react-native-markdown-renderer';
const copy = `# h1 Heading 8-)

| Option | Description |
| ------ | ----------- |
| data   | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext    | extension to be used for dest files. |
`;

export default class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      copy: copy
    }
  }

  componentDidMount() {
    this.fetchLocalFile();
  }

  fetchLocalFile = async () => {
    let file = Expo.Asset.fromModule(require("./assets/markdown/test-1.md"))
    await file.downloadAsync() // Optional, saves file into cache
    file = await fetch(file.uri)
    file = await file.text()

    this.setState({copy: file});
  }


  render() {
    return (
        <Markdown>{this.state.copy}</Markdown>
    );
  }
}

编辑:为了消除错误

Unable to resolve "./assets/markdown/test-1.md" from "App.js"

您需要将@Filipe 代码段的 packagerOpts 部分添加到您的 app.json 文件中。

app.json

{
  "expo": {
    ...
    "assetBundlePatterns": [
      "**/*"
    ],
    "packagerOpts": {
      "assetExts": ["md"]
    },
    ...
  }
}

编辑 2: 回答@Norfeldt 的评论: 虽然我在做自己的项目时使用 react-native init,因此我对 Expo 不是很熟悉,但我得到了这个 Expo Snack,它可能会为您提供一些答案:https://snack.expo.io/Hk8Ghxoqm.

由于读取非JSON 文件的问题,它不会在 expo snack 上运行,但如果您愿意,可以在本地测试它。

使用 file.downloadAsync() 将阻止该应用程序对在该应用程序会话中托管您的文件的服务器进行 XHR 调用(只要用户不关闭并重新打开该应用程序)。

如果您更改文件或修改文件(通过调用 Expo.FileSystem.writeAsStringAsync() 模拟),只要您的组件重新呈现并重新下载文件,它就应该显示更新。

每次您的应用程序关闭并重新打开时都会发生这种情况,因为就我而言 file.localUri 不会在每个会话中持续存在,因此您的应用程序将始终调用 file.downloadAsync()每次打开至少一次。所以显示更新后的文件应该没有问题。

我还花了一些时间来测试使用 fetch 与使用 Expo.FileSystem.readAsStringAsync() 的速度,它们的平均速度相同。通常 Expo.FileSystem.readAsStringAsync 快了约 200 毫秒,但在我看来这不是一个破坏交易的因素。

我创建了三种不同的方法来获取同一个文件。

export default class MarkdownRenderer extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      copy: ""
    }
  }

  componentDidMount() {
    this.fetch()
  }

  fetch = () => {
    if (this.state.copy) {
      // Clear current state, then refetch data
      this.setState({copy: ""}, this.fetch)
      return;
    }
    let asset = Expo.Asset.fromModule(md)
    const id = Math.floor(Math.random()  * 100) % 40;
    console.log(`[${id}] Started fetching data`, asset.localUri)
    let start = new Date(), end;

    const save = (res) => {
      this.setState({copy: res})
      let end = new Date();
      console.info(`[${id}] Completed fetching data in ${(end - start) / 1000} seconds`)
    }

    // Using Expo.FileSystem.readAsStringAsync.
    // Makes it a single asynchronous call, but must always use localUri
    // Therefore, downloadAsync is required
    let method1 = () => {
      if (!asset.localUri) {
        asset.downloadAsync().then(()=>{
          Expo.FileSystem.readAsStringAsync(asset.localUri).then(save)
        })
      } else {
        Expo.FileSystem.readAsStringAsync(asset.localUri).then(save)
      }
    }

    // Use fetch ensuring the usage of a localUri
    let method2 = () => {
      if (!asset.localUri) {
        asset.downloadAsync().then(()=>{
          fetch(asset.localUri).then(res => res.text()).then(save)
        })
      } else {
        fetch(asset.localUri).then(res => res.text()).then(save)
      }
    }

    // Use fetch but using `asset.uri` (not the local file)
    let method3 = () => {
      fetch(asset.uri).then(res => res.text()).then(save)
    }

    // method1()
    // method2()
    method3()
  }

  changeText = () => {
    let asset = Expo.Asset.fromModule(md)
    Expo.FileSystem.writeAsStringAsync(asset.localUri, "Hello World");
  }

  render() {
    return (
        <ScrollView style={{maxHeight: "90%"}}>
          <Button onPress={this.fetch} title="Refetch"/>
          <Button onPress={this.changeText} title="Change Text"/>
            <Markdown>{this.state.copy}</Markdown>
        </ScrollView>
    );
  }
}

只需在三者之间交替查看日志中的差异。

如果你想用 react-native cli(没有 expo)加载 .md 文件。我有一个解决方案给你)

  1. https://github.com/feats/babel-plugin-inline-import 添加到您的项目
  2. 添加配置 .babelrc 文件,其中包含代码:
{
   "presets": ["module:metro-react-native-babel-preset"],
   "plugins": [
       [
           "inline-import",
           {
               "extensions": [".md", ".txt"]
           }
       ],
       [
           "module-resolver",
           {
               "root": ["./src"],
               "alias": {}
           }
       ]
   ]
}
  1. 添加到您的metro.config.js这样的代码
const metroDefault = require('metro-config/src/defaults/defaults.js');
...
  resolver: {
    sourceExts: metroDefault.sourceExts.concat(['md', 'txt']),
  }
....
  1. 重新加载您的应用程序