过年期间,因为自己项目需要,官方插件也没有一个合适的小程序端图文编辑器,所以根据微信官方提供的小程序插件开发文档开发了一款小程序端的简单图文排版编辑器。把开发过程跟各位分享一下。
首先说一下需求,因为需要例如图片,文字段落,视频等混合排版的功能,而且编辑的方式必须符合移动端的操作体验,找遍了百度也没有找到合适的轮子,所以只好自己造一个轮子(刚好之前也没尝试过小程序插件的开发)
最终效果图:
一、开发工具
依据官方的开发文档,开发一个插件需要用到微信开发者工具,在新建项目的时候,开发模式选择插件,appid找一个自己小程序的appid即可。
创建完可以看到如下的目录结构:
doc文件夹中放置的是插件的文档,miniprogram是示例,plugin是插件的主代码内容。我们主要的代码编写就在plugin中,doc中主要用markdown写一些文本,而miniprogram中就跟普通的小程序写法一摸一样(如果不知道小程序怎么写的朋友建议可以入门学学,对于快速实现一个应用有很大的帮助)
二、设计思路
整体的操作设计考虑为模块式设计,因为之前有做过公司的一个app,大概也是这么类似的操作。
1、设计属性
考虑到作为插件(类似一个子组件),肯定是希望内部的功能是能够比较独立的,一些需要自定义的内容可以通过属性传递,我所想到的可能需要自定义的内容有初始化的数据,绘画单位,是否使用scroll-view,容器的宽度,容器的高度,支持编辑的类型,还有一个获取已有文本的方法,对应的就是这几个属性(虽然有些属性最后好像没用到),对应的默认值可以设置value
2、设计编辑模块的对象
不同类型的模块也需要有不同的对象,但是这些对象最好也进行一些字段的统一,这样在一些代码上可以少去很多if else的判断,例如图片和视频的地址都是src,文本都是desc。设计的对象大致是这样子:
图片类型
{
// 是否是编辑状态
"isEdting": true
// 模块类型
"type": "image",
// 图片地址
"src": "",
// 图片描述
"desc": "",
// 容器宽(rpx)
"width": "",
// 容器高(rpx)
"height": "",
// 图片模式,选择了图片模式后,宽高将不生效
"mode": ""
}
文本类型
{
// 是否是编辑状态
"isEdting": true
// 模块类型
"type": "text",
// 文本内容
"desc": "",
// 颜色
"color": "#999999",
// 字体大小(rpx)
"size": 24,
// 容器宽(rpx)
"width": "",
// 容器高(rpx)
"height": ""
}
视频
{
// 是否是编辑状态
"isEdting": true
// 模块类型
"type": "video",
// 视频地址
"src": "",
// 容器宽(rpx)
"width": "",
// 容器高(rpx)
"height": "",
// 视频描述
"desc": "",
// 视频模式
"mode": ""
}
3、设计数据绑定的方式
在数据绑定的设计上,有两种,一种是数据一直绑定在父组件上,子组件里面的图文模块改变后,提供方法给使用者去修改,这样子设计的优点是返回的数据用户自己处理,自由度更高,但是,使用起来需要记住组件提供的方法,对于微信小程序插件查看文档还需要登录公众号后台去看的体验,可谓相当的麻烦。所以我选择了第二种,初始化的时候父组件的数据传给子组件,但是初始化完数据将会copy一份到子组件自己的data中,整个组件就开放一个getObject的方法,在保存模块的时候传递一个当前图文模块的数据给使用者。
三、具体代码实现
其实主要方法就几个,新增,保存,删除,上移,下移,还有input的数据绑定。
新增的时候,对点击的类型进行判断(通过设置data-index)读取对应的新增的模块类型,点击主控制条的时候,把模块增加到最后一个模块后面,如果是点击新增按钮的话,就增加到对应的模块后面,这里主要是一些数组操作,可以使用push和splice方法实现。新增之后的模块属于编辑中状态(通过控制isEditing),当一个模块处于编辑中的状态时候,其他的模块不允许进行编辑,我在这里通过加了一层白色的遮罩去防止点击事件。
新增代码
triggerEvent: function(e) {
let self = this
let index = e.currentTarget.dataset.index
if (self.data.globalEditing) {
wx.showToast({
title: '请完成已编辑的内容',
icon: 'none'
})
return false
}
let insertType = e.currentTarget.dataset.type
switch (insertType) {
case "image":
if (index !== undefined) {
self.data.innerInitData.splice(index + 1, 0, {
isEditing: true,
"type": "image",
"src": "",
"desc": "",
"mode": ""
})
} else {
self.data.innerInitData.push({
isEditing: true,
"type": "image",
"src": "",
"desc": "",
"mode": ""
})
}
break
case "text":
if (index !== undefined) {
self.data.innerInitData.splice(index + 1, 0, {
isEditing: true,
// 模块类型
"type": "text",
// 文本内容
"desc": "",
// 容器宽
"width": ""
})
} else {
self.data.innerInitData.push({
isEditing: true,
// 模块类型
"type": "text",
// 文本内容
"desc": "",
// 容器宽
"width": ""
})
}
break
case "video":
if (index !== undefined) {
self.data.innerInitData.splice(index + 1, 0, {
isEditing: true,
// 模块类型
"type": "video",
// 视频地址
"src": "",
// 容器宽
"width": "",
// 容器高
"height": "",
// 视频描述
"desc": "",
// 视频模式
"mode": ""
})
} else {
self.data.innerInitData.push({
isEditing: true,
// 模块类型
"type": "video",
// 视频地址
"src": "",
// 容器宽
"width": "",
// 容器高
"height": "",
// 视频描述
"desc": "",
// 视频模式
"mode": ""
})
}
break
}
if (index !== undefined) {
self.setData({
globalEditing: true,
innerInitData: self.data.innerInitData,
curIndex: index + 1
})
} else {
self.setData({
globalEditing: true,
innerInitData: self.data.innerInitData,
curIndex: self.data.innerInitData.length - 1
})
}
}
删除,上移,下移都是简单的数组操作,这里我就不赘述了,相信有编程基础的朋友都是比较清楚的。
保存操作则是根据需求对对应的模块对象内的key和value进行判断,例如图片的话,保存的时候肯定是需要对图片进行校空配置,这部分都放在保存这里进行判断。同时保存的时候会调用一次getObject的方法,这样,父组件可以通过这个方法获取到最新的图文块数组。
最后使用这就可以根据获取的图文块数组进行个性化的操作了。
tips
如果有需要对插件提供一些接口的话,可以通过在index.js中设置来开放一些接口,然后小程序代码里面通过requirePlugin('插件名')来获取这些方法。
四、文档编写
文档编写在doc目录下面,编写完毕后,记得按下右下角的上传按钮,并且到公众号管理后台那里更新版本,不然是没有办法把插件提交审核的(我在最后审核那里弄了很久都不明白为什么文档没有上传发布,原因是文档是单独上传,单独发布的)
五、发布
小程序插件的发布需要在公众号管理后台那里,按照需要填写一堆东西,提交审核即可。
最后是这个项目的源码,有些地方可能不太完善,大家有兴趣的可以一起完善下,欢迎pr。
https://github.com/yiptsangkin/wx-sapp-rich-text-editor
文章有说的不明白的也欢迎留言提问(目前插件已经提交审核,但是过年期间好像没人审核,所以如果需要使用到类似的功能的朋友可以等等,或者自己拿源码改改用)
参考文档
小程序插件官方文档:
https://developers.weixin.qq.com/miniprogram/dev/framework/plugin/development.html