2023年8月14日 周一 需求确认阶段
作为前端开发团队的核心成员,我接到了来自产品总监的一项紧急任务:
在现有的金融业务平台中集成 Word 和 PDF 文档的导入能力,需确保文档中的表格样式、企业LOGO以及金融相关图表能够完整保留。
当前系统的整体架构如下:
在需求评审会上,我特别指出:“关键挑战在于样式的高保真还原,尤其是那些包含条件格式的财务报表和矢量型图表。” 技术总监随即补充道:“项目周期为三周,最终方案必须通过金融级压力测试——客户可能上传长达200页的招股说明书。”
8月15日 - 8月17日 开源解决方案调研期
第一天:TinyMCE 生态兼容性分析
我们首先对 TinyMCE 的插件生态进行了评估:
测试了某商业插件
tinymce-powerpaste,虽然其样式保留效果良好,但每年599美元的授权费用超出了预算范围。
随后尝试使用开源库
mammoth.js 进行文档结构提取,发现它无法有效处理复杂嵌套表格和图像元素。
进一步实验
docx-preview 所生成的 HTML 内容与 TinyMCE 存在兼容问题,导致多层嵌套表格出现严重错位现象。
第二天:PDF 解析的技术瓶颈
采用
pdf.js 实现页面渲染时,导入内容在编辑器内出现了明显的布局偏移。
而通过
pdf2htmlEX 转换后的 HTML 包含大量冗余的 `` 标签,造成富文本编辑器响应迟缓甚至卡顿。
测试
Apache PDFBox 显示,后端单页 PDF 处理耗时高达3秒,性能难以满足实际应用场景。
第三天:混合架构初步尝试
我们尝试构建一种前后端协同的处理模式:
mammoth.js 提取文本内容及图片元数据Apache POI 处理复杂的排版逻辑但在实施过程中暴露出跨域问题——当图片上传至七牛云存储时,临时 token 经常失效,影响文件传输稳定性。
8月18日 技术路径重大突破
凌晨两点,在重新查阅 TinyMCE 官方文档的过程中,我意识到一个全新的解决思路:
是否可以将文档内容进行分层解析与处理?
最终确定的整体架构设计如下:
1. 前端预处理层
[table:finance])用于后续映射2. 后端处理层
3. 编辑器适配层
paste[table:finance] → 对应预设样式表)8月21日 - 8月25日 核心模块编码实现
前端部分(Vue组件实现)
// DocxImporter.vue
export default {
methods: {
async handleFile(file) {
// 1. 文件类型校验
if (!file.name.match(/\.(docx|pdf)$/)) {
this.$message.error('仅支持docx/pdf格式');
return;
}
// 2. 启动Web Worker进行异步解析
const worker = new Worker('./docx-parser.worker.js');
worker.postMessage({ file });
// 3. 监听进度与结果反馈
worker.onmessage = (e) => {
if (e.data.type === 'progress') {
this.progress = e.data.value;
} else if (e.data.type === 'result') {
this.insertToEditor(e.data.html);
}
};
},
insertToEditor(html) {
// 金融级样式增强处理
const enhancedHtml = html
.replace(/<table/g, '<table class="financial-table"')
.replace(/<img/g, '<img class="embedded-chart"');
// 插入到TinyMCE编辑器
this.$refs.editor.execCommand('mceInsertContent', false, enhancedHtml);
}
}
}
Web Worker 中的文档解析流程(独立线程执行)
self.addEventListener('message', async (e) => {
const { file } = e.data;
// 1. 获取文件ArrayBuffer
const buffer = await file.arrayBuffer();
// 2. 使用mammoth进行基础内容提取
const result = await mammoth.extractRawText({ arrayBuffer: buffer });
// 3. 自定义图片提取处理器
const images = [];
result.messages.forEach(msg => {
if (msg.type === 'warning' && msg.message.includes('image')) {
const imageId = msg.message.match(/image-(\d+)/)[1];
images.push(extractImage(buffer, imageId));
}
});
// 4. 并发上传图片至OSS
const imageUrls = await Promise.all(
images.map(img => uploadToOSS(img))
);
// 5. 替换HTML中的占位图标记
let html = result.value;
imageUrls.forEach((url, idx) => {
html = html.replace(`src="image-${idx}"`, `src="${url}"`);
});
// 6. 发送处理完成的消息回主线程
self.postMessage({
type: 'result',
html: html
});
});
为解决大文件在传输过程中因网络波动或请求超时导致的失败,采用分片上传与断点续传机制。
通过将文件切分为多个固定大小的数据块(默认每片5MB),并行发送至服务端,显著提升上传成功率和整体效率。同时利用文件哈希值标识唯一文件,支持中断后从断点恢复上传。
针对大文档解析过程中可能出现的JVM堆内存溢出问题,引入流式处理机制,避免将整个文件加载到内存中。
上传的文件首先被写入临时存储路径,随后通过输入流逐段读取并解析内容,实现低内存占用下的高效转换。处理完成后自动清理临时资源,保障系统稳定性。
@PostMapping("/import")
public ResponseEntity importDocument(@RequestParam("file") MultipartFile file) {
try {
// 保存至临时文件
Path tempFile = Files.createTempFile("doc-", ".tmp");
file.transferTo(tempFile.toFile());
// 使用流式方式解析
try (InputStream is = Files.newInputStream(tempFile)) {
String html = documentParser.parse(is);
String sanitized = sanitizer.sanitize(html);
return ResponseEntity.ok(sanitized);
}
} finally {
// 清理临时文件
// ...
}
}
为确保用户提交的内容不包含恶意脚本或非法结构,服务端集成基于Jsoup的HTML净化组件,执行多层级安全过滤。
@Service
public class DocumentSanitizer {
// 定义允许使用的样式类
private static final Set<String> ALLOWED_CLASSES =
Set.of("finance-table", "finance-title", "chart-container");
public String sanitize(String html) {
Document doc = Jsoup.parse(html);
// 删除高危标签
doc.select("script, iframe, object, embed, form, input").remove();
// 过滤非法class属性
doc.select("*").forEach(element -> {
String classAttr = element.attr("class");
if (!classAttr.isEmpty()) {
String[] classes = classAttr.split("\\s+");
List<String> validClasses = Arrays.stream(classes)
.filter(ALLOWED_CLASSES::contains)
.collect(Collectors.toList());
element.attr("class", String.join(" ", validClasses));
}
});
// 为表格添加必要属性
doc.select("table")
.attr("border", "1")
.attr("cellspacing", "0")
.attr("cellpadding", "5");
return doc.html();
}
}
构建多层次安全防线,覆盖从前端到网关再到服务端的完整链路,防范各类常见攻击与异常输入。
结合前端预处理与后端清洗流程,全面防御跨站脚本攻击。前端剥离非必要标签,后端再次进行严格净化,确保输出内容符合金融级安全标准。
// 前端内容转义处理函数
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """")
.replace(/'/g, "'");
}
为确保文档导入过程可追溯,系统建立完整的操作记录机制:
CREATE TABLE document_import_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(32) NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_size BIGINT NOT NULL,
ip_address VARCHAR(45) NOT NULL,
import_result TINYINT NOT NULL COMMENT '0:成功 1:失败 2:部分成功',
error_message TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
测试用例1:200页招股说明书
- 导入耗时:3分15秒(满足原定≤5分钟要求)
- 样式保留率:92%(复杂图表需人工微调)
- 图片完整率:100%
测试用例2:含宏的Word文档
系统自动拦截并提示:“检测到宏内容,已自动清除”
测试用例3:PDF表单文件
文本内容提取成功,表单控件转换为静态图片呈现
当系统顺利完成某券商300页IPO材料的导入任务时,测试总监感慨道:“其稳定性甚至超过专业文档转换工具。” 此刻,所有通宵调试的辛劳都转化为强烈的成就感——团队不仅实现了既定目标,更重新设定了金融行业文档处理的技术标杆。
npm install jquery
在Vue组件中引入相关模块:
// 引入富文本编辑器及扩展功能
import Editor from '@tinymce/tinymce-vue'
import {WordPaster} from '../../static/WordPaster/js/w'
import {zyOffice} from '../../static/zyOffice/js/o'
import {zyCapture} from '../../static/zyCapture/z'
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor).importExcel()
}
var register$1 = function (editor) {
editor.ui.registry.addButton('excelimport', {
text: '',
tooltip: '导入Excel文档',
onAction: function () {
selectLocalImages(editor)
}
});
editor.ui.registry.addMenuItem('excelimport', {
text: '',
tooltip: '导入Excel文档',
onAction: function () {
selectLocalImages(editor)
}
});
};
var Buttons = { register: register$1 };
function Plugin () {
global.add('excelimport', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor);
WordPaster.getInstance().importWordToImg()
}
// 导入PDF文档功能插件定义
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor);
WordPaster.getInstance().ImportPDF();
}
var register$1 = function (editor) {
editor.ui.registry.addButton('pdfimport', {
text: '',
tooltip: '导入pdf文档',
onAction: function () {
selectLocalImages(editor);
}
});
editor.ui.registry.addMenuItem('pdfimport', {
text: '',
tooltip: '导入pdf文档',
onAction: function () {
selectLocalImages(editor);
}
});
};
var Buttons = { register: register$1 };
function Plugin() {
global.add('pdfimport', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// 添加Word转图片功能按钮
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor);
WordPaster.getInstance().UploadNetImg();
}
var register$1 = function (editor) {
editor.ui.registry.addButton('netpaster', {
text: '',
tooltip: '网络图片一键上传',
onAction: function () {
selectLocalImages(editor);
}
});
editor.ui.registry.addMenuItem('netpaster', {
text: '',
tooltip: '网络图片一键上传',
onAction: function () {
selectLocalImages(editor);
}
});
};
var Buttons = { register: register$1 };
function Plugin() {
global.add('netpaster', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// 实现PPT文件导入功能
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor);
WordPaster.getInstance().importPPT();
}
var register$1 = function (editor) {
editor.ui.registry.addButton('importwordtoimg', {
text: '',
tooltip: 'Word转图片',
onAction: function () {
selectLocalImages(editor);
}
});
editor.ui.registry.addMenuItem('importwordtoimg', {
text: '',
tooltip: 'Word转图片',
onAction: function () {
selectLocalImages(editor);
}
});
};
var Buttons = { register: register$1 };
function Plugin() {
global.add('importwordtoimg', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// 添加导入PowerPoint按钮
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor).importPPT();
}
var register$1 = function (editor) {
editor.ui.registry.addButton('pptimport', {
text: '',
tooltip: '导入PowerPoint文档',
onAction: function () {
selectLocalImages(editor);
}
});
editor.ui.registry.addMenuItem('pptimport', {
text: '',
tooltip: '导入PowerPoint文档',
onAction: function () {
selectLocalImages(editor);
}
});
};
var Buttons = { register: register$1 };
function Plugin() {
global.add('pptimport', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// 添加导入Word按钮
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor).importWord();
}
var register$1 = function (editor) {
editor.ui.registry.addButton('wordimport', {
text: '',
tooltip: '导入Word文档',
onAction: function () {
selectLocalImages(editor);
}
});
editor.ui.registry.addMenuItem('wordimport', {
text: '',
tooltip: '导入Word文档',
onAction: function () {
selectLocalImages(editor);
}
});
};
var Buttons = { register: register$1 };
function Plugin() {
global.add('wordimport', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
// 添加Word一键粘贴按钮
(function () {
'use strict';
var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
var ico = "http://localhost:8080/static/WordPaster/plugin/word.png";
function selectLocalImages(editor) {
WordPaster.getInstance().SetEditor(editor).PasteManual();
}
var register$1 = function (editor) {
editor.ui.registry.addButton('wordpaster', {
text: '',
tooltip: 'Word一键粘贴',
onAction: function () {
selectLocalImages(editor);
}
});
editor.ui.registry.addMenuItem('wordpaster', {
text: '',
tooltip: 'Word一键粘贴',
onAction: function () {
selectLocalImages(editor);
}
});
};
var Buttons = { register: register$1 };
function Plugin() {
global.add('wordpaster', function (editor) {
Buttons.register(editor);
});
}
Plugin();
}());
在线代码示例:
配置插件参数
plugins: {
type: [String, Array],
初始化组件配置
default: 'autoresize code autolink autosave image imagetools paste preview table powertables' },
组件初始化代码如下:
WordPaster.getInstance({
PostUrl: 'http://localhost:8891/upload.aspx',
ImageUrl: 'http://localhost:8891{url}',
FileFieldName: 'file',
ImageMatch: ''
})
在页面中引入该组件后,编辑器将具备多种高效文档处理能力。
提供一键粘贴Word内容的功能,系统会自动识别并上传文档中的所有图片,同时保留原有的文字样式和排版结构。
支持将Word、PDF、PPT等办公文档一键转换为图片格式,并直接上传至服务器,便于内容展示与管理。
当用户在编辑器中粘贴或插入网络图片时,系统可实现自动抓取并重新上传至本地服务器,避免外链失效问题。
点击下载完整功能演示示例包,包含前端调用代码与后端接收接口参考实现。
扫码加好友,拉您进群



收藏
