如何编写一个 jQuery 插件

参考资料

  1. How to Create a Basic Plugin

2016-8-13 更新

https://github.com/zcfan/sket... 重写了本文的初步功能实现,支持一个页面多个画图板。但为简单起见,本文保持不变。

正文

简单的说一个 jQuery 插件只是我们拿来扩展 jQuery prototype 对象的一个方法。通过扩展 prototype 对象,我们可以让所有的 jQuery 对象继承我们添加的方法。

下面我们以一个画图板插件为目标,完成后它将能够把一个 div 标记扩展成最基本的画图板。

基本的插件

从最简单开始,我们要做的第一件事是给选中的div加一个边框,好让用户能看到画板的区域。

创建 index.html 文件,引入 jQuery ,然后创建并引入我们的插件文件。尽管只是一个例子,但规范命名还是个好习惯,就叫做jquery.sketchpad.js好了

<!-- file: index.html -->
<!DOCTYPE HTML>
<html>

<head>
<script src="http://cdn.bootcss.com/jquery/3.1.0/jquery.min.js"></script>
<script src="jquery.sketchpad.js"></script>
<style>
  #sketchpad {
    width: 200px;
    height: 200px;
  }
</style>
</head>

<body>
<div id="sketchpad"></div>
</body>

</html>
// file: jquery.sketchpad.js
$.fn.sketchpad = function() {
  this.css('border', '1px solid #000');
};
// 使用
$('.sketchpad').sketchpad(); // 给所有.sketchpad加上边框

如上,想要添加一个名为 sketchpad 的插件,只需要给 $.fn 对象添加一个新方法就可以了。然后这个方法就可以在所有的 jQuery 对象上调用。

刷新页面,应该能够看到一个 200x200 的黑框

支持链式调用

上面的代码能工作,但是还欠缺了很多必要的东西。jQuery的一个特色就是允许链式调用,它使你可以对一个选择器选中的元素连着执行许多操作。

这个特性的实现方式是让所有的 jQuery 方法都返回一开始的 jQuery 对象。(有一些例外,比如.width()如果不提供参数的话就会返回所选元素的宽度,并且不可链式调用)

因此想让我们的插件能够支持链式调用只需要多一行代码:

// file: jquery.sketchpad.js
$.fn.sketchpad = function() {
  this.css('border', '1px solid #000');
  return this; // 返回该 jQuery 对象以支持链式调用
};
// 使用
$('.sketchpad').sketchpad().hide(); // 支持链式调用

保护 $ 化名并引入私有作用域

$作为一个简写化名确实非常方便,但是在实际使用中,总免不了会与其它 js 代码产生冲突。这时我们会需要调用jQuery.noConflict()让jquery不再使用$化名以避免冲突。

这个时候,我们前面的插件就会出问题,因为它编写的时候用到了$化名。为了能够和其它的诸多插件友好相处,并且能够继续使用方便的$,我们需要把所有代码扔进一个“立即执行函数的表达式”里,传入jQuery作为实参,形参处命名为$

// file: jquery.sketchpad.js
(function($){
    
  $.fn.sketchpad = function() {
    this.css('border', '1px solid #000');
    return this;
  };
    
}(jQuery));

这样做还有另一个重要的原因,加入这样一个函数还能允许我们引入一个私有的变量作用域,不至于插件里的一些变量污染公共空间。比如说我们现在要创建一个 canvas 对象用来画图,就可以这样做:

// file: jquery.sketchpad.js
(function($){

  // 本插件的私有函数
  function createCanvas(width, height) {
    var canvas = $('<canvas></canvas>');
    canvas[0].width = width;
    canvas[0].height = height;
    return canvas;
  }
    
  $.fn.sketchpad = function() {
    this.css('border', '1px solid #000');
    var canvas = createCanvas(this.width(), this.height());
    var ctx = canvas[0].getContext('2d'); // 获得 context 用于画图
    this.append(canvas);
    
    return this;
  };
    
}(jQuery));

我们创建了一个新的私有函数 createCanvas 用于创建画布,避免将冗长的初始化代码堆在主函数里。

完成剩余的插件功能

// jquery.sketchpad.js
(function($){

  function createCanvas(width, height) {
    var canvas = $('<canvas></canvas>');
    canvas[0].width = width;
    canvas[0].height = height;
    return canvas;
  }

  $.fn.sketchpad = function() {
    this.css('border', '1px solid #000');
    var canvas = createCanvas(this.width(), this.height());
    var ctx = canvas[0].getContext('2d');
    this.append(canvas);

    var isDrawing = false;
    var offset = {
      left: this[0].offsetLeft,
      top:  this[0].offsetTop
    }
    var pos
    var prevX = 0,
        prevY = 0,
        currX = 0,
        currY = 0;

    canvas.mousedown(mousedown);
    canvas.mouseup(mouseup);
    canvas.mousemove(mousemove);

    function calXY(e) {
      return {
        x: e.clientX - offset.left,
        y: e.clientY - offset.top
      };
    }

    function mousedown(e) {
      var pos = calXY(e);
      prevX = pos.x;
      prevY = pos.y;
      isDrawing = true;
    }

    function mouseup(e) {
      isDrawing = false;
    }

    function mousemove(e) {
      if (!isDrawing) return;
      var pos = calXY(e);
      currX = pos.x;
      currY = pos.y;

      ctx.beginPath();
      ctx.moveTo(prevX, prevY);
      ctx.lineTo(currX, currY);
      ctx.strokeStyle = '#000';
      ctx.lineWidth = 2;
      ctx.stroke();
      ctx.closePath();

      prevX = currX;
      prevY = currY;
    }

    return this;
  };
    
}(jQuery));

本插件仅作为范例,功能较为简单,只是使用鼠标事件以及 canvas 画线。到此已经实现了开头说明的功能,但仍然可以继续扩展下去:保存载入、橡皮擦、色板甚至滤镜直至成为一个可以真正投入使用的插件。但随着插件发展与复杂度的增加,还有许多其他的地方需要注意。

尽量减少插件名字占用

编写插件时应该只占用$.fn的一个位置。因为其它的插件也都在往这里塞东西,只占用一个名字能够避免我们的插件覆盖别人的名字或者被别人覆盖。举例说明,这样做不好:

(function( $ ) {
 
    $.fn.openPopup = function() {
        // Open popup code.
    };
 
    $.fn.closePopup = function() {
        // Close popup code.
    };
 
}( jQuery ));

这样就好多了,只占用一个名字,并且使用参数来调整插件的行为:

(function( $ ) {
 
    $.fn.popup = function( action ) {
 
        if ( action === "open") {
            // Open popup code.
        }
 
        if ( action === "close" ) {
            // Close popup code.
        }
 
    };
 
}( jQuery ));

使用each()方法

一个典型的 jQuery 对象会包含任意数量元素的引用,这也就是为什么 jQuery 对象经常是以集合的形式返回的。如果你想要对特定的某个元素做一些操作的话(比如获取数据属性,计算特定的位置),你就会需要使用each()来枚举这些元素。

$.fn.myNewPlugin = function() {
 
    return this.each(function() {
        // 对每个单独的元素做一些操作
    });
 
};

让插件能够配置

随着我们的插件以后越来越复杂,让它能够通过传入设置选项来自定义就十个好主意了。要做到这点有个最简单的办法(特别是当可配置项特别多的时候),那就是通过传入一个 object 。下面我们继续修改前面的 greenify 插件来接受配置:

(function($){
    
    $.fn.greenify = function(options) {
        
        // 实现默认设置的最简单方式
        var settings = $.extend({
            // 默认的设置
            color: '#00ff00',
            backgroundColor: 'white'
        }, options);
        
        return this.css({
            color: settings.color,
            backgroundColor: settings.backgroundColor
        });
    };
    
}(jQuery));

休整一下,择日再战

看完上面的内容之后,我们大概了解了一个简单的插件是怎么编写的,并且了解了一些社区总结的设计经验。当然这只是一个开端,还有许多进阶的内容没有介绍。比如参考资料中的下一篇 Advanced Plugin Concepts 就是进一步学习的好选择。

但今天已经够了,我们已经 get 了一个新技能,还有待实践来融汇贯通。欲速则不达,现在应该是开瓶可乐(你可以换啤酒),让自己沉浸在成就感中画幅骄傲的自画像的时候才对。

?干杯!~

我刚刚把代码扔到了 Github 上了,玩着蛮有意思的,打算做一个业余项目继续完善。然后一搜发现了好几个类似的项目。。。?https://github.com/zcfan/sket...

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏梦魇小栈

面试分享:2018阿里巴巴前端面试总结(题目+答案)

最开始的思路是用定时器实现,最后没有想的太完整,面试官给出的答案是用requestAnimationFrame。

1173
来自专栏web前端

JavaScript基础学习--02属性操作

一、思路 1、模拟手机聊天思路:      a.静态页面html+css,包括双发短信发送成功后的基本样式。      b.获取头像、输入框、发送按钮和聊天内...

2009
来自专栏余生开发

base64图片转码

方法一: new fileReader().readAsDataURL(file) 方法二: var imgCanvas=document.createE...

4573
来自专栏偏前端工程师的驿站

前端构建:Less入了个门

一、前言                                说到前端构建怎能缺少CSS预处理器呢!其实CSS的预处理器有很多啦,比较出名的有Scs...

1917
来自专栏云瓣

深入Redux架构

关于redux 之前写了一篇通过一个demo了解Redux,但对于redux的核心方法没有进行深入剖析,在此重新总结学习,完整的代码看这里。(参考了React ...

2816
来自专栏逸鹏说道

Jupyter-Notebook版的博客园美化

文章汇总:https://www.cnblogs.com/dotnetcrazy/p/9160514.html

1472
来自专栏更流畅、简洁的软件开发方式

见到了“公司”定义一个Company类,那么见到了“字段”是不是也可定义一个Column类?

  既然见到了公司,我们可以定义一个Class Company ,那么我们见到了字段,是不是也可以定义一个Class ColumnInfo呢? 公司的描述信息类...

2499
来自专栏知道一点点

走进AngularJs(二) ng模板中常用指令的使用方式

  通过使用模板,我们可以把model和controller中的数据组装起来呈现给浏览器,还可以通过数据绑定,实时更新视图,让我们的页面变成动态的。ng的模板真...

1432
来自专栏Spring相关

前端的CRUD增删改查的小例子

1113
来自专栏进步博客

解决CSV文件中长数字以科学记数格式保存问题

992

扫码关注云+社区

领取腾讯云代金券