webpack+typescript实现简易版富文本编辑器

256 阅读4分钟

富文本编辑器

项目需求

不用任何框架,从0开始开发一个简单的富文本编辑器,实现“加粗”“设置标题”“设置颜色”三个功能,使用typescript和webpack,进行编译打包。

实现思路

webpack 配置

  1. entry output常规配置,因为打算做个第三方包,所以打包出来的文件没用(hashcontent-防止缓存)进行文件名填充。
  2. resolve 配置一些常用的路径匹配规则以及解析后缀为.ts的文件,不然webpack配合typescript找不到ts文件
  3. loader:
  • sass-loader 支持scss文件解析 postcss-loader 添加特定于浏览器厂商的前缀、支持未来 CSS语法 css-loader 配置modules参数以使得css可以模块化导入,隔绝样式污染。
  • asset/resource webpack5 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • babel-loader + ts-loader ts文件编译+babel支持es5
  1. plugins:
  • 使用MiniCssExtractPlugin 进行css打包拆分 PS:webpack打包默认会把css打包到output文件当中
  • HtmlWebpackPlugin 打包自动以模板生成html
  • CleanWebpackPlugin 打包之前清除dist文件夹里的文件
    const path = require('path')
    const resolve = (dir) => path.join(path.resolve(__dirname, '../'), dir)
    module.exports = {entry:resolve('src/index.ts'),
      output: {
        path: resolve('dist'),
        filename:'index.js'
      },
      resolve: {
        extensions: ['.tsx', '.ts', '.js'],
        alias: {
          '@': resolve('src/'),
        }
      },
      module: {
        rules: [
          {
            //组件化moudle scss配置
            test: /\.scss$/i,
            include:resolve("src/components"),
            use: [
              MiniCssExtractPlugin.loader,
              {
                loader: 'css-loader',
                options: {
                  importLoaders:2,
                  modules: {
                     localIdentName: "leo-[local]"
                  }
                }
              }
              ,'postcss-loader'
              , 'sass-loader'],
          },
          {
             test: /\.(png|svg|jpg|jpeg|gif)$/i,
             type: 'asset/resource',
           },
          {
            test: /\.(woff|woff2|eot|ttf|otf)$/i,
            type: 'asset/resource',
          },
          {
            test: /\.ts$/,
            use: ['babel-loader','ts-loader'],
            exclude: /node-modules/ 
          },
        ],
      },

      plugins: [
        new HtmlWebpackPlugin({template:resolve('src/index.html')}),
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin()
      ]
   }   

ts.config.js

 {
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
  },
  "include": [
    "src/"
  ],
  "exclude": [
    "node_modules",
  ]
}
  • .babelrc @babel/plugin-transform-runtime 支持polify和一些函数的helper 按需导入
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs":3
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}
    

代码实现前信息获取

查阅了相关的资料文档获得以下信息

contenteditable 指示元素是否应该由用户进行编辑 MDN文档

 <div contenteditable="true"></div>

实现富文本编辑器,最重要的一个点就是光标定位,let selection = window.getSelection() 方法可以操作光标,selection.getRangeAt()返回所有光标的集合,因为富文本只有一个光标所以通常selection.getRangeAt(0)获取当前的光标Range对象。MDN文档

微信截图_20210408142503.png

如图是选中的Range对象,里面包含了

  • startoffset,endoffset是鼠标选中拖拽的起始和终点位置.
  • startContainer,endContainer 返回Range开始和结束的节点.
  • collapsed:startoffsetendoffset相等则返回true即当前光标的位置.

操作字体变粗改色等一些列操作也有对应实现的api document.execCommand(aCommandName, aShowDefaultUI, aValueArgument) 通过这些命令可以作用到当前Range对象区域。虽然官方已经Deprecated ... MDN文档

document.execCommand("formatBlock", false, "H1") //设置为H1标题
document.execCommand("foreColor", false, "#D4F2E7") //改变颜色
document.execCommand("bold", false) //加粗

实现代码

项目实现图

aa.png

封装光标操作函数

因为在实战中我们经常需要保存当前Range对象 然后在某个时间点重置到上次保存的节点 比如点击toolbar时,会失去焦点,此时Range对象与我们在编辑器时编辑的不是一个对象,所以会出现执行命令没无反应的效果

utils.ts
/**
 * @function 保存当前的Range对象
 */
export const saveSelection = () => {
  let selection = window.getSelection();
  if (selection.rangeCount > 0) {
    return selection.getRangeAt(0);
  }
  return null;
};
/**
 * @function 重置Range对象
 * @param selectedRange 之前光标的Range对象
 */
export const restoreSelection = (selectedRange: Range) => {
  let selection = window.getSelection();
  if (selectedRange) {
    selection.removeAllRanges();
    selection.addRange(selectedRange);
  }
};

execCommand简单封装

export const execCommand = (id: string, val?: string) => {
  return document.execCommand(id, false, val);
};

代码实现模板

然后抽离下常见的interface 比如所有的components都需要create初始化操作,template模板html挂载,handler添加监听事件

export interface LComponent {
  readonly create: () => void;
  readonly template: () => void;
  readonly handler: () => void;
} 

定义props 实现接口,此处就不贴所有的代码实现了,此处有伪代码!

interface EditContainerConfig {
  height: string;
  focus: boolean;
  zIndex: number;
}
export default class EditContianer implements LComponent {
  elem: HTMLElement;
  config: EditContainerConfig;
  selectedRange: Range;
  currentSeleted: HTMLElement;
  constructor(selector: string) {
    this.elem = document.querySelector(selector);
    this.config = {
      height: "400px",
      zIndex: 10001,
      focus: true,
    };
  }
  create() {
     this.template()
     this.handler()
  }

  template() {
     this.elem.innerHTML = `
      <div id="leoEditor" contenteditable="true">
          <p id="keep-p"><br></p>
      </div>
    `;
  }
  handler() {
   const toolBar: HTMLDivElement = document.querySelector(".leo-tool-bar");
   this.elem.addEventListener(
      "keyup",
      (e) => {
        //监听删除按键keep p标签
        if (e.keyCode === 8 && keepP.innerHTML === "<br>") {
          leoEditor.innerHTML = `<p id="keep-p"><br></p>`;
        }
        this.selectedRange = saveSelection();
      }
    );
  }
  //事件监听
  delegate(toolBar, "click", "i", (e) => {
       if(xxx) execCommand();
 });
 }
}

总结

虽然已经简单完成,但是bug特别多,算是初次窥探了下富文本编辑器的大坑,自己很多代码是实现上目前还没有很好的实现方案。如果真要逐一解决这些问题的话,需要耗费大量的精力,另外自定义光标选择器可能才是未来的的主流吧.

项目github链接leoeditor