自定义渲染器
自定义渲染器允许你为特定的文件类型提供自定义的预览实现,并按内置渲染器同样的规范声明工具组、派发事件。
基本概念
一个自定义渲染器是一个对象,至少包含:
test:判断是否应该使用该渲染器render:返回 React 节点 / Vue 组件
可选地:
getToolbarGroups:按ToolbarGroup[]规范声明自定义工具组(命中时替代内置文件类型工具组)events:事件名白名单(仅用于 TS 与文档约定,运行时不拦截)
类型定义
ts
// React
interface CustomRenderer {
test: (file: PreviewFile) => boolean
render: (file: PreviewFile, ctx?: CustomRendererContext) => React.ReactNode
getToolbarGroups?: (file: PreviewFile, ctx: CustomRendererContext) => ToolbarGroup[]
events?: readonly string[]
}
interface CustomRendererContext {
emit: (name: string, payload?: unknown) => void
t: Translator
theme: 'dark' | 'light'
locale: Locale
}
interface CustomRendererEventPayload<T = unknown> {
name: string
payload?: T
file: PreviewFile
}Vue 端 render 返回 Component,其余字段同构。
基本示例
tsx
import { FilePreviewModal, CustomRenderer, PreviewFile } from '@eternalheart/react-file-preview'
const customRenderers: CustomRenderer[] = [
{
// 判断是否使用该渲染器
test: (file: PreviewFile) => {
return file.name.endsWith('.custom')
},
// 渲染预览内容
render: (file: PreviewFile) => {
return (
<div className="custom-preview">
<h2>自定义文件预览</h2>
<p>文件名: {file.name}</p>
</div>
)
}
}
]
function App() {
return (
<FilePreviewModal
isOpen={true}
onClose={() => {}}
files={files}
currentIndex={0}
customRenderers={customRenderers}
/>
)
}高级示例
自定义 JSON 查看器
tsx
import { useState, useEffect } from 'react'
function JsonViewer({ url }: { url: string }) {
const [content, setContent] = useState<string>('加载中...')
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(json => setContent(JSON.stringify(json, null, 2)))
.catch(err => setContent(`加载失败: ${err.message}`))
}, [url])
return (
<div className="w-full h-full p-8 overflow-auto">
<pre className="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm">
{content}
</pre>
</div>
)
}
const jsonRenderer: CustomRenderer = {
test: (file) => file.name.endsWith('.json'),
render: (file) => <JsonViewer url={file.url} />,
}自定义 CSV 表格查看器
tsx
import { useState, useEffect } from 'react'
function CsvViewer({ url }: { url: string }) {
const [rows, setRows] = useState<string[][]>([])
useEffect(() => {
fetch(url)
.then(res => res.text())
.then(text => {
const lines = text.split('\n')
const data = lines.map(line => line.split(','))
setRows(data)
})
}, [url])
return (
<div className="w-full h-full p-8 overflow-auto">
<table className="border-collapse border border-gray-300 bg-white">
<thead>
<tr className="bg-gray-100">
{rows[0]?.map((cell, i) => (
<th key={i} className="border border-gray-300 px-4 py-2 font-semibold">
{cell}
</th>
))}
</tr>
</thead>
<tbody>
{rows.slice(1).map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td key={j} className="border border-gray-300 px-4 py-2">
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
const csvRenderer: CustomRenderer = {
test: (file) => file.name.endsWith('.csv'),
render: (file) => <CsvViewer url={file.url} />,
}渲染器优先级
自定义渲染器的优先级高于内置渲染器。渲染器按照数组顺序进行测试,第一个匹配的渲染器将被使用。
tsx
const customRenderers = [
// 这个会先被测试
{ test: (file) => file.name.endsWith('.txt'), render: CustomTextRenderer },
// 如果上面的不匹配,才会测试这个
{ test: (file) => file.name.endsWith('.log'), render: LogRenderer }
]访问文件内容
所有文件都通过 file.url 访问,这个 URL 可能是:
- HTTP/HTTPS URL(远程文件)
- Blob URL(本地 File 对象)
- Data URL(Base64 编码)
读取文本内容
tsx
function TextViewer({ url }: { url: string }) {
const [content, setContent] = useState<string>('加载中...')
useEffect(() => {
fetch(url)
.then(res => res.text())
.then(setContent)
.catch(err => setContent(`加载失败: ${err.message}`))
}, [url])
return <div>{content}</div>
}读取 JSON 内容
tsx
function JsonViewer({ url }: { url: string }) {
const [data, setData] = useState<any>(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(err => console.error(err))
}, [url])
return <pre>{JSON.stringify(data, null, 2)}</pre>
}读取二进制内容
tsx
function BinaryViewer({ url }: { url: string }) {
const [buffer, setBuffer] = useState<ArrayBuffer | null>(null)
useEffect(() => {
fetch(url)
.then(res => res.arrayBuffer())
.then(setBuffer)
}, [url])
return <div>文件大小: {buffer?.byteLength} 字节</div>
}样式建议
建议为自定义渲染器添加适当的样式,以保持与内置渲染器的一致性:
css
.custom-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
overflow: auto;
}工具组与事件(v1.4+)
命中自定义渲染器后,除了渲染内容,还可以:
- 通过
getToolbarGroups声明自定义工具按钮,与内置 Image / Pdf 等工具栏同一视觉风格 - 通过
ctx.emit(name, payload)派发事件,宿主在FilePreviewContent/Modal/Embed上通过onCustomEvent(React)或@custom-event(Vue)统一接收
React 示例
tsx
import { FilePreviewModal } from '@eternalheart/react-file-preview'
import type {
CustomRenderer,
CustomRendererEventPayload,
} from '@eternalheart/react-file-preview'
import { Sparkles } from 'lucide-react'
const demoRenderers: CustomRenderer[] = [
{
test: (file) => file.name.endsWith('.demo'),
render: (file, ctx) => (
<div style={{ color: ctx?.theme === 'light' ? '#111' : '#fff', padding: 24 }}>
<p>file: {file.name}</p>
<p>locale: {ctx?.locale} · theme: {ctx?.theme}</p>
</div>
),
getToolbarGroups: (_file, ctx) => [
{
items: [
{
type: 'button',
icon: <Sparkles className="rfp-w-4 rfp-h-4" />,
tooltip: 'Say Hello',
action: () => ctx.emit('hello', { ok: true }),
},
],
},
],
events: ['hello'] as const,
},
]
function App() {
return (
<FilePreviewModal
files={files}
currentIndex={0}
isOpen={open}
onClose={() => setOpen(false)}
customRenderers={demoRenderers}
onCustomEvent={(e: CustomRendererEventPayload) => {
console.log(e.name, e.payload, e.file)
}}
/>
)
}Vue 示例
vue
<script setup lang="ts">
import { h, defineComponent } from 'vue'
import {
FilePreviewModal,
type CustomRenderer,
type CustomRendererContext,
type CustomRendererEventPayload,
} from '@eternalheart/vue-file-preview'
import { Sparkles } from 'lucide-vue-next'
const DemoRenderer = defineComponent({
props: {
file: { type: Object, required: true },
ctx: { type: Object as () => CustomRendererContext | undefined, default: undefined },
},
setup(props) {
return () => h('div', { style: { padding: '24px' } }, `file: ${props.file.name}`)
},
})
const demoRenderers: CustomRenderer[] = [
{
test: (file) => file.name.endsWith('.demo'),
render: () => DemoRenderer,
getToolbarGroups: (_file, ctx) => [
{
items: [
{
type: 'button',
icon: Sparkles,
tooltip: 'Say Hello',
action: () => ctx.emit('hello', { ok: true }),
},
],
},
],
events: ['hello'] as const,
},
]
function onCustomEvent(e: CustomRendererEventPayload) {
console.log(e.name, e.payload, e.file)
}
</script>
<template>
<FilePreviewModal
:files="files"
:current-index="0"
:is-open="open"
:custom-renderers="demoRenderers"
@close="open = false"
@custom-event="onCustomEvent"
/>
</template>工具组语义
- 自定义
getToolbarGroups返回结果会替代内置文件类型工具组;通用操作组(下载、关闭)保持不变 - 未声明
getToolbarGroups时工具栏仅保留通用操作组 headless === true时不渲染任何工具栏(包括自定义工具组)
事件语义
ctx.emit(name, payload)会通过顶层FilePreviewContent的onCustomEvent/custom-event出口向宿主转发- 载荷固定为
{ name, payload, file }(file为当前渲染的PreviewFile) - 宿主未绑定监听时静默忽略,不抛错
FilePreviewModal/FilePreviewEmbed会自动透传事件
向后兼容
- 旧版
render: (file) => <Comp />仍然可用;ctx是可选参数 - 不声明
getToolbarGroups/events/ 不绑定事件出口时,行为与旧版完全一致