背景首先是之前使用 js-pdf + html2canvs库去生成PDF,但是我发现这样做是有弊端的,它是根据当前的DOM结构,生成一个快照。所以里面的一些具体细节,比如 A 标签,是没法生成的。所以最终选择了使用react-pdf/render。 它是使用它提供的组件去描述PDF内容,而不是生成快照。
因为需要引入字体,引入字体过后,这个字体差不多在20M,放在浏览器上引入会有顾虑,因为我们框架使用的是NextJS,所以我们考虑使用NextJS封装一层请求,放在node端去生成Pdf。
1.安装对应字体
因为react-pdf其实对中文支持不友好,除了英文不需要特殊配置,其它的例如中文,韩语,日语都需要配置对应字体。
- 首先我们可以去Google字体库找到对应的字体,点击进入详情。
- 点击Get font -> Get embbed code, 左边的One value是设置字体的font-weight,右边link的href是我们需要使用到的。
- 因为react-pdf目前只支持ttf文件,所以我们打开上面的link,其实发现全是woff2格式的文件。不过我们可以使用curl去请求这个link,获取ttf文件,我也不知道为什么通过curl,google就会返回ttf文件格式的link。
2.配置字体
此处的src我目前只发现使用有效的link才能生效,例如我上面所获取的中文的ttf字体文件link,如果下载到前端本地,通过ESM形式引入ttf文件,我发现是不生效的。
import { Font, StyleSheet} from '@react-pdf/renderer';
Font.register({
family: 'NotoSans',
src: 'xxxxx.ttf',
});
const styles = StyleSheet.create({
page: {
fontFamily: 'NotoSans',
Ï },
});
export function Demo(){
return (
<Document>
<Page style={[styles.page]}>
<Text>你好</Text>
</Page>
</Document>
);
}
3.多语言支持
我相信很多朋友有项目多语言的需求,可能是中文,韩文,英文等等, 英文我们不需要引入特殊字体。
- 如果说Dom结构是固定的,你知道哪个标签使用中文使用韩文, 还是像上面一样继续使用Font.register注册就好,标签中的style使用不同的family。
import { Font, StyleSheet} from '@react-pdf/renderer';
Font.register({
family: 'NotoSans',
src: 'xxxxx.ttf',
});
Font.register({
family: 'NotoSans_kr',
src: 'xxxxx.ttf',
});
const styles = StyleSheet.create({
text1: {
fontFamily: 'NotoSans',
},
text2: {
fontFamily: 'NotoSans_kr',
},
});
export function Demo(){
return (
<Document>
<Page style={[styles.page]}>
<Text style={styles.text1}>你好</Text>
<Text style={styles.text2}>주문 관리</Text>
</Page>
</Document>
);
}
3.1 动态显示多语言
如果说像我这种,数据是从后台获取的,是动态的。无法确定文字到底是哪个语言。就不好办了。 我尝试在Page根节点添加样式,添加多个fontFamily,发现它无法识别。所以我就只能自己去把china和korea的字体的ttf文件合并成一个ttf文件了。
- 使用工具fontforge,大家就按照官网正常下载就好。
- 按照上面的流程,下载china和korea的ttf文件。
- 这个App打开是这样,文件 -> 打开, 例如先打开China的ttf文件,打开成功后,再点击元素 ->合并字体,选择Korea的ttf文件。中间有什么提示点击 “是”就ok了。
- 最终生成了新的文件,选择TrueType格式,然后就会生成一个新的ttf文件,然后你再使用这个ttf文件,你会发现中文韩文都支持了。
4. 中文换行
上面说过对中文支持很不友好,所以大家使用过程肯定会发现如果使用中文,当文字超过当前屏幕,中文不会进行换行。好在react-pdf其实提供了换行的逻辑回调,代码放在下面。
Font.registerHyphenationCallback((word) => {
if (word.length === 1) {
return [word];
}
return Array.from(word)
.map((char) => [char, ''])
.reduce((arr, current) => {
arr.push(...current);
return arr;
}, []);
});
5.元素当前页显示不下,自动换行到第二页,而不是当前页显示一半,第二页显示一半。
我的pdf有一个table,这个table很长,超过了第一页。 react-pdf会自动帮我们放在第二页, 但是渲染cell单元格的时候,第一页渲染了一点文字,第二页渲染了一点文字。很不友好。
官方也提供了wrap这个属性,解决这个问题,这个wrap属性可以传给View,Text等等,除了Image组件,其它组件默认都是true。 为true就代表这个节点下面的元素可以破碎,当前页显示不下,就在当前页显示部分,下一页显示部分。 如果为false,就代表如果当前页显示不下完整结构,就自动放到下一页显示。
6. react-pdf如何识别后端返回的
<View style={{ width: '30%', borderRightWidth: 1, padding: '0px 1px' }}>
<Text>{item.productName.replace(/<br\/>/g, '\n')}</Text>
</View>
7.NextJS封装请求返回PDF
- NextJS使用需要在next.cofig.js配置
experimental: {
serverComponentsExternalPackages: ['@react-pdf/renderer'],
},
- 引入静态文件,例如字体图片,发现只能使用process.cwd()的形式引入才生效
path.join(process.cwd(), 'public', 'NotoSerifSC-Regular.ttf'),
- 服务端代码
import { Contract } from '@/types/orderhistory';
import { Text, Page, Document, renderToBuffer, Font, StyleSheet } from '@react-pdf/renderer';
import { NextResponse } from 'next/server';
import path from 'path';
type RequestData = Contract & Record<'lang', string>;
export async function POST(request: Request) {
const data: RequestData = await request.json();
const buffer = await renderToBuffer(generatePDF(data));
return new NextResponse(buffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="app.pdf"`,
},
});
}
function generatePDF(data: RequestData) {
Font.register({
family: 'NotoSans',
src: path.join(process.cwd(), 'public', 'NotoSerifSC-Regular.ttf'),
});
Font.registerHyphenationCallback((word) => {
if (word.length === 1) {
return [word];
}
return Array.from(word)
.map((char) => [char, ''])
.reduce((arr, current) => {
arr.push(...current);
return arr;
}, []);
});
const styles = StyleSheet.create({
page: {
fontFamily: 'NotoSans',
fontSize: 7,
padding: 24,
},
});
return (
<Document>
<Page style={[styles.page]}>
<Text>你好</Text>
</Page>
</Document>
);
}
- 客户端请求使用
const onExport = () => {
if (!data) return;
setLoading(true);
fetch('/apac/pdf/print-contract', {
method: 'POST',
body: JSON.stringify({
...data,
lang,
}),
})
.then((res) => res.blob())
.then((blob) => {
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
})
.finally(() => setLoading(false));
};