概述

以我们写的这个emoji插件为例,网上已经有一些相关的插件了,但你总感觉有些部分的需求不能被满足(如:可以自行添加新的表情包而不用去改源代码等等)

详细

很久没有写文章了,说实话本人现在受困于五月病已经快变成一条死咸鱼了(T_T),本次就当写一个简单的js插件教程了。本项目的代码相对比较简单,至于里面有些变量命名的问题就请你们不要吐槽了Σ(゚д゚lll)(好的,我承认我英语就小学水平好吧。除了hello和goodbye其他的都不会了____orz)。 废话就讲到这里,下面开始正文。

一、事前准备

事实上在写一个插件前我们都需要事先想好你要实现哪些功能,怎么去实现,这些大方向的东西是需要事先考虑的,至于具体细节和优化选项我们可以在写代码的过程中再进行修改。

就以我们写的这个emoji插件为例,网上已经有一些相关的插件了,但你总感觉有些部分的需求不能被满足(如:可以自行添加新的表情包而不用去改源代码等等),这时我们就可以列出你想实现的功能项了:

  1. 需要满足基本的表情插件的需求,包括图片和对应code的相互转换

  2. 希望可以通过参数来调整每行以及每列表情图片的显示个数,并且可以针对不同表情包单独调整

  3. 希望用户可以在不了解源代码的情况下也能自行主动添加新的表情包

  4. 模板界面简单,可以进行自适应,并且兼容移动端

  5. 尽可能只提供简单的api接口和方法,避免内部涉及其他不是很相关的功能(如绑定某个特定的元素或者在内部进行数据传输等等),保持插件的灵活性等等

以上就是我们暂时能想到的功能和需求,下面就开始写一个完整的插件了(当然原生js插件某种程度来讲使用起来相对比较自由,因为不需要依赖某些特定库,而且也不需要按照某些库类的格式标准进行插件的编写,但少了一些封装好的方法也会使得插件写起来更费力,至于怎么取舍就需要看个人需求来定了)

二、进行结构划分

当我们正式开始代码编写的时候,当然想自己写出来的代码不敢说很强势,但至少结构清晰,易于读懂,而且代码的性能也需要保证。这时我们就需要回到前面的需求了,由上面列出的5点可以看出,大部分的功能需求都是在我们程序内部去实现的,唯一需要考虑的是上面的第3点。

这时我们可能已经想到办法了,比如说将新的表情包填好相关的参数后由接口传入程序内部去作处理。当然这是一个合理的选择,但考虑到代码的复杂度和使用的简易度,我们最好还是建立一个对应config文件。因为首先这样我们可以提供一些默认的表情包,并且配置好相关的参数并注释,后面的使用者只需要按照相关的格式复制然后修改就行了。而且将一些非逻辑性的数据单独隔离开来有利于维持清晰的代码结构,增加代码的易读性。所以到这里已经可以基本上确定我们需要的文件了:

一个模板css文件; 2. 一个数据配置文件config.js; 3. 一个逻辑实现文件js;

三、填写配置文件

这里先填写配置文件是为了有一个更明确的需求,以及防止在coding过程中忘记了某些需求(像我一样,老了,脑袋不好使゚゚(゚´Д`゚)゚),当然并不是所有插件都用配置文件比较好,新手请务必不要有这样的误区,下面是我写的配置代码:

 var path = "http://localhost/wantEmoji/",  //项目所在的根地址
emojis = {
"paopao" : {
"name" : "泡泡", //名字
"col" : 10, //每一行最大的表情个数(建议填选的时候值不要太大或太小)
"path" : path+"emojiSources/paopao/", //相对于项目根地址的路径
"enable" : true, //是否启用本表情包
"sources" : ["1.jpg"] //中间的值也支持{title:"笑",url:"1.jpg"}的形式,且可单独设置
}
}

这部分代码考虑了几个点:

  • 一是考虑到可能会在不同路径的文件中调用同一个配置文件,所以为了保证路径不出错,需要确定每个包的绝对路径值。

  • 二是考虑到某些表情包现在可能并不想用,但代码删来删去可能会很麻烦,所以提供了一个是否启用的接口。

  • 三是考虑到不同表情包的图片尺寸可能不同,为了让每张图片尽可能清晰我们允许调整每行显示的图片个数(在程序中每个单项的size都是自动计算的)

  • 四是考虑到每张表情图片可能有的需要设置title来提示用户这个表情是什么意思,所以允许sources项数组中的值可以为string也可以为object

最后也是主要考虑的问题,我们希望每个表情对应的code值能够自动生成而不是人为的对每个图片去进行单独设置,所以需要保证每个code的值都是唯一的,而且是容易被解析的。

这里emojis变量不是数组而是对象就是基于这个原因。 (我们最终生成的code值为[wem:emojis的key值_图片名_图片类型:wem]这种形式,如[wem:paopao_1_jpg:wem],表示的是paopao表情包里面的1.jpg)

四、插件开写

前面的准备工作都做好后,现在我们终于可以开始写真正的代码了。虽然前面的内容不怎么多,但对于一个插件乃至一个项目来说都是必不可少的一个步骤,特别是初学者,开始动手写自己的插件时多想想该怎么做总是没错的。

首先我们需要创建一个对象(当然你通过闭包来写也是可以的),明确好哪些数据和函数是可以共用的,哪些是不能共用的。就我个人的经验来讲,一般对于用来保存数据用的变量,最好都放在函数体内,而方法则都放在原型上。

var wantEmoji = function(options){
options = options || {};
var selector = options.wrapper || "body"; this.wrapper = document.querySelector(selector); //包裹元素
this.row = options.row || 4; //每页表情的行数
this.callback = options.callback || function(){}; //当表情被点击时的回调,返回表情的code值 this.emojis = window.emojis || emojis; //加载表情包配置 this.content = null; //.wEmoji-content
this.navRow = null; //.wEmoji-row
this.currentWrapper = null; //.wEmoji-wrapper[data-choose="true"] this.activePage = 0;
this.totalPage = 0;
this.eachPartsNum = 4; //每一批显示的表情包数(导航栏的表情包的最大显示个数) this.wrapWidth = 0;
this.count = this.getEMJPackageCount(); if(options.autoInit) //当设置了autoInit之后会自动调用init函数,默认不会
this.init();
};

上面的代码我都加了注释就不做细说了,下面是各个功能部分的实现(马上就可以看到我英语捉急的地方了(`・ω・´))。

首先是init(): 完成某些数据的获取以及确认进入哪种情况

init : function(){
//当表情包的实际启用个数大于设定值时,启用.wEmoji-more
if(this.count > this.eachPartsNum)
this.wrapper.className += " wEmoji wEmoji-more";
else
this.wrapper.className += " wEmoji"; this.wrapWidth = this.wrapper.clientWidth; this.initTemplete();
},

initTemplete(): 初始化模板,更新某些数据变量,并执行接下来的工作

initTemplete : function(){

        var wrapper = this.wrapper,
tpl = '<div class="wEmoji-header">'+
'<div class="wEmoji-prev-btn">&lt;</div>'+
'<div class="wEmoji-nav">'+
'<div class="wEmoji-row"></div>'+
'</div>'+
'<div class="wEmoji-next-btn">&gt;</div>'+
'</div>'+
'<div class="wEmoji-container">'+
'<div class="wEmoji-content"></div>'+
'<div class="wEmoji-pages"></div>'+
'</div>'; wrapper.innerHTML = tpl; this.content = wrapper.querySelector(".wEmoji-content");
this.navRow = wrapper.querySelector(".wEmoji-row"); this.__initData();
this.__bindEvent();
},

接下来是__initData():生成具体的表情图片和导航等,这里需要注意的是进行dom操作时不要让重排发生多次,使需要操作的dom元素脱离文档流是减少重排的方法之一。另外这里还将许多属性保存为临时变量是为了提高程序性能(至于代码优化需要自己去找资料看,这里就简单提一下)。

__initData : function(){        var emojis = this.emojis,
wrapper = this.wrapper,
navRow = this.navRow,
content = this.content,
rowWidth = navRow.clientWidth,
count = this.count; //减少重排
wrapper.style.display = "none"; content.innerHTML = "";
navRow.style.width = count / this.eachPartsNum * 100 + "%"; for( var key in emojis ){ var emj = emojis[key]; if(!emj.enable) continue; //将每个生成的表情包的容器放入content中
content.appendChild(this.__initContent(key,emj));
navRow.innerHTML += '<div class="wEmoji-list" data-eid="'+key+'" style="width:'+(1/count*100)+'%;">'+emj.name+'</div>';
} this.__initStyle(); this.wrapper.style.display = "block";
},

事件绑定:正常流程来走就行,注意某些地方需要用事件委托来提升性能,而这里没用addEventListener是为了防止多次初始化init的时候导致事件重复绑定,on+“event”事实上已经够用了。

__bindEvent : function(){
var _self = this,
wrapper = this.wrapper,
row = this.navRow,
pageBox = wrapper.querySelector('.wEmoji-pages'),
prev = wrapper.querySelector('.wEmoji-prev-btn'),
next = wrapper.querySelector('.wEmoji-next-btn'),
content = this.content,
down = "ontouchstart" in document ? "touchstart" : "mousedown",
up = "ontouchend" in document ? "touchend" : "mouseup",
move = "ontouchmove" in document ? "touchmove" : "mousemove",
drag = false,
x = 0; pageBox.onclick = function(e){
e = e || event;
var target = e.target || e.srcElement,
idx = target.getAttribute("data-pageIdx");
if(target.tagName.toLowerCase() != "li" || !idx){
return false;
}
_self.showPage(idx-1);
}; row.onclick = function(e){
e = e || event;
var target = e.target || e.srcElement,
eid = target.getAttribute("data-eid"); if( eid && _self.emojis[eid] ){
_self.chooseEmoji(eid);
_self.showPage(0);
}
}; var parts = Math.ceil(this.count / this.eachPartsNum), //可以将表情包数分为N批(默认4个一批)
partsIdx = 0,
navWidth = wrapper.querySelector(".wEmoji-nav").clientWidth; prev.onclick = function(e){
partsIdx = partsIdx - 1 < 0 ? 0 : partsIdx - 1;
row.style.webkitTransform = "translateX("+(-partsIdx * navWidth)+"px) translateZ(0px)";
row.style.transform = "translateX("+(-partsIdx * navWidth)+"px) translateZ(0px)";
}; next.onclick = function(e){
partsIdx = partsIdx + 1 >= parts ? partsIdx : partsIdx + 1;
row.style.webkitTransform = "translateX("+(-partsIdx * navWidth)+"px) translateZ(0px)";
row.style.transform = "translateX("+(-partsIdx * navWidth)+"px) translateZ(0px)";
}; content.onclick = function(e){
e = e || event;
var target = e.target || e.srcElement,
trueTarget = getTargetNode(target,".wEmoji-item"),
emjCode; if(trueTarget)
emjCode = trueTarget.getAttribute("data-emj"); if(!emjCode)
return false; _self.callback.call(_self,emjCode);
console.log(emjCode);
}; content["on"+down] = function(e){
e = e || event;
drag = true;
x = e.pageX || e.touches[0].pageX;
}; content["on"+move] = function(e){
e = e || event;
e.stopPropagation();
e.preventDefault();
}; content["on"+up] = function(e){
e = e || event;
if(drag){
drag = false;
var endX = e.pageX || e.changedTouches[0].pageX,
dis = endX - x,
idx; if(dis > 50){
idx = Math.max(_self.activePage - 1,0);
_self.showPage(idx);
} else if (dis < -50){
idx = Math.min(_self.activePage + 1,_self.totalPage - 1);
_self.showPage(idx);
}
x = 0;
}
}; },

下面是选择表情包的功能chooseEmoji():封装好后只需要调用接口即可,不管是初始化的时候还是事件触发的时候,将表情包改变时会发生操作全都放一起,因为大部分操作都是同时变化的,所以没必要继续细分了。

chooseEmoji : function(eid){
var navRow = this.navRow,
content = this.content,
targetWrapper = content.querySelector(".wEmoji-wrapper[data-eid='"+eid+"']"),
targetList = navRow.querySelector(".wEmoji-list[data-eid='"+eid+"']"),
chooseWrapper = content.querySelector(".wEmoji-wrapper[data-choose='true']"),
chooseList = navRow.querySelector(".wEmoji-list[data-choose='true']"); if(chooseWrapper){
chooseList.setAttribute("data-choose","false");
chooseWrapper.setAttribute("data-choose","false");
}
targetWrapper.setAttribute("data-choose","true");
targetList.setAttribute("data-choose","true"); this.currentWrapper = targetWrapper;
this.__createPageList();
},

下面是页面的切换showPage():完成初始化和事件触发时页面的切换

showPage : function(idx){
this.activePage = idx;
var wrapper = this.wrapper,
currentWrapper = this.currentWrapper,
pageTargetList = wrapper.querySelector(".wEmoji-page-list[data-pageIdx='"+(idx+1)+"']"),
pageChoose = wrapper.querySelector(".wEmoji-page-list[data-choose='true']"); if(pageChoose)
pageChoose.setAttribute("data-choose","false");
pageTargetList.setAttribute("data-choose","true"); currentWrapper.style.webkitTransform = "translateX("+(-this.wrapWidth*idx)+"px) translateZ(0px)";
currentWrapper.style.transform = "translateX("+(-this.wrapWidth*idx)+"px) translateZ(0px)";
}

最后一个是将code解释成img的功能函数explain(): 大家通过前面的介绍可以知道code的生成规则

explain : function(str){
var reg = /\[wem:(\w+):wem\]/g,
_self = this; return str.replace(reg,function(str,target){
var tempArr = target.split("_"),
eid = tempArr.shift(),
type = tempArr.pop(),
name = tempArr.join("_");
path = _self.emojis[eid].path;
url = name+"."+type; return '<img src="'+path+url+'" />';
});
},

基本上主要代码就这么多了,还有一部分代码可以看源代码来了解,因为我基本上都有写注释所以应该不怎么难理解。

五、演示效果

演示demo效果

六、文件截图以及运行操作

1、文件截图

2、运行操作:

双击index.html即可看到效果。

六、文件截图以及运行操作

1、目前只兼容ie8+,firefox,360浏览器等主流浏览器

注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权

用javascript写一个emoji表情插件的更多相关文章

  1. 给Ionic写一个cordova(PhoneGap)插件

    给Ionic写一个cordova(PhoneGap)插件 之前由javaWeb转html5开发,由于面临新技术,遂在适应的过程中极为挣扎,不过还好~,这个过程也极为短暂:现如今面临一些较为复杂的需求还 ...

  2. 如何写一个Js上传图片插件。

    项目里面需要一个上传图片的插件,找了半天没有找到满意的,算了 不找了,自己写一个吧,顺便复习一下js方面的知识.完成之后效果还不错,当然还要继续优化,源码在最后. 介绍一种常见的js插件的写法 ; ( ...

  3. 怎么用JavaScript写一个区块链?

    几乎所有语言都可以编写区块链开发程序.那么如何用JavaScript写一个区块链?以下我将要用JavaScript来创建1个简单的区块链来演示它们的内部到底是怎样工作的.我将会称作SavjeeCoin ...

  4. Skywalking-02:如何写一个Skywalking trace插件

    如何写一个Skywalking trace插件 javaagent 原理 美团技术团队-Java 动态调试技术原理及实践 类图 实现 ConsumeMessageConcurrentlyInstrum ...

  5. JavaScript写一个连连看的游戏

    天天看到别人玩连连看, 表示没有认真玩过, 不就把两个一样的图片连接在一起么, 我自己写一个都可以呢. 使用Javascript写了一个, 托管到github, 在线DEMO地址查看:打开 最终的效果 ...

  6. 写一个Vue loading 插件

    什么是vue插件? 从功能上说,插件是为Vue添加全局功能的一种机制,比如给Vue添加一个全局组件,全局指令等: 从代码结构上说,插件就是一个必须拥有install方法的对象,这个方法的接收的第一个参 ...

  7. 用JavaScript写一个区块链

    几乎每个人都听说过像比特币和以太币这样的加密货币,但是只有极少数人懂得隐藏在它们背后的技术.在这篇博客中,我将会用JavaScript来创建一个简单的区块链来演示它们的内部究竟是如何工作的.我将会称之 ...

  8. 改变滚动条的原始样式: chrome 可以改变, IE只能变相关颜色,firfox好像也不好改。最好是自己写一个或是用插件

    相关作者链接地址: https://www.lyblog.net/detail/314.html 问题: 1.我在项目中遇到的问题: 在设置了::-webkit-scrollbar 后,滚动条不见了! ...

  9. 用javascript写一个前端等待控件

    前端等待控件有啥新奇的?什么jquery啦,第三方控件啦,好多好多,信手拈来. 因为项目使用了bootstrap的原因,不想轻易使用第三方,怕不兼容.自己写一个. 技术点包括动态加载CSS,javas ...

随机推荐

  1. IPHONE IOS6 模拟器没有HOME按键解决方法

    替代home键的快捷键是 Cmd-Shift-H: 双击HOME键就是 Cmd-Shift-H 按两次: 参考:http://www.cnblogs.com/yingkong1987/archive/ ...

  2. ExtJS BorderLayout

    <HTML> <HEAD> <TITLE>布局</TITLE> <link rel="stylesheet" type=&qu ...

  3. wrap ConcurrentDictionary in BlockingCollection

    ConcurrentDictionary<int, BlockingCollection<string>> mailBoxes = new ConcurrentDictiona ...

  4. oracle extract函数

    oracle Extract 函数 //oracle中extract()函数从oracle 9i中引入,用于从一个date或者interval类型中截取到特定的部分   //语法如下:   EXTRA ...

  5. dom4j怎么获得指定名称的节点信息

    <?xml version="1.0" encoding="utf-8" ?> <MgUtil> <db_config> & ...

  6. connection to sys should be as sysdba or sysoper 解决的方法

    连接时提示: 出现了例如以下的情况:EXP-00056;遇到ORACLE错误28009. ORA-28009:connection   to   sys   should   be   as   sy ...

  7. UVA 10012 How Big Is It?(暴力枚举)

      How Big Is It?  Ian's going to California, and he has to pack his things, including his collection ...

  8. GoLang中如何使用多参数属性传参

    我们常常因为传入的参数不确定而头疼不已,golang 为我们提供了接入多值参数用于解决这个问题.但是一般我们直接写已知代码即所有的值都知道一个一个塞进去就好了,但是绝大部分我们是得到用户的大量输入想通 ...

  9. 廖雪峰的python学习网址

    http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/00140737570055886 ...

  10. Cognos让指定用户不具有删除内容的权限

    为了方便用户使用Cognos,现在很多对权限要求不够严格的用户就想到了可以让用户实现匿名登陆,即不登陆系统即可实现访问报表,当然这也仅仅是按照客户的需求,我个人认为一个安全性的数据平台还是需要对登陆. ...