Leaflet 学习系列(四)如何写插件理论篇

本篇主要介绍 Leaflet 开发自定义插件的一些理论知识,在日常开发中,仅仅使用 Leaflet 提供的类虽然能满足一些基本需求,但是在一些功能点的实现上需要我们自己写插件来实现功能。如上文中为了实现点聚合的效果,我们就使用了三方插件,目前 Leaflet 相关的三方插件 (opens new window)已经基本满足大部分需求,如果你有一些需求不知道怎么实现,可以先从这里 (opens new window)找一找,如果没有找到的合适的话,然后再自己实现插件。所谓插件即对一些功能的封装,比如之前 TileLayer 我们通过传入不同的参数,来实现加载不同的地图,如果通过扩展 Leaflet 提供的 TileLayer 类,把这些参数定义在子类内部,然后当做一个独立类导出,这样在其他人使用时就无需关心内部逻辑,极大降低了使用难度,同时也避免了大量冗余代码。为 Leaflet 编写插件需要以下基础知识:

  • JavaScript
  • DOM 操作
  • 面向对象编程思想
  • 熟悉 Leaflet 原有类

首先理论篇和实践篇主要翻译自以下三篇官方教程,当然翻译过程中加入了一些自己的理解,对于英文水平较好且对 Leaflet 已经有一定实践经验的还是建议阅读官方文档和教程。

# Leaflet 结构

首先来熟悉下 Leaflet 的结构,只有熟悉了 Leaflet 都有哪些类,才能在编写插件时得心应手,知道要扩展那个类,或者哪些类一起使用能实现需求。

在线查看 (opens new window)

Leaflet 有以下几种扩展方式:

  • 最常用的方式:使用 L.Class.extend() 创建 L.Layer, L.Handler 或者 L.Control 的子类实现几种需求
    • 地图移动的同时移动图层
    • 浏览器事件
    • 地图上的控件元素
  • 使用 L.Class.include()Class 添加功能
    • 添加新的 methodsoptions
    • 修改一些 methods
    • 使用 addInitHook 执行额外的构造函数代码
  • 使用 L.Class.include() 更改现有 Class 的部分

# L.Class

我们知道在 ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链,其优缺点以及实现的方式就不一一介绍了。Leaflet 中的 L.Class 提供了 extend()include()initialize() 方法,可以方便实现类的继承。

# L.Class.extend()

使用 .extend() 方法创建 Leaflet 中类的子类,方法可传入一个包含键值对的普通对象,对应的 key 就是子类的属性名或者方法名,key 对应的 value 就是其默认值或者方法的实现。

var MyDemoClass = L.Class.extend({
    // A property with initial value = 42
    myDemoProperty: 42,   
    // A method 
    myDemoMethod: function() { return this.myDemoProperty; }
});

var myDemoInstance = new MyDemoClass();
// This will output "42" to the development console
console.log( myDemoInstance.myDemoMethod() );   

命名 classes, methods 和 properties 时建议遵循以下建议:

  • 函数,方法,属性和工厂命名应该使用小驼峰法 lowerCamelCase
  • 类名应该使用大驼峰法 UpperCamelCase
  • 私有属性和方法建议使用下划线 _ 开头。对于 JavaScript 来说所有方法和属性都能被访问,增加下划线前缀只是让我们容易区分属性和方法的状态。

# L.Class.include()

使用 .include() 可以为一个已经定义的类重新定义属性、方法或者添加新的属性和方法。

MyDemoClass.include({
    // Adding a new property to the class
    _myPrivateProperty: 78,
    // Redefining a method
    myDemoMethod: function() { return this._myPrivateProperty; }
});

var mySecondDemoInstance = new MyDemoClass();
// This will output "78"
console.log( mySecondDemoInstance.myDemoMethod() );
// However, properties and methods from before still exist
// This will output "42"
console.log( mySecondDemoInstance.myDemoProperty );

# L.Class.initialize()

Leaflet 中的类,构造函数的方法命名为 initialize。我们可以在构造函数中处理自定义类和原始 options 的合并操作以及其他初始化操作。

var MyBoxClass = L.Class.extend({
    options: {
        width: 1,
        height: 1
    },
    initialize: function(name, options) {
        this.name = name;
        L.setOptions(this, options);
    }
});

var instance = new MyBoxClass('Red', {width: 10});

console.log(instance.name); // Outputs "Red"
console.log(instance.options.width); // Outputs "10"
console.log(instance.options.height); // Outputs "1", the default

子类会继承父类中的 options

var MyCubeClass = MyBoxClass.extend({
    options: {
        depth: 1
    }
});

var instance = new MyCubeClass('Blue');

console.log(instance.options.width); // Outputs "1", parent class default
console.log(instance.options.height); // Outputs "1", parent class default
console.log(instance.options.depth); // Outputs "1"

我们经常会有这种需求,先执行父类的构造函数,然后在执行子类的构造函数。此时可以使用 L.Class.addInitHook() 实现。在 initialize 函数执行之后会触发钩子执行该方法,此时 this.options 已存在且可被访问。

MyBoxClass.addInitHook(function(){
    this._area = this.options.width * this.options.length;
});

或者下面这种写法:

MyCubeClass.include({
    _calculateVolume: function(arg1, arg2) {
        this._volume = this.options.width * this.options.length * this.options.depth;
    }
});

MyCubeClass.addInitHook('_calculateVolume', argValue1, argValue2);

# 父类方法

使用 Funcation.call(...) 调用父类的方法。

L.FeatureGroup = L.LayerGroup.extend({

    addLayer: function (layer) {L.LayerGroup.prototype.addLayer.call(this, layer);
    },
    
    removeLayer: function (layer) {L.LayerGroup.prototype.removeLayer.call(this, layer);
    },});

调用父类的构造函数 ParentClass.prototype.initialize.call(this, …)

# 工厂函数

大多数 Leaflet 中的类都有一个相应的工厂函数,函数名同类名相同,不过命名由大写驼峰改为了小写驼峰。

function myBoxClass(name, options) {
    return new MyBoxClass(name, options);
}

# 命名约定

在为插件命名时,建议遵循以下约定:

  • 不在插件中暴露全局变量
  • 如果有一个新的类,直接放在 L 命名空间下 (L.MyPlugin)
  • 如果继承自一个已经存在的类,将其设置为子属性 (L.TileLayer.BaiduLayer)