前言: 最近接到消息,github开源项目github.com/wangeditor-… 在招募团队,作为一个多年前端开发小学生蠢蠢欲动,于是果断报名了。那么问题来了,如何从零开始搭建一个web富文本编辑器呢?阿蛮带你走进科学......
首先,开发一个富文本编辑器,需要了解富文本编辑器所需要的知识点。本文基于webpack+typescript开发,前期准备知识点如下:
- webpack www.webpackjs.com/concepts/
- typescript www.tslang.cn/
- sass sass.bootcss.com/documentati…
- document.execCommand developer.mozilla.org/zh-CN/docs/…
- selection developer.mozilla.org/en-US/docs/…
本文项目地址 github.com/lnimpossibl…
正文:
1.首先创建目录lnEditor
2.初始化package.json
npm init
3.安装依赖
npm install webpack webpack-cli webpack-dev-server
npm install ts-loader source-map-loader
npm install css-loader node-sass sass-loader
4.项目结构如下
dist--
--index.html (项目入口)
--bundle.js (打包后文件)
src--
--plugin (存放js插件,如颜色选择)
--styles (存放样式文件.scss)
--index.ts (脚本主入口)

5. webpack.config.js 配置如下
const path = require('path');
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
// Enable sourcemaps for debugging webpack's output.
devtool: "source-map",
resolve: {
// Add `.ts` and `.tsx` as a resolvable extension.
extensions: [".ts", ".tsx", ".js"]
},
//新建一个开发服务器,并且当代码更新的时候自动刷新浏览器。
devServer: {
contentBase: path.join(__dirname, "dist"),
compress: true,
port: 9000
},
// module.loaders 是最关键的一块配置。它告知 webpack每一种文件都需要使用什么加载器来处理:
module: {
rules: [
//解析.css文件
{
test: /\.scss$/,
use: [{
loader: "style-loader" // 将 JS 字符串生成为 style 节点
}, {
loader: "css-loader" // 将 CSS 转化成 CommonJS 模块
}, {
loader: "sass-loader" // 将 Sass 编译成 CSS
}]
},
{ test: /\.js$/, loader: "source-map-loader" },
{ test: /\.tsx?$/, loader: "ts-loader" },
]
},
// 其它解决方案配置
resolve: {
extensions: ['.ts', '.js', '.html', '.css', '.scss'], // 自动扩展文件后缀名,意味着我们引入模块可以不写后缀名
alias: { // 模块别名定义
"@": path.join(__dirname, "src")
},
}
// Other options...
};
6. 核心代码
1.封装document.execComman如下:
const exec = (command: string, value: any = null) => {
document.execCommand(command, false, value);
};
- 富文本编辑器顶部button数据
const actions = {
bold: {
icon: 'B',
title: 'Bold',
result: () => exec('bold')
},
italic: {
icon: 'I',
title: 'Italic',
result: () => exec('italic')
},
underline: {
icon: 'U',
title: 'Underline',
result: () => exec('underline')
},
link: {
icon: '🔗',
title: 'Link',
result: () => {
const url = window.prompt('Enter the link URL')
if (url) exec('createLink', url)
}
},
color: {
icon: '',
title: 'color',
result: () => {
// exec('foreColor', 'red') (由于颜色选取不是简单的点击事件,需要在色板change的时候操作exec)
}
}
// … others button
}
for (let k in actions) {
const v: any = actions[k];
// 新建一个按钮元素
const button = document.createElement('button')
// 给按钮加上 css 样式
button.className = 'item'
// 把 icon 属性作为内容显示出来
button.innerHTML = v.icon
button.title = v.title
// 把 result 属性赋给按钮作为点击事件
button.onclick = v.result
// 将创建的按钮添加到工具栏上
actionbar.appendChild(button)
}
7. 颜色选择器 jsColor
const myPicker: void = new wid.jscolor('#colorButton', {
onChange: function () {
const rgba: string = this.toRGBAString()
// 改变颜色的时候,执行exec更改文字颜色
exec('foreColor', rgba)
}
})
8. 补充:实现点击外围空白处,再点击顶部按钮,仍能对上次选中的文字进行编辑
思路如下:
- 在执行execCommand之前先判断selection是否有选中range,如果没有则使用上次的range
- 在编辑器增加onmouseleave事件,每次离开的时候,只要有选中的range 就保存 lastRange
- selection对象isCollapsed用了判断是否框选,开始偏移量与结束偏移量不相同,所以false是为已经框选 具体代码
let lastRange: any = null
document.getElementById("inputBody").onmouseleave = function() {
var selObj = window.getSelection(); // Selection对象
// 判断是否框选了, 开始偏移量与结束偏移量不相同,所以为false.
if(selObj.isCollapsed === false) {
var range = selObj.getRangeAt(0); // Range 对象
// 保存上次的框选range
lastRange = range
}
}
const exec = (command: string, value: string | null = null) => {
var selObj = window.getSelection(); // Selection对象
if(selObj.isCollapsed === true) {
selObj.removeAllRanges()
// 使用上次的框选区域range
selObj.addRange(lastRange)
}
document.execCommand(command, false, value);
};