一、引言:为何我们需要告别'远古'的 HTML 转 PDF 方案?
1.1 无处不在的导出需求
在数字化浪潮中,PDF 作为一种跨平台、格式固定的文档格式,其地位无可替代。试想以下场景,你是否感到熟悉?
- 📊 报表系统:用户在线分析了复杂的数据看板后,希望将最终的图表和结论为报告,用于邮件汇报或线下存档。
介绍基于 SnapDOM 和 jsPDF 的高保真 HTML 转 PDF 方案,解决了传统 html2canvas 在样式还原、布局兼容及清晰度上的不足。文章详细阐述了环境准备、DOM 预处理、高清截图、智能分页及 PDF 生成的核心实现步骤,并提供了性能优化策略(如 Web Worker)及高级特性(页眉页脚、元数据)。此外,还探讨了结合 AI 进行布局分析与样式增强的未来方向,以及在不同业务场景下的适配方案与测试策略,为开发者提供完整的现代化导出解决方案。
在数字化浪潮中,PDF 作为一种跨平台、格式固定的文档格式,其地位无可替代。试想以下场景,你是否感到熟悉?
这些场景都对前端导出 PDF 的质量、效率和可靠性提出了极高要求。
html2canvas力有不逮?长久以来,html2canvas配合jsPDF是前端实现 HTML 转 PDF 的'标配'方案。其核心思路是:先将 DOM 元素渲染到<canvas>画布上,再将 canvas 图像内容嵌入 PDF。然而,这一方案在面对日益复杂的现代 Web 应用时,其弊端暴露无遗:
| 痛点维度 | html2canvas 方案表现 | 带来的业务困扰 |
|---|---|---|
| 🎨 样式还原度 | 部分 CSS3 属性(如混合模式、滤镜)无法完美支持,字体、间距可能存在细微偏差。 | 导出的 PDF 与用户在网页上看到的'长得不一样',专业度大打折扣。 |
| 📏 布局兼容性 | 对 Flexbox、Grid 等现代布局模型的解析有时会出现错乱,导致内容重叠或错位。 | 精心设计的响应式布局在 PDF 中面目全非,需要为导出做大量适配 hack。 |
| 🖼 图像清晰度 | 尽管可以通过缩放倍数提升清晰度,但会显著增加处理时间和文件体积,权衡困难。 | 二维码、条形码等关键信息可能模糊不清,影响扫描识别。 |
| ⚡ 性能与内存 | 渲染超长页面时容易导致浏览器卡顿甚至内存溢出(OOM)。 | 用户体验差,无法导出大型报表或长对话记录。 |
| 📦 维护性 | 库的更新迭代相对缓慢,对新浏览器特性的跟进可能存在延迟。 | 长期项目有技术债风险。 |
流程图:传统方案的核心痛点闭环
样式/布局解析偏差 -> 开发者投入额外 hack 成本 -> 复杂现代 UI 布局 -> html2canvas 渲染 -> Canvas 图像失真 -> jsPDF 打包成 PDF -> 最终 PDF 质量不佳 -> 用户不满意/业务价值低
这个闭环清晰地表明,传统方案已难以满足当前高品质、高效率的导出需求。我们迫切需要一种新的、更可靠的破局之道。
我们的新方案由两位'主角'构成:
html2canvas的'模拟渲染',而是致力于更精准地捕获 DOM 的视觉表现。它更轻量(压缩后约 20KB),对现代 CSS 特性(如 Flexbox、Grid、渐变、阴影)的支持近乎完美,从根源上保障了源与果的一致性。这个组合的核心优势在于'各司其职':SnapDOM 专注于完美地'拍照',jsPDF 专注于专业地'装订'。这种解耦使得整个流程更加清晰、可靠和易于维护。
为了更直观地展示新方案的优越性,我们进行一场全方位的'擂台赛':
| 特性维度 | SnapDOM + jsPDF (新方案) | html2canvas + jsPDF (传统方案) | 优势解读 |
|---|---|---|---|
| 🎯 保真度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | SnapDOM 从底层渲染引擎捕获样式,还原度极高,所见即所得。 |
| 🛠 现代 CSS 支持 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 对 Flex/Grid/渐变/字体支持出色,无需为导出调整布局。 |
| 📷 清晰度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 默认支持高分屏(Retina)截图,文字和图像边缘锐利。 |
| 🚀 性能 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 更轻量,渲染效率更高,尤其在复杂页面上优势明显。 |
| 🧩 维护性 | ⭐⭐⭐⭐ | ⭐⭐ | SnapDOM 更活跃,API 设计更现代,技术债风险低。 |
| 📚 社区生态 | ⭐⭐⭐ | ⭐⭐⭐⭐ | jsPDF 生态强大,但 SnapDOM 作为新秀,社区仍在成长中。 |
表:新老方案核心能力对比
一套健壮的导出流程,其内部运作如同一条精密的自动化生产线。下图清晰地展示了从 HTML 到 PDF 的完整工作流:
成功 -> 失败 -> 原始 HTML 节点 -> 预处理 样式调整 -> SnapDOM 高清截图 -> 高清 Canvas/PNG -> 降级处理 基础 Canvas -> 分页计算与切割 -> PDF Page 1 -> PDF Page 2 -> ... -> jsPDF 组装与压缩 -> 最终 PDF 文件
接下来,我们将按照工作流,逐一拆解每个关键步骤的实现细节。
首先,我们需要在项目中引入这两个库。
# 使用 npm 或 yarn 安装
npm install snappdom jspdf
# 或 yarn add snappdom jspdf
然后,在你的导出模块中引入它们:
import SnapDOM from 'snapdom';
import jsPDF from 'jspdf';
在截图前,对目标 DOM 节点进行适当的预处理,是确保结果完美的关键。这就像拍照前整理场景一样重要。
/**
* 预处理 DOM 节点,优化截图效果
* @param {HTMLElement} element - 目标 DOM 元素
*/
function prepareElementForSnapshot(element) {
// 1. 临时禁用滚动条,确保捕获完整内容
const originalStyle = {
overflow: element.style.overflow,
overflowX: element.style.overflowX,
overflowY: element.style.overflowY,
};
Object.assign(element.style, {
overflow: 'visible',
overflowX: 'visible',
overflowY: 'visible',
});
// 2. 强制触发浏览器重绘,确保样式应用
element.offsetHeight; // 通过读取布局属性触发重绘
// 返回一个恢复原样的函数
return () => {
Object.assign(element.style, originalStyle);
};
}
// 使用示例
const targetElement = document.getElementById('export-content');
const restoreStyles = prepareElementForSnapshot(targetElement);
这是整个流程的灵魂所在。我们配置 SnapDOM 以高质量完成截图。
/**
* 使用 SnapDOM 进行高保真截图
* @param {HTMLElement} element - 预处理后的 DOM 元素
* @param {Object} options - 截图配置
* @returns {Promise<string>} - 返回 Base64 格式的 PNG 图片
*/
async function captureHighFidelitySnapshot(element, options = {}) {
const {
scale = 2, // 缩放倍数,2 为高清 Retina 屏优化
backgroundColor = '#ffffff',
quality = 1,
} = options;
try {
// 创建 SnapDOM 实例
const snapDOM = new SnapDOM();
// 配置选项
const snapshotOptions = {
scale,
backgroundColor,
quality,
useCORS: true, // 启用跨域图像处理
allowTaint: false,
logging: false, // 生产环境可关闭日志
};
// 执行截图,优先尝试转换为 PNG
const dataUrl = await snapDOM.toPng(element, snapshotOptions);
return dataUrl;
} catch (error) {
console.error('SnapDOM 截图失败,尝试降级方案:', error);
// 降级方案:尝试转换为 Canvas
try {
const canvas = await snapDOM.toCanvas(element, { scale, backgroundColor });
return canvas.toDataURL('image/png', quality);
} catch (fallbackError) {
console.error('降级方案也失败:', fallbackError);
throw new Error('无法完成 DOM 截图');
}
}
}
拿到高清长图后,我们需要根据 PDF 的页面尺寸(如 A4)将其智能地分割成多页。
/**
* 将长图按 PDF 页面尺寸进行分页处理
* @param {string} dataUrl - 原始长图的 Base64 数据
* @param {Object} pdfConfig - PDF 配置
* @returns {Array} - 分页后的图片数据数组
*/
function paginateImageForPDF(dataUrl, pdfConfig) {
const { pageWidth, pageHeight, pageMargin = 0 } = pdfConfig;
return new Promise((resolve) => {
const img = new Image();
img.src = dataUrl;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 计算有效内容区的宽高
const contentWidth = pageWidth - (pageMargin * 2);
const contentHeight = pageHeight - (pageMargin * 2);
// 根据缩放比例调整内容尺寸
const scaledContentWidth = contentWidth * img.width / img.naturalWidth;
const scaledContentHeight = contentHeight * img.height / img.naturalWidth;
const totalPages = Math.ceil(img.height / scaledContentHeight);
const pages = [];
for (let i = 0; i < totalPages; i++) {
canvas.width = scaledContentWidth;
canvas.height = Math.min(scaledContentHeight, img.height - (i * scaledContentHeight));
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制当前页的内容
ctx.drawImage(
img,
0,
i * scaledContentHeight, // 源图像开始裁剪的坐标
img.width,
Math.min(scaledContentHeight, img.height - (i * scaledContentHeight)), // 源图像裁剪的宽高
0,
0, // 在画布上开始绘制的坐标
canvas.width,
canvas.height // 在画布上绘制的宽高
);
pages.push(canvas.toDataURL('image/png', 1.0));
}
resolve(pages);
};
});
}
最后,使用 jsPDF 将分页后的图片组装成最终的 PDF 文件。
/**
* 生成并导出 PDF 文件
* @param {Array} pageImages - 分页后的图片数据数组
* @param {Object} options - 导出配置
*/
function generatePDFFromPages(pageImages, options = {}) {
const {
filename = 'exported-document.pdf',
orientation = 'portrait', // 'portrait' or 'landscape'
unit = 'mm',
format = 'a4',
pageMargin = 10,
compress = true,
} = options;
// 创建 jsPDF 实例
const pdf = new jsPDF({ orientation, unit, format, compress });
// 获取 PDF 页面尺寸
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
// 计算内容区域尺寸
const contentWidth = pageWidth - (pageMargin * 2);
const contentHeight = pageHeight - (pageMargin * 2);
// 添加每一页
pageImages.forEach((imgData, index) => {
if (index > 0) {
pdf.addPage(); // 从第二页开始需要添加新页面
}
// 添加图片到当前页
pdf.addImage({
imageData: imgData,
x: pageMargin,
y: pageMargin,
width: contentWidth,
height: contentHeight,
});
});
// 保存 PDF
pdf.save(filename);
}
现在,我们将所有步骤整合到一个统一的函数中,提供简洁的 API 给业务层调用。
/**
* 统一的 HTML 转 PDF 导出函数
* @param {Object} config - 导出配置
*/
async function exportToPDF(config = {}) {
const {
elementSelector = '#export-content',
filename = `export-${new Date().getTime()}.pdf`,
scale = 2,
quality = 0.95,
onProgress = null, // 进度回调
} = config;
try {
// 1. 获取目标元素
const targetElement = document.querySelector(elementSelector);
if (!targetElement) {
throw new Error(`未找到选择器为 ${elementSelector} 的元素`);
}
if (onProgress) onProgress(10, '开始预处理 DOM');
// 2. DOM 预处理
const restoreStyles = prepareElementForSnapshot(targetElement);
if (onProgress) onProgress(30, '正在进行高保真截图');
// 3. 使用 SnapDOM 截图
const snapshotDataUrl = await captureHighFidelitySnapshot(targetElement, {
scale,
quality,
});
if (onProgress) onProgress(60, '图片分页处理中');
// 4. 分页处理
const pageImages = await paginateImageForPDF(snapshotDataUrl, {
pageWidth: 210, // A4 宽度 210mm
pageHeight: 297, // A4 高度 297mm
pageMargin: 10,
});
if (onProgress) onProgress(90, '生成 PDF 文件中');
// 5. 生成并导出 PDF
generatePDFFromPages(pageImages, { filename });
// 6. 恢复 DOM 样式
restoreStyles();
if (onProgress) onProgress(100, '导出完成');
return {
success: true,
message: 'PDF 导出成功',
};
} catch (error) {
console.error('导出 PDF 失败:', error);
if (onProgress) onProgress(0, `导出失败:${error.message}`);
return {
success: false,
message: `导出失败:${error.message}`,
};
}
}
// 使用示例
document.getElementById('export-btn').addEventListener('click', async () => {
const result = await exportToPDF({
elementSelector: '.report-container',
filename: `业务报表-${new Date().toLocaleDateString()}.pdf`,
onProgress: (percent, message) => {
updateProgressBar(percent, message); // 自定义进度更新函数
},
});
if (!result.success) {
alert(result.message);
}
});
面对大量数据或复杂布局,性能优化至关重要。
对于超长内容,可以将其分割成多个部分分别截图,避免一次性处理导致的性能问题。
async function captureInSections(container, sectionHeight = 2000) {
const sections = [];
let currentPosition = 0;
const totalHeight = container.scrollHeight;
// 临时隐藏滚动条,避免截图时出现
const originalOverflow = container.style.overflow;
container.style.overflow = 'hidden';
while (currentPosition < totalHeight) {
// 移动容器滚动位置,模拟视口
container.scrollTop = currentPosition;
// 等待浏览器重绘
await new Promise((resolve) => requestAnimationFrame(resolve));
// 截图当前可视区域
const sectionData = await captureHighFidelitySnapshot(container, {
scale: 2,
windowHeight: Math.min(sectionHeight, totalHeight - currentPosition),
});
sections.push({
data: sectionData,
height: Math.min(sectionHeight, totalHeight - currentPosition),
});
currentPosition += sectionHeight;
// 可选:释放事件循环,避免阻塞 UI
await new Promise((resolve) => setTimeout(resolve, 50));
}
// 恢复原始样式
container.style.overflow = originalOverflow;
container.scrollTop = 0;
return sections;
}
将耗时的截图和分页操作放入 Web Worker,避免阻塞主线程。
// worker.js
self.addEventListener('message', async (e) => {
const { type, payload } = e.data;
if (type === 'EXPORT_PDF') {
try {
// 在这里执行截图和 PDF 生成逻辑
const result = await processExport(payload);
self.postMessage({
success: true,
data: result,
});
} catch (error) {
self.postMessage({
success: false,
error: error.message,
});
}
}
});
// main.js
class PDFExportWorker {
constructor() {
this.worker = new Worker('./pdf-worker.js');
}
async export(config) {
return new Promise((resolve, reject) => {
this.worker.onmessage = (e) => {
if (e.data.success) {
resolve(e.data.data);
} else {
reject(new Error(e.data.error));
}
};
this.worker.postMessage({
type: 'EXPORT_PDF',
payload: config,
});
});
}
}
通过操作 PDF 上下文,可以为每一页添加自定义的页眉页脚。
function addHeaderFooter(pdf, headerText, footerText) {
const totalPages = pdf.getNumberOfPages();
for (let i = 1; i <= totalPages; i++) {
pdf.setPage(i);
// 添加页眉
pdf.setFontSize(10);
pdf.setTextColor(100);
pdf.text(headerText, pdf.internal.pageSize.getWidth() / 2, 10, {
align: 'center',
});
// 添加页脚(页码)
const pageText = `${footerText} - 第 ${i} 页 / 共 ${totalPages} 页`;
pdf.text(
pageText,
pdf.internal.pageSize.getWidth() / 2,
pdf.internal.pageSize.getHeight() - 10,
{
align: 'center',
}
);
}
}
设置 PDF 的元数据,并可以添加基础的安全保护。
function setPDFMetadata(pdf, metadata = {}) {
const {
title = '导出文档',
subject = '通过 SnapDOM + jsPDF 生成',
author = '业务系统',
keywords = '导出,报表,PDF',
creator = 'SnapDOM-to-PDF Processor',
} = metadata;
pdf.setProperties({
title,
subject,
author,
keywords,
creator,
});
// 基础安全设置(禁止修改/打印等)
pdf.setEncryption({
userPassword: '', // 用户密码
ownerPassword: 'owner-pass', // 所有者密码
permissions: {
printing: 'lowResolution', // 允许低分辨率打印
modifying: false,
copying: true,
annotating: true,
fillingForms: true,
contentAccessibility: true,
documentAssembly: true,
},
});
}
在 AI 技术蓬勃发展的今天,我们可以将智能能力融入传统工作流,创造出更强大的解决方案。
痛点:复杂动态布局在分页时可能导致元素被不当切割,影响阅读体验。
AI 解决方案:使用计算机视觉模型分析 DOM 截图,识别逻辑内容块(如表格、段落、图片),实现智能分页。
原始 HTML -> SnapDOM 高清截图 -> AI 布局分析 -> 识别内容区块 -> 智能分页决策 -> 避免在表格/图片中间分页 -> 优化后的分页结果 -> 调整布局适应 PDF -> 高质量 PDF 输出
痛点:网页样式在转换为 PDF 时,某些效果(如半透明、复杂动画)可能丢失。
AI 解决方案:训练深度学习模型学习网页样式到 PDF 样式的映射,自动进行样式修复和优化。
// 概念代码:AI 样式增强
async function enhanceWithAI(domElement, pdfConfig) {
// 1. 提取 DOM 的样式特征
const styleFeatures = extractStyleFeatures(domElement);
// 2. 调用 AI 服务进行样式优化建议
const aiSuggestions = await fetchAIStyleSuggestions({
features: styleFeatures,
targetMedium: 'pdf',
config: pdfConfig,
});
// 3. 应用 AI 优化建议
return applyAISuggestions(domElement, aiSuggestions);
}
// AI 可能给出的优化建议示例
const aiSuggestions = {
contrast: 1.2, // 增加对比度以适应打印
fontSizes: { // 针对 PDF 优化字体大小
base: '14pt',
heading: '18pt',
},
colorAdjustment: { // 颜色调整以适应打印
saturation: 1.1,
brightness: 0.95,
},
layout: 'single-column', // 建议转换为单列布局
};
痛点:一份数据需要根据不同用户角色生成不同样式和内容的 PDF。
AI 解决方案:基于用户画像和行为数据,智能生成个性化的 PDF 文档。
| 用户类型 | 传统方案输出 | AI 增强输出 |
|---|---|---|
| 管理层 | 详细数据报表 | 智能摘要:关键指标突出显示,自动生成执行摘要 |
| 技术员 | 统一技术文档 | 深度版本:包含技术细节、调试日志、相关文档链接 |
| 客户 | 标准合同文本 | 通俗版本:专业术语解释,重点条款提示,个性化问候 |
挑战:报表通常包含大量数据表格、图表,需要保持高清晰度和正确分页。
专用解决方案:
class ReportExporter {
constructor() {
this.complexCharts = []; // 存储复杂图表引用
}
async exportReport(config) {
// 1. 预处理:确保所有图表渲染完成
await this.ensureChartsRendered();
// 2. 临时替换复杂动画为静态版本
this.replaceAnimationsWithStatic();
// 3. 使用更高的缩放比例保证数据清晰度
const result = await exportToPDF({
...config,
scale: 3, // 报表需要更高清晰度
preProcess: (element) => this.preProcessReport(element),
});
// 4. 恢复原始状态
this.restoreAnimations();
return result;
}
async ensureChartsRendered() {
// 等待所有图表动画完成
const chartPromises = this.complexCharts.map((chart) => {
return new Promise((resolve) => {
if (chart.animationComplete) {
resolve();
} else {
chart.onAnimationComplete = resolve;
}
});
});
await Promise.all(chartPromises);
}
}
挑战:聊天记录具有时序性,需要保持对话连贯性,避免消息被分页切断。
专用解决方案:
function exportConversation(messages, config = {}) {
// 1. 智能分组:将相关消息保持在同一页
const messageGroups = groupMessagesByPage(messages, {
maxPageHeight: 280, // 预留页眉页脚空间
keepRepliesTogether: true, // 保持对话连续性
});
// 2. 为每页生成独立的 DOM 结构
const pagesHTML = messageGroups.map((group, index) => {
return generateConversationPage(group, {
pageNumber: index + 1,
totalPages: messageGroups.length,
showTimestamps: config.showTimestamps,
});
});
// 3. 分别处理每一页
return exportPaginatedContent(pagesHTML, config);
}
function groupMessagesByPage(messages, config) {
const groups = [];
let currentGroup = [];
let currentHeight = 0;
for (const message of messages) {
const messageHeight = calculateMessageHeight(message);
// 如果当前消息放入后超出页面限制,且不是回复链的开始
if (
currentHeight + messageHeight > config.maxPageHeight &&
!isStartOfThread(message)
) {
// 开启新的一页
groups.push([...currentGroup]);
currentGroup = [message];
currentHeight = messageHeight;
} else {
currentGroup.push(message);
currentHeight += messageHeight;
}
}
if (currentGroup.length > 0) {
groups.push(currentGroup);
}
return groups;
}
为确保导出功能的可靠性,需要建立多层次的测试策略。
单元测试 -> 核心工具函数 集成测试 -> 模块间协作 视觉回归测试 -> PDF 输出对比 E2E 测试 -> 完整用户流程
// __tests__/pdf-utils.test.js
import {
prepareElementForSnapshot,
paginateImageForPDF,
} from '../src/pdf-utils';
describe('PDF 工具函数', () => {
test('DOM 预处理应正确修改并恢复样式', () => {
// 设置测试 DOM
document.body.innerHTML = '<div></div>';
const element = document.getElementById('test');
// 执行预处理
const restore = prepareElementForSnapshot(element);
// 断言样式已修改
expect(element.style.overflow).toBe('visible');
// 执行恢复
restore();
// 断言样式已恢复
expect(element.style.overflow).toBe('auto');
});
test('图片分页应正确处理不同尺寸', async () => {
// 创建测试图片
const mockImageData = 'data:image/png;base64,...';
// 测试长图分页
const pages = await paginateImageForPDF(mockImageData, {
pageWidth: 210,
pageHeight: 297,
});
expect(pages.length).toBeGreaterThan(1);
expect(pages[0]).toMatch(/^data:image\/png;base64,/);
});
});
使用类似jest-image-snapshot进行 PDF 输出的视觉一致性测试。
// __tests__/visual-regression.test.js
describe('PDF 视觉回归测试', () => {
test('业务报表导出应保持视觉一致性', async () => {
// 渲染测试报表
renderTestReport();
// 执行导出
const pdfBuffer = await exportToPDF({
elementSelector: '.test-report',
returnAsBuffer: true,
});
// 将 PDF 转换为图片进行比较
const pdfImage = await convertPDFToImage(pdfBuffer);
// 与基线截图对比
expect(pdfImage).toMatchImageSnapshot({
customDiffConfig: {
threshold: 0.1,
},
failureThreshold: 0.02,
failureThresholdType: 'percent',
});
});
});
SnapDOM + jsPDF 方案为前端 PDF 导出带来了质的飞跃:
随着技术的不断发展,HTML 转 PDF 方案还将继续进化:
| 方向 | 描述 | 潜在影响 |
|---|---|---|
| Web Assembly 增强 | 使用 WASM 重编核心算法,进一步提升性能 | 处理速度提升 5-10 倍 |
| 3D 内容支持 | 支持导出 WebGL、3D 模型等内容 | 扩展导出内容范围 |
| 实时协作集成 | 支持多人同时编辑的文档导出 | 满足协同办公需求 |
| 无障碍优化 | 自动生成符合无障碍标准的 PDF | 满足法规要求,提升包容性 |
开始使用 SnapDOM + jsPDF 的方案:
# 1. 安装依赖
npm install snappdom jspdf
# 2. 参考本文的实现示例
# 3. 根据业务需求进行定制化开发
# 4. 建立完善的测试覆盖
最佳实践建议:
通过本文的详细阐述,我们不仅提供了一个高质量的 HTML 转 PDF 技术实现,更重要的是展示了一种面向未来、可演进的技术架构思路。在 AI 技术快速发展的背景下,传统的前端任务正迎来智能化升级的历史机遇。SnapDOM + jsPDF 方案既是当前业务需求的优秀解答,也是通向更智能文档处理世界的桥梁。
技术的选择不应只看眼前,更要考量其长期演进潜力。这正是 SnapDOM + jsPDF 组合的核心价值所在——它为我们打下了坚实的地基,让我们能够在上面建造更加智能、高效的文档处理大厦。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online