基于TS + Webpack实现简易的编辑器

avatar
前端

本文主要说下如何从零实现一个简易的编辑器,从webpack环境的搭建,到编辑器的实现都会讲到。 其中会涉及到个人的一些思考和总结。感兴趣的同学可以查看本文的源码仓库:简易编辑器

技术栈

  • webpack
  • typescript

环境搭建

需要装的依赖:

npm install webpack webpack-cli typescript ts-loader css-loader style-loader clean-webpack-plugin html-webpack-plugin webpack-dev-server -D

webpack 大家都很熟悉,去年发布的5版本,功能有了新的调整,具体可以看下官网:webpack中文官网

因为该编辑器是基于ts写的,所以针对ts需要进行编译和解析。需要typescript 和 对应的ts-loader配置webpack对.ts文件解析。

剩下的依赖想必大家都很熟悉了,就不一一介绍了。有想进一步了解的同学可以看对应依赖的npm介绍。

依赖装完后,为了方便本地开发需要开启服务器,这里就需要用webpack-dev-serve

创建个空文件夹,运行相应的命令初始化(npm init -y)

在项目根目录下创建webpack.config.js, 其基本的配置如下, 根据webpack5新的功能,可以利用--config-name字段配置对应的开发或者生产环境。

module.exports = [
  {
    ...config,
    name: 'dev',
    mode: 'development',
    devtool: "source-map",
    devServer: {
      port: 9000,
      open: true,
      hot: true,
    },
  },
  {
    ...config,
    name: 'prod',
    mode: 'production',
    output: {
      filename: './gavinEditor.js',
      path: path.resolve(__dirname, "./dist")
    },
  },
];

调整package.json

  "scripts": {
    "dev": "webpack serve --config-name dev",
    "prod": "webpack --config-name prod"
  },

具体的如何配置config, 不熟悉的同学可以看下webpack官网,比如css的解析,ts的解析等等。下面是一张我配置的截图:

到这里,基本的环境已经完成了,为了提高开发效率,我们还配置了热更新和开发环境启动服务。

写之前的思考

自我罗列了几个问题,分别如下:

  • 如何让一个容器可以编辑?
  • 如何获取选中的文本信息?
  • 当操作了对应的菜单时,如何响应到选择的文本上?

带着问题去参考了wangEditor,在参考和查看资料后,上面的问题都得到了答案,接下来就是将想法用代码表达出来。

如何让一个容器可以编辑:

  • 想要让一个容器是可编辑的状态,只要给容器设置个属性contentEditable = true。下面引用MDN文档的基本语法。
editable = element.contentEditable
element.contentEditable = "true"

"true" 表明该元素可编辑。
"false" 表明该元素不可编辑。
"inherit" 表明该元素继承了其父元素的可编辑状态。

如何获取选中的文本信息:

  • 获取选中的文本信息,用window.getSelection(),其返回一个Selection对象, Selection表示用户选择的文本范围或插入符号的当前位置。更加具体的说明请移步MDN
var selObj = window.getSelection();
var range  = selObj.getRangeAt(0);

selObj 被赋予一个 Selection对象
range 被赋予一个 Range 对象

当操作了对应的菜单时,如何响应到选中的文本上:

  • 使用document.execCommand去执行对应的命令,那相应的命令怎么来,可以有很多的方法,在本编辑器中的做法是使用标签的data-自定义属性,记录对应菜单项的命令和值。

  • 关于document.execCommanddata-不了解的可以查看MDN

编译器部分源码实现和展示效果

下面会讲解下部分代码的实现,完整的代码可以看这里github

初始化编辑器

经过webpack打包出一个.js,页面引入之后new就行了,这里的写法参照的是wangEditor。传入一个编辑器挂载的节点。

// 这里是打包后的部分代码,因为Editor挂到了全局,用new Editor() 创建实例

<!doctype html>
<html lang="en">
  <head>
    <title>Document</title>
    <link rel="stylesheet" href="xx.icon.com">
    <script src="./gavinEditor.js"></script>
  </head>
  <body>
    <div id="editor"></div>

    <script>
      const editor = new Editor('editor')
      editor.create()
    </script>
  </body>
</html>

初始化的效果图

编辑器内部实现

这里讲解下,编辑器内部如何实例化,然后如何去挂载。

初始化对应的容器

constructor(id: string) {
   const containerDom: HTMLElement = document.getElementById(id)

   if(!containerDom) {
       throw new Error('请传入编辑器挂载的容器id')
    }

    this.id = `Editor_${UNI_ID}`
    this.$containerDom = containerDom
    this.$menuDom = new Menu(this)
    this.$textDom = new Text(this)
}

调用create()发生的事情

this.initDom(this)

this.$menuDom.init()

this.$textDom.init()

分别是:
初始化了编辑器的容器,设置样式等等
初始化菜单项
初始化编辑区

缓存选区的操作 考虑到一种操作,可能是用户在选择内容后,误点击了编辑器外部的其他内容,这时用户回来想直接点击菜单进行操作。如果没有做选区的缓存,就会导致用户需要再次的选择后才能设置, 无形就增加了用户的操作工作。

解决的办法就是,想办法缓存用户选中的内容,最简单的办法就是给编辑区绑定 鼠标弹起事件,然后利用window.getSelection获取对应的select对象,然后根据select.getRangeAt(0)获取选区内容,然后就是缓存起来。思路有了,来看下代码的实现

public init() {
   this.initSelect()
}

public initSelect():void {
  this.TextContainer.addEventListener("mouseup", e => {
      this.editor.$rangeCache = window.getSelection().getRangeAt(0)
  })
}
  
  在初始化编辑区,同时绑定事件, 进行缓存。
  
  this.editor.$rangeCache是定义在editor上的一个变量,用来缓存选区。

缓存内容已经有了,当触发菜单项时,就需要做一些处理,先获取select对象,然后将对象的的选区内容清空用:select.removeAllRanges(),在添加被缓存起来的选区,添加使用:select.addRange(), 下面是部分实现过程:

let select = window.getSelection()

select.removeAllRanges()

if (this.editor.$rangeCache) {
  select.addRange(this.editor.$rangeCache)
}

上面体现了一些编辑器的实现,关于菜单的初始化$menuDom.init()和编辑区初始化this.$textDom.init()的其他实现细节,可以详细的看下源码

需要注意的是,菜单项根据配置生产,以及事件的绑定逻辑。

编辑器实现效果图

总结

到这里,基于ts + webpack实现的简易编辑器已经有点雏形了,有感兴趣的同学可以自行去源码仓库查看,喜欢的可以点👍 start。