React Native(Expo)加载Markdown文件

16

我在将Markdown文件(.md)加载到React Native(非detach 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文件。


1
请查看此 Stack Overflow 问题:https://dev59.com/0VgQ5IYBdhLWcg3wWCc0。该问题已经有答案了。 - Hemadri Dasari
可能是如何将Markdown文件加载到React组件中?的重复问题。 - Hemadri Dasari
1
我已经测试了这些答案,而且这个问题不是重复的,因为在React Native Expo中它似乎工作方式不同。 - Norfeldt
@Think-Twice,我已经更新了我的问题。 - Norfeldt
4个回答

17

多亏了@Filipe的回答,我得到了一些指导,并获得了一个符合您需求的工作示例。

在我的案例中,我有一个.md文件位于assets/markdown/文件夹中,该文件名为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>
    );
  }
}

编辑:为了消除错误

无法从"App.js"解析"./assets/markdown/test-1.md"

您需要将@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>
    );
  }
}

只需在这三者之间交替使用即可查看日志中的差异。


非常感谢!我显然没有技能来找出如何获取和提取其中的文本。file.downloadAsync()是不是意味着每次使用应用程序时都不会下载文件?如果我在部署后更改了Markdown的内容并进行OTA - 它会使用缓存的文件还是检测到已更改并缓存新文件? - Norfeldt
我已经尽力在我的更新答案中回答了你的问题。 - Luis Rizo
为了使其正常工作,您还需要修改您的metro.config.js文件:const defaultAssetExts = require("metro-config/src/defaults/defaults").assetExts; module.exports = { resolver: { //... assetExts: [ ...defaultAssetExts, "md" ], //... }; - Saad Zafar
如果有人在expo 40中看到了这个答案,它不会起作用-请查看下面的我的答案。 - Jono
对于更新的 Expo 版本,请查看以下链接 https://docs.expo.dev/guides/customizing-metro/,以允许在 metro.config.js 中使用 MD 扩展名。该文件必须静态导入,动态使用 require('file.md') 对我来说还没有起作用。 - Nour Wolf

2
据我所知,在Expo中无法完成此操作。我使用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转换。

非常感谢您抽出时间尝试回答我的问题。我以前从未听说过haul,看起来很有趣。我想避免从expo中弹出(我喜欢OTA功能),所以我会继续保持问题的开放,并且如果没有更好的答案,那么我将把您的答案标记为答案。谢谢! - Norfeldt
感谢您,我从未不得不编辑 haul.config.js 文件,现在我知道该怎么做了 :) - illiteratewriter
我很高兴你也从中获得了一些东西。再次感谢你的回答。 - Norfeldt

2
我不确定问题存在于哪里,但我将HTML文件添加到项目中,我想这应该非常相似。
在您的app.json文件内,尝试添加以下字段:
"assetBundlePatterns": [
  "assets/**",
],
"packagerOpts": {
  "assetExts": ["md"]
},
packagerOpts 可以使独立应用打包 .md 文件。我想你已经有了一个资产文件夹,但为了以防万一,你需要一个。
然后,在 AppLoading 中使用 Asset.loadAsync 加载资产可能不是必需的,但最好排除一下。请查看有关如何使用它的文档
在导入文件时,有三种方法可以根据环境进行更改。我将从我的Medium文章中复制此摘录:

在模拟器中,您可以访问项目中的任何文件。因此,source={require(./pathToFile.html)} 可行。然而,在构建独立应用时,它并不完全相同。我的意思是,至少对于 Android 来说不是这样的。Android 的 webView 由于某种原因无法���别 asset:/// uri。您必须获取 file:/// 路径。幸运的是,这很容易。资产被捆绑在 file:///android_asset 中(小心,不要写 assets),并且 Expo.Asset.fromModule(require(‘./pathToFile.html')).localUri 返回 asset:///nameOfFile.html。但这还不是全部。在前几次,此 uri 将是正确的。然而,一段时间后,它会变成另一个文件方案,并且无法以同样的方式访问。相反,您将必须直接访问 localUri。因此,完整的解决方案如下:

/* 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’)
}

(URI的一个固定部分是ExponentAsset,所以我选择检查它是否包含在内)这应该可以解决你的问题。如果不能,请评论说明出了什么问题,我会尽力帮助你。祝好!

我可能很蠢,但我不知道如何读取文件内容。我尝试使用的包没有源属性,但却希望我将其作为字符串放置。执行 console.log(Expo.Asset.fromModule(require('./pathToFile.html'))) 没有显示任何原始内容。 - Norfeldt
提取uri使我能够通过Linking.openURL(uri)在浏览器中“打开”文件。 - Norfeldt
downloadCallbacks: [] downloaded: false downloading: false hash: "43d299a4986521da6b28a00eb764b2a7" localUri: null name: "C05" type: "md" uri: "http://localhost:19001/assets/assets/markdown/C05.md?platform=ios&hash=43d299a4986521da6b28a00eb764b2a7" - Norfeldt
检查我的答案,它应该回答你的问题。 - Luis Rizo
是的,抱歉,我没有看一下您使用的库。@Luiz Rizo的回答应该可以解决这个问题。 - Filipe

2

如果你想在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']),
  }
....

重新加载您的应用程序。

这非常棒。已确认在使用rn-cli的非Expo应用程序中导入.md文件有效。感谢分享。 - jakeforaker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接