如何为Leaflet插件添加Typescript定义

8
3个回答

22

步骤1 - 点亮错误

第一步是使用未注释的示例代码,错误将开始在VS Code中点亮。

// sample.ts
L.easyBar([
  L.easyButton('fa-file', function(btn, map){ }),
  L.easyButton('fa-save', function(btn, map){ }),
  L.easyButton('fa-edit', function(btn, map){ }),
  L.easyButton('fa-dot-circle-o', function(btn, map){ })
]).addTo(map);

我们需要创建一个名为“easy-button.d.ts”的文件,并在我们的Typescript文件中引用它。
// sample.ts
import "./easy-button"
L.easyBar([
  L.easyButton('fa-file', function(btn, map){ }),
  L.easyButton('fa-save', function(btn, map){ }),
  L.easyButton('fa-edit', function(btn, map){ }),
  L.easyButton('fa-dot-circle-o', function(btn, map){ })
]).addTo(map);

而在 easy-button.d.ts 中没有任何内容。

// easy-button.d.ts
// empty for now

错误信息为:
error TS2339: Property 'easyBar' does not exist on type 'typeof L'.
error TS2339: Property 'easyButton' does not exist on type 'typeof L'.

这很合理,因为我们还没有定义它们。

如果您参考easyBareasyButton的定义此处此处, 您会发现在原始的Javascript声明中有一些神奇的情况。看起来这两个函数不需要任何参数,但实际上它们是需要的。

L.easyButton = function(/* args will pass automatically */){
  var args = Array.prototype.concat.apply([L.Control.EasyButton],arguments);
  return new (Function.prototype.bind.apply(L.Control.EasyButton, args));
};

这个函数将会在 L.Control.EasyButton 类上调用 new。参数有些晦涩,但你可以从 this line 推断出它们的含义。
initialize: function(icon, onClick, title, id)

步骤2 - 添加类型

// easy-button.d.ts
declare namespace L {
    function easyBar();
    function easyButton();
}

现在我们更接近了一些:

error TS2346: Supplied parameters do not match any signature of call target

很显然,这是因为我们向easyButton提供了2个参数'fa-edit'和回调函数,但是我们没有在参数中声明它们。我们的第二次尝试现在看起来像这样:

// easy-button.d.ts
declare namespace L {
    function easyBar(buttons: any[]);
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void);
}

现在所有的Typescript警告都已经消失了。但是还有更多可以做的事情。首先,easyButton实际上需要4个参数。这很容易解决-观察可选参数如何具有?后缀:
// easy-button.d.ts
declare namespace L {
    function easyBar(buttons: any[]);
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string);
}

步骤三 - 提供返回值

easyButton 方法实际上返回一个 L.Control.EasyButton 实例。目前,Typescript 的定义暗示了 easyButton 返回类型为 any。我们不想要那样!只有当我们提供类型时,Typescript 才是有用的。

declare namespace L {
    function easyBar(buttons: Control.EasyButton[]): Control.EasyBar;
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string) : Control.EasyButton;

    namespace Control {
        class EasyButton { };
        class EasyBar { };
    }
}

Typescript又开始提供有用的警告了:

error TS2339: Property 'addTo' does not exist on type 'EasyBar'.

这是因为EasyBar是L.Control的子类,所以我们需要将该定义引入到我们的定义文件中。
declare namespace L {
    function easyBar(buttons: Control.EasyButton[]): Control.EasyBar;
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string) : Control.EasyButton;

    namespace Control {
        class EasyButton extends L.Control { }
        class EasyBar extends L.Control { }
    }
}

步骤四 - 为EasyButton和EasyBar提供构造函数参数

如果您尝试实例化一个新的EasyButton,代码补全会建议您传递一个L.ControlOptions对象来配置它。实际上,我们需要定义自己的选项。代码补全

declare namespace L {
    function easyBar(buttons: Control.EasyButton[], options?: EasyBarOptions): Control.EasyBar;
    function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string) : Control.EasyButton;

    interface EasyBarOptions {
        position?: ControlPosition
        id?: string
        leafletClasses?: boolean
    }

    interface EasyButtonOptions {
        position?: ControlPosition
        id?: string
        type?: 'replace'|'animate'
        states?: any
        leafletClasses?: boolean
        tagName?: string
    }

    namespace Control {
        class EasyButton extends L.Control {
            constructor(options?: EasyButtonOptions)
        }
        class EasyBar extends L.Control {
            constructor(options?: EasyBarOptions)
        }
    }
}

现在代码完成的情况看起来更好了: 新的EasyButton代码完成

然而,我在states选项上作弊了。我将其声明为any,但实际上应该是

    interface EasyButtonOptions {
        position?: ControlPosition
        id?: string
        type?: 'replace'|'animate'
        states?: EasyButtonState[]
        leafletClasses?: boolean
        tagName?: string
    }

    interface EasyButtonState {
        stateName: string
        onClick: () => void
        title: string
        icon: string
    }

第五步 - 添加jsdoc提示

Typescript将为该插件的用户提供有用的注释。以下是我们可能为easyButton提供文档的示例。

   /**
     * Creates a easyButton
     * @param icon e.g. fa-globe
     * @param onClick the button click handler
     * @param label on the button
     * @param an id to tag the button with
     * @example
     * var helloPopup = L.popup().setContent('Hello World!');
     *
     * L.easyButton('fa-globe', function(btn, map){
     *      helloPopup.setLatLng(map.getCenter()).openOn(map);
     *  }).addTo( YOUR_LEAFLET_MAP );
     */
    function easyButton(
        icon: string,
        onClick: (btn: Control.EasyButton, map: L.Map) => void,
        title?: string,
        id?: string): Control.EasyButton;

步骤 6 修改leaflet类型定义

(从 Leaflet 1.2.0 开始) 删除命名空间声明:

declare namespace L {

并用模块扩展替换它:
import * as L from 'leaflet'
declare module 'leaflet' {

你的测试代码现在应该像这样:

import * as L from 'leaflet'
import 'easy-button'

步骤7:将其整合到原始项目源代码中

打开 node_modules\leaflet-easybutton\package.json 文件,在 style 条目下面添加以下行:

  "main": "src/easy-button.js",
  "style": "src/easy-button.css",
  "typings": "src/easy-button.d.ts",

将我们的easy-button.d.ts移动到node_modules/leaflet-easybutton/src,并测试一切是否正常工作。

然后提交一个拉取请求,这样每个人都可以从这项工作中受益!


你的易于按钮构造器界面不包括L.easyButton的初始化方法。 - Steve
谢谢Steve,我看了一下,不太确定你的意思。这里是类型定义文件 https://github.com/CliffCloud/Leaflet.EasyButton/blob/master/src/easy-button.d.ts ,如果你在Github上提交一个问题,我们可以在那里讨论。 - Chui Tey
正是我需要的!已经找了好几个小时了,谢谢你,Chui! - Juntong Chen
非常感谢您提供这么详细的答案!我仍在努力理解 TypeScript 的工作原理(例如,L.Control 如何既是类又是命名空间?),但这些正是我需要的示例,可以帮助我为另一个 Leaflet 插件编写类型。 - HighCommander4

4
现在它确实可以了。你应该只是
import * as L from 'leaflet';
import 'leaflet-easybutton/src/easy-button';


这对我起作用了。但是我不得不将@import 'leaflet-easybutton/src/easy-button/easy-button.css';添加到styles.scss的顶部,而不是直接在组件中添加css引用。我正在使用Angular 12。 - Neil J

0

类EasyButton扩展自L.Control { constructor(options?: EasyButtonOptions) }

您的新L.EasyButton需要一个'EasyButtonOptions'对象作为构造函数的参数。这与'L.easyButton'示例不匹配。

L.easyButton具有以下选项:

function easyButton(icon: string, onClick: (btn: any, map: any)=>void, title?: string, id?: string) : Control.EasyButton;

而EasyButtonOptions没有'icon'、'onClick'、'title'或'id',而是采用以下选项:

interface EasyButtonOptions { position?: ControlPosition id?: string type?: 'replace'|'animate' states?: EasyButtonState[] leafletClasses?: boolean tagName?: string }

因此,new L.EasyButton()的等价物是什么,以便与L.easyButton('fa fa-icon', () => '..', 'title)相匹配?


嗨,史蒂夫,该模块导出了两个函数,一个是 easyButton 函数,另一个是 EasyButton 类。类构造函数必须调用 new L.EasyButton(...),而 easyButton 函数将为您提供一个新的 EasyButton 实例。希望我已经解释得足够清楚了。 :) - Chui Tey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接