如何在 Expo Go 项目中显示 PDF?

问题描述 投票:0回答:1

我正在使用 Expo Go 开发一个 React Native 项目,我需要在应用程序中显示 PDF 文件。

我研究过使用像 react-native-pdfrn-pdf-reader-jsreact-native-file-viewer 这样的库,但它们似乎需要自定义本机代码,而 Expo Go 不需要不支持。

有没有办法在 Expo Go 中显示 PDF 文件而不弹出或使用本机模块?

理想情况下,我希望将 PDF 查看器保留在应用程序本身内(因此请不要使用外部浏览器或应用程序内浏览器)。如果有人有在 Expo Go 中有效的建议或替代方案,我将非常感激!

react-native pdf expo expo-go
1个回答
0
投票

一种方法是使用 WebView 来显示 PDF。

以下代码提供了一种通过 URL 或 base64 编码加载 PDF 的方法。该解决方案由

TypeScript
functional components
构建,样式和功能完全可定制。

它完全在 Expo Go 中工作,不需要任何自定义本机模块!

我使用了以下库(Expo Go 均支持):

显然:

  • 反应(
    version 18.2.0
    )
  • 反应原生 (
    version 0.74.5
    )
  • 博览会(
    version ~51.0.28
    )
import * as CSS from "csstype";
import * as FileSystem from "expo-file-system";
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
import { WebView } from "react-native-webview";
import {
  WebViewErrorEvent,
  WebViewHttpErrorEvent,
  WebViewNavigationEvent,
  WebViewSource,
} from "react-native-webview/lib/WebViewTypes";

const {
  cacheDirectory,
  writeAsStringAsync,
  deleteAsync,
  getInfoAsync,
  EncodingType,
} = FileSystem;

export type RenderType =
  | "DIRECT_URL"
  | "DIRECT_BASE64"
  | "BASE64_TO_LOCAL_PDF"
  | "URL_TO_BASE64"
  | "GOOGLE_READER"
  | "GOOGLE_DRIVE_VIEWER";

export interface CustomStyle {
  readerContainer?: CSS.Properties;
  readerContainerDocument?: CSS.Properties;
  readerContainerNumbers?: CSS.Properties;
  readerContainerNumbersContent?: CSS.Properties;
  readerContainerZoomContainer?: CSS.Properties;
  readerContainerZoomContainerButton?: CSS.Properties;
  readerContainerNavigate?: CSS.Properties;
  readerContainerNavigateArrow?: CSS.Properties;
}

export interface Source {
  uri?: string;
  base64?: string;
  headers?: { [key: string]: string };
}

export interface Props {
  source: Source;
  style?: View["props"]["style"];
  webviewStyle?: WebView["props"]["style"];
  webviewProps?: WebView["props"];
  noLoader?: boolean;
  customStyle?: CustomStyle;
  useGoogleDriveViewer?: boolean;
  useGoogleReader?: boolean;
  withScroll?: boolean;
  withPinchZoom?: boolean;
  maximumPinchZoomScale?: number;
  onLoad?: (event: WebViewNavigationEvent) => void;
  onLoadEnd?: (event: WebViewNavigationEvent | WebViewErrorEvent) => void;
  onError?: (event: WebViewErrorEvent | WebViewHttpErrorEvent | string) => void;
}

const originWhitelist = [
  "http://*",
  "https://*",
  "file://*",
  "data:*",
  "content:*",
];

const htmlPath = `${cacheDirectory}index.html`;
const pdfPath = `${cacheDirectory}file.pdf`;

async function writePDFAsync(base64: string) {
  await writeAsStringAsync(
    pdfPath,
    base64.replace("data:application/pdf;base64,", ""),
    { encoding: EncodingType.Base64 }
  );
}

export async function removeFilesAsync(): Promise<void> {
  const { exists: htmlPathExist } = await getInfoAsync(htmlPath);
  if (htmlPathExist) {
    await deleteAsync(htmlPath, { idempotent: true });
  }

  const { exists: pdfPathExist } = await getInfoAsync(pdfPath);
  if (pdfPathExist) {
    await deleteAsync(pdfPath, { idempotent: true });
  }
}

const getGoogleReaderUrl = (url: string) =>
  `https://docs.google.com/viewer?url=${url}`;
const getGoogleDriveUrl = (url: string) =>
  `https://drive.google.com/viewerng/viewer?embedded=true&url=${url}`;

const Loader = () => (
  <View
    style={{
      alignItems: "center",
      justifyContent: "center",
    }}
  >
    <ActivityIndicator size="large" />
  </View>
);

const validate = ({
  onError,
  renderType,
  source,
}: {
  onError: (event: WebViewErrorEvent | WebViewHttpErrorEvent | string) => void;
  renderType: RenderType;
  source: Source;
}) => {
  if (!renderType || !source) {
    onError("source is undefined");
  } else if (
    (renderType === "DIRECT_URL" ||
      renderType === "GOOGLE_READER" ||
      renderType === "GOOGLE_DRIVE_VIEWER" ||
      renderType === "URL_TO_BASE64") &&
    (!source.uri ||
      !(
        source.uri.startsWith("http") ||
        source.uri.startsWith("file") ||
        source.uri.startsWith("content")
      ))
  ) {
    onError(
      `source.uri is undefined or not started with http, file or content source.uri = ${source.uri}`
    );
  } else if (
    (renderType === "BASE64_TO_LOCAL_PDF" || renderType === "DIRECT_BASE64") &&
    (!source.base64 ||
      !source.base64.startsWith("data:application/pdf;base64,"))
  ) {
    onError(
      "Base64 is not correct (ie. start with data:application/pdf;base64,)"
    );
  }
};

const init = async ({
  renderType,
  setReady,
  source,
}: {
  renderType?: RenderType;
  setReady: Dispatch<SetStateAction<boolean>>;
  source: Source;
}) => {
  try {
    switch (renderType!) {
      case "GOOGLE_DRIVE_VIEWER": {
        break;
      }

      case "BASE64_TO_LOCAL_PDF": {
        await writePDFAsync(source.base64!);
        break;
      }

      default:
        break;
    }

    setReady(true);
  } catch (error) {
    console.error(error);
  }
};

const getRenderType = ({
  source,
  useGoogleDriveViewer,
  useGoogleReader,
}: {
  source: Source;
  useGoogleDriveViewer?: boolean;
  useGoogleReader?: boolean;
}) => {
  const { uri, base64 } = source;

  if (useGoogleReader) {
    return "GOOGLE_READER";
  }

  if (useGoogleDriveViewer) {
    return "GOOGLE_DRIVE_VIEWER";
  }

  if (Platform.OS === "ios") {
    if (uri !== undefined) {
      return "DIRECT_URL";
    }
    if (base64 !== undefined) {
      return "BASE64_TO_LOCAL_PDF";
    }
  }

  if (base64 !== undefined) {
    return "DIRECT_BASE64";
  }

  if (uri !== undefined) {
    return "URL_TO_BASE64";
  }

  return undefined;
};

const getWebviewSource = ({
  source,
  renderType,
  onError,
}: {
  source: Source;
  renderType?: RenderType;
  onError: (event: WebViewErrorEvent | WebViewHttpErrorEvent | string) => void;
}): WebViewSource | undefined => {
  const { uri, headers } = source;

  switch (renderType!) {
    case "GOOGLE_READER":
      return { uri: getGoogleReaderUrl(uri!) };
    case "GOOGLE_DRIVE_VIEWER":
      return { uri: getGoogleDriveUrl(uri || "") };
    case "DIRECT_BASE64":
    case "URL_TO_BASE64":
      return { uri: htmlPath };
    case "DIRECT_URL":
      return { headers, uri: uri! };
    case "BASE64_TO_LOCAL_PDF":
      return { uri: pdfPath };
    default: {
      onError!("Unknown RenderType");
      return undefined;
    }
  }
};

const PdfViewer = ({
  source,
  style,
  webviewStyle,
  webviewProps,
  noLoader = false,
  useGoogleDriveViewer,
  useGoogleReader,
  onLoad,
  onLoadEnd,
  onError = console.error,
}: Props) => {
  const [ready, setReady] = useState<boolean>(false);
  const [renderType, setRenderType] = useState<RenderType | undefined>(
    undefined
  );
  const [renderedOnce, setRenderedOnce] = useState<boolean>(false);

  useEffect(() => {
    if (renderType) {
      console.debug(renderType);
      validate({ onError, renderType, source });
      init({ renderType, setReady, source });
    }

    return () => {
      if (
        renderType === "DIRECT_BASE64" ||
        renderType === "URL_TO_BASE64" ||
        renderType === "BASE64_TO_LOCAL_PDF"
      ) {
        removeFilesAsync();
      }
    };
  }, [renderType]);

  useEffect(() => {
    if (source.uri || source.base64) {
      setReady(false);
      setRenderType(
        getRenderType({ source, useGoogleDriveViewer, useGoogleReader })
      );
    }
  }, [source.uri, source.base64]);

  const sourceToUse = useMemo(() => {
    if (!!onError && renderType && source) {
      return getWebviewSource({ onError, renderType, source });
    }
    return undefined;
  }, [getWebviewSource, onError, renderType, source]);

  const isAndroid = useMemo(() => Platform.OS === "android", [Platform]);

  return ready ? (
    <View style={[styles.container, style]}>
      <WebView
        {...{
          onError,
          onHttpError: onError,
          onLoad: (event) => {
            setRenderedOnce(true);
            if (onLoad) {
              onLoad(event);
            }
          },
          onLoadEnd,
          originWhitelist,
          source: renderedOnce || !isAndroid ? sourceToUse : undefined,
          style: [styles.webview, webviewStyle],
        }}
        allowFileAccess={isAndroid}
        allowFileAccessFromFileURLs={isAndroid}
        allowUniversalAccessFromFileURLs={isAndroid}
        scalesPageToFit={Platform.select({ android: false })}
        mixedContentMode={isAndroid ? "always" : undefined}
        sharedCookiesEnabled={false}
        startInLoadingState={!noLoader}
        renderLoading={() => (noLoader ? <View /> : <Loader />)}
        {...webviewProps}
      />
    </View>
  ) : (
    <View style={styles.loaderContainer}>{!noLoader && <Loader />}</View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  loaderContainer: {
    alignItems: "center",
    flex: 1,
    justifyContent: "center",
  },
  webview: {
    flex: 1,
  },
});

export default PdfViewer;

  • 渲染类型:此代码支持多种显示 PDF 的方式,例如直接 URL、base64 和 Google Drive Viewer。
  • PDF 存储和清理: writePDFAsync 函数将 PDF 写入 缓存以在查看器中使用,removeFilesAsync 处理清理。
  • 验证和初始化:validate检查源有效性,init根据renderType初始化渲染。
  • WebView设置:WebView用于显示基于PDF的内容 选择渲染类型,Loader 在加载时提供反馈。
© www.soinside.com 2019 - 2024. All rights reserved.