wangEditor支持信创系统word粘贴兼容处理

Word图片一键转存功能开发全记录:从需求调研到技术落地

一、需求背景与技术选型

作为软件工程专业大二学生,暑期独立开发"Word图片一键转存系统"的初衷源于两个痛点:

  1. 学术场景中,教师需手动提取学生提交的Word文档中的实验图片进行归档
  2. 企业OA系统中,Word格式的报销单需分离发票图片进行财务审核

技术栈选型依据

组件技术选型决策依据
前端框架Vue2-cli学校课程已系统学习,社区生态成熟,适合快速原型开发
富文本编辑器wangEditor轻量级(核心包仅200KB),支持自定义图片上传钩子,与Vue2兼容性良好
后端语言PHP阿里云ECS服务器预装LAMP环境,可直接部署,且PHP对文件流处理效率较高
数据库MySQL与PHP天然适配,支持BLOB类型存储二进制图片,但考虑性能最终采用文件系统存储
服务器阿里云ECS学生优惠套餐(1核2G 1Mbps带宽),提供安全组、RDS等企业级功能

二、核心功能开发历程

1. Word图片提取技术调研

通过分析知乎专栏《一次性提取word文件中的图片》和微信公众平台《Word图片如何批量转存?》,确定两种技术路线:

  • 压缩包解压法:将.docx重命名为.zip,解压后从word/media/目录提取图片
  • HTML转换法:使用另存为HTML功能,图片会保存在生成的文件夹中

技术验证
在本地使用Node.js编写测试脚本:

const fs = require('fs');
const AdmZip = require('adm-zip');

function extractFromZip(docxPath) {
  const zip = new AdmZip(docxPath);
  const mediaEntries = zip.getEntries().filter(entry => 
    entry.entryName.startsWith('word/media/')
  );
  return mediaEntries.map(entry => zip.readFile(entry));
}

2. 前端实现(Vue2 + wangEditor)

2.1 编辑器集成

参考wangEditor官方文档和CSDN博客《wangEditor的使用》,实现核心配置:

// editor.config.js
import { createEditor, createToolbar } from '@wangeditor/editor'

export default {
  initEditor(domId) {
    const editorConfig = {
      placeholder: '请上传Word文档...',
      MENU_CONF: {
        uploadImage: {
          server: '/api/upload', // 实际使用PHP接口
          fieldName: 'word_file',
          maxFileSize: 10 * 1024 * 1024, // 10MB
          allowedFileTypes: ['.docx'],
          customInsert(res, editor) {
            // 解析PHP返回的图片URL数组
            const urls = res.data.map(img => ``)
            editor.cmd.do('insertHTML', urls.join(''))
          }
        }
      }
    }
    return createEditor({
      selector: `#${domId}`,
      config: editorConfig,
      mode: 'default'
    })
  }
}
2.2 文件处理流程
  1. 用户通过工具栏上传Word文件
  2. 前端使用FormData封装请求:
async handleUpload(file) {
  const formData = new FormData()
  formData.append('word_file', file)
  
  try {
    const res = await axios.post('/api/parse', formData, {
      headers: { 'Content-Type': 'multipart/form-data' }
    })
    this.editor.cmd.do('insertHTML', res.data.map(url => 
      ``
    ).join(''))
  } catch (e) {
    this.$message.error('解析失败:' + e.message)
  }
}

3. 后端实现(PHP + MySQL)

3.1 文件解析服务

参考阿里云部署教程搭建PHP环境后,编写核心处理逻辑:

// api/parse.php
header('Content-Type: application/json');
$uploadDir = '/tmp/word_uploads/';
if (!file_exists($uploadDir)) mkdir($uploadDir, 0777, true);

$fileName = uniqid() . '.docx';
$filePath = $uploadDir . $fileName;

if (move_uploaded_file($_FILES['word_file']['tmp_name'], $filePath)) {
    $zip = new ZipArchive();
    if ($zip->open($filePath) === TRUE) {
        $images = [];
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $entry = $zip->getNameIndex($i);
            if (preg_match('/^word\/media\/image\d+\.(jpeg|jpg|png|gif)$/i', $entry)) {
                $imageData = $zip->getFromIndex($i);
                $imagePath = '/uploads/' . uniqid() . '.' . pathinfo($entry, PATHINFO_EXTENSION);
                file_put_contents($_SERVER['DOCUMENT_ROOT'] . $imagePath, $imageData);
                $images[] = $imagePath;
            }
        }
        $zip->close();
        unlink($filePath); // 删除临时文件
        echo json_encode(['code' => 0, 'data' => $images]);
    } else {
        echo json_encode(['code' => 500, 'msg' => '文件解压失败']);
    }
} else {
    echo json_encode(['code' => 400, 'msg' => '文件上传失败']);
}
3.2 数据库设计

采用MySQL存储图片元信息(实际存储路径):

CREATE TABLE `word_images` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `original_filename` varchar(255) NOT NULL,
  `storage_path` varchar(512) NOT NULL,
  `upload_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `doc_id` varchar(64) DEFAULT NULL COMMENT '关联文档ID',
  PRIMARY KEY (`id`),
  KEY `idx_doc_id` (`doc_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4. 阿里云部署优化

  1. 安全组配置:开放80/443端口,限制源IP为学校教育网段
  2. OSS对象存储:将图片从ECS本地存储迁移至OSS,配置CDN加速
  3. 性能优化
    • 启用PHP-FPM进程池
    • 配置MySQL查询缓存
    • 使用Nginx的gzip_static压缩静态资源

三、技术难点突破

1. 大文件处理内存溢出

问题:当上传超过50MB的Word文件时,PHP脚本因内存不足终止
解决方案

  • 修改php.ini配置:
    memory_limit = 256M
    upload_max_filesize = 100M
    post_max_size = 100M
    
  • 采用流式处理替代完全加载到内存(需升级至PHP 7.4+使用ZipArchive的流式接口)

2. 图片排序混乱

问题:提取的图片顺序与Word文档中的排版不一致
解决方案

  • 解析Word的[Content_Types].xml获取图片索引顺序
  • 修改PHP处理逻辑:
// 按文件名数字排序(如image1.jpg, image2.jpg...)
usort($images, function($a, $b) {
    preg_match('/image(\d+)/', basename($a), $matchesA);
    preg_match('/image(\d+)/', basename($b), $matchesB);
    return $matchesA[1] - $matchesB[1];
});

四、同行交流与资源整合

1. 加入专业社群

  • QQ群:223813913(Word文档处理技术交流)
    • 获取到docx4j的Java实现参考,但因环境限制最终采用PHP方案
    • 了解到Apache POI的XWPF组件,为后续Java版本开发预留接口

2. 开源项目参考

  • 百度UEditor:其Word图片转存插件(参考荆门泽优软件文档)提供前端按钮集成思路
  • wangEditor官方示例:学习到多编辑器实例管理技巧,实现主从编辑器联动

3. 性能测试数据

在阿里云1核2G环境下测试:

并发数平均响应时间成功率
101.2s100%
503.8s92%
1008.5s78%

优化措施

  • 引入Redis缓存解析结果(针对重复文档)
  • 对大文件启用异步处理队列(RabbitMQ)

五、项目成果与展望

1. 当前成果

  • 实现Word文档中图片的无损提取(支持JPEG/PNG/GIF格式)
  • 前端支持拖拽上传、实时预览、批量下载
  • 后端提供RESTful API,可被其他系统集成

2. 后续规划

  1. 功能扩展
    • 增加PDF图片提取支持(结合PDF.js)
    • 实现图片OCR文字识别(集成百度AI接口)
  2. 技术升级
    • 迁移至Vue3+Vite架构
    • 后端改用Go语言提升并发性能
  3. 商业化探索
    • 申请软件著作权(已准备材料)
    • 开发企业版(增加权限管理、审计日志)

开发感悟
这个项目让我深刻体会到:

  1. 技术选型要平衡学习成本与生产需求
  2. 文件处理类项目必须做好异常流程设计(如断点续传、文件校验)
  3. 参与开源社区交流能获得远超预期的技术支持

(附:项目GitHub仓库地址及在线演示链接)

复制插件文件

WordPaster插件文件夹
安装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'
    }
  },

整合效果

wangEditor4整合效果

导入Word文档,支持doc,docx

粘贴Word和图片

导入Excel文档,支持xls,xlsx

粘贴Word和图片

粘贴Word

一键粘贴Word内容,自动上传Word中的图片,保留文字样式。
粘贴Word和图片

Word转图片

一键导入Word文件,并将Word文件转换成图片上传到服务器中。
导入Word转图片

导入PDF

一键导入PDF文件,并将PDF转换成图片上传到服务器中。
导入PDF转图片

导入PPT

一键导入PPT文件,并将PPT转换成图片上传到服务器中。
导入PPT转图片

上传网络图片

一键自动上传网络图片,自动下载远程服务器图片,自动上传远程服务器图片
自动上传网络图片

下载示例

点击下载完整示例

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值