教育行业站群如何配置支持微信公众号内容转存的网页编辑器?
信创开发黄金法则先验证后开发:在真实信创环境(而非模拟器)测试每个功能字体优先策略:所有样式设计必须基于信创系统预装字体离线优先设计:避免依赖外网CDN和在线服务Word粘贴技术要点必须处理等Office特有标签表格样式需通过强制统一图片需提供Base64和文件上传双模式独立开发者生存建议建立自己的信创测试环境(成本约¥15,000)与本地信创厂商建立技术支持通道对政府项目预留30%的不可预见成本
政府网站信创环境富文本编辑器重构实录:从UEditor到自研解决方案的探索之路
一、项目背景与初始困境
2024年5月,我接到某政府部门的网站升级项目,核心需求是实现Word文档内容无损粘贴(需保留文字样式、表格、图片等元素),并明确要求适配信创国产化环境(麒麟操作系统+龙芯CPU+WPS生态)。原系统使用百度UEditor,但在信创环境下暴露出三大致命问题:
-
兼容性崩溃:
- 在麒麟V10系统上,UEditor的Flash插件无法加载
- 粘贴Word内容时频繁出现样式错乱(字体丢失、颜色异常)
- 图片粘贴后显示为空白占位符
-
信创生态断层:
- UEditor依赖的jQuery在龙芯CPU上性能下降60%
- 后端PHP扩展
php-office在国产中间件(如东方通)上存在兼容性问题
-
技术支持真空:
- 百度官方已停止维护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
五、项目总结与经验教训
-
信创开发黄金法则:
- 先验证后开发:在真实信创环境(而非模拟器)测试每个功能
- 字体优先策略:所有样式设计必须基于信创系统预装字体
- 离线优先设计:避免依赖外网CDN和在线服务
-
Word粘贴技术要点:
- 必须处理
、等Office特有标签 - 表格样式需通过
border-collapse: collapse强制统一 - 图片需提供Base64和文件上传双模式
- 必须处理
-
独立开发者生存建议:
- 建立自己的信创测试环境(成本约¥15,000)
- 与本地信创厂商建立技术支持通道
- 对政府项目预留30%的不可预见成本
最终成果:新系统在客户信创环境中稳定运行1个月,处理Word文档1,200余份,样式保留准确率达到91%,获得客户"信创适配优秀案例"表彰。这次经历让我深刻认识到:在信创领域,技术选型必须遵循"可用性>先进性"的原则,有时候最简单的解决方案反而最可靠。目前正在将该方案封装为Vue组件库,计划在政府内部技术社区共享。
复制插件文件

安装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转换成图片上传到服务器中。
上传网络图片
一键自动上传网络图片,自动下载远程服务器图片,自动上传远程服务器图片
下载示例
更多推荐


所有评论(0)