如何开发一个 Atom 插件

quzx 7年前
   <p><img src="https://simg.open-open.com/show/6cfba6f848cb8eb9be6af5abc3970a56.png"></p>    <h2><strong>准备</strong></h2>    <ul>     <li>工具: <a href="/misc/goto?guid=4959751017650928913" rel="nofollow,noindex"> Atom </a></li>     <li>语法: <a href="/misc/goto?guid=4959751017738763826" rel="nofollow,noindex"> ES6 </a></li>    </ul>    <h2><strong>基础知识</strong></h2>    <p>在开始编写插件之前,了解一些基本的 Atom 知识是必要的。以下是我根据官方文档加自己在开发过程中的一些理解的归纳,没有包含所有的细节,但在稍微复杂点的插件中基本都会用到。</p>    <h2><strong>1、生成一个插件</strong></h2>    <p>Atom 生成插件很简单,打开命令面板( cmd+shift+p )输入“Generate Package”,将出现一个对话框,输入你将要建立的包名字,回车即可。Atom 会自动创建一个已刚输入的包名字命名的文件夹,里面包含了默认生成的文件,默认的位置在 ~/atom/package 中,其目录结构如下:</p>    <pre>  <code class="language-javascript">my-package      ├─ keymaps/      ├─ lib/      ├─ menus/      ├─ spec/      ├─ styles/      └─ package.json</code></pre>    <p>其实,基本的路径结构还包括 snippets 和 grammars 目录,下面实战中将会用到 snippets,用来存放自定义的代码段文件。</p>    <p><strong>keymaps</strong></p>    <p>我们可以为插件自定义快捷键,形如上面创建包的 kemaps/my-package.json 文件中定义:</p>    <pre>  <code class="language-javascript">{      "atom-workspace": {          "ctrl-alt-o": "my-package:toggle"      }  }</code></pre>    <p>这样,我们在键盘上按组合键 ctrl+alt+o 就会执行命令 my-package:toggle, 其中键 atom-workspace 是设置快捷键起作用范围,这里 atom-workspace 元素是 Atom UI 的父级,所以只要在工作区域按快捷键就可触发命令。如果定义 atom-text-editor,那么就只能在编辑区域触发命令。 <a href="/misc/goto?guid=4959751017821270907" rel="nofollow,noindex"> 了解更多 </a></p>    <p><strong>lib</strong></p>    <p>该目录下是主要是实现插件功能的代码,并且必须包含插件的主入口文件(其将在 package.json 的 main 字段指定。如不指定,默认 lib 下的 index.js 或 <a href="/misc/goto?guid=4959751017906809903" rel="nofollow,noindex"> index.coffee </a> 文件), 主入口文件可以实现以下基本方法:</p>    <ul>     <li>config: 该对象中可以为包自定义配置。</li>     <li>activate(state):该方法是在包激活的时候调用。如果你的包实现了 serialize() 方法,那么将会传递上次窗口序列化的 state 数据给该方法</li>     <li>initialize(state):( Atom1.14 以上版本可用 ) 类似于 activate(), 但在它之前调用。intialize() 是在你的发序列化器或视图构建之前调用,activate() 是在工作区环境都已经准备后调用。</li>     <li>serialize():窗口关闭后调用,允许返回一个组件状态的 JSON 对象。当你下次窗口启动时传递给 activate() 方法。</li>     <li>deactivate():当窗口关闭时调用。如果包正在使用某些文件或其他外部资源,将在这里释放它们。</li>    </ul>    <p><strong>styles</strong></p>    <p>styles 目录下存放的是包的样式文件。可以使用 CSS 或 LESS 编写。</p>    <p><strong>snippets</strong></p>    <p>snippets 目录下存放的是包含常用的代码段文件,这样就可以通过输入缩写前缀快速生成常用的代码语法,实战中将会详细讲解。</p>    <p><strong>menus</strong></p>    <p>该目录下存放的是创建应用菜单和编辑区域菜单文件。形如:</p>    <pre>  <code class="language-javascript">{      "context-menu": {          "atom-text-editor": [{              "label": "Toggle my-package",              "command": "my-package:toggle"          }]      },      "menu": [{          "label": "Packages",          "submenu": [{              "label": "my-package",              "submenu": [{                  "label": "Toggle",                  "command": "my-package:toggle"              }]          }]      }]  }</code></pre>    <p>context-menu 字段定义的上下文菜单,通过在你定义的元素(示例中是 atom-text-editor 字段)范围内点击右键呼出菜单栏,点击 Toggle my-package 会执行 my-package:toggle 命令。</p>    <p>menu 字段定义应用的菜单,其出现在 Atom 主菜单栏中,定义类似 context-menu。</p>    <h2><strong>2、配置</strong></h2>    <p>为了让开发的包可配置化,Atom 为我们提供了 Configuration API。</p>    <p>atom.config.set 方法为包写入配置。</p>    <p>atom.config.setSchema 方法为包写入配置 schema, 下面示例 demo 中定了一个 type 为 string 的枚举类型 schema。 <a href="/misc/goto?guid=4959751017993225199" rel="nofollow,noindex"> 了解更多 </a></p>    <p>atom.config.get 方法读取包配置。</p>    <p>下面是一些 demo:</p>    <pre>  <code class="language-javascript">const versionSchema = {      title: 'Element Version',      description: 'Document version of Element UI.',      type: 'string',      default: '1.3',      enum: ['1.1', '1.2', '1.3'],      order: 1  };  // 设置配置schema  atom.config.setSchema('element-helper.element_version', versionSchema);    // 修改配置的element-helper.element_version值为1.2  atom.config.set('element-helper.element_version', '1.2');    // 获取配置element-helper.element_version  atom.config.get('element-helper.element_version'); // 1.2</code></pre>    <p>为了监听 config 的变化 Atom 提供了 observe 和 onDidChange 两个方法。前者会在指定keypath 的值时立即调用它的回调函数,以后改变也会调用。后者则是在 keypath 下次改变后调用它的回调函数。使用 demo 如下:</p>    <pre>  <code class="language-javascript">atom.config.observe('element-helper.element_version', (newValue) => {      console.log(newValue);  });    atom.config.onDidChange('element-helper.element_version', (event) => {      console.log(event.newValue, event.oldValue);  });</code></pre>    <h2><strong>3、作用域</strong></h2>    <p>Atom 中的作用域是一个很重要的概念,类似于 CSS 的 class,通过作用域的名称来选择操作作用的范围。打开一个 .vue 文件,按 alt+cmd+i 打开开发者工具,切换到 Elements 选项。会看到类似如下的 HTML 结构:</p>    <p><img src="https://simg.open-open.com/show/3d1af9b7bfdcfc1b8f602ef41a7a15e0.png"></p>    <p>图上 span 元素的 class 的值就是作用域名称。那怎么来利用作用域名称来选择作用范围呢?比如要选择 .vue 文件中所有元素标签名称作用范围。其实就像 CSS 选择器一样,如 “text.html.vue .entity.name.tag.other.html” 是选择 .vue 文件中所有标签名称节点,注意这里是不要加 syntax-- 前缀的,Atom 内部会处理。其可用于 snippets、config 等需要限定作用范围的功能中,后面实战中很多地方都有用到。 <a href="/misc/goto?guid=4959751018088159030" rel="nofollow,noindex"> 了解更多 </a></p>    <h2><strong>4、package.json</strong></h2>    <p>类似于 Node modules,Atom 包也包含一个 package.json 文件,但是 Atom 拥有自己定义的字段,如 main 指定包的主入口文件;activationCommands 指定那些命令可以激活入口文件中 activate 方法;menus, styles, keymaps, snippets 分别指定目录,样式,快键键映射,代码段的文件加载位置和顺序。 <a href="/misc/goto?guid=4959751018177107965" rel="nofollow,noindex"> 了解更多 </a></p>    <h2><strong>5、与其他包交互</strong></h2>    <p>可以在 package.json 中指定一个或多个版本号的外包来为自己提供服务。如:</p>    <pre>  <code class="language-javascript">"providedServices": {      "autocomplete.provider": {          "versions": {              "2.0.0": "provide"          }      }  }</code></pre>    <p>这里使用 autocomplete+ 包提供的 provide API,版本是 2.0.0。这样只要在 package.json 中main 字段指定的主文件中实现 provide 方法,就可以在包激活的任何时候调用。如:</p>    <pre>  <code class="language-javascript">export default {      activate(state) {}        provide() {          return yourProviderHere;      }  }</code></pre>    <h2><strong>实战</strong></h2>    <p>经过简单的基础知识介绍,是不是有点跃跃欲试了?好,满足你。接下来我们将通过实战来运用这些知识,深入了解其原理和工作机制。下面示例都是以 <a href="/misc/goto?guid=4959751018255910579" rel="nofollow,noindex"> Element-Helper </a> 插件为样本,你先不必着急去看源码。</p>    <h2><strong>1、自动补全</strong></h2>    <p>这里通过使用 <a href="/misc/goto?guid=4959751018343798863" rel="nofollow,noindex"> autocomplete+ </a> 提供的服务( <a href="/misc/goto?guid=4959751018431031031" rel="nofollow,noindex"> Provide API </a> )来实现自动补全功能。关于怎么建立交互请看基础知识中的第 5 小节,那么我们要实现的就是定义自己的 yourProiverHere, 它是一个对象包含如下示例中的方法:</p>    <pre>  <code class="language-javascript">const provider = {      // 选择器,指定工作的作用域范围,这里是只有在选择器 '.text.html' 作用域下才能工作, 如 html 文件有作用域 '.text.html.basic', vue 文件有作用域 '.text.html.vue' 都是包含于 '.text.html' 的      selector: '.text.html',      // 排除工作作用域中子作用域,如 html, vue 文件中的注释。可选      disableForSelector: '.text.html .comment',      // 表示提示建议显示优先级, 数字越高优先级越高,默认优先级是0。可选      inclusionPriority: 1,      // 如果为 true,那么根据 inclusionPriority 大小,高优先级就会阻止其他低优先级的建议服务提供者。可选      excludeLowerPriority: true,      // 在提示下拉选项中的排序, 默认为 1,数字越高越靠前。可选      suggestionPriority: 2        // 返回一个 promise 对象、包含提示的数组或者 null。这里返回提示列表给 autocomplete+ 提供的服务展示,我们不用关心如何展示      getSuggestions(request) {          // todo          return [];      },      // provide 提供的建议(即 getSuggetion 方法返回的提示)插入到缓冲区时被调用。可选      onDidInsertSuggestion({editor, triggerPosition, suggestion}) {          // todo      },      // provider 销毁后的善后工作,可选      dispose() {          // todo      }  }</code></pre>    <p>重点介绍下 getSuggestion 的参数 request 对象,它包含下面属性:</p>    <ul>     <li>editor: 当前的 <a href="/misc/goto?guid=4959751018510144288" rel="nofollow,noindex"> 文本编辑上下文 </a></li>     <li>bufferPosition:当前光标的 <a href="/misc/goto?guid=4959751018594098398" rel="nofollow,noindex"> 位置 </a> ,包含属性 row 和 column。</li>     <li>scopeDescriptor: 当前光标位置所在的作用域描述符,可通过其 .getScopesArray 方法获取到包含所有自己和祖先作用域选择器的数组。你可以通过按 cmd+shift+p 打开命令面板输入 Log Cursor scope 来查看作用描述符。</li>     <li>prefix:当前光标输入位置所在单词的前缀,注意 autocomplete+ 不会捕获 ‘<’, ‘@’ 和 ‘:’ 字符,所以后面我们得自己做处理。原来没有仔细阅读文档(衰),我发现我原来实现的方法比较局限,其实这里教你怎么定义 <a href="/misc/goto?guid=4959751018679744732" rel="nofollow,noindex"> 自己的 prefix </a> 了</li>     <li>activateManually:这个提示是否是用户 <a href="/misc/goto?guid=4959751018760989988" rel="nofollow,noindex"> 手动触发 </a></li>    </ul>    <p>介绍完 API 了,是时候来一起小试牛刀了。这里就以 <a href="/misc/goto?guid=4959751018847150233" rel="nofollow,noindex"> Element UI </a> 标签的属性值自动提示为例:</p>    <p><img src="https://simg.open-open.com/show/f5b2db9cc41d351bb1e97c8463691fb6.gif"></p>    <p>autocomplete+ 提供的 provider 会在用户包激活后任何时候调用(比如输入字符),我们只需在 getSuggestion 方法中返回提示信息(建议)数组就好了。那么问题重点来了,怎么获取这个提示信息数组?观察示例,想一想,可以分两大部分:判断提示出现的时机和过滤出提示信息数组。</p>    <p><strong>1、判断提示出现时机</strong></p>    <p>示例中的时机是是否是标签属性值开始(isAttrValueStart),我们先实现三个方法:是否在字符串作用域范围内(hasStringScope)、是否在标签作用域范围内(hasTagScope)和输入字符位置前是否具有属性名称(getPreAttr):</p>    <pre>  <code class="language-javascript">// scopes 是否包含单引号和双引号作用域选择器来决定是否在字符串中  function hasStringScope(scopes) {      return (scopes.includes('string.quoted.double.html') ||          scopes.includes('string.quoted.single.html'));  }    // scopes 是否存在标签(tag)的作用域选择器来决定是否在标签作用域内,这里也是存在多种 tag 作用域选择器  function hasTagScope(scopes) {      return (scopes.includes('meta.tag.any.html') ||      scopes.includes('meta.tag.other.html') ||      scopes.includes('meta.tag.block.any.html') ||      scopes.includes('meta.tag.inline.any.html') ||      scopes.includes('meta.tag.structure.any.html'));  }  // 获取当前输入位置存在的属性名  function getPreAttr(editor, bufferPosition) {      // 初始引号的位置      let quoteIndex = bufferPosition.column - 1;      // 引号的作用域描述符      let preScopeDescriptor = null;      // 引号的作用域描述符字符串数组      let scopes = null;      // 在当前行循环知道找到引号或索引为 0      while (quoteIndex) {          // 获取位置的作用描述符          preScopeDescriptor = editor.scopeDescriptorForBufferPosition([bufferPosition.row, quoteIndex]);          scopes = preScopeDescriptor.getScopesArray();          // 当前位置不在字符串作用域内或为引号起始位置, 则跳出循环          if (!this.hasStringScope(scopes) || scopes.includes('punctuation.definition.string.begin.html')) {              break;          }          quoteIndex--;      }      // 属性名匹配正则表达      let attrReg = /\s+[:@]*([a-zA-Z][-a-zA-Z]*)\s*=\s*$/;      // 正则匹配当前行引号之前的文本      let attr = attrReg.exec(editor.getTextInBufferRange([[bufferPosition.row, 0], [bufferPosition.row, quoteIndex]]));      return attr && attr[1];  }</code></pre>    <p>说明:</p>    <ol>     <li>参数 scopes 是前面讲的作用域描述符。如果不是很清楚,可以打开命令面板输入 Log Cursor scope 来查看。</li>     <li>scopeDescriptorForBufferPosition 方法是获取给定位置的作用域描述符,具体请查看 <a href="/misc/goto?guid=4959751018088159030" rel="nofollow,noindex"> 这里 </a> 。</li>     <li>getTextInBufferRange 方法是根据位置范围( <a href="/misc/goto?guid=4959751018927377489" rel="nofollow,noindex"> Range </a> )获取文本字符串,具体请查看 <a href="/misc/goto?guid=4959751019024360699" rel="nofollow,noindex"> 这里 </a> ,他有个别称 getTextInRange(官方文档里是没有的,可以查看源代码 <a href="/misc/goto?guid=4959751019098774529" rel="nofollow,noindex"> L1024 </a> 和 <a href="/misc/goto?guid=4959751019190151560" rel="nofollow,noindex"> L931 </a> ,实现一毛一样)。</li>    </ol>    <p>那么接下来结合三个方法来实现 isAttrValueStart 方法:</p>    <pre>  <code class="language-javascript">// 参数解释请看 ‘自动补全’ 小节  function isAttrValueStart({scopeDescriptor, bufferPosition, editor}) {      // 获取作用域描述符字符串数组, 形如 ['text.html.vue', 'meta.tag.other.html', 'string.quoted.double.html', 'punctuation.definition.string.end.html']      const scopes = scopeDescriptor.getScopesArray();      // 获取当前位置的前一个字符位置      const preBufferPosition = [bufferPosition.row, Math.max(0, bufferPosition.column - 1)];      // 获取前一个字符位置的作用域描述符      const preScopeDescriptor = editor.scopeDescriptorForBufferPosition(preBufferPosition);      // 获取作用域描述符字符串数组      const preScopes = preScopeDescriptor.getScopesArray();        // 当前鼠标位置 and 前一个位置(这个里主要是判断 attr= 再输入 ' 或 " 这种情况)是包含在字符串作用域中 and 前一个字符不能是字符串定义结束字符(' or ")为真,就说明是开始输入属性值      return (this.hasStringScope(scopes) &&          this.hasStringScope(preScopes) &&          !preScopes.includes('punctuation.definition.string.end.html') &&          this.hasTagScope(scopes) &&          this.getPreAttr(editor, bufferPosition));  }</code></pre>    <p><strong>2、过滤出提示信息数组</strong></p>    <p>前面已经判断提示信息出现的时机,剩下就是如何展示相应标签属性的值了, 这真是个精细化工作。惯例,先做些准备工作:1.获取输入位置所在的标签名(getPreTag); 2.获取输入位置所在的属性名(getPreAttr) - 这个上小节已实现;3.既然知道标签名和属性名,那么就可以从事先纯手工打造的 attributes.json 文件(具体请看 <a href="/misc/goto?guid=4959751019267115936"> element-helper-json </a> )中找到对应的属性值了(getAttrValues)- 这个就是遍历 json 对象属性,不具体解释。</p>    <pre>  <code class="language-javascript">// 标签名匹配正则表达式 - 标签匹配有很多情况,这里并不完善...,仅供参考。  let tagReg = /<([-\w]*)(?:\s|$)/;  // 参数请查看上面 getSuggestion 参数对象属性解析  function getPreTag(editor, bufferPosition) {      // 当前行      let row = bufferPosition.row;      // 标签名      let tag = null;      // 文件逐行向上遍历知道找到正则匹配的字符串,或 row = 0;      while (row) {          // lineTextForBufferRow 获取当前行文本字符串          tag = tagReg.exec(editor.lineTextForBufferRow(row));          if (tag && tag[1]) {              return tag[1];          }          row--;      }      return;  }</code></pre>    <p>OK,准备工作好了,我们来对获取到的属性值数组进行格式化处理,获得 getSuggestions 能识别的数据结构数组:</p>    <pre>  <code class="language-javascript">function getAttrValueSuggestion({editor, bufferPosition, prefix}) {      // 存放提示信息对象数据      const suggestions = [];      // 获取当前所在标签名      const tag = this.getPreTag(editor, bufferPosition);      // 获取当前所在属性名称      const attr = this.getPreAttr(editor, bufferPosition);      // 获取当前所在标签属性名下的属性值      const values = this.getAttrValues(tag, attr);      // 属性值数组进行格式化处理      values.forEach(value => {          if (this.firstCharsEqual(value, prefix) || !prefix) {              suggestions.push(buildAttrValueSuggestion(tag, attr, value));          }      });      // 返回符合 autocompete+ 服务解析的数据结构数组      return suggestions;  }  // 对原始数据加工处理  function buildAttrValueSuggestion(tag, attr, value) {      // ATTRS 是 attributes.json 文件解析出的 json 对象      const attrItem = ATTRS[`${tag}/${attr}`] || ATTRS[attr];      // 返回 suggestion 对象 具体格式说明请看:https://github.com/atom/autocomplete-plus/wiki/Provider-API#suggestions      return {          text: value,          // 插入文本编辑器,替换 prefix          type: 'value',        // 提示类型,用于列表提示左边的 icon 展示,有变量(varabale), 方法(method)和函数(function)等可选          description: attrItem.description, // 用于选中提示条目后,提示框下面展示的信息          rightLabel: attrItem.global ? 'element-ui' : `<${tag}>`  // 右边展示的文本信息      };  }</code></pre>    <p>经过以上两步,只需在 getSuggestions 方法中返回数组给 autocomplete+ 服务即可:</p>    <pre>  <code class="language-javascript">// ...  getSuggestions(request) {      if (this.isAttrValueStart(request)) {          return this.getAttrValueSuggestion(request);      }  }  // ...</code></pre>    <p>到这里大家应该明白自动补全工作原理了吧,其他的可以依葫芦画瓢啦,be happy。</p>    <h2><strong>2、代码段</strong></h2>    <p>定义代码段的方式有三种方式:</p>    <ul>     <li>全局定义。在 Atom -> Snippets 菜单中定义,定义方式同第二种</li>     <li>包内定义。在基础知识部分,我们介绍了生成包后的文件目录结构和作用,其中 snippets 文件夹里放的就是我们自定义的常用代码块 json 文件,这里我使用为 coffeescript 对象提供的 <a href="/misc/goto?guid=4959751019346803006" rel="nofollow,noindex"> cson </a> 文件,类似 json,但语法没有那么严格且支持多行字符串,如官方介绍:</li>    </ul>    <p>Which is far more lenient than JSON, way nicer to write and read, no need to quote and escape everything, has comments and readable multi-line strings, and won't fail if you forget a comma.</p>    <p>现在来看下如何编写一个代码段,基本格式如下:</p>    <pre>  <code class="language-javascript">".source.js":      "notification":          "prefix": "notify",          "body": """              this.$notify({                  title: '${1:title}',                  message: '${2:string|VNode}'              });          """</code></pre>    <p>最顶层的键字符串(.source.js)是作用域选择器,指定在文本编辑器中那个范围内可触发匹配(示例中是在 js 文件或 script 标签域中触发代码段匹配)。下一层键字符串(notification)是表示代码段的描述,将展示在下拉条目的右边文本;prefix 字段是匹配输入字符的前缀;body 字段是插入的文本,可以通过 """ 来使用多行语法,body 中 $ 符表示占位符,每按一次 tab 键,都会按 ${num} 中的 num 顺序移动位置。如果要在占位符位置填充字符的话,可以这样 ${num: yourString}。示例效果如下: <a href="/misc/goto?guid=4959751019432301556"> 了解更多 </a></p>    <p><img src="https://simg.open-open.com/show/5c021d43893a912f0a222ee14979ac29.gif"></p>    <ul>     <li>在‘自动补全’小节中可以定义返回 snippet 的提示,只需在 suggestion 对象中定义 snippet 和 displayText 属性即可,不要定义 text 属性。snippet 语法同第二种方式中基本格式中的body字段, displayText 用于提示展示文本,snippet 是插入文本编辑器的代码段。</li>    </ul>    <h2><strong>3、创建一个 modal 下拉列表和文档视图</strong></h2>    <p>ATOM 为实现这两个功能提供了 npm 包 <a href="/misc/goto?guid=4959751019519995685" rel="nofollow,noindex"> atom-space-pen-views </a> ,它包括三个视图类:文本编辑器视图类( <a href="/misc/goto?guid=4959751019604205285" rel="nofollow,noindex"> TextEditorView </a> )、滚动文档视图类( <a href="/misc/goto?guid=4959751019684914067" rel="nofollow,noindex"> ScrollView </a> )和下拉选项视图类( <a href="/misc/goto?guid=4959751019769802583" rel="nofollow,noindex"> SelectListView </a> ),我们只需继承视图类,实现类方法即可。下面重点讲下拉列表和滚动视图类:</p>    <p><strong>模态下拉列表</strong></p>    <p>我们只要提供用于展示的条目给 SelectListView,实现两个必选方法,它会帮我们的条目渲染成一个下拉列表形式,如下:</p>    <pre>  <code class="language-javascript">// file: search-veiw.js   import { SelectListView } from 'atom-space-pen-views';    class SearchView extends SelectListView {      // keyword:用于初始化列表搜索框值,items:用于展示列表的条目数组,eg: [{name: 'el-button'}, {name: 'el-alert'}]      constructor(keyword, items) {          super();  // 执行 SelectListView 的构造函数          // ATOM API:addModalPanel(options), 添加一个模态框, item 是用于模块框展示的 DOM 元素,JQuery元素或实现veiw model          this.panel = atom.workspace.addModalPanel({item: this});            // 给下拉列表搜索框写入文本          this.filterEditorView.setText(keyword);          // 下拉列表可展示条目的最大数目          this.setMaxItems(50);          // 设置用于展示的下拉条目数组           this.setItems(items);          // 鼠标焦距到列表搜索框          this.focusFilterEditor();      }      // 必须实现。自定义列表条目展示视图,该方法会在 setItems(items) 中单条 item 插入到下拉列表视图时调用      veiwForItem(item) {          return `<li>${item.name}</li>`;      }      // 必须实现。 当下拉列表条目被选中后触发,参数 item 为选中条目对象      confirmed(item) {          // todo          this.cancel(); // 选中后关闭视图      }      // 当列表视图关闭后调用      cancelled () {          // todo      }      // 搜索框输入值,按 item 对象哪个键值模糊匹配,eg: item.name      getFilterKey() {          return 'name';      }  }  export default SearchView;</code></pre>    <p>视图效果和 DOM 树结构如下图,我们可以看到通过 addModalPanel 把 selectListView 的 HTML 元素添加到模态框元素中了</p>    <p><img src="https://simg.open-open.com/show/12824a06ea9f18e30d64e91c224b1b3c.png"></p>    <p>定义好了视图类,那怎么渲染展示呢?少年莫慌,我们可以在主文件 activate 方法中注册命令来触发视图展示(当然你可以用其他方式,只要你确定想要触发时机执行方法就行了):</p>    <pre>  <code class="language-javascript">import SearchView from './search-view.js';    export default {      activate() {          // 实例化一个销毁容器,便于清除订阅到 Atom 系统的事件          this.subscriptions = new CompositeDisposable();          // 这里需在 keymaps 目录下的文件中配置 keymap.          this.subscriptions.add(atom.commands.add('atom-workspace', {              'element-helper:search': () => {                  // 获取当前正在编辑(活跃)的文本编辑器                  if (editor = atom.workspace.getActiveTextEditor()) {                      // 获取你光标选中的文本                      const selectedText = editor.getSelectedText();                      // 获取光标下的单词字符串                      const wordUnderCursor = editor.getWordUnderCursor({ includeNonWordCharacters: false });                      // 用于下拉列表展示的数据, 这里只是个 demo                      const items = [{                          "type": "basic",                          "name": "Button 按钮",                          "path": "button",                          "tag": "el-button",                          "description": "el-button,Button 按钮"                      }];                      // 没有范围选中文本,就用当前光标下的单词                      const queryText = selectedText ? selectedText : wordUnderCursor;                      // 实例化搜索下拉列表视图                      new SearchView(queryText, items);                  }              }          }));      }  }</code></pre>    <p><strong>文档视图</strong></p>    <p>Atom 提供了打开一个空白文本编辑器的 API ( <a href="/misc/goto?guid=4959751019852114848" rel="nofollow,noindex"> atom.workspace.open </a> ) 和注册 URI 打开钩子(opener)函数的 API ( <a href="/misc/goto?guid=4959751019925098260" rel="nofollow,noindex">atom.workspace.addOpener(opener)</a> ),那么再结合 ScrollView 可以打开一个可滚动的文档窗口。No BB, show my code:</p>    <pre>  <code class="language-javascript">/**  * file: doc-view.js  * 继承 ScrollView 类, 实现自己的文档视图类  */  import { Emitter, disposable } from 'atom';  import { ScrollView } from 'atom-space-pen-views';    class DocView extends ScrollView {        // 视图html      static content(){          // this.div 方法将会创建一个包含参数指定属性的 div 元素,可以换成其他 html 标签,eg: this.section()将创建 section 标签元素          return this.div({class: 'yourClassName', otherAttributeName: 'value'});      }        constructor(uri) {          super();          // 实例化事件监听器,用于监听和触发事件          this.emitter_ = new Emitter();          // 文档标题,tab名称          this.title_ = 'Loading...';          this.pane_ = null;          this.uri_ = uri;      }      // 自定义方法,用户可自定义视图中展示的内容, 具体可查看 Element-Helper 源码      setView(args) {          // todo, demo          this.element.innerHTML = '<h1>Welcome to use Atom</h1>';          this.title_ = "Welcome!";          this.emitter_.emit('did-change-title');               }        // 当视图 html 插入文本编辑器后触发,注意 view 被激活后才会触发视图的插入      attached() {          // 这里可以在视图插入 DOM 后做些操作,比如监听事件,操作 DOM 等等          // 通过 atom.workspace.open 打开文本编辑器的URI获取视图所在的窗口容器,看下图比较容易理解什么是窗口容器          this.pane_ = atom.workspace.paneForURI(this.uri_);          this.pane_.activateItem(this);      }        // 文档标题被激活执行      onDidChangeTitle(callback) {          // 监听自定义事件          return this.emitter_.on('did-change-title', callback);      }      // 文档视图关闭后销毁函数      detory() {          // 销毁文档视图          this.pane_.destroyItem(this);          // 如果当前窗口容器中只有文档视图,那么把容器都销毁掉          if (this.pane_.getItems().length === 0) {              this.pane_.destroy();          }      }      // 标题改变后触发事件 did-change-title, callback 内部将调用改方法      getTitle {          return this.title_;      }  }</code></pre>    <p><img src="https://simg.open-open.com/show/d8d77ee9e81c25949bfcd337414f739e.png"></p>    <pre>  <code class="language-javascript">/**  * 主文件, 这里只写 activate 函数里的关键代码  */  import Url from 'url';  import DocView from './doc-view.js';  // ....  // 初始化文档视图对象  this.docView_  = null;  // 便于测试沿用上面搜索命令,触发打开视图  this.subscriptions.add(atom.commands.add('atom-workspace', {      'element-helper:search': () => {            // 异步打开一个给定 URI 的资源或文本编辑器,URI 定义请看:https://en.wikipedia.org/wiki/Uniform_Resource_Identifier, 参数 split 确定打开视图的位置,activatePane 是否激活窗口容器            atom.workspace.open('element-docs://document-view', { split: 'right', activatePane: false})              .then(docView => { // docView 是经过 addOpener 添加钩子处理后的视图对象,如果没有相应的 opener 返回数据,默认为文本编辑器对象(TextEditor)                  this.docView_ = docView;                  // 为docView填充内容,具体展示的内容请在DocView中定义的setView方法中操作                  this.docView_.setView(yourArgments);              });      }  });  // 为 URI 注册一个打开钩子,一旦 WorkSpace::open 打开 URI 资源,addOpener 里面的方法就会将会执行  this.subscriptions.add(atom.workspace.addOpener((url) => {      if (Url.parse(url).protocol == 'element-docs:') {          return new DocView(url);      }  }));  // ...</code></pre>    <p>如果还对这两个API有所疑虑,请查看上面提供的链接。那么最终效果如下:</p>    <p><img src="https://simg.open-open.com/show/82cc8769c776be4fc6010629430cd7d6.png"></p>    <h2>写在最后</h2>    <p>这算是我在编写 <a href="/misc/goto?guid=4959751020013097437"> Element-Helper </a> 插件时的一些总结和过程吧,实现方式不一定完善或优雅,表述不清楚或错误的地方请留言指正。题外话,如果你想开发 <a href="/misc/goto?guid=4958872398868295437" rel="nofollow,noindex"> VSCode </a> 插件,以 Atom 插件开发为入门也是不错的选择,它们都是基于 <a href="/misc/goto?guid=4959751020133036073"> Electron </a> 框架,很多概念都互通,但 Atom 更易入手和灵活(个人见解)。最后,希望本文能为大家提供些许帮助。Enjoy it!</p>    <p> </p>    <p>来自:https://zhuanlan.zhihu.com/p/27913291</p>    <p> </p>