OpenLayers 6 + webpack 通过源码分析来实现一个简单的自定义控件

晚上鼓捣webpack环境的时候,忽然心血来潮,想试试实现一个自定义的地图控件。其实网络上已经有很多相关的教程,实现思路大多是采用jQuery做一个UI组件外挂上去。不过这种方式实现的控件可重用性不是很高。

为了有别于其他人的方案,达到可重用的要求,我采用的是通过继承ol.control类的方式来实现。
基本思路:

通过阅读OpenLayers的源码,仿制一个点击之后可以弹出一个警告框的控件。
在这里插入图片描述

实现步骤:

先看一看OpenLayers的源码。为了简化问题,我找了最简单的FullScreen的控件源码来参考:

import Control from './Control.js';
import {CLASS_CONTROL, CLASS_UNSELECTABLE, CLASS_UNSUPPORTED} from '../css.js';
import {replaceNode} from '../dom.js';
import {listen} from '../events.js';
import EventType from '../events/EventType.js';
 
const events = ['fullscreenchange', 'webkitfullscreenchange', 'MSFullscreenChange'];
 
/**
 * @typedef {Object} Options
 * @property {string} [className='ol-full-screen'] CSS class name.
 * @property {string|Text} [label='\u2922'] Text label to use for the button.
 * Instead of text, also an element (e.g. a `span` element) can be used.
 * @property {string|Text} [labelActive='\u00d7'] Text label to use for the
 * button when full-screen is active.
 * Instead of text, also an element (e.g. a `span` element) can be used.
 * @property {string} [tipLabel='Toggle full-screen'] Text label to use for the button tip.
 * @property {boolean} [keys=false] Full keyboard access.
 * @property {HTMLElement|string} [target] Specify a target if you want the
 * control to be rendered outside of the map's viewport.
 * @property {HTMLElement|string} [source] The element to be displayed
 * fullscreen. When not provided, the element containing the map viewport will
 * be displayed fullscreen.
 */
 
class FullScreen extends Control {
 
  /**
   * @param {Options=} opt_options Options.
   */
  constructor(opt_options) {
 
    const options = opt_options ? opt_options : {};
 
    super({
      element: document.createElement('div'),
      target: options.target
    });
 
    /**
     * @private
     * @type {string}
     */
    this.cssClassName_ = options.className !== undefined ? options.className :
      'ol-full-screen';
 
    const label = options.label !== undefined ? options.label : '\u2922';
 
    /**
     * @private
     * @type {Text}
     */
    this.labelNode_ = typeof label === 'string' ?
      document.createTextNode(label) : label;
 
    const labelActive = options.labelActive !== undefined ? options.labelActive : '\u00d7';
 
    /**
     * @private
     * @type {Text}
     */
    this.labelActiveNode_ = typeof labelActive === 'string' ?
      document.createTextNode(labelActive) : labelActive;
 
    /**
     * @private
     * @type {HTMLElement}
     */
    this.button_ = document.createElement('button');
 
    const tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen';
    this.setClassName_(this.button_, isFullScreen());
    this.button_.setAttribute('type', 'button');
    this.button_.title = tipLabel;
    this.button_.appendChild(this.labelNode_);
 
    this.button_.addEventListener(EventType.CLICK, this.handleClick_.bind(this), false);
 
    const cssClasses = this.cssClassName_ + ' ' + CLASS_UNSELECTABLE +
        ' ' + CLASS_CONTROL + ' ' +
        (!isFullScreenSupported() ? CLASS_UNSUPPORTED : '');
    const element = this.element;
    element.className = cssClasses;
    element.appendChild(this.button_);
 
    /**
     * @private
     * @type {boolean}
     */
    this.keys_ = options.keys !== undefined ? options.keys : false;
 
    /**
     * @private
     * @type {HTMLElement|string|undefined}
     */
    this.source_ = options.source;
 
  }
 
  /**
   * @param {MouseEvent} event The event to handle
   * @private
   */
  handleClick_(event) {
    event.preventDefault();
    this.handleFullScreen_();
  }
……

直接看一下构造函数:这里是对初始化的时候所用的配置项的处理,以及对父类中的成员进行初始化的过程,可以不用动。

  constructor(opt_options) {
 
    const options = opt_options ? opt_options : {};
 
    super({
      element: document.createElement('div'),
      target: options.target
    });

接下来是定义控件的css样式,也不用动:

    /**
     * @private
     * @type {string}
     */
    this.cssClassName_ = options.className !== undefined ? options.className :
      'ol-full-screen';

接下来是定义按钮上的文字标签,原来的FullScreen控件定义了两个,对应是否全屏的两个状态,我们后面只需要定义一个就行了:

    const label = options.label !== undefined ? options.label : '\u2922';
 
    /**
     * @private
     * @type {Text}
     */
    this.labelNode_ = typeof label === 'string' ?
      document.createTextNode(label) : label;
 
    const labelActive = options.labelActive !== undefined ? options.labelActive : '\u00d7';
 
    /**
     * @private
     * @type {Text}
     */
    this.labelActiveNode_ = typeof labelActive === 'string' ?
      document.createTextNode(labelActive) : labelActive;

下面几行代码功能比较密集,在代码注释里面加以说明:

    /**
     * @private
     * @type {HTMLElement}
     */
 
    //创建控件的HTML控件,是一个按钮
    this.button_ = document.createElement('button');
    
    //为这个按钮初始化hint文字
    const tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen';
    
    //根据全屏状态设置按钮的样式名称
    this.setClassName_(this.button_, isFullScreen());
    
    //为控件添加一个button的类型属性
    this.button_.setAttribute('type', 'button');
    
    //为这个按钮添加hint文字
    this.button_.title = tipLabel;
    
    //将按钮文字标签添加到按钮上
    this.button_.appendChild(this.labelNode_);
 
    //为HTML按钮绑定点击事件和回调函数
    this.button_.addEventListener(EventType.CLICK, this.handleClick_.bind(this), false);
 
    //构造css类名
    const cssClasses = this.cssClassName_ + ' ' + CLASS_UNSELECTABLE +
        ' ' + CLASS_CONTROL + ' ' +
        (!isFullScreenSupported() ? CLASS_UNSUPPORTED : '');
    
    //获取本控件的DOM对象句柄,并将按钮添加为这个DOM对象的子节点
    const element = this.element;
    element.className = cssClasses;
    element.appendChild(this.button_);
 
    //初始化另外两个属性
    /**
     * @private
     * @type {boolean}
     */
    this.keys_ = options.keys !== undefined ? options.keys : false;
 
    /**
     * @private
     * @type {HTMLElement|string|undefined}
     */
    this.source_ = options.source;
 
  }

接下来是处理点击事件的回调函数:

/**
   * @param {MouseEvent} event The event to handle
   * @private
   */
  handleClick_(event) {
    event.preventDefault();
    this.handleFullScreen_();
  }

到这里,我们所需要的东西就基本上具备了,接下来就可以仿照写一个自己的MyControl控件了。

import Control from 'ol/control/Control';
import {CLASS_CONTROL, CLASS_UNSELECTABLE, CLASS_UNSUPPORTED} from 'ol/css';
import {listen} from 'ol/events';
import EventType from 'ol/events/EventType';
 
class MyControl extends Control {
 
  constructor(opt_options) {
 
    const options = opt_options ? opt_options : {};
 
    super({
      element: document.createElement('div'),
      target: options.target
    });
 
    this.cssClassName_ = options.className !== undefined ? options.className :
      'ol-full-screen';
 
    const label = options.label !== undefined ? options.label : '\u00f7';
    this.labelNode_ = typeof label === 'string' ?
      document.createTextNode(label) : label;
 
    this.button_ = document.createElement('button');
    const tipLabel = options.tipLabel ? options.tipLabel : '点我';
 
    this.button_.setAttribute('type', 'button');
    this.button_.title = tipLabel;
    this.button_.appendChild(this.labelNode_);
    listen(this.button_, EventType.CLICK,
      this.handleClick_, this);
    const element = this.element;
    const cssClasses = this.cssClassName_ + ' ' + CLASS_UNSELECTABLE +
    ' ' + CLASS_CONTROL;
    element.className = cssClasses;
    element.appendChild(this.button_);
 
  }
 
  handleClick_(event) {
    event.preventDefault();
    alert('Your control is online!');
  }
}

使用的时候,做如下调用(下面的代码段省略了各种import):

let map = new Map({
  controls: defaultControls().extend([
    new MyControl()
  ]),
  target: 'map',
  layers: [
    new TileLayer({
      source: new XYZ({
        url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png'
      })
    })
  ],
  view: new View({
    center: [0, 0],
    zoom: 2
  })
});

和原生风格完全一样,并且可重用性绝对OK。