本文主要说下如何从零实现一个简易的编辑器,从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.execCommand
和data-
不了解的可以查看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。