浅谈WEB富文本实现

916 阅读5分钟

作者:宇文涛

一些概念与误解

什么是富文本?

微软公司开发用来多平台阅读编辑的一种文件类型,RTF文档(Rich Text Forma),类似于word,现在多数指对文字可进行编辑排版的工具,如vscode也可以称为富文本

前端所说的富文本指的是?

统一是web端的富文本编辑器,也具备多端能力主要是网页本身具备多端运行能力

常用的富文本编辑器

富文本编辑器的类型

来源于 document.execCommand的探索

类型描述代表优劣
L01. 基于浏览器的contenteditable属性完成富文本输入框
  1. 使用document.execCommand操作命令完成排版 | 轻量级编辑器 典型代表:wangEditor,UEditor | 优:短时间内快速研发 劣:可定制空间非常有限 | | L1 | 1. 基于浏览器的contentditable属性富文本输入框
  2. 自主实现操作命令完成排版 | 典型代表:draft.js(初始化了一个编辑区)、TinyMCE等 | 优:在浏览器的基础上,能满足大部分业务 劣:无法突破浏览器本身的排版效果,性能与HTML的局限问题 | | L2 | 1. 自主实现富文本输入框
  3. 只依赖少量浏览器API | Google Doc,腾讯文档, | 优:都自己实现,可控度都掌握在开发者手里 劣:技术难度大 |

此次分享仅仅针对 L0与L1做一次技术分享

关于contenteditable 属性

全局属性[所有html元素属性] contenteditable 是一个枚举属性,表示元素是否可被用户编辑。 如果可以,浏览器会修改元素的部件以允许编辑, 默认继承父元素的当前属性值

  • 注意事项

    • 该属性是一个枚举属性,而非布尔属性。这意味着必须显式设置其值为 truefalse 或空字符串中的一个,并且不允许简写为 <label contenteditable>Example Label</label>正确的用法是 <label contenteditable="true">Example Label</label>
  • oninput事件

    • 通过事件对象可获取编辑区域的Dom对象,从而拿到innerHtml 与innerText

举例

import React, { useState, useCallback } from "react";

function App() {
    const [isEdit, setIsEdit] = useState(true);
  	const [htmlIfo, setHtmlIfo] = useState(null);

  	const onEdit = useCallback((e) => {
    	const target = e.target;
    	setHtmlIfo(target.innerHTML);
      }, []);
    
    return (
           <div
            className="demoBox"
            contentEditable={isEdit}
            suppressContentEditableWarning
            onInput={onEdit}
          >
            <h2>我是标题</h2>
            <p>我是段落</p>
            <p>我是图片</p>
          </div>
      );
}

export default App;

关于 document.execCommand 的JS 方法

document.execCommand的介绍

  • execcommand文档
    • 当一个HTML文档切换到设计模式时,document暴露 execCommand 方法,该方法允许运行命令来操纵可编辑内容区域的元素。大多数命令影响documentselection(粗体,斜体等)
  • 设计模式
    • document.designMode 控制整个文档是否可编辑。
  • selection对象
    • 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。

举例

import React, { useState, useCallback } from "react";

const commandMapData = [
  {
    name: "背景色",
    commandKey: "backColor",
    value: "#ccc",
  },
  {
    name: "字体色",
    commandKey: "foreColor",
    value: "red",
  },
  {
    name: "字体加粗",
    commandKey: "bold",
  },
  {
    name: "删除",
    commandKey: "delete",
  },
  {
    name: "撤销",
    commandKey: "undo",
  },
  {
    name: "恢复",
    commandKey: "redo",
  },
];

function App() {
    const [isEdit, setIsEdit] = useState(true);
  	const [htmlIfo, setHtmlIfo] = useState(null);

  	const onEdit = useCallback((e) => {
    	const target = e.target;
    	setHtmlIfo(target.innerHTML);
      }, []);

      // 执行指令
      const actionCommand = useCallback((e) => {
        const key = e.target.dataset.key;

        const index = commandMapData.findIndex((res) => res.commandKey === key);
        document.execCommand(key, false, commandMapData[index].value);
      }, []);
    
    return (
           <div>
            <div>
              {commandMapData.map((res) => (
                <button
                  key={res.commandKey}
                  data-key={res.commandKey}
                  onClick={actionCommand}
                >
                  {res.name}
                </button>
              ))}
            </div>
        	<div
                className="demoBox"
                contentEditable={isEdit}
                suppressContentEditableWarning
                onInput={onEdit}
              >
                <h2>我是标题</h2>
                <p>我是段落</p>
                <p>我是图片</p>
              </div>   
            </div>
      );
}

export default App;

document.execCommand的局限性

  • 兼容性问题,MDN已经显示主流浏览器废弃了这个api
    • 但是现在firefox还是chrom 等主流浏览器都还是支持的,所以废弃态度更像是不维护,不修复
  • 缺乏扩展性,一些业务BUG
    • undo与 redo的全局影响
    • 排版受限,排版必须按照指定命令来执行
    • 等等

业界富文本组件在做什么?

既然有浏览器行业标准API,那么那些富文本编辑器代码是在干什么?

  • 处理 document.execCommand 的兼容问题
    • 如 insertHTML 和 increaseFontSize 不支持IE浏览器
      • 解决方案,利用selection找到光标位置,然后插入需要处理的数据
  • 处理 document.execCommand 的业务逻辑BUG
    • 如undo与 redo对整个页面进行操作
      • 需要自己写一个栈数据,对用户操作记载,然后undo和redo
  • 新的富文本利用selection和Range这两个属性重写 execCommand
    • 下面会详细说
  • 富文本功能的插件化
    • 功能可配置化
  • 处理 用户操作的负面问题
    • 如xss攻击预防
      • 字符串的正则检查,数据过滤
    • 如复制粘贴去除无用节点(更多是从word文件复制过来)
      • 正则检查,数据过滤
    • 如图片插入上传与复制上传处理
      • 插入上传利用 execCommand 可以做到,允许嵌入在线图片链接的
      • 静态图片上传
      • 复制上传需要监听 paste 事件,存在多种情况
        • 单独复制图片
          • 拿到 clipboardData 数据 然后进行处理
        • 和文本一块复制图片(HTML格式)
          • 复制图片为本地路径
            • 暂无解决方案
          • 复制图片为在线路径
            • 在线路径 利用正侧查找 img 标签,单独请求链接 然后处理

关于Selection 与 Range

均为实验属性

Selection 对象

Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。

所说的横跨多个元素,用户操作无法完成,必须JS进行选区添加,场景就是 多人协同修改文档,显示光标位置

Range对象

表示一个包含节点与文本节点的一部分的文档片段

可以说selection对象有选区,那么就会有range对象

尝试实现execCommand 功能

已现有DEMO 为例,进行功能实现

  • 功能

    • 背景色
    • 字体色
    • 加粗
    • 撤销与恢复
  • 方案

    • 获取光标选择区
    • 对光标选择区进行编辑
    • undo与redo写一个数据操作栈,然后完成操作
  • 难点?

    • 关于Range 范围的嵌套Dom处理
    • 关于undo与redo的光标位置处理
    • ...
  • 现有demo地址, richTextDemo

参考链接

document.execCommand的探索

为什么都说富文本编辑器是天坑?

MDN 文档

新版 Google 文档有什么不同?