注册
web

前端实现文件预览img、docx、xlsx、pptx、pdf、md、txt、audio、video

前言

最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇

具体的预览需求: 预览需要支持的文件类型有: png、jpg、jpeg、docx、xlsx、pptx、pdf、md、txt、audio、video,另外对于不同文档还需要有定位的功能。例如:pdf 定位到页码,txtmarkdown定位到文字并滚动到指定的位置,音视频定位到具体的时间等等。


⚠️ 补充: 我的需求是需要先将文件上传到后台,然后我拿到url地址去展示,对于markdowntxt的文件需要先用fetch获取,其他的展示则直接使用url链接就可以。

不同文件的实现方式不同,下面分类讲解,总共分为以下几类:

  1. 自有标签文件:png、jpg、jpeg、audio、video
  2. 纯文字的文件: markdown 、txt
  3. office 类型的文件: docx、xlsx、pptx
  4. embed 引入文件:pdf
  5. iframe:引入外部完整的网站,例如:https://www.baidu.com/

自有标签文件:png、jpg、jpeg、audio、video

对于图片、音视频的预览,直接使用对应的标签即可,如下:

图片:png、jpg、jpeg

示例代码:

 <img src={url} key={docId} alt={name} width="100%" />;

预览效果如下:

截屏2024-04-30 11.18.01.png

音频:audio

示例代码:


预览效果如下:

截屏2024-04-30 11.18.45.png

视频:video

示例代码:


预览效果如下:

截屏2024-05-13 18.21.13.png

关于音视频的定位的完整代码:

import React, { useRef, useEffect } from 'react';

interface IProps {
type: 'audio' | 'video';
url: string;
timeInSeconds: number;
}

function AudioAndVideo(props: IProps) {
const { type, url, timeInSeconds } = props;
const videoRef = useRef(null);
const audioRef = useRef(null);

useEffect(() => {
// 音视频定位
const secondsTime = timeInSeconds / 1000;
if (type === 'audio' && audioRef.current) {
audioRef.current.currentTime = secondsTime;
}
if (type === 'video' && videoRef.current) {
videoRef.current.currentTime = secondsTime;
}
}, [type, timeInSeconds]);

return (

{type === 'audio' ? (

) : (

)}

);
}

export default AudioAndVideo;

纯文字的文件: markdown & txt

对于markdown、txt类型的文件,如果拿到的是文件的url的话,则无法直接显示,需要请求到内容,再进行展示。

markdown 文件

在展示markdown文件时,需要满足字体高亮、代码高亮,如果有字体高亮,需要滚动到字体所在位置,如果有外部链接,需要新开tab页面再打开。

需要引入两个库:

marked:它的作用是将markdown文本转换(解析)为HTML

highlight: 它允许开发者在网页上高亮显示代码。

字体高亮的代码实现:

高亮的样式,可以在行间样式定义

  const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `id='first-match' style="color: red;">${match}`;
}
return `style="color: red;">${match}`;
});
};

代码高亮的代码实现:

需要借助hljs这个库进行转换

marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `
class="hljs ${infostring}">${highlighted}
`;
}
},
});

链接跳转新tab页的代码实现:

marked.use({
renderer: {
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}`;
}
return `href="${href}" title="${title}">${text}`;
},
},
});

滚动到高亮的位置的代码实现:

需要配合上面的代码高亮的方法

const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

完整的代码如下:

入参的docUrl 是markdown文件的线上url地址,searchText 是需要高亮的内容。

import React, { useEffect, useState, useRef } from 'react';
import { marked } from 'marked';
import hljs from 'highlight.js';

const preStyle = {
width: '100%',
maxHeight: '64vh',
minHeight: '64vh',
overflow: 'auto',
};

// Markdown展示组件
function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
const [markdown, setMarkdown] = useState('');
const markdownRef = useRef<HTMLDivElement | null>(null);

const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `${match}`;
}
return `${match}`;
});
};

useEffect(() => {
// 如果没有搜索内容,直接加载原始Markdown文本
fetch(docUrl)
.then((response) => response.text())
.then((text) => {
const highlightedText = searchText ? highlightAndMarkFirst(text, searchText) : text;
setMarkdown(highlightedText);
})
.catch((error) => console.error('加载Markdown文件失败:', error));
}, [searchText, docUrl]);

useEffect(() => {
if (markdownRef.current) {
// 支持代码高亮
marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `
${infostring}">${highlighted}
`;
},
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}`;
}
return `${href}" title="${title}">${text}`;
},
},
});
const htmlContent = marked.parse(markdown);
markdownRef.current!.innerHTML = htmlContent as string;
// 当markdown更新后,检查是否需要滚动到高亮位置
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [markdown]);

return (
<div style={preStyle}>
<div ref={markdownRef} />
div>
);
}

export default MarkdownViewer;

预览效果如下:

截屏2024-05-13 17.59.04.png

txt 文件预览展示

支持高亮和滚动到指定位置

支持文字高亮的代码:

  function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `style="color: red">$1`);
}

完整代码:

import React, { useEffect, useState, useRef } from 'react';
import { preStyle } from './config';

function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
const [paragraphs, setParagraphs] = useState<string[]>([]);
const targetRef = useRef<HTMLDivElement | null>(null);

function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `$1`);
}

useEffect(() => {
fetch(docurl)
.then((response) => response.text())
.then((text) => {
const highlightedText = highlightText(text);
const paras = highlightedText
.split('\n')
.map((para) => para.trim())
.filter((para) => para);
setParagraphs(paras);
})
.catch((error) => {
console.error('加载文本文件出错:', error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [docurl, searchText]);

useEffect(() => {
// 处理高亮段落的滚动逻辑
const timer = setTimeout(() => {
if (targetRef.current) {
targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);

return () => clearTimeout(timer);
}, [paragraphs]);

return (
<div style={preStyle}>
{paragraphs.map((para: string, index: number) => {
const paraKey = para + index;

// 确定这个段落是否包含高亮文本
const isTarget = para.includes(`>${searchText}<`);
return (
<p key={paraKey} ref={isTarget && !targetRef.current ? targetRef : null}>
<div dangerouslySetInnerHTML={{ __html: para }} />
p>
);
})}
div>
);
}

export default TextFileViewer;

预览效果如下:

截屏2024-05-13 18.34.27.png


office 类型的文件: docx、xlsx、pptx

docx、xlsx、pptx 文件的预览,用的是office的线上预览链接 + 我们文件的线上url即可。

关于定位:用这种方法我暂时尝试是无法定位页码的,所以定位的功能我采取的是后端将office 文件转成pdf,再进行定位,如果只是纯展示,忽略这个问题即可。

示例代码:


预览效果如下:

截屏2024-05-07 17.58.45.png


embed 引入文件:pdf

pdf文档预览时,可以采用embed的方式,这个httpsUrl就是你的pdf文档的链接地址

示例代码:

 src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;

关于定位,其实是地址上拼接的页码sourcePage,如下:

 const httpsUrl = sourcePage
? `${doc.url}#page=${sourcePage}`
: doc.url;

src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;

预览效果如下:

截屏2024-05-07 17.50.07.png


iframe:引入外部完整的网站

除了上面的各种文件,我们还需要预览一些外部的网址,那就要用到iframe的方式

示例代码:

 < iframe
title="网址"
width="100%"
height="100%"
src={doc.url}
allow="microphone;camera;midi;encrypted-media;"/>

预览效果如下:


6f8d7dbc3f9a40dea27a2803148ef488.jpg




课后附加题:



有些网站设置了X-Frame-Options不允许其他网站嵌入,X-Frame-Options 是一个 HTTP 响应头,用于控制浏览器是否允许一个页面在 <frame>、 <iframe>、 <embed>、 或 <object> 中被嵌入。



X-Frame-Options有以下三种配置:



  • DENY:完全禁止该页面被嵌入到任何框架中,无论嵌入页面的来源是什么。
  • SAMEORIGIN:允许同源的页面嵌入该页面。
  • ALLOW-FROM uri:允许指定的来源嵌入该页面。这个选项允许你指定一个 URI,只有来自该 URI 的页面可以嵌入当前页面。

但是无论是哪种配置,我们作为非同源的网站,都无法将其嵌入到页面中,且在前端也是拿不到这个报错的信息。


此时我们的解决方案是:



当文档为网址时,由后端服务去请求,检测响应头里是否携带X-Frame-Options字段,由后端将是否携带的信息返回前端,前端再根据是否可以嵌入进行页面的个性化展示。



预览效果如下:


15e29155fa8be64149878f59fa194303.jpg




总结: 到这里我们支持的所有文件都讲述完了,有什么问题,欢迎评论区留言!


作者:玖月晴空
链接:juejin.cn/post/7366432628440924170

0 个评论

要回复文章请先登录注册