专栏首页Alan's Lab如何编写一个 jQuery 插件

如何编写一个 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 条评论
登录 后参与评论

相关文章

  • [swift]读取svg图片为UIBezierPath,开心做动画

    最近手痒又想整点动画玩玩,但是想了几个主意发现稍微复杂一点的手写都一定会累爆。这篇文章记录一下今天折腾的一个方案。说来简单,就是用矢量设计工具舒舒服服的做好设计...

    Alan Zhang
  • [Coursera][From Nand to Tetris / Part I] 第六周 汇编器项目 python 实现

    今天折腾一上午,终于 完成了 Coursera 上 From Nand to Tetris / Part I 这个课程的最后一个汇编器项目。这套课程真是没白跟,...

    Alan Zhang
  • Angular2 返回时组件生命周期函数不被调用的解决方法

    这两天使用 Angular2 遇到的一个 @angular/router 的 bug:

    Alan Zhang
  • MacOS:快速配置单体k8s开发环境

    记录下我们如何在MacOS上快速搭建一套k8s的开发学习环境; 既然快速,那就尽可能减少编译这些行为了. 所以用了 brew 来把玩;

    CRPER
  • 线性判别分析LDA原理总结

        在主成分分析(PCA)原理总结中,我们对降维算法PCA做了总结。这里我们就对另外一种经典的降维方法线性判别分析(Linear Discriminant ...

    刘建平Pinard
  • 外链建设:SEO最重要最难部分

    外链建设是SEO营销中最重要最难部分,如果网站SEO没有做外链建设这部分内容,那么你可能会错失很多潜在网络业务。外链建设将帮助你创建高质量的SEO营销活动,这将...

    林雍岷
  • 1.4 VR扫描:哎呦喂,1.6亿参股!不愧是歌尔果然财大又气粗

    VRPinea
  • 干货 | python数据分析超简单入门 -- 项目实践篇

    ? ? | 导语 适用于数据分析小白们~ ------ up主也是小白一枚,大家一起交流哈 写在前面的话: PS:文末有上期留言活动开奖结果哦! ①.项目来...

    腾讯NEXT学位
  • python 数据分析超简单入门 : 项目实践篇

    适用于数据分析小白们, up 主也是小白一枚,项目来源于 up 主自学 udacity 中的一个项目实践,up 主自身能力不足,因此文章很浅显, 期待和大神们一...

    刘妍
  • 系列3|走进Node.js之多进程模型

    文:正龙(沪江网校Web前端工程师) 本文原创,转载请注明作者及出处 之前的文章“走进Node.js之HTTP实现分析”中,大家已经了解 Node.js 是...

    iKcamp

扫码关注云+社区

领取腾讯云代金券