《一个独立开发者的"Word粘贴大作战":从抓狂到真香的技术冒险》
——上海野生程序员的血泪实录
第一章:当甲方爸爸说"要能粘贴Word图片"时,我的表情是这样的
事情是这样的:我独自开发了一个网站(前端Vue2 + wangEditor,后端PHP + MySQL,服务器在阿里云),结果甲方突然提出灵魂三连:
- “用户要能直接从Word粘贴内容,图片自动上传到服务器!”
- “还要支持导入Word/Excel/PPT/PDF,图片不能丢,样式不能变!”
- “对了,后端得用对象存储(阿里云/腾讯云/华为云…都行)!”
我当时的内心OS:“您直接说想要个Office 365得了?”
但作为独立开发者,我只能默默打开GitHub,开始我的"技术考古"之旅…
第二章:前端篇——wangEditor的"粘贴魔法"
1. 拦截粘贴事件:和Word的"脏HTML"斗智斗勇
wangEditor默认粘贴Word内容会带一堆、
等Office专属标签,直接显示会乱码。我的解决方案:
// 在wangEditor配置中拦截paste事件
editor.config.pasteFilterStyle = false; // 先允许样式
editor.config.pasteIgnoreImg = false; // 不忽略图片
editor.config.customPaste = (editor, html) => {
// 1. 清理Office冗余标签
let cleanHtml = html.replace(/<\/o:p>/g, '')
.replace(//g, '');
// 2. 提取图片并上传(后面细说)
return cleanHtml;
};
2. 图片自动上传:从Base64到Blob的逆袭
Word粘贴的图片默认是data:image/png;base64
格式,直接存数据库会炸(Base64比二进制大33%!)。我的操作:
// 提取Base64图片并上传
const extractImages = (html) => {
const div = document.createElement('div');
div.innerHTML = html;
const imgs = div.querySelectorAll('img[src^="data:image"]');
imgs.forEach(img => {
const base64 = img.src.split(',')[1];
const blob = base64ToBlob(base64); // 自定义转换函数
// 调用后端API上传
fetch('/api/upload', { method: 'POST', body: blob })
.then(res => res.json())
.then(data => {
img.src = data.url; // 替换为服务器URL
});
});
return div.innerHTML;
};
效果:用户粘贴Word后,图片自动上传到服务器,编辑器里显示的是可访问的URL,样式(字体、颜色)完美保留!
第三章:后端篇——PHP和对象存储的"相爱相杀"
1. 接收图片:从Blob到文件存储
PHP处理上传的Blob需要点技巧:
// api/upload.php
$input = file_get_contents('php://input');
$filename = uniqid() . '.png';
file_put_contents('/tmp/' . $filename, $input);
// 调用对象存储SDK(以阿里云OSS为例)
require_once 'aliyun-oss-sdk.php';
$ossClient = new OSS\OssClient('key', 'secret', 'endpoint');
$ossClient->putObject('your-bucket', 'uploads/' . $filename, fopen('/tmp/' . $filename, 'r'));
echo json_encode(['url' => 'https://your-bucket.oss-cn-shanghai.aliyuncs.com/uploads/' . $filename]);
2. 文档导入:Word/Excel/PPT/PDF全制霸
这里我用了Unoconv(LibreOffice的命令行工具)转换文档为HTML:
// 导入Word/Excel/PPT
function convertToHtml($filePath) {
$outputPath = '/tmp/' . basename($filePath, '.docx') . '.html';
exec("unoconv -f html -o /tmp $filePath");
return file_get_contents($outputPath);
}
// 导入PDF(用pdf2htmlEX)
function convertPdfToHtml($filePath) {
$outputPath = '/tmp/' . basename($filePath, '.pdf') . '.html';
exec("pdf2htmlEX --zoom 1.3 $filePath $outputPath");
return file_get_contents($outputPath);
}
坑点:
- Unoconv需要安装LibreOffice,服务器上配置略麻烦
- PDF转换后的HTML可能很乱,需要用CSS重置样式
第四章:同行交流——QQ群里的"技术互救"
在开发过程中,我加了几个技术群(比如QQ群:223813913),发现大家的问题出奇地一致:
- A君:“Word粘贴的图片怎么去掉EXIF信息?”
- B妹:“Excel导入后表格线全没了怎么办?”
- C大佬:“推荐用Mammoth.js处理Word,比Unoconv轻量!”
我的感悟:独立开发不孤单,群里问一句能省半天调试时间!
最终成果:甲方爸爸满意,我也能睡个好觉了
现在网站支持:
✅ Word粘贴:内容+图片自动上传,样式保留
✅ 多文档导入:Word/Excel/PPT/PDF一键转换
✅ 对象存储:图片存阿里云OSS,速度飞起
技术栈总结:
- 前端:Vue2 + wangEditor(粘贴拦截+图片上传)
- 后端:PHP(文件处理+对象存储SDK)
- 工具:Unoconv(文档转换)+ pdf2htmlEX(PDF处理)
最后广告:
如果你也在搞富文本编辑器+Word粘贴,欢迎加入QQ群:223813913,一起吐槽技术难题,分享解决方案!
(完)
——上海野生程序员·老张
复制插件文件
安装jquery
npm install jquery
导入组件
import E from 'wangeditor'
const { $, BtnMenu, DropListMenu, PanelMenu, DropList, Panel, Tooltip } = E
import {WordPaster} from '../../static/WordPaster/js/w'
import {zyCapture} from '../../static/zyCapture/z'
import {zyOffice} from '../../static/zyOffice/js/o'
初始化组件
//zyCapture Button
class zyCaptureBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="截屏">
<img src="../../static/zyCapture/z.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
window.zyCapture.setEditor(this.editor).Capture();
}
tryChangeActive() {this.active()}
}
//zyOffice Button
class importWordBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入Word文档(docx)">
<img src="../../static/zyOffice/css/w.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
window.zyOffice.SetEditor(this.editor).api.openDoc();
}
tryChangeActive() {this.active()}
}
//zyOffice Button
class exportWordBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导出Word文档(docx)">
<img src="../../static/zyOffice/css/exword.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
window.zyOffice.SetEditor(this.editor).api.exportWord();
}
tryChangeActive() {this.active()}
}
//zyOffice Button
class importPdfBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入PDF文档">
<img src="../../static/zyOffice/css/pdf.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
window.zyOffice.SetEditor(this.editor).api.openPdf();
}
tryChangeActive() {this.active()}
}
//WordPaster Button
class WordPasterBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="Word一键粘贴">
<img src="../../static/WordPaster/w.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).Paste();
}
tryChangeActive() {this.active()}
}
//wordImport Button
class WordImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入Word文档">
<img src="../../static/WordPaster/css/doc.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).importWord();
}
tryChangeActive() {this.active()}
}
//excelImport Button
class ExcelImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入Excel文档">
<img src="../../static/WordPaster/css/xls.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).importExcel();
}
tryChangeActive() {this.active()}
}
//ppt paster Button
class PPTImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入PPT文档">
<img src="../../static/WordPaster/css/ppt1.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).importPPT();
}
tryChangeActive() {this.active()}
}
//pdf paster Button
class PDFImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="导入PDF文档">
<img src="../../static/WordPaster/css/pdf.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor);
WordPaster.getInstance().ImportPDF();
}
tryChangeActive() {this.active()}
}
//importWordToImg Button
class ImportWordToImgBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="Word转图片">
<img src="../../static/WordPaster/word1.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor).importWordToImg();
}
tryChangeActive() {this.active()}
}
//network paster Button
class NetImportBtn extends BtnMenu {
constructor(editor) {
const $elem = E.$(
`<div class="w-e-menu" data-title="网络图片一键上传">
<img src="../../static/WordPaster/net.png"/>
</div>`
)
super($elem, editor)
}
clickHandler() {
WordPaster.getInstance().SetEditor(this.editor);
WordPaster.getInstance().UploadNetImg();
}
tryChangeActive() {this.active()}
}
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
mounted(){
var editor = new E('#editor');
WordPaster.getInstance({
//上传接口:http://www.ncmem.com/doc/view.aspx?id=d88b60a2b0204af1ba62fa66288203ed
PostUrl: "http://localhost:8891/upload.aspx",
License2:"",
//为图片地址增加域名:http://www.ncmem.com/doc/view.aspx?id=704cd302ebd346b486adf39cf4553936
ImageUrl:"http://localhost:8891{url}",
//设置文件字段名称:http://www.ncmem.com/doc/view.aspx?id=c3ad06c2ae31454cb418ceb2b8da7c45
FileFieldName: "file",
//提取图片地址:http://www.ncmem.com/doc/view.aspx?id=07e3f323d22d4571ad213441ab8530d1
ImageMatch: ''
});
zyCapture.getInstance({
config: {
PostUrl: "http://localhost:8891/upload.aspx",
License2: '',
FileFieldName: "file",
Fields: { uname: "test" },
ImageUrl: 'http://localhost:8891{url}'
}
})
// zyoffice,
// 使用前请在服务端部署zyoffice,
// http://www.ncmem.com/doc/view.aspx?id=82170058de824b5c86e2e666e5be319c
zyOffice.getInstance({
word: 'http://localhost:13710/zyoffice/word/convert',
wordExport: 'http://localhost:13710/zyoffice/word/export',
pdf: 'http://localhost:13710/zyoffice/pdf/upload'
})
// 注册菜单
E.registerMenu("zyCaptureBtn", zyCaptureBtn)
E.registerMenu("WordPasterBtn", WordPasterBtn)
E.registerMenu("ImportWordToImgBtn", ImportWordToImgBtn)
E.registerMenu("NetImportBtn", NetImportBtn)
E.registerMenu("WordImportBtn", WordImportBtn)
E.registerMenu("ExcelImportBtn", ExcelImportBtn)
E.registerMenu("PPTImportBtn", PPTImportBtn)
E.registerMenu("PDFImportBtn", PDFImportBtn)
E.registerMenu("importWordBtn", importWordBtn)
E.registerMenu("exportWordBtn", exportWordBtn)
E.registerMenu("importPdfBtn", importPdfBtn)
//挂载粘贴事件
editor.txt.eventHooks.pasteEvents.length=0;
editor.txt.eventHooks.pasteEvents.push(function(){
WordPaster.getInstance().SetEditor(editor).Paste();
e.preventDefault();
});
editor.create();
var edt2 = new E('#editor2');
//挂载粘贴事件
edt2.txt.eventHooks.pasteEvents.length=0;
edt2.txt.eventHooks.pasteEvents.push(function(){
WordPaster.getInstance().SetEditor(edt2).Paste();
e.preventDefault();
return;
});
edt2.create();
}
}
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
测试前请配置图片上传接口并测试成功
接口测试
接口返回JSON格式参考
为编辑器添加按钮
components: { Editor, Toolbar },
data () {
return {
editor: null,
html: 'dd',
toolbarConfig: {
insertKeys: {
index: 0,
keys: ['zycapture', 'wordpaster', 'pptimport', 'pdfimport', 'netimg', 'importword', 'exportword', 'importpdf']
}
},
editorConfig: {
placeholder: ''
},
mode: 'default' // or 'simple'
}
},
整合效果
导入Word文档,支持doc,docx
导入Excel文档,支持xls,xlsx
粘贴Word
一键粘贴Word内容,自动上传Word中的图片,保留文字样式。
Word转图片
一键导入Word文件,并将Word文件转换成图片上传到服务器中。
导入PDF
一键导入PDF文件,并将PDF转换成图片上传到服务器中。
导入PPT
一键导入PPT文件,并将PPT转换成图片上传到服务器中。
上传网络图片
一键自动上传网络图片,自动下载远程服务器图片,自动上传远程服务器图片