政府网站信创环境富文本编辑器重构实录:从UEditor到自研解决方案的探索之路

一、项目背景与初始困境

2024年5月,我接到某政府部门的网站升级项目,核心需求是实现Word文档内容无损粘贴(需保留文字样式、表格、图片等元素),并明确要求适配信创国产化环境(麒麟操作系统+龙芯CPU+WPS生态)。原系统使用百度UEditor,但在信创环境下暴露出三大致命问题:

  1. 兼容性崩溃

    • 在麒麟V10系统上,UEditor的Flash插件无法加载
    • 粘贴Word内容时频繁出现样式错乱(字体丢失、颜色异常)
    • 图片粘贴后显示为空白占位符
  2. 信创生态断层

    • UEditor依赖的jQuery在龙芯CPU上性能下降60%
    • 后端PHP扩展php-office在国产中间件(如东方通)上存在兼容性问题
  3. 技术支持真空

    • 百度官方已停止维护UEditor
    • 信创环境下的技术问题在Stack Overflow上无解决方案
二、技术选型探索:信创环境下的特殊挑战

1. 备选方案评估

方案 信创适配度 Word粘贴质量 开发成本 风险点
TinyMCE 5 ★★☆☆☆ ★★★☆☆ 需重写图片处理模块
WangEditor 5 ★★★☆☆ ★★☆☆☆ 对复杂样式支持不足
自主开发 ★★★★★ ★★★★★ 需2个月开发周期
改写UEditor ★★★☆☆ ★★★☆☆ 历史债务多,维护成本高

2. 关键发现

  • 信创环境限制

    • 禁止使用任何闭源JavaScript库
    • 必须通过国产浏览器(360安全浏览器信创版)的兼容性认证
    • 后端需支持国产数据库(达梦/人大金仓)
  • Word粘贴技术本质

    • 现代浏览器通过Clipboard API获取Word的HTML片段
    • 关键在于解析mso-前缀的CSS样式和v:shape等Office特有标签
三、开发实施过程

1. 前端改造(Vue3实现)

// WordPasteEditor.vue
import { onMounted, ref } from 'vue'
import { parseWordHtml } from './word-parser' // 自定义Word解析器

export default {
  setup() {
    const editorContent = ref('')
    const isPasteProcessing = ref(false)

    // 监听系统粘贴事件
    const handlePaste = async (e) => {
      if (!e.clipboardData || !e.clipboardData.types.includes('text/html')) return
      
      isPasteProcessing.value = true
      try {
        // 获取Word的HTML片段
        const wordHtml = e.clipboardData.getData('text/html')
        
        // 信创环境特殊处理:移除Flash相关标签
        const cleanedHtml = wordHtml
          .replace(/]*>/gi, '')
          .replace(/]*>/gi, '')
        
        // 解析为Vue可渲染的VNode
        const parsedContent = await parseWordHtml(cleanedHtml)
        editorContent.value = parsedContent
      } catch (error) {
        console.error('Word解析失败:', error)
      } finally {
        isPasteProcessing.value = false
      }
    }

    onMounted(() => {
      document.addEventListener('paste', handlePaste)
    })

    return { editorContent, isPasteProcessing }
  }
}

2. 核心难题攻克:Word样式解析

// word-parser.js
export const parseWordHtml = (html) => {
  // 创建临时DOM容器
  const container = document.createElement('div')
  container.innerHTML = html

  // 处理信创环境特有的样式问题
  const styleFixes = [
    // 替换Office特有字体为信创支持字体
    { from: /font-family:"Microsoft YaHei"/g, to: 'font-family:"方正仿宋_GBK"' },
    // 修复表格边框样式
    { from: /border:none/g, to: 'border:1px solid #000' }
  ]

  styleFixes.forEach(({ from, to }) => {
    container.innerHTML = container.innerHTML.replace(from, to)
  })

  // 提取图片并转换为Base64(适配信创内网环境)
  const images = container.querySelectorAll('img')
  images.forEach(img => {
    if (img.src.startsWith('file://')) {
      // 本地文件处理(需用户授权)
      const fileInput = document.createElement('input')
      fileInput.type = 'file'
      // 实际项目中需通过弹窗引导用户上传
      img.src = '/placeholder-image.png'
    } else if (!img.src.startsWith('data:')) {
      // 外网图片需下载后转Base64
      fetchImageAsBase64(img.src).then(base64 => {
        img.src = base64
      })
    }
  })

  return container.innerHTML
}

3. 后端适配(PHP信创改造)

// api/upload.php(处理图片上传)
header('Content-Type: application/json');

// 信创环境安全校验
if (!in_array($_SERVER['HTTP_USER_AGENT'], [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    '360SE-XinCao/11.0.0.0' // 360信创版UA
])) {
    http_response_code(403);
    die('非法访问');
}

// 达梦数据库适配
function saveToDmdb($imageData, $fileName) {
    try {
        $db = new PDO('dm:host=localhost;port=5236', 'SYSDBA', 'SYSDBA');
        $stmt = $db->prepare("INSERT INTO ATTACHMENTS (NAME, DATA) VALUES (?, ?)");
        $stmt->bindParam(1, $fileName);
        $stmt->bindParam(2, $imageData, PDO::PARAM_LOB);
        return $stmt->execute();
    } catch (PDOException $e) {
        error_log("达梦数据库错误: " . $e->getMessage());
        return false;
    }
}

// 处理Base64图片
if (isset($_POST['image_base64'])) {
    $imageData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $_POST['image_base64']));
    $fileName = 'attach_' . uniqid() . '.png';
    
    if (saveToDmdb($imageData, $fileName)) {
        echo json_encode(['url' => "/attachments/$fileName"]);
    } else {
        http_response_code(500);
    }
}

4. 信创环境特殊处理

  • 字体适配方案

    /* 强制使用信创支持字体栈 */
    .editor-content {
      font-family: "方正仿宋_GBK", "方正楷体_GBK", "思源黑体 CN", sans-serif !important;
    }
    
  • 浏览器兼容性补丁

    // 修复360信创版Clipboard API的bug
    if (navigator.userAgent.includes('360SE-XinCao')) {
      const nativePaste = HTMLDocument.prototype.paste;
      HTMLDocument.prototype.paste = function() {
        setTimeout(() => {
          // 延迟处理粘贴内容
          const event = new Event('customPaste')
          document.dispatchEvent(event)
        }, 100)
        nativePaste.apply(this, arguments)
      }
    }
    
四、测试与部署

1. 信创环境测试矩阵

测试项 麒麟V10+龙芯 统信UOS+飞腾 中标麒麟+兆芯
Word粘贴完整性 98% 95% 92%
图片上传成功率 100% 98% 95%
样式保留准确率 92% 89% 85%

2. 性能优化措施

  • 大文件分块处理

    // 分块上传Word文档(超过10MB时触发)
    async function uploadLargeDocument(file) {
      const chunkSize = 5 * 1024 * 1024 // 5MB分块
      const chunks = Math.ceil(file.size / chunkSize)
      
      for (let i = 0; i < chunks; i++) {
        const blob = file.slice(i * chunkSize, (i + 1) * chunkSize)
        await uploadChunk(blob, i, chunks)
      }
    }
    
  • PHP内存管理

    ; php.ini信创专项配置
    memory_limit = 256M
    max_input_vars = 3000
    realpath_cache_size = 4096K
    
五、项目总结与经验教训
  1. 信创开发黄金法则

    • 先验证后开发:在真实信创环境(而非模拟器)测试每个功能
    • 字体优先策略:所有样式设计必须基于信创系统预装字体
    • 离线优先设计:避免依赖外网CDN和在线服务
  2. Word粘贴技术要点

    • 必须处理等Office特有标签
    • 表格样式需通过border-collapse: collapse强制统一
    • 图片需提供Base64和文件上传双模式
  3. 独立开发者生存建议

    • 建立自己的信创测试环境(成本约¥15,000)
    • 与本地信创厂商建立技术支持通道
    • 对政府项目预留30%的不可预见成本

最终成果:新系统在客户信创环境中稳定运行1个月,处理Word文档1,200余份,样式保留准确率达到91%,获得客户"信创适配优秀案例"表彰。这次经历让我深刻认识到:在信创领域,技术选型必须遵循"可用性>先进性"的原则,有时候最简单的解决方案反而最可靠。目前正在将该方案封装为Vue组件库,计划在政府内部技术社区共享。

复制插件文件

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转图片

上传网络图片

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

下载示例

点击下载完整示例

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐