【element-tiptap】导出word

前言:前面的文章 【element-tiptap】导入word并解析成HTML 已经介绍过如何在 element-tiptap 中导入 word。这篇文章来探究一下怎么将编辑器的内容导出成word

(一)创建菜单项

1、图标

首先上 fontawesome 这个网站上找一个合适的图标,把它的 svg 复制下来

然后创建 src/icons/export.svg ,增加 widthheightfill 属性

javascript 复制代码
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="32" viewBox="0 0 576 512">
  <path fill="currentColor" d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 128-168 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l168 0 0 112c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zM384 336l0-48 110.1 0-39-39c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l80 80c9.4 9.4 9.4 24.6 0 33.9l-80 80c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l39-39L384 336zm0-208l-128 0L256 0 384 128z"/>
</svg>
2、创建扩展

src/extensions/export.ts

javascript 复制代码
import { Extension } from '@tiptap/core';
import { Editor } from '@tiptap/vue-3';
import ExportDropdown from '@/components/menu-commands/export.dropdown.vue';

export interface ExportOptions {
}

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    export: {
      exportToWord: () => ReturnType;
    };
  }
}

const Export = Extension.create<ExportOptions>({
  name: 'export',

  addCommands() {
    return {
      exportToWord:
        () =>
        ({ editor }) => {
          // 这里可以添加导出 Word 的具体实现
          return true;
        },
      exportToPdf:
        () =>
        ({ editor }) => {
          // 这里可以添加导出 PDF 的具体实现
          return true;
        },
    };
  },

  addOptions() {
      return {
          button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {
              return {
                  component: CommandButton,
                  componentProps: {
                      command: () => {
                          editor.commands.exportToWord();
                      },
                      isActive: editor.isActive('export'),
                      icon: 'export',
                      tooltip: t('editor.extensions.Export.tooltip'),
                  },
              };
          },
      };
  },
});

export default Export; 
2、在根组件中引入当前的扩展

老生常谈的步骤,这里就不讲了啦

以及定义菜单的提示语
src/i18n/locales/zh/index.ts

javascript 复制代码
Export: {
  tooltip: '导出文档',
},

(二)实现导出 word

使用开源库 prosemirror-docx

1、安装依赖
javascript 复制代码
npm install prosemirror-docx --save

后来,我发现源码里还是有很多不完善的地方,官网还有处于 open 状态的提交记录😂

其中的 #35 还是很关键的,是修复的图片尺寸检测失败的 bug。所以还是将源码中的 src 文件夹都放在自己的项目里面

另外还需要安装一些别的依赖,直接使用 npm install xxx 安装即可

typescript 复制代码
"docx": "^9.0.3",
"file-saver": "^2.0.5",
"image-dimensions": "^2.3.0",
2、图片 buffer

根据 export-tiptap-content-to-microsoft-word 上给出的示例代码,

js 复制代码
import {
  writeDocx,
  DocxSerializer,
  defaultNodes,
  defaultMarks
} from "prosemirror-docx";
import { saveAs } from "file-saver";

const nodeSerializer = {
  ...defaultNodes,
  hardBreak: defaultNodes.hard_break,
  codeBlock: defaultNodes.code_block,
  orderedList: defaultNodes.ordered_list,
  listItem: defaultNodes.list_item,
  bulletList: defaultNodes.bullet_list,
  horizontalRule: defaultNodes.horizontal_rule,
  image(state, node) {
    // No image
    state.renderInline(node);
    state.closeBlock(node);
  }
};

const docxSerializer = new DocxSerializer(nodeSerializer, defaultMarks);

const write = useCallback(async () => {
   const opts = {
     getImageBuffer(src) {
       return Buffer.from("Real buffer here");
     }
   };

   const wordDocument = docxSerializer.serialize(editor.state.doc, opts);

   await writeDocx(wordDocument, (buffer) => {
     saveAs(new Blob([buffer]), "example.docx");
   });
 }, [editor?.state.doc]);

使用这个库需要将图片转成 buffer

使用 Buffer.from() 方法,需要传递一个 base64 字符串。当前的图片,是保存在服务器返回的地址上,需要转换成 base64。起先我想在 getImageBuffer 方法中异步加载远程的图片资源,然后转成 base64,后来发现这个方法只能是同步的,所以就需要先遍历文档所有节点,将所有的图片都转成 base64。

ts 复制代码
addCommands() {
    return {
        exportToWord:
            () =>
                async ({ editor }) => {
                    // 给编辑器中所有的图片的src转换为base64

                    const images: Node[] = [];
                    editor.state.doc.descendants((node) => {
                        if (node.type.name === 'image') {
                            images.push(node);
                        }
                    });

                    for (const image of images) {
                        const src = image.attrs.src;
                        if (!src.startsWith('data:')) {
                            try {
                                const response = await fetch(src);
                                const blob = await response.blob();
                                const reader = new FileReader();
                                const base64 = await new Promise<string>((resolve) => {
                                    reader.onload = () => resolve(reader.result as string);
                                    reader.readAsDataURL(blob);
                                });

                                const tr = editor.state.tr;
                                const pos = editor.state.doc.resolve(0);
                                editor.state.doc.nodesBetween(0, editor.state.doc.content.size, (node, pos) => {
                                    if (node.type.name === 'image' && node.attrs.src === src) {
                                        tr.setNodeMarkup(pos, undefined, {
                                            ...node.attrs,
                                            src: base64
                                        });
                                    }
                                });
                                editor.view.dispatch(tr);
                            } catch (error) {
                                console.error('Failed to convert image to base64:', error);
                            }
                        }
                    }


                    const opts = {
                        getImageBuffer(src) {
                            // base64转 Buffer
                            return Buffer.from(src.split(',')[1], 'base64');
                        }
                    };

                    const wordDocument = docxSerializer.serialize(editor.state.doc, opts);
                    await writeDocx(wordDocument, (buffer) => {
                        saveAs(new Blob([buffer]), "example.docx");
                    });


                    return true;
                },
    };
},
3、源码bug修复

使用这个代码出现的一些问题:

1、图片尺寸检测不出来,需要根据 这个提交 去修改代码

2、图片会变得很大,是因为图片的宽高没有传进去,使用的是默认的宽高

3、图片默认居中,对齐方式显示成 left 即可
src/utils/prosemirror-docx/schema.ts

typescript 复制代码
// Presentational
image(state, node) {
  const { src, width, height } = node.attrs;
  state.image(src, width, height);
  state.closeBlock(node);
},

src/utils/prosemirror-docx/serializer.ts

ts 复制代码
image(
  src: string,
  width: number,
  height: number,
  widthPercent = 70,
  align: AlignOptions = 'left',
  imageRunOpts?: IImageOptions,
) {
  const buffer = this.options.getImageBuffer(src);

  const dimensions = imageDimensionsFromData(buffer);
  if (!dimensions) return;
  const aspect = dimensions.height / dimensions.width;
  if(!width) {
    width = this.maxImageWidth * (widthPercent / 100);
  }

  this.current.push(
    new ImageRun({
      data: buffer,
      transformation: {
        width,
        height: height || width * aspect,
      },
      ...imageRunOpts,
    }),
  );

  return this.addParagraphOptions({
    alignment: align,
  });
}

以上就可以实现导出成 word 的功能啦!

看下效果:

文档如下:

导出word如下:

相关推荐
腾讯TNTWeb前端团队6 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom11 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom11 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom11 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试