Atom插件开发入门教程(三)

插件: 文本计数器

让我们开始写一个非常简单的而又实用的插件吧。文本计数插件:统计当前缓冲里有多少文字,并用一个小窗口把结果显示出来。

插件包生成器(Package Generator)

创建插件最简单的方法是用Atom内置的插件包生成器( package generator)。 你可能已经猜到了, package generator 自己也是一个插件包。(详情参见:package-generator)

你可以在command palette里搜索 "Generate Package"来运行它。这时会弹出一个对话框,让你输入插件的名字,建议输入: your-name-word-count. Atom 会创建一个目录并插入默认的插件代码。默认位置是:%USERPROFILE%\.atom\packages ,这样重启Atom时,就会自动加载这个插件了。

注意: 如果你的插件包没有被加载。那很可能是因为有一个相同名称的插件已经被上传到了atom.io 。所以建议大家建立插件时,加上自己名字的缩写或者公司缩写,来保证插件名称的唯一性。

如上图, Atom 已经创建了插件包的基础代码。下面我们来介绍插件包的代码是怎样构成的:

最基础的路径结构如下:

my-package/
├─ grammars/
├─ keymaps/
├─ lib/
├─ menus/
├─ spec/
├─ snippets/
├─ styles/
├─ index.coffee
└─ package.json

不是每个插件都需要这些文件,自动创建的代码里,也没有snippets 和 grammars目录。

package.json

类似于 Node modules, Atom 插件包的根目录必须有一个 package.json 文件。这里包含了插件包的基础信息,比如 "main" 模块的路径,依赖库, 和指定的资源加载顺序。

下面介绍 package.json文件中各个参数的意义:

  • main: CoffeeScript(JavaScript)代码文件的路径,如果不填写此项目, Atom 会自动寻找 index.coffee 或 index.js。
  • styles: 一个字符串数组,说明Styles文件的加载顺序,如果不填写,Atom会自动在styles目录下按文件名字母排序的方式去加载样式文件。注意,样式支持css和less文件。
  • keymaps: 一个字符串数组,说明keymaps文件的加载顺序,如果不填,则Atom自动在keymaps目录下按字母排序的方式去加载。
  • menus: 一个字符串数组,说明memus配置文件在哪里加载。如果不填, 则Atom自动在menus 目录下按字母排序的方式加载。
  • snippets: 一个字符串数组,说明插件中使用的snippets从哪里加载。如果不填,则Atom自动在 snippets 目录下按字母排序的方式去加载。
  • activationCommands: 一个对象, 说明哪个命令来激活你的插件。如果不填写, 则Atom会在插件激活时,自动调用 activate()函数。
  • activationHooks: 一个字符串数组,说明使用哪种语法来解释脚本。一般来说,我们填写: language-package-name:grammar-used (e.g.,language-javascript:grammar-used)

下面是一个 package.json 的例子:

{
  "name": "wordcount",
  "main": "./lib/wordcount",
  "version": "0.0.0",
  "description": "A short description of your package",
  "activationCommands": {
    "atom-workspace": "wordcount:toggle"
  },
  "repository": "https://github.com/atom/your-name-word-count",
  "license": "MIT",
  "engines": {
    "atom": ">=1.0.0 <2.0.0"
  },
  "dependencies": {
  }
}

如果使用了 activationHooks, 代码是:

{
  "name": "wordcount",
  "main": "./lib/wordcount",
  "version": "0.0.0",
  "description": "A short description of your package",
  "activationHooks": ["language-javascript:grammar-used", "language-coffee-script:grammar-used"],
  "repository": "https://github.com/atom/your-name-word-count",
  "license": "MIT",
  "engines": {
    "atom": ">=1.0.0 <2.0.0"
  },
  "dependencies": {
  }
}

首先,要填写这些基本信息。

Warning: 切记要更新 repository URL. 否则你的更改将无法分享给他人。

源代码

首先你的插件必须包含一个[top-level]模块, 在 package.json 文件里的main字段下声明它。对于我们这个例子来说,[top-level]文件是 lib/wordcount.coffee. 你编写的源代码应该放在 lib 目录下,并且用[top-level]文件中required它。 如果main 没写,则Atom自动选择 index.coffee 或 index.js 作为[top-level]文件。

插件的[top-level] 模块是一个单例类。生命周期由 Atom来管理。 不论你创建多少Dom或者View, 它们全部由[top-level]来管理。

插件的[top-level] 模块可以实现下列的基本接口:

  • activate(state): 插件激活时被调用。如果你实现了 serialize() 接口,它会传递窗口上次的state. 一般用这个接口来初始化插件。
  • initialize(state): (Atom 1.14以后的版本支持) 与activate() 功能类似,不过被调用的更早,可以说在所有操作之前被调用。 如果你想等环境运行完毕了再初始化,请用 activate() 。如果需要在画面构造前执行什么,请用initialize() 。
  • serialize(): 在窗口被关闭的时候,允许你返回 JSON 序列,来保存当前的状态。你保存的信息,可以传递给activate() 接口,来在下次启动时恢复你的窗口状态。
  • deactivate(): 窗口关闭时会调用这个接口,如果你的插件正在使用某些资源,或关联着某些文件,请在这里释放他们。 
样式

之前说过,插件的样式应该放在 styles 目录里。 支持 CSS 或 Less, 推荐使用Less。

理想的来说, 你不需要这种方式来做样式。Atom提供了基础的UI样式。 打开 command palette Ctrl+Shift+P + Ctrl+Shift+G来查找Atom默认提供的插件UI样式吧。

如果你需要特别的UI样式,建议只保留结构样式。如果你要使用特别的颜色、字体等,建议使用Atom提供的主题样式。参见 ui-variables.less.

快捷键

插件可以绑定多个快捷键, 举例参见 keymaps/wordcount.cson 文件:

'atom-workspace':
  'ctrl-alt-o': 'wordcount:toggle'

这将快捷键 Alt+Ctrl+O, 设置为运行 your-name-word-count:toggle 命令。创建插件时,Atom会自动分配一个快捷键给它,当然你也可以自行定义。

快捷键配置文件在 keymaps 子路径中。默认的所有快捷键配置文件按字母排序来加载。如果需要特别的加载顺序,请在 package.json 文件中配置。

快捷键的启动,也受到当前Dom元素(当前正在操作的View)是哪个的影响。在本例中 your-name-word-count:toggle 命令是在atom-workspace 元素上按下Alt+Ctrl+O 时会被执行。因为 atom-workspace 是整个 Atom UI的上级。这意味着,任何时候按下Alt+Ctrl+O 都会被执行。

如果想更深入的了解快捷键有关的内容,请参见 Keymaps in Depth.

菜单

菜单在 menus 子目录下。它定义了右键弹出菜单和工具栏菜单。

菜单默认按字母排序加载。 你可以在package.json 中编写自定义的加载顺序。

Application 菜单

如果你的插件不是对特定元素使用的,建议你在Packages菜单下创建一个application菜单项。 在menus/your-name-word-count.cson 文件中,我们能看到如下代码片段:

'menu': [
  {
    'label': 'Packages'
    'submenu': [
      'label': 'Word Count'
      'submenu': [
        {
          'label': 'Toggle'
          'command': 'your-name-word-count:toggle'
        }
      ]
    ]
  }
]

上述代码在Packages菜单下,添加了WorkCount子项,并在其中添加了Toggle项目。

如果选择这个选项,会执行 your-name-word-count:toggle 命令。

菜单项会自动和其他菜单项合并,他们按照加载的顺序来显示。

环境菜单(右键弹出的菜单)

建议为插件制作一些环境菜单。 在 menus/your-name-word-count.cson 文件中,你会看到一些默认的代码片段。

'context-menu':
  'atom-text-editor': [
    {
      'label': 'Toggle Word Count'
      'command': 'your-name-word-count:toggle'
    }
  ]

在编辑页面点击右键,我们会看到这个菜单项。

点击它,会执行 your-name-word-count:toggle 方法。

环境菜单也可以添加子项目, 如下:

'context-menu':
  'atom-workspace': [
    {
      label: 'Text'
      submenu: [
        {label: 'Inspect Element', command: 'core:inspect'}
        {type: 'separator'}
        {label: 'Selector All', command: 'core:select-all'}
        {type: 'separator'}
        {label: 'Deleted Selected Text', command: 'core:delete'}
      ]
    }
  ]

开发插件

在我们之前创建的插件框架基础上,运行 your-name-word-count:toggle我们会看到一个弹窗: "The Wordcount package is Alive! It's ALIVE!".

理解自动生成的代码

看一下 lib 目录。

在 lib 目录下,现在有两个文件。其中 (lib/your-name-word-count.coffee)是主文件, 在 package.json 文件中我们指定它为主文件。它负责整个插件的逻辑。

第二要注意的是 View class文件lib/your-name-word-count-view.coffee, 这里主要是插件的UI代码

module.exports =
class YourNameWordCountView
  constructor: (serializedState) ->
    # Create root element
    @element = document.createElement('div')
    @element.classList.add('your-name-word-count')

    # Create message element
    message = document.createElement('div')
    message.textContent = "The Your Name Word Count package is Alive! It's ALIVE!"
    message.classList.add('message')
    @element.appendChild(message)

  # Returns an object that can be retrieved when package is activated
  serialize: ->

  # Tear down any state and detach
  destroy: ->
    @element.remove()

  getElement: ->
    @element

代码的具体含义这里不在赘述。

这里重点要注意的是 DOM methods: createElement() 和appendChild().两个函数的用法。

第二个文件有两个主入口,它们是在 package.json 文件里配置的.文件代码如下:

YourNameWordCountView = require './your-name-word-count-view'
{CompositeDisposable} = require 'atom'

module.exports = YourNameWordCount =
  yourNameWordCountView: null
  modalPanel: null
  subscriptions: null

  activate: (state) ->
    @yourNameWordCountView = new YourNameWordCountView(state.yourNameWordCountViewState)
    @modalPanel = atom.workspace.addModalPanel(item: @yourNameWordCountView.getElement(), visible: false)

    # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
    @subscriptions = new CompositeDisposable

    # Register command that toggles this view
    @subscriptions.add atom.commands.add 'atom-workspace',
      'your-name-word-count:toggle': => @toggle()

  deactivate: ->
    @modalPanel.destroy()
    @subscriptions.dispose()
    @wordcountView.destroy()

  serialize: ->
    yourNameWordCountViewState: @yourNameWordCountView.serialize()

  toggle: ->
    console.log 'YourNameWordCount was toggled!'

    if @modalPanel.isVisible()
      @modalPanel.hide()
    else
      @modalPanel.show()

首先,我们看到这里定义了4个方法,其中只有 activate. 是必要的。 deactivate 是 serialize Atom推荐但可选的。  toggle 方法不是由Atom调用的,我们必须手动或者配置的方式去调用它。一般来说,都是在 activationCommandssection (package.json文件中) 或者菜单中。

 deactivate 函数只负责销毁,serialize 只负责序列化View的信息,建议不要在这两个函数里做其他事情。

activate 函数不是在Atom启动时就被调用的。它仅在 activationCommands(package.json中声明) 中定义的方法被调用时 才会被调用。所以 activate 仅当 togglecommand被调用时,才会被调用。如果没人点菜单,则它永远不会被调用。

对于本例来说,它做了两件事:一、创建了一个窗口并把这个窗口插入到 Atom workspace中。

@yourNameWordCountView = new YourNameWordCountView(state.yourNameWordCountViewState)
@modalPanel = atom.workspace.addModalPanel(
                  item: @yourNameWordCountView.getElement(),
                  visible: false
              )

这里我们可以无视state 参数 本例中并没有用到它。

二、创建了一个CompositeDisposable 的实例,以注册插件中所有可以被调用的命令。

# Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
@subscriptions = new CompositeDisposable

# Register command that toggles this view
@subscriptions.add atom.commands.add 'atom-workspace', 'your-name-word-count:toggle': => @toggle()

下一步,我们来编写 toggle 函数。这个函数仅仅是把我们在activate 函数中声明的View做了显示/隐藏 处理。

toggle: ->
  console.log 'YourNameWordCount was toggled!'

  if @modalPanel.isVisible()
    @modalPanel.hide()
  else
    @modalPanel.show()

现在,我们来验证一下插件是否有效。

The Flow

现在,我们来回顾一下整个包的执行流程:

  1. Atom 运行
  2. Atom 开始加载插件
  3. Atom 读取你编写的 package.json
  4. Atom 读取 keymaps, menus, styles 和main 模块
  5. Atom 完成加载插件
  6. 用户通过 your-name-word-count:toggle来运行你的插件
  7. Atom 运行你main模块中的 activate 函数(本例中,插入了一个窗口)
  8. 用户再次点击 your-name-word-count:toggle 时,Atom隐藏了你的插件窗口。
  9. 用户再次点击your-name-word-count:toggle command ,Atom再次显示你的插件窗口
  10. Atom 中途不会释放你的插件
  11. 最终, Atom 关闭时会 调用所有的插件定义的serializations

提示: 注意,如果你用了activationCommands ,流程会有所不同。

插件:单词计数器

现在,我们已经理解了Atom插件的基本工作原理,让我们来修改代码,实现一个小插件吧。

当插件被激活时,我们来数一数Atom当前正在编辑的文档窗口里,有多少个单词吧。

toggle: ->
  if @modalPanel.isVisible()
    @modalPanel.hide()
  else
    editor = atom.workspace.getActiveTextEditor()
    words = editor.getText().split(/\s+/).length
    @yourNameWordCountView.setCount(words)
    @modalPanel.show()

 atom.workspace.getActiveTextEditor().获得了当前Atom编辑窗口。

 getText() 获得了字符串。

最后,我们调用setCount()(在view中我们稍后会声明它),来把结果显示出来。

在 your-name-word-count-view.coffee 文件中,我们声明了函数setCount()

setCount: (count) ->
  displayText = "There are #{count} words."
  @element.children[0].textContent = displayText

运行结果如下:

基础的Debugging

你应该注意到了代码中的 console.log  因为Atom是基于 Chromium 开发的,所以有一些常规的调试工具供你使用。

用快捷键 Ctrl+Shift+I打开开发者模式,激活开发者工具。

对于做过Web开发的同学们来说,这应该是很熟悉的。

测试

建议测试一下你的插件,如果把插件放置到 spec 目录下,Atom就可以运行它。

Jasmine v1.3 可以用来执行测试

运行测试

点击 Alt+Ctrl+P 或通过菜单的 View > Developer > Run Package Specs 项目。

也可以在控制台用 atom --test spec 命令来运行它。

总结

现在,你已经完成了你的第一个插件,现在来发布它吧。详见 publish


你可能感兴趣的:(Atom,Web)