CMS企业官网Word导入功能开发手记

需求分析与技术调研

作为北京的一名.NET开发工程师,最近接手的企业CMS官网项目新增了文档导入需求。客户希望在新闻发布模块中实现Word/Excel/PPT/PDF文档导入和一键粘贴功能,同时保留完整样式和多媒体内容。

需求拆解

  1. 核心功能需求

    • 支持Word/Excel/PPT/PDF文档导入
    • 支持微信公众号内容导入
    • 保留原始文档的样式(字体、字号、颜色等)
    • 保留复杂元素(表格、公式、形状等)
    • 图片自动上传至阿里云OSS
    • 以插件形式集成到CKEditor 4
  2. 非功能性需求

    • 预算控制在99元以内
    • 不影响现有功能
    • 易于集成部署
    • 基于现有技术栈(Vue2 CLI、.NET Core、SQL Server)

技术方案调研

经过市场调研,发现以下可能的解决方案:

  1. 商业插件方案

    • CKEditor官方插件(超出预算)
    • TinyMCE相关插件(同样价格昂贵)
  2. 开源方案

    • Mammoth.js(纯前端解析.docx)
    • docx-parser(开源Word解析库)
    • pdf.js(PDF解析)
    • pptx.js(PPT解析)
  3. 自定义开发方案

    • 基于.NET Core开发后端解析服务
    • 前端使用开源库预处理

结论:考虑到预算限制,决定采用开源库组合+少量自定义开发的混合方案。

实施方案设计

架构设计

[前端]
CKEditor 4 (Vue2集成)
├─ 自定义插件按钮
├─ Mammoth.js (Word处理)
├─ pdf.js (PDF处理)
└─ 图片上传组件(对接OSS)

[后端]
.NET Core API
├─ 文件接收端点
├─ Office文档解析服务
├─ 图片上传服务(OSS SDK)
└─ 内容清洗与格式化

技术选型

  1. Word处理:Mammoth.js + 少量自定义样式处理
  2. Excel处理:SheetJS社区版
  3. PPT处理:pptx.js基础解析
  4. PDF处理:pdf.js文本提取
  5. 微信公众号:自定义HTML清洗逻辑
  6. 图片上传:阿里云OSS JavaScript SDK

开发实施过程

第1步:CKEditor插件开发

static/js目录下创建customplugins文件夹,新增wordimport插件:

// wordimport/plugin.js
CKEDITOR.plugins.add('wordimport', {
    icons: 'wordimport',
    init: function(editor) {
        editor.ui.addButton('WordImport', {
            label: '导入Office文档',
            command: 'wordImportCommand',
            icon: this.path + 'icons/wordimport.png'
        });
        
        editor.addCommand('wordImportCommand', {
            exec: function(editor) {
                // 创建文件上传对话框
                createUploadDialog(editor);
            }
        });
    }
});

第2步:前端文件处理逻辑

// 在Vue组件中扩展方法
methods: {
    handleFileUpload(file) {
        const fileType = file.name.split('.').pop().toLowerCase();
        
        switch(fileType) {
            case 'docx':
                this.processWord(file);
                break;
            case 'xlsx':
                this.processExcel(file);
                break;
            // 其他类型处理...
        }
    },
    
    async processWord(file) {
        const arrayBuffer = await file.arrayBuffer();
        const result = await mammoth.extractRawText({arrayBuffer});
        
        // 处理图片
        const images = await this.extractImages(arrayBuffer);
        await this.uploadImages(images);
        
        // 插入编辑器
        this.editorInstance.insertHtml(result.value);
    },
    
    async uploadImages(images) {
        for (const img of images) {
            const formData = new FormData();
            formData.append('file', img.blob, img.filename);
            
            await axios.post('/api/upload', formData, {
                headers: {
                    'Content-Type': 'multipart/form-data'
                }
            });
        }
    }
}

第3步:.NET Core后端服务

// UploadController.cs
[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
    private readonly IOssService _ossService;
    
    public UploadController(IOssService ossService)
    {
        _ossService = ossService;
    }
    
    [HttpPost]
    public async Task Upload(IFormFile file)
    {
        if (file == null || file.Length == 0)
            return BadRequest("无效文件");
            
        var fileExt = Path.GetExtension(file.FileName).ToLower();
        var allowedExtensions = new[] { ".jpg", ".png", ".gif", ".jpeg" };
        
        if (!allowedExtensions.Contains(fileExt))
            return BadRequest("不支持的文件类型");
            
        try
        {
            var fileUrl = await _ossService.UploadFileAsync(file);
            return Ok(new { url = fileUrl });
        }
        catch (Exception ex)
        {
            return StatusCode(500, $"上传失败: {ex.Message}");
        }
    }
}

// OfficeProcessingService.cs
public class OfficeProcessingService
{
    public async Task ProcessWord(Stream fileStream)
    {
        // 使用OpenXML SDK处理Word文档
        using (WordprocessingDocument doc = WordprocessingDocument.Open(fileStream, false))
        {
            var body = doc.MainDocumentPart.Document.Body;
            // 转换为HTML并保留样式
            return ConvertToHtml(body);
        }
    }
    
    private string ConvertToHtml(Body body)
    {
        // 实现Word到HTML的转换逻辑
        // 处理段落、样式、表格等
    }
}

第4步:阿里云OSS集成

// OssService.cs
public class OssService : IOssService
{
    private readonly OssClient _client;
    private readonly string _bucketName;
    
    public OssService(IConfiguration configuration)
    {
        var accessKeyId = configuration["OSS:AccessKeyId"];
        var accessKeySecret = configuration["OSS:AccessKeySecret"];
        var endpoint = configuration["OSS:Endpoint"];
        _bucketName = configuration["OSS:BucketName"];
        
        _client = new OssClient(endpoint, accessKeyId, accessKeySecret);
    }
    
    public async Task UploadFileAsync(IFormFile file)
    {
        var objectName = GenerateObjectName(file.FileName);
        
        using (var stream = file.OpenReadStream())
        {
            var putObjectRequest = new PutObjectRequest(_bucketName, objectName, stream);
            await Task.Run(() => _client.PutObject(putObjectRequest));
        }
        
        return $"https://{_bucketName}.{_client.Endpoint}/{objectName}";
    }
    
    private string GenerateObjectName(string originalFileName)
    {
        var ext = Path.GetExtension(originalFileName);
        return $"uploads/{DateTime.Now:yyyyMMdd}/{Guid.NewGuid()}{ext}";
    }
}

部署与测试

部署步骤

  1. 前端部署

    npm run build
    # 将dist目录内容部署到Web服务器
    
  2. 后端部署

    • 发布.NET Core应用到阿里云ECS
    • 配置IIS/Nginx反向代理
  3. 环境配置

    • 设置阿里云OSS访问密钥
    • 配置数据库连接字符串

测试用例

  1. Word文档导入测试

    • 包含多种样式(标题、正文、列表)
    • 包含表格和图片
    • 包含数学公式
  2. 微信公众号导入测试

    • 从公众号文章复制内容
    • 验证图片自动上传
    • 检查样式保留情况
  3. 性能测试

    • 大文档(50页+)导入
    • 多图片文档处理

遇到的问题与解决方案

问题1:Word公式转换失真

现象:MathType公式转换为图片后质量差

解决方案

  • 使用mammoth.js的转换选项增强公式处理
  • 对于复杂公式,回退到服务器端使用LibreOffice转换
const options = {
    convertImage: mammoth.images.imgElement(function(image) {
        return image.read("base64").then(function(imageBuffer) {
            return {
                src: "data:" + image.contentType + ";base64," + imageBuffer
            };
        });
    }),
    styleMap: [
        "p[style-name='Equation'] => p.equation"
    ]
};

问题2:跨域图片上传

现象:微信公众号图片直接复制会导致跨域问题

解决方案

  1. 实现图片代理下载服务
  2. 前端检测图片URL,如果是外部图片则先通过后端下载
[HttpPost("proxy-download")]
public async Task ProxyDownload([FromBody] DownloadRequest request)
{
    using (var httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(request.Url);
        if (!response.IsSuccessStatusCode)
            return BadRequest("下载失败");
            
        var stream = await response.Content.ReadAsStreamAsync();
        var fileName = Guid.NewGuid() + ".jpg";
        
        var fileUrl = await _ossService.UploadStreamAsync(stream, fileName);
        return Ok(new { url = fileUrl });
    }
}

问题3:预算控制

挑战:99元预算限制

解决方案

  1. 完全采用开源库避免授权费用
  2. 利用阿里云OSS免费额度(足够中小流量使用)
  3. 简化PPT/PDF处理,仅提取文本内容
  4. 自主开发核心功能,减少商业依赖

最终效果与优化

实现的功能

  1. Word文档导入

    • 完美保留文字样式
    • 支持表格、列表等复杂结构
    • 公式转换为图片显示
  2. 一键粘贴

    • 从Word直接粘贴保留基本格式
    • 自动上传外部图片
  3. 微信公众号导入

    • 智能识别公众号内容
    • 自动下载远程图片

性能优化

  1. 前端优化

    • 大文件分片处理
    • 使用Web Worker进行解析
  2. 后端优化

    • 引入内存缓存
    • 异步处理队列
services.AddHostedService();

public class UploadProcessingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var job = await _queue.DequeueAsync(stoppingToken);
            await ProcessJob(job);
        }
    }
}

项目总结

在99元预算内成功实现了客户需求,主要得益于:

  1. 合理的技术选型,平衡功能与成本
  2. 有效利用开源社区资源
  3. 针对性地自主开发核心功能
  4. 充分利用云服务免费额度

遗留问题

  • PPT/PDF的复杂样式支持有限
  • 超大文档处理性能有待优化

后续计划

  1. 增加文档导入进度显示
  2. 实现导入前的样式预览
  3. 添加文档批量导入功能

通过这个项目,我深刻理解了如何在有限预算下实现复杂功能的技术取舍和方案设计。

复制插件

说明:此教程以CKEditor4.x为例,使用其他编辑器的查看对应教程。
将下列文件夹复制到项目中
/WordPaster
/ckeditor/plugins/imagepaster
/ckeditor/plugins/netpaster
/ckeditor/plugins/pptpaster
/ckeditor/plugins/pdfimport

上传插件

wordpaster文件夹

上传插件文件夹

将imagepaster,netpaster文件夹上传到现有项目ckeditor/plugins目录中
插件文件夹

在工具栏中增加插件按钮

插件按钮

引用js

引用JS






初始化控件

WordPaster.getInstance({
	//上传接口:http://www.ncmem.com/doc/view.aspx?id=d88b60a2b0204af1ba62fa66288203ed
	PostUrl: api,
	//为图片地址增加域名:http://www.ncmem.com/doc/view.aspx?id=704cd302ebd346b486adf39cf4553936
	ImageUrl: "",
	//设置文件字段名称:http://www.ncmem.com/doc/view.aspx?id=c3ad06c2ae31454cb418ceb2b8da7c45
	FileFieldName: "file",
	//提取图片地址:http://www.ncmem.com/doc/view.aspx?id=07e3f323d22d4571ad213441ab8530d1
	ImageMatch: '',
	Cookie: 'PHPSESSID='			
});//加载控件

配置上传接口

初始化控件

注意

1.如果接口字段名称不是file,请配置FileFieldName。ueditor接口中使用的upfile字段
字段名称
点击查看详细教程

配置ImageMatch

用于匹配JSON数据,
匹配地址
点击查看详细教程

配置ImageUrl

用于为图片增加域名前缀
自定义域名
点击查看详细教程

配置Session

如果接口有权限验证(登陆验证,SESSION验证),请配置COOKIE。或取消权限验证。
参考:点击查看详细教程

说明

1.请先测试您的接口:点击查看详细教程

功能演示

编辑器界面

image

导入Word文档,支持doc,docx

粘贴Word和图片

导入Excel文档,支持xls,xlsx

粘贴Word和图片

粘贴Word

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

Word转图片

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

导入PDF

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

导入PPT

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

上传网络图片

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

下载示例

点击下载完整示例

更多推荐