Leaflet的包很小,Leaflet 的核心包只提供了构造地图必须的功能,其他的功能像测量、标绘工具等通过第三方插件来实现。有赖于Leaflet强大的而简单的类架构及扩展体系,网络上有丰富的Leaflet插件,当你要实现一个功能时,例如热力图,大概总能从网上找到一款合适的插件,可以直接使用或者提供思路改造后使用。另一方面,这也造成了基于Leaflet开发的另一个问题,就是你需要花挺大一部分时间来选择合适的插件。无论如何,Leaflet的类扩展体系,绝对是其最精彩的部分之一。
就我个人使用的情况来看,Leaflet的类体系确实很强大、实用、便捷。再加上Leaflet的架构清晰,是学习JavaScript的优秀示范。我个人也在使用Leaflet的过程中基于其类体系,扩展了很多内容。在支撑项目的过程中,项目上也基于其类体系扩展了项目上的方法库,在调试时其动态扩展功能也发挥了很大作用。然而Leaflet 在类方面也不是毫无缺点,像L.Point等对象是通过funtion实现的,而在渲染方面,也没有将Canvas的绘图方式像ArcGIS for JavaScript一样,抽象为Graphic、Geometry、Symbol等内容。
有在犹豫要不要继续Leaflet的官方教程翻译系列,一方面工作中Leaflet用的越来越少了,另一方面现在翻译软件越来越强大,再有似乎看的人也不多,于人于己似乎都用处不大。因为Leaflet的OOP实在是比较经典,在使用过程中也是印象深刻,所以,勉为其难,再翻译这一篇吧,就算作是给Leaflet的告别仪式。(当然,如果大家喜欢的话,可以继续翻其他的,毕竟官方教程统共也没几篇,哈哈)
Extending Leaflet
扩展Leaflet
Leaflet has literally hundreds of plugins. These expand the capabilities of Leaflet: sometimes in a generic way, sometimes in a very use-case-specific way.
Leaflet 数百个插件。这些插件扩展了Leaflet的功能,有的以通用方式,有的是特定用例。
Part of the reason there are so many plugins is that Leaflet is easy to extend. This tutorial will cover the most commonly used ways of doing so.
Leaflet有这么多插件的原因之一是Leaflet易于扩展。本教程会介绍最常用的方面。
Please note that this tutorial assumes you have a good grasp of:
注意,本教程假定你已经具备以下技能。
- JavaScript
- DOM handling
- Object-oriented programming (understanding concepts like classes, instances, inheritance, methods and properties)
- JavaScript 编程知识
- DOM 操作
- 面向对象编程(理解类、实例、集成、方法和属性等概念)
Leaflet architecture
Leaflet 类体系
Let’s have a look at a simplified UML Class diagram for Leaflet 1.0.0. There are more than 60 JavaScript classes, so the diagram is a bit big. Luckily we can make a zoomable image with a L.ImageOverlay
:
我们来看一下Leaflet 1.0.0的简化的UML 类图。类图有点大,有60多个JavaScript类。幸运的是Leaflet 提供的L.ImageOverlay
类可以将类图转换为一个可缩放的图片。
See this example stand-alone. |
From a technical point of view, Leaflet can be extended in different ways:
从技术角度看,Leaflet可以通过多种方式扩展:
- The most common: creating a new subclass of
L.Layer
,L.Handler
orL.Control
, withL.Class.extend()
- Layers move when the map is moved/zoomed
- Handlers are invisible and interpret browser events
- Controls are fixed interface elements
- Including more functionality in an existing class with
L.Class.include()
- Adding new methods and options
- Changing some methods
- Using
addInitHook
to run extra constructor code.
- Changing parts of an existing class (replacing how a class method works) with
L.Class.include()
. - 最常用的:通过L.Class.extend()方式来扩展像L.Layer,L.Handler 或L.Control的子类。
- Layers 图层:在地图平移缩放时会移动。
- Handlers 处理器:处理器是不可见的,用于处理浏览器事件。
- Controls地图控件:地图控件是固定在界面上的组件。
- 在一个已有的类中追加功能,通过L.Class.include()方式扩展。
- 增加新的方法及options。
- 改变方法的实现。
- 通过addInitHook在构造方法中追加代码。
This tutorial covers some classes and methods available only in Leaflet 1.0.0. Use caution if you are developing a plugin for a previous version.
本教程涉及了一些在Leaflet 1.0.0版本中的类和方法。如果你正在为之前的版本开发插件,需要注意兼容性问题。
L.Class
L.Class
JavaScript is a bit of a weird language. It’s not really an object-oriented language, but rather a prototype-oriented language. This has made JavaScript historically difficult to use class inheritance in the classic OOP meaning of the term.
JavaScript是一种有点奇怪的语言。它不是真正意义上的面向对象的语言,而是一种面向原型的语言。这使得JavaScript很难实现经典的面向对象编程。
Leaflet works around this by having L.Class
, which eases up class inheritance.
Leaflet 通过L.Class实现OOP,简化了类继承。
Even though modern JavaScript can use ES6 classes, Leaflet is not designed around them.
尽管现代JavaScript可以使用ES6类,但Leaflet不是基于此设计的。
L.Class.extend()
L.Class.extend()
In order to create a subclass of anything in Leaflet, use the .extend()
method. This accepts one parameter: a plain object with key-value pairs, each key being the name of a property or method, and each value being the initial value of a property, or the implementation of a method:
在Leaflet中通过.extend()方法来创建某个类的子类。该方法需要一个参数,一个包含多个键值对的普通数值对象,每个键对应一个属性名或方法名,每个值对应这个属性的初始值或者方法的实现。
var MyDemoClass = L.Class.extend({
// A property with initial value = 42
// 一个属性,初始值为 42
myDemoProperty: 42,
// A method
// 一个方法
myDemoMethod: function() { return this.myDemoProperty; }
});
var myDemoInstance = new MyDemoClass();
// This will output "42" to the development console
// 以下代码会在控制台输出 42
console.log( myDemoInstance.myDemoMethod() );
When naming classes, methods and properties, adhere to the following conventions:
给类、方法、属性命名时,遵循以下约定:
- Function, method, property and factory names should be in lowerCamelCase.
- Class names should be in UpperCamelCase.
- Private properties and methods start with an underscore (
_
). This doesn’t make them private, just recommends developers not to use them directly. - 函数、方法、属性以及 工厂名应为首字母小写的驼峰命名法。
- 类名首字母大写,驼峰命名。
- 私有属性和方法应以下划线_开头。这样的命名并不会真的让他们变成私有属性或方法,只是提醒开发者不要直接使用它们。
L.Class.include()
L.Class.include()
If a class is already defined, existing properties/methods can be redefined, or new ones can be added by using .include()
:
如果已经定义了一个类,可以使用.include()方法重新定义其中的属性、方法,也可以添加新的属性、方法。
MyDemoClass.include({
// Adding a new property to the class
// 为类增加一个新的属性
_myPrivateProperty: 78,
// Redefining a method
// 重新定义myDemoMethod方法
myDemoMethod: function() { return this._myPrivateProperty; }
});
var mySecondDemoInstance = new MyDemoClass();
// This will output "78"
// 以下会在控制台输出 78
console.log( mySecondDemoInstance.myDemoMethod() );
// However, properties and methods from before still exist
// This will output "42"
// 原有的属性和方法调用
// 以下会输出 42
console.log( mySecondDemoInstance.myDemoProperty );
L.Class.initialize()
L.Class
.initialize()
In OOP, classes have a constructor method. In Leaflet’s L.Class
, the constructor method is always named initialize
.
在面向对象编程中,类有构造方法。在Leaflet的L.Class中,构造方法统一命名为initialize。
If your class has some specific options
, it’s a good idea to initialize them with L.setOptions()
in the constructor. This utility function will merge the provided options with the default options of the class.
如果你的类有一些特殊的options,可以在构造函数中通过L.setOptions方法来对这些options进行初始化。这会将这些options与默认的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" 输出 Red
console.log(instance.options.width); // Outputs "10" 输出 10
console.log(instance.options.height); // Outputs "1", the default 输出 1,默认值
Leaflet handles the options
property in a special way: options available for a parent class will be inherited by a children class:.
Leaflet用特殊的方式处理options属性:子类会继承父类的options属性。
var MyCubeClass = MyBoxClass.extend({
options: {
depth: 1
}
});
var instance = new MyCubeClass('Blue');
// Outputs "1", parent class default
// 输出1 父类中的默认值
console.log(instance.options.width);
// Outputs "1", parent class default
// 输出 1 父类中的默认值
console.log(instance.options.height);
// Outputs "1"
// 输出 1
console.log(instance.options.depth);
It’s quite common for child classes to run the parent’s constructor, and then their own constructor. In Leaflet this is achieved using L.Class.addInitHook()
. This method can be used to “hook” initialization functions that run right after the class’ initialize()
, for example:
在子类中经常需要涉及先调用父类的构造函数,再调用自身的构造函数。在Leaflet中通过L.Class.addInitHook()方法实现,这个方法会把新添加的初始化方法追加到类的initialize()方法之后,例如:
MyBoxClass.addInitHook(function(){
this._area = this.options.width * this.options.length;
});
That will run after initialize()
is called (which calls setOptions()
). This means that this.options
exist and is valid when the init hook runs.
上面hook方法中的代码会在MyBoxClass类的initialize()方法之后调用(initialize方法中会调用setOptions方法)。这意味着在hook方法执行时,this.options已经完成初始化可以使用了。
addInitHook
has an alternate syntax, which uses method names and can fill method arguments in:
addInitHook 方法还有另外一种形式,使用方法名称,并且可以传递参数。
MyCubeClass.include({
_calculateVolume: function(arg1, arg2) {
this._volume = this.options.width * this.options.length * this.options.depth;
}
});
MyCubeClass.addInitHook('_calculateVolume', argValue1, argValue2);
Methods of the parent class
父类的方法
Calling a method of a parent class is achieved by reaching into the prototype of the parent class and using Function.call(…). This can be seen, for example, in the code for L.FeatureGroup
:
调用父类的方法是通过访问父类的原型(prototype)并使用Function.call来实现。开发者们应该在Leaflet的代码中见到过,例如在L.FeatureGroup中:
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);
},
…
});
Calling the parent’s constructor is done in a similar way, but using ParentClass.prototype.initialize.call(this, …)
instead.
调用父类的构造方法类似,通过ParentClass.prototype.initialize.call(this, …)
实现。
Factories
工厂方法
Most Leaflet classes have a corresponding factory function. A factory function has the same name as the class, but in lowerCamelCase
instead of UpperCamelCase
:
大部分Leaflet的类都有一个对应的工厂方法。工厂方法名字与类名一样,只是采用首字母小写的驼峰命名形式而非首字母大写的驼峰命名形式。
function myBoxClass(name, options) {
return new MyBoxClass(name, options);
}
Naming conventions
命名约定
When naming classes for Leaflet plugins, please adhere to the following naming conventions:
当给Leaflet插件类命名时,请遵循以下命名约定:
- Never expose global variables in your plugin.
- If you have a new class, put it directly in the
L
namespace (L.MyPlugin
). - If you inherit one of the existing classes, make it a sub-property (
L.TileLayer.Banana
). - 不要在插件中公开全局变量。
- 如果是创建一个新类(区别于下面的继承已有的类),直接放到L命名空间下(例如:L.MyPlugin)。
- 如果是继承一个已有的类,将其放到子空间下(例如:L.TileLayer.Banana)。