一、前言
项目背景: vue+Django+pdfkit+wkhtmltopdf
思路: 后端建立一个txt文件格式的html模板(具体内容请看下文),然后将需要的css放在模板的head中。前端通过设置页面id,抓取id标签下面的所有内容,传到后台,和模板结合。最后利用pdfkit库和wkhtmltopdf,将html页面导出成pdf
二、问题
- 因为vue项目是一个单页应用,所有的css会被解析放到html页面的
head
标签中,如果简单的将所有css复制到模板文件中,会有造成css很大的冗余。并且还使用了element组件开发,增加了筛选css的难度。 - vue项目开发有个特点,放在
<style scoped></style>
标签中的私有css在被webpack打包时,class对应的css名称会带上唯一的hash值,导致模板不可以重复使用。 - 使用element开发,有一些组件在导出成pdf时,会消失,具体原因不明。
- 图片问题,如果使用比较大的图片,同样不能导出,因为模板中无法加载图片链接。
- 导出pdf清晰度问题,这个和css有很大关系。
三、解决办法
- 针对问题1、2、3,最好的策略是使用传统html和css,单独开发一个导出页面,这样就可以省去不可控的外部组件,而且css可以统一放在一个单独的公共scss文件中,这样可以去除打包时产生的hash值问题,同样可以抵消css冗余问题。
- 如果项目进度比较急 ,可以使用element的一些简单组件,比如button和布局等一些简单的不需要js逻辑处理的组件,目前探索的也不是很多,可以自行测试。
- 针对问题4,可以使用小于webpack规定大小的图片,这样图片可以通过
url-loader
压缩成base64
编码,直接显示在html页面中,无需通过路径加载图片。 - 针对问题5,css样式不要使用阴影,很影响清晰度。
四、其他思路
1、pdfkit有一个导出是通过html的链接来导出pdf,亲测百度链接有效。但是因为项目时间紧迫,没来的急在生产进行这方面的测试,以后有条件可以去试试。
2、前端直接生成图片,再导出成pdf,是一个可行的思路。但是有个问题就是前端生成图片清晰度不够,目前还没有找到比较好的方法,生成清晰的图片。
五、技术问题和解决办法
导出table,在pdf换页时表头有重复问题,解决办法是设置table标签的样式,办法如下:
thead {
display: table-row-group;// 使用thead默认每页都显示表头
}
六、具体实现部分
后端
模板export_file.txt
:
EXPORT_FILE = os.path.join(PROJECT_ROOT, 'fixtures/export_file.txt')
@csrf_exempt
def export_pdf(request):
try:
# params = json.loads(request.body)
# report_id = params.get('report_id', '')
# path = params.get('path', '')
content = request.POST.get("content")
html_string = format_export_string(content)
report_id = request.GET["report_id"]
check_report = CheckReport.objects.get(id=report_id)
file_name = check_report.task_name + "-" + check_report.created_time + ".pdf"
options = {
'page-size': 'A4',
'encoding': "UTF-8",
"javascript-delay": "5000",
"margin-top": "0",
"margin-bottom": "0",
"margin-left": "0",
"margin-right": "0",
'quiet': "",
}
if settings.DEBUG:
configuration = pdfkit.configuration(wkhtmltopdf=r'E:\wkhtmltopdf\bin\wkhtmltopdf.exe')
else:
configuration = pdfkit.configuration(wkhtmltopdf='/usr/bin/wkhtmltopdf')
pdf_path = False
pdf_file = pdfkit.from_string(html_string, pdf_path, options=options, configuration=configuration)
# Pdf.objects.create(report_id=report_id, name=file_name, status=True, operator=request.user.username,
# value=pdf_file)
return download_file(pdf_file, file_name)
except Exception as e:
return JsonResponse({"result": False, "message": str(e)})
def format_export_string(content):
export_file = open(EXPORT_FILE, encoding='utf-8')
file_content = export_file.read()
export_file.close()
html_content = content.replace("&", "&").replace(">", ">") \
.replace('"', '"') \
.replace("'", r"'") \
.replace("<", "<") \
.replace(" ", " ") \
.replace("\n", '') \
.replace("\r", "").strip()
return str(file_content).replace('{body_content}', html_content)
def download_file(file_buffer, file_name):
response = HttpResponse(file_buffer, content_type='APPLICATION/OCTET-STREAM')
response['Content-Disposition'] = 'attachment; filename=' + file_name
response['Content-Length'] = len(file_buffer)
return response
前端
在vue项目中,css样式放到公共scss中,不要放在vue文件的style中
downLoadPdf() {
const VueEnv = process.env.NODE_ENV
const ApiUrl = VueEnv === 'production' ? window.siteUrl : 'http://127.0.0.1:8083/'
const eleForm = document.createElement('form')
eleForm.id = 'eleForm'
eleForm.method = 'post'
eleForm.action = ApiUrl + 'check/export_detail_pdf/?report_id=' + this.reportId + '&ip=' + this.ipAddress
eleForm.target = '导出报告'
const eleInput = document.createElement('input')
eleInput.type = 'hidden'
eleInput.name = 'content'
eleInput.value = $('#export-pdf').html()
eleForm.appendChild(eleInput)
eleForm.addEventListener('onsubmit', function() {
this.$message.success('导出报告')
})
document.body.appendChild(eleForm)
eleForm.submit()
document.body.removeChild(eleForm)
},
七、Django下载文件,文件名为中文时,出错问题
解决办法:
response['Content-Disposition'] = "attachment; filename*=utf-8''{}".format(escape_uri_path(file_name))