最近需要做一个下载vue页面为pdf格式的功能,网上方案有很多,也尝试了其他方案,最终采用的方案 流程就是点击下载报告按钮,则请求后端一个接口,例如接口地址为example.cn/file/downlo…, 则返回一个pdf的文件地址 即可实现下载。背后原理就是利用爬虫抓取页面然后转为pdf,存储在服务器静态资源目录,当然过程是怎样呢?接下来记录一下:
1.[前端]准备好一个vue页面
此页面就是最终需要被转为pdf后下载的页面
2.[前端]页面点击按钮,发起请求,传入参数
<el-button type="primary" @click="downloadReport">报告下载</el-button>
<script> import { reportDownload } from '@/api/request/analysis'// 下载的接口
const url = host + `/report_download/${params.id}` // url 则是需要下载的vue页面路径
const downloadReport = () => {
reportDownload({
id: params.id,// 传入id和url
url: url
}).then((res:any) => {
if (res) {
const url = host + res
window.open(url, '_blank') // 直接通过window.open下载
}
}).catch((err) => {
return err
})
} </script>
3.[后端] 准备接口(report_download) 并返回pdf路径
3.1 利用谷歌的爬虫 puppeteer (这儿使用node版)
编写node爬虫脚本(就不具体介绍puppeteer了),java后端或者其他后端,在收到前端的请求后(/download),在执行node脚本。 具体可查看github github.com/puppeteer/p…
一个官网最简单的 puppeteer 转pdf脚本:
// hn.js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {
waitUntil: 'networkidle2',
});
await page.pdf({ path: 'hn.pdf', format: 'a4' });
await browser.close();
})();
命令行执行
node hn.js
则可生成一个hn.pdf
当然项目中考虑的需求要复杂一些:比如 1.页面需要请求多个接口,而接口请求是异步的,需要耗时,那需要判断接口加载完成,也就是数据渲染了 才转为PDF,所以肯能需要在 puppeteer 脚本中监听前端页面的控制台console,用来标识请求数据完成,但是一般情况下打包后的页面又删除调了所有console。需要做个一处理
const print = console;
print.log('report generate done');
// report generate done 这是前端vue页面和 puppeteer 脚本统一的标识,用来判断数据请求为完成状态 再利用爬虫功能 转为pdf
2.后端返回的pdf文件 数据格式有问题,前端用window.open 则不能直接下载 而是在浏览器预览,需要后端或者nginx设置header Content-Type application/octet-stream;
Nginx配置
server {
listen 8888;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root F:/1/pdf;
if ($request_filename ~* ^.*?\.(html|doc|pdf|zip|docx)$) {
add_header Content-Disposition attachment;
add_header Content-Type application/octet-stream;
}
}
}
3.我的index.html页面限制了html的宽高 需要在预览页面重置body html app 的宽高
这次用到的代码
const puppeteer = require('puppeteer');
let errorHandler = function(when, exit=true) {
return function(error) {
console.log(`error happened when ${when}: ${error}`);
if (exit) {
process.exit(1);
}
};
};
class Reporter {
constructor(timeout){
this._promise = new Promise(((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
}));
this._timer = setTimeout(() => {
this._reject('timeout');
}, timeout);
}
async render(url, pdf_path, header, footer) {
let browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'], ignoreHTTPSErrors: true, headless: true}).catch(errorHandler('launch browser'));
let page = await browser.newPage().catch(errorHandler('create page'));
page.on('console', message => { // 页面必须在控制台输出console 才代表页面加载完成 才能开始转pdf
console.log(message.text());
if (message.text() === 'report generate done') {
this._resolve();
}
});
await page.goto(url, {timeout: 120 * 1000, waitUntil: ["domcontentloaded", "networkidle2"]}).catch(errorHandler('goto url'));
await this._promise.catch(errorHandler('wait report'));
clearTimeout(this._timer);
await page.waitFor(3000);//新版已弃用
await page.pdf({ // 生成pdf
path: pdf_path,
printBackground: true,
preferCSSPageSize: true,
displayHeaderFooter: true,
format: 'A4',
margin: {
top: '2cm',
bottom: '2cm'
},
headerTemplate: `<div style="width:100%;text-align:center;font-size:10px">` + header + `</div>`,
footerTemplate: `<div style="width:100%;text-align:center;font-size:10px">` + footer + `</div>`}
).catch(errorHandler('generate pdf'));
await browser.close().catch(errorHandler('close browser'));
}
}
(async () => {
let reporter = new Reporter(30 * 60 * 1000); // 10 分钟 超时
await reporter.render(process.argv[2], process.argv[3], process.argv[4], process.argv[5]);
})();