Tiểu sử. Vài tháng trước, một trong những khách hàng của RisingStack đã yêu cầu chúng tôi phát triển một tính năng mà người dùng có thể yêu cầu trang React ở định dạng PDF. Trang đó về cơ bản là báo cáo/kết quả cho bệnh nhân có trực quan hóa dữ liệu, chứa rất nhiều SVG. Hơn nữa, có một số yêu cầu đặc biệt để thao tác bố cục và thực hiện một số sắp xếp lại các phần tử HTML. Vì vậy, PDF phải có kiểu dáng và bổ sung khác so với trang React ban đầu
Vì nhiệm vụ phức tạp hơn một chút so với những gì có thể được giải quyết bằng các quy tắc CSS đơn giản, trước tiên chúng tôi khám phá các triển khai khả thi. Về cơ bản, chúng tôi tìm thấy 3 giải pháp chính. Bài đăng trên blog này sẽ hướng dẫn bạn về các khả năng này và các triển khai cuối cùng
Nhận xét cá nhân trước khi chúng tôi bắt đầu. nó khá rắc rối, vì vậy hãy thắt dây an toàn
Mục lục
- Tạo PDF phía máy khách hay phía phụ trợ?
- lựa chọn 1. Tạo ảnh chụp màn hình từ DOM
- Lựa chọn 2. Chỉ sử dụng thư viện PDF
- Phương án cuối cùng 3. Puppeteer, Chrome không đầu với Node. js
- Thao tác kiểu
- Gửi tệp cho khách hàng và lưu nó
- Sử dụng Puppeteer với Docker
- Tùy chọn 3 +1. Quy tắc in CSS
- Tóm lược
Tạo PDF phía máy khách hay phía máy chủ?
Có thể tạo tệp PDF ở cả phía máy khách và phía máy chủ. Tuy nhiên, có lẽ sẽ hợp lý hơn nếu để phần phụ trợ xử lý nó, vì bạn không muốn sử dụng hết tài nguyên mà trình duyệt của người dùng có thể cung cấp
Mặc dù vậy, tôi vẫn sẽ đưa ra các giải pháp cho cả hai phương pháp
lựa chọn 1. Tạo ảnh chụp màn hình từ DOM
Thoạt nhìn, giải pháp này có vẻ đơn giản nhất, hóa ra lại đúng, nhưng nó có những hạn chế riêng. Nếu bạn không có nhu cầu đặc biệt, chẳng hạn như văn bản có thể chọn hoặc có thể tìm kiếm trong PDF, thì đó là một cách hay và đơn giản để tạo một văn bản
Phương pháp này là đơn giản và đơn giản. tạo ảnh chụp màn hình từ trang và đặt nó vào tệp PDF. khá đơn giản. Chúng tôi đã sử dụng hai gói cho phương pháp này
Html2canvas, để tạo ảnh chụp màn hình từ DOM
jsPdf, một thư viện để tạo PDF
Hãy bắt đầu mã hóa
doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
1import html2canvas from 'html2canvas'
import jsPdf from 'jspdf'
function printPDF [] {
const domElement = document.getElementById['your-id']
html2canvas[domElement, { onclone: [document] => {
document.getElementById['print-button'].style.visibility = 'hidden'
}}]
.then[[canvas] => {
const img = canvas.toDataURL['image/png']
const pdf = new jsPdf[]
pdf.addImage[imgData, 'JPEG', 0, 0, width, height]
pdf.save['your-filename.pdf']
}]
Và đó là nó
Hãy chắc chắn rằng bạn đã xem phương pháp
doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
2 doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
3. Nó có thể tỏ ra tiện dụng khi bạn cần nhanh chóng chụp ảnh nhanh và thao tác với DOM [e. g. ẩn nút in] trước khi chụp ảnh. Tôi có thể thấy khá nhiều trường hợp sử dụng cho gói này. Thật không may, của chúng tôi không phải là một, vì chúng tôi cần xử lý việc tạo PDF ở phía phụ trợLựa chọn 2. Chỉ sử dụng Thư viện PDF
Có một số thư viện trên NPMnpm là sổ đăng ký phần mềm phục vụ hơn 1. 3 triệu gói. npm được sử dụng bởi các nhà phát triển nguồn mở từ khắp nơi trên thế giới để chia sẻ và mượn mã, cũng như nhiều doanh nghiệp. Có ba thành phần để npm. trang web Giao diện dòng lệnh [CLI] sổ đăng ký Sử dụng trang web để khám phá và tải xuống các gói, tạo hồ sơ người dùng và. cho mục đích này, chẳng hạn như jsPDF [đã đề cập ở trên] hoặc PDFKit. Vấn đề với chúng là tôi sẽ phải tạo lại cấu trúc trang nếu tôi muốn sử dụng các thư viện này. Điều đó chắc chắn ảnh hưởng đến khả năng bảo trì, vì tôi cần áp dụng tất cả các thay đổi tiếp theo cho cả mẫu PDF và trang React.
Hãy nhìn vào đoạn mã dưới đây. Bạn cần tự tạo tài liệu PDF bằng tay. Bây giờ bạn có thể duyệt qua DOM và tìm ra cách dịch từng phần tử sang PDF, nhưng đó là một công việc tẻ nhạt. Nó phải là một con đường dễ dàng hơn
doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
Đoạn mã này là từ tài liệu PDFKit. Tuy nhiên, nó có thể hữu ích nếu mục tiêu của bạn là tệp PDF ngay lập tức chứ không phải chuyển đổi trang HTML đã tồn tại [và luôn thay đổi]
Lựa chọn cuối cùng 3. Người múa rối, Chrome không đầu có nút. js
Người múa rối là gì?
Puppeteer là thư viện Node cung cấp API cấp cao để kiểm soát Chrome hoặc Chromium qua Giao thức DevTools. Puppeteer chạy không đầu theo mặc định, nhưng có thể được định cấu hình để chạy Chrome hoặc Chromium đầy đủ [không đầu]
Về cơ bản, đây là một trình duyệt mà bạn có thể chạy từ Node. js. Nếu bạn đọc tài liệu, điều đầu tiên nó nói về Puppeteer là bạn có thể sử dụng nó để Tạo ảnh chụp màn hình và tệp PDF của các trang'. Xuất sắc. Đó là những gì chúng tôi đang tìm kiếm
Hãy cài đặt Puppeteer với
doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
4 và triển khai trường hợp sử dụng của chúng taconst puppeteer = require['puppeteer']
async function printPDF[] {
const browser = await puppeteer.launch[{ headless: true }];
const page = await browser.newPage[];
await page.goto['//blog.risingstack.com', {waitUntil: 'networkidle0'}];
const pdf = await page.pdf[{ format: 'A4' }];
await browser.close[];
return pdf
}]
Đây là một chức năng đơn giản điều hướng đến một URL và tạo tệp PDF của trang web
Đầu tiên, chúng tôi khởi chạy trình duyệt [chỉ hỗ trợ tạo PDF ở chế độ trình duyệt không đầu], sau đó chúng tôi mở một trang mới, đặt kích thước khung nhìn và điều hướng đến URL được cung cấp
Đặt tùy chọn
doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
5 có nghĩa là Puppeteer coi việc điều hướng kết thúc khi không có kết nối mạng trong ít nhất 500 mili giây. [Xem tài liệu API để biết thêm thông tin. ]Sau đó, chúng tôi lưu tệp PDF vào một biến, chúng tôi đóng trình duyệt và trả lại tệp PDF
Ghi chú. Phương thức
doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
6 nhận một đối tượng doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
7, nơi bạn cũng có thể lưu tệp vào đĩa bằng tùy chọn 'đường dẫn'. Nếu đường dẫn không được cung cấp, tệp PDF sẽ không được lưu vào đĩa, thay vào đó, bạn sẽ nhận được một bộ đệm. Lát nữa mình bàn bạn xử lý thế nào. ]Trong trường hợp trước tiên bạn cần đăng nhập để tạo tệp PDF từ một trang được bảo vệ, trước tiên bạn cần điều hướng đến trang đăng nhập, kiểm tra các thành phần của biểu mẫu để tìm ID hoặc tên, điền vào, sau đó gửi biểu mẫu
await page.type['#email', process.env.PDF_USER]
await page.type['#password', process.env.PDF_PASSWORD]
await page.click['#submit']
Luôn lưu trữ thông tin đăng nhập trong các biến môi trường, không mã hóa chúng
Thao tác phong cách
Puppeteer cũng có một giải pháp cho thao tác kiểu này. Bạn có thể chèn các thẻ kiểu trước khi tạo PDF và Puppeteer sẽ tạo một tệp có các kiểu đã sửa đổi
await page.addStyleTag[{ content: '.nav { display: none} .navbar { border: 0px} #print-button {display: none}' }]
Gửi tệp cho khách hàng và lưu nó
Được rồi, bây giờ bạn đã tạo một tệp PDF trên phần phụ trợ. Làm gì bây giờ?
Như tôi đã đề cập ở trên, nếu bạn không lưu tệp vào đĩa, bạn sẽ nhận được một bộ đệm. Bạn chỉ cần gửi bộ đệm đó với loại nội dung phù hợp tới giao diện người dùng
printPDF[].then[pdf => {
res.set[{ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length }]
res.send[pdf]
}]
Giờ đây, bạn có thể chỉ cần gửi yêu cầu đến máy chủ để nhận tệp PDF đã tạo
function getPDF[] {
return axios.get[`${API_URL}/your-pdf-endpoint`, {
responseType: 'arraybuffer',
headers: {
'Accept': 'application/pdf'
}
}]
Khi bạn đã gửi yêu cầu, bộ đệm sẽ bắt đầu tải xuống. Bây giờ, bước cuối cùng là chuyển đổi bộ đệm thành tệp PDF
savePDF = [] => {
this.openModal[‘Loading…’] // open modal
return getPDF[] // API call
.then[[response] => {
const blob = new Blob[[response.data], {type: 'application/pdf'}]
const link = document.createElement['a']
link.href = window.URL.createObjectURL[blob]
link.download = `your-file-name.pdf`
link.click[]
this.closeModal[] // close modal
}]
.catch[err => /** error handling **/]
}
Save as PDF
Điều đó là vậy đó. Nếu bạn nhấp vào nút lưu, tệp PDF sẽ được lưu bởi trình duyệt
Sử dụng Puppeteer với Docker
Tôi nghĩ đây là phần khó nhất trong quá trình triển khai – vì vậy hãy để tôi giúp bạn tiết kiệm vài giờ tra Google
Tài liệu chính thức nêu rõ rằng "việc thiết lập và chạy headless Chrome trong Docker có thể khó khăn". Các tài liệu chính thức có một phần, tại thời điểm viết bài này, bạn có thể tìm thấy tất cả thông tin cần thiết về cách cài đặt nghệ sĩ múa rối với Docker
Nếu bạn cài đặt Puppeteer trên ảnh Alpine, hãy đảm bảo rằng bạn cuộn xuống một chút để. Mặt khác, bạn có thể che đậy sự thật rằng bạn không thể chạy phiên bản Puppeteer mới nhất và bạn cũng cần tắt sử dụng shm, sử dụng cờ
const browser = await puppeteer.launch[{
headless: true,
args: ['--disable-dev-shm-usage']
}];
Nếu không, quy trình con Puppeteer có thể hết bộ nhớ trước khi nó được bắt đầu đúng cách. Thông tin thêm về điều đó trên liên kết khắc phục sự cố ở trên
Tùy chọn 3 + 1. Quy tắc in CSS
Người ta có thể nghĩ rằng chỉ cần sử dụng các quy tắc in CSS là dễ dàng từ quan điểm của nhà phát triển. Không có mô-đun NPM hoặc nút, chỉ có CSS thuần túy. Nhưng làm thế nào để họ có giá khi nói đến khả năng tương thích giữa các trình duyệt?
Khi chọn quy tắc in CSS, bạn phải kiểm tra kết quả trong mọi trình duyệt để đảm bảo rằng nó cung cấp bố cục giống nhau và điều đó không đúng 100%
Ví dụ: chèn dấu ngắt sau một phần tử nhất định không thể được coi là một trường hợp sử dụng bí truyền, nhưng bạn có thể ngạc nhiên rằng mình cần sử dụng các giải pháp thay thế
Trừ khi bạn là một nhà ảo thuật CSS thiện chiến với nhiều kinh nghiệm trong việc tạo các trang có thể in được, nếu không thì việc này có thể tốn nhiều thời gian
Các quy tắc in sẽ rất tuyệt nếu bạn có thể giữ cho các biểu định kiểu in đơn giản
Hãy xem một ví dụ
doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
0CSS ở trên ẩn nút in và chèn dấu ngắt trang sau mỗi _______0_______8 với lớp
doc = new PDFDocument
doc.pipe fs.createWriteStream['output.pdf']
doc.font['fonts/PalatinoBold.ttf']
.fontSize[25]
.text['Some text with an embedded font!', 100, 100]
doc.image['path/to/image.png', {
fit: [250, 300],
align: 'center',
valign: 'center'
}];
doc.addPage[]
.fontSize[25]
.text['Here is some vector graphics...', 100, 100]
doc.end[]
9 Có một bài viết hay tóm tắt những gì bạn có thể làm với quy tắc in và những khó khăn với chúng, bao gồm cả khả năng tương thích với trình duyệtCân nhắc mọi thứ, các quy tắc in CSS rất tuyệt vời và hiệu quả nếu bạn muốn tạo PDF từ một trang không quá phức tạp