前端实现文件预览img、docx、xlsx、ppt、pdf、md、txt、audio、video
大厂技术高级前端Node进阶点击上方程序员成长指北,关注公众号回复1,加入高级Node交流群前言❝最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,答案在下面 ????:❞「具体的预览需求:」预览需要支持的文件类型有:png、jpg、jpeg、docx、xlsx、ppt、pdf、md、...
大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
前言
❝最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,答案在下面 👇:
❞
「具体的预览需求:」预览需要支持的文件类型有:png、jpg、jpeg、docx、xlsx、ppt、pdf、md、txt、audio、video
,另外对于不同文档还需要有定位的功能。例如:pdf
定位到页码,txt
和markdown
定位到文字并滚动到指定的位置,音视频定位到具体的时间等等。
「⚠️ 补充:我的需求是需要先将文件上传到后台,所以我拿到的是url
地址去展示,对于markdown
和txt
的文件需要先用fetch
获取,其他的展示则直接使用url
链接就可以。」
不同文件的实现方式不同,下面分类讲解,总共分为以下几类:
自有标签文件:
png、jpg、jpeg、audio、video
纯文字的文件:
markdown & txt
office
类型的文件:docx、xlsx、ppt
embed
引入文件:pdf
iframe
:引入外部完整的网站
自有标签文件:png、jpg、jpeg、audio、video
❝对于图片、音视频的预览,直接使用对应的标签即可,如下:
❞
图片:png、jpg、jpeg
「示例代码:」
<img src={url} key={docId} alt={name} width="100%" />;
「预览效果如下:」
![35bda558f4c59edb1ba43a6e3132a098.png](https://img-blog.csdnimg.cn/img_convert/35bda558f4c59edb1ba43a6e3132a098.png)
音频:audio
「示例代码:」
<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>
「预览效果如下:」
![a63282de8cd5ddb741e68537aa560a60.png](https://img-blog.csdnimg.cn/img_convert/a63282de8cd5ddb741e68537aa560a60.png)
视频:video
「示例代码:」
<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>
「预览效果如下:」
![4d082edf982c7bf1e9985610d091711b.png](https://img-blog.csdnimg.cn/img_convert/4d082edf982c7bf1e9985610d091711b.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<HTMLVideoElement>(null);
const audioRef = useRef<HTMLAudioElement>(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 (
<div>
{type === 'audio' ? (
<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>
) : (
<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>
)}
</div>
);
}
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 `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};
「代码高亮的代码实现:」
❝需要借助
❞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 `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
}
},
});
「链接跳转新tab
页的代码实现:」
marked.use({
renderer: {
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});
「滚动到高亮的位置的代码实现:」
❝需要配合上面的代码高亮的方法
❞
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
「完整的代码如下:」
❝入参的
❞docUrl
是markdown
文件的线上ur
l地址,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 `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};
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 `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
},
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});
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;
「预览效果如下:」
![7c65dcf913751581ab1e8d0091eb827c.png](https://img-blog.csdnimg.cn/img_convert/7c65dcf913751581ab1e8d0091eb827c.png)
txt
文件预览展示
❝支持高亮和滚动到指定位置
❞
「支持高亮的代码:」
function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `<span style="color: red">$1</span>`);
}
「完整代码:」
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, `<span style="color: red">$1</span>`);
}
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;
「预览效果如下:」
![63bcf9b6fff5cc5664ac9592ba617da5.png](https://img-blog.csdnimg.cn/img_convert/63bcf9b6fff5cc5664ac9592ba617da5.png)
office
类型的文件:docx、xlsx、ppt
❝❞
docx、xlsx、ppt
文件的预览,用的是office
的线上预览链接 + 我们文件的线上url
即可。
❝关于定位:用这种方法我暂时尝试是无法定位页码的,所以定位的功能我采取的是后端将
❞office
文件转成
「示例代码:」
<iframe
src={`https://view.officeapps.live.com/op/view.aspx?src=${url}`}
width="100%"
height="500px"
frameBorder="0"
></iframe>
「预览效果如下:」
![018c38692878c5f866ae7e9537813988.png](https://img-blog.csdnimg.cn/img_convert/018c38692878c5f866ae7e9537813988.png)
embed
引入文件:pdf
❝在
❞embed
的方式,这个httpsUrl
就是你的
「示例代码:」
<embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;
关于定位,其实是地址上拼接的页码sourcePage
,如下:
const httpsUrl = sourcePage
? `${doc.url}#page=${sourcePage}`
: doc.url;
<embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;
「预览效果如下:」
![4a8aa0ed7c1c60eae1551cecf4bca5cb.png](https://img-blog.csdnimg.cn/img_convert/4a8aa0ed7c1c60eae1551cecf4bca5cb.png)
iframe
:引入外部完整的网站
❝除了上面的各种文件,我们还需要预览一些外部的网址,那就要用到
❞iframe
的方式
「示例代码:」
<iframe
title="网址"
width="100%"
height="100%"
src={doc.url}
allow="microphone;camera;midi;encrypted-media;"/>
「预览效果如下:」
![115ba316b437c1310a97f29f66699ac1.png](https://img-blog.csdnimg.cn/img_convert/115ba316b437c1310a97f29f66699ac1.png)
总结:到这里我们支持的所有文件都讲述完了,有什么问题,欢迎评论区留言!
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一下
更多推荐
所有评论(0)