如何開發一個 Atom 插件

quzx 7年前發布 | 71K 次閱讀 Atom RSS/ATOM相關

準備

基礎知識

在開始編寫插件之前,了解一些基本的 Atom 知識是必要的。以下是我根據官方文檔加自己在開發過程中的一些理解的歸納,沒有包含所有的細節,但在稍微復雜點的插件中基本都會用到。

1、生成一個插件

Atom 生成插件很簡單,打開命令面板( cmd+shift+p )輸入“Generate Package”,將出現一個對話框,輸入你將要建立的包名字,回車即可。Atom 會自動創建一個已剛輸入的包名字命名的文件夾,里面包含了默認生成的文件,默認的位置在 ~/atom/package 中,其目錄結構如下:

my-package
    ├─ keymaps/
    ├─ lib/
    ├─ menus/
    ├─ spec/
    ├─ styles/
    └─ package.json

其實,基本的路徑結構還包括 snippets 和 grammars 目錄,下面實戰中將會用到 snippets,用來存放自定義的代碼段文件。

keymaps

我們可以為插件自定義快捷鍵,形如上面創建包的 kemaps/my-package.json 文件中定義:

{
    "atom-workspace": {
        "ctrl-alt-o": "my-package:toggle"
    }
}

這樣,我們在鍵盤上按組合鍵 ctrl+alt+o 就會執行命令 my-package:toggle, 其中鍵 atom-workspace 是設置快捷鍵起作用范圍,這里 atom-workspace 元素是 Atom UI 的父級,所以只要在工作區域按快捷鍵就可觸發命令。如果定義 atom-text-editor,那么就只能在編輯區域觸發命令。 了解更多

lib

該目錄下是主要是實現插件功能的代碼,并且必須包含插件的主入口文件(其將在 package.json 的 main 字段指定。如不指定,默認 lib 下的 index.js 或 index.coffee 文件), 主入口文件可以實現以下基本方法:

  • config: 該對象中可以為包自定義配置。
  • activate(state):該方法是在包激活的時候調用。如果你的包實現了 serialize() 方法,那么將會傳遞上次窗口序列化的 state 數據給該方法
  • initialize(state):( Atom1.14 以上版本可用 ) 類似于 activate(), 但在它之前調用。intialize() 是在你的發序列化器或視圖構建之前調用,activate() 是在工作區環境都已經準備后調用。
  • serialize():窗口關閉后調用,允許返回一個組件狀態的 JSON 對象。當你下次窗口啟動時傳遞給 activate() 方法。
  • deactivate():當窗口關閉時調用。如果包正在使用某些文件或其他外部資源,將在這里釋放它們。

styles

styles 目錄下存放的是包的樣式文件。可以使用 CSS 或 LESS 編寫。

snippets

snippets 目錄下存放的是包含常用的代碼段文件,這樣就可以通過輸入縮寫前綴快速生成常用的代碼語法,實戰中將會詳細講解。

menus

該目錄下存放的是創建應用菜單和編輯區域菜單文件。形如:

{
    "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"
            }]
        }]
    }]
}

context-menu 字段定義的上下文菜單,通過在你定義的元素(示例中是 atom-text-editor 字段)范圍內點擊右鍵呼出菜單欄,點擊 Toggle my-package 會執行 my-package:toggle 命令。

menu 字段定義應用的菜單,其出現在 Atom 主菜單欄中,定義類似 context-menu。

2、配置

為了讓開發的包可配置化,Atom 為我們提供了 Configuration API。

atom.config.set 方法為包寫入配置。

atom.config.setSchema 方法為包寫入配置 schema, 下面示例 demo 中定了一個 type 為 string 的枚舉類型 schema。 了解更多

atom.config.get 方法讀取包配置。

下面是一些 demo:

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

為了監聽 config 的變化 Atom 提供了 observe 和 onDidChange 兩個方法。前者會在指定keypath 的值時立即調用它的回調函數,以后改變也會調用。后者則是在 keypath 下次改變后調用它的回調函數。使用 demo 如下:

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);
});

3、作用域

Atom 中的作用域是一個很重要的概念,類似于 CSS 的 class,通過作用域的名稱來選擇操作作用的范圍。打開一個 .vue 文件,按 alt+cmd+i 打開開發者工具,切換到 Elements 選項。會看到類似如下的 HTML 結構:

圖上 span 元素的 class 的值就是作用域名稱。那怎么來利用作用域名稱來選擇作用范圍呢?比如要選擇 .vue 文件中所有元素標簽名稱作用范圍。其實就像 CSS 選擇器一樣,如 “text.html.vue .entity.name.tag.other.html” 是選擇 .vue 文件中所有標簽名稱節點,注意這里是不要加 syntax-- 前綴的,Atom 內部會處理。其可用于 snippets、config 等需要限定作用范圍的功能中,后面實戰中很多地方都有用到。 了解更多

4、package.json

類似于 Node modules,Atom 包也包含一個 package.json 文件,但是 Atom 擁有自己定義的字段,如 main 指定包的主入口文件;activationCommands 指定那些命令可以激活入口文件中 activate 方法;menus, styles, keymaps, snippets 分別指定目錄,樣式,快鍵鍵映射,代碼段的文件加載位置和順序。 了解更多

5、與其他包交互

可以在 package.json 中指定一個或多個版本號的外包來為自己提供服務。如:

"providedServices": {
    "autocomplete.provider": {
        "versions": {
            "2.0.0": "provide"
        }
    }
}

這里使用 autocomplete+ 包提供的 provide API,版本是 2.0.0。這樣只要在 package.json 中main 字段指定的主文件中實現 provide 方法,就可以在包激活的任何時候調用。如:

export default {
    activate(state) {}

    provide() {
        return yourProviderHere;
    }
}

實戰

經過簡單的基礎知識介紹,是不是有點躍躍欲試了?好,滿足你。接下來我們將通過實戰來運用這些知識,深入了解其原理和工作機制。下面示例都是以 Element-Helper 插件為樣本,你先不必著急去看源碼。

1、自動補全

這里通過使用 autocomplete+ 提供的服務( Provide API )來實現自動補全功能。關于怎么建立交互請看基礎知識中的第 5 小節,那么我們要實現的就是定義自己的 yourProiverHere, 它是一個對象包含如下示例中的方法:

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
    }
}

重點介紹下 getSuggestion 的參數 request 對象,它包含下面屬性:

  • editor: 當前的 文本編輯上下文
  • bufferPosition:當前光標的 位置 ,包含屬性 row 和 column。
  • scopeDescriptor: 當前光標位置所在的作用域描述符,可通過其 .getScopesArray 方法獲取到包含所有自己和祖先作用域選擇器的數組。你可以通過按 cmd+shift+p 打開命令面板輸入 Log Cursor scope 來查看作用描述符。
  • prefix:當前光標輸入位置所在單詞的前綴,注意 autocomplete+ 不會捕獲 ‘<’, ‘@’ 和 ‘:’ 字符,所以后面我們得自己做處理。原來沒有仔細閱讀文檔(衰),我發現我原來實現的方法比較局限,其實這里教你怎么定義 自己的 prefix
  • activateManually:這個提示是否是用戶 手動觸發

介紹完 API 了,是時候來一起小試牛刀了。這里就以 Element UI 標簽的屬性值自動提示為例:

autocomplete+ 提供的 provider 會在用戶包激活后任何時候調用(比如輸入字符),我們只需在 getSuggestion 方法中返回提示信息(建議)數組就好了。那么問題重點來了,怎么獲取這個提示信息數組?觀察示例,想一想,可以分兩大部分:判斷提示出現的時機和過濾出提示信息數組。

1、判斷提示出現時機

示例中的時機是是否是標簽屬性值開始(isAttrValueStart),我們先實現三個方法:是否在字符串作用域范圍內(hasStringScope)、是否在標簽作用域范圍內(hasTagScope)和輸入字符位置前是否具有屬性名稱(getPreAttr):

// 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];
}

說明:

  1. 參數 scopes 是前面講的作用域描述符。如果不是很清楚,可以打開命令面板輸入 Log Cursor scope 來查看。
  2. scopeDescriptorForBufferPosition 方法是獲取給定位置的作用域描述符,具體請查看 這里
  3. getTextInBufferRange 方法是根據位置范圍( Range )獲取文本字符串,具體請查看 這里 ,他有個別稱 getTextInRange(官方文檔里是沒有的,可以查看源代碼 L1024 L931 ,實現一毛一樣)。

那么接下來結合三個方法來實現 isAttrValueStart 方法:

// 參數解釋請看 ‘自動補全’ 小節
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));
}

2、過濾出提示信息數組

前面已經判斷提示信息出現的時機,剩下就是如何展示相應標簽屬性的值了, 這真是個精細化工作。慣例,先做些準備工作:1.獲取輸入位置所在的標簽名(getPreTag); 2.獲取輸入位置所在的屬性名(getPreAttr) - 這個上小節已實現;3.既然知道標簽名和屬性名,那么就可以從事先純手工打造的 attributes.json 文件(具體請看 element-helper-json )中找到對應的屬性值了(getAttrValues)- 這個就是遍歷 json 對象屬性,不具體解釋。

// 標簽名匹配正則表達式 - 標簽匹配有很多情況,這里并不完善...,僅供參考。
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;
}

OK,準備工作好了,我們來對獲取到的屬性值數組進行格式化處理,獲得 getSuggestions 能識別的數據結構數組:

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}>`  // 右邊展示的文本信息
    };
}

經過以上兩步,只需在 getSuggestions 方法中返回數組給 autocomplete+ 服務即可:

// ...
getSuggestions(request) {
    if (this.isAttrValueStart(request)) {
        return this.getAttrValueSuggestion(request);
    }
}
// ...

到這里大家應該明白自動補全工作原理了吧,其他的可以依葫蘆畫瓢啦,be happy。

2、代碼段

定義代碼段的方式有三種方式:

  • 全局定義。在 Atom -> Snippets 菜單中定義,定義方式同第二種
  • 包內定義。在基礎知識部分,我們介紹了生成包后的文件目錄結構和作用,其中 snippets 文件夾里放的就是我們自定義的常用代碼塊 json 文件,這里我使用為 coffeescript 對象提供的 cson 文件,類似 json,但語法沒有那么嚴格且支持多行字符串,如官方介紹:

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.

現在來看下如何編寫一個代碼段,基本格式如下:

".source.js":
    "notification":
        "prefix": "notify",
        "body": """
            this.$notify({
                title: '${1:title}',
                message: '${2:string|VNode}'
            });
        """

最頂層的鍵字符串(.source.js)是作用域選擇器,指定在文本編輯器中那個范圍內可觸發匹配(示例中是在 js 文件或 script 標簽域中觸發代碼段匹配)。下一層鍵字符串(notification)是表示代碼段的描述,將展示在下拉條目的右邊文本;prefix 字段是匹配輸入字符的前綴;body 字段是插入的文本,可以通過 """ 來使用多行語法,body 中 $ 符表示占位符,每按一次 tab 鍵,都會按 ${num} 中的 num 順序移動位置。如果要在占位符位置填充字符的話,可以這樣 ${num: yourString}。示例效果如下: 了解更多

  • 在‘自動補全’小節中可以定義返回 snippet 的提示,只需在 suggestion 對象中定義 snippet 和 displayText 屬性即可,不要定義 text 屬性。snippet 語法同第二種方式中基本格式中的body字段, displayText 用于提示展示文本,snippet 是插入文本編輯器的代碼段。

3、創建一個 modal 下拉列表和文檔視圖

ATOM 為實現這兩個功能提供了 npm 包 atom-space-pen-views ,它包括三個視圖類:文本編輯器視圖類( TextEditorView )、滾動文檔視圖類( ScrollView )和下拉選項視圖類( SelectListView ),我們只需繼承視圖類,實現類方法即可。下面重點講下拉列表和滾動視圖類:

模態下拉列表

我們只要提供用于展示的條目給 SelectListView,實現兩個必選方法,它會幫我們的條目渲染成一個下拉列表形式,如下:

// 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;

視圖效果和 DOM 樹結構如下圖,我們可以看到通過 addModalPanel 把 selectListView 的 HTML 元素添加到模態框元素中了

定義好了視圖類,那怎么渲染展示呢?少年莫慌,我們可以在主文件 activate 方法中注冊命令來觸發視圖展示(當然你可以用其他方式,只要你確定想要觸發時機執行方法就行了):

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);
                }
            }
        }));
    }
}

文檔視圖

Atom 提供了打開一個空白文本編輯器的 API ( atom.workspace.open ) 和注冊 URI 打開鉤子(opener)函數的 API ( atom.workspace.addOpener(opener) ),那么再結合 ScrollView 可以打開一個可滾動的文檔窗口。No BB, show my code:

/**
* 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_;
    }
}

/**
* 主文件, 這里只寫 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);
    }
}));
// ...

如果還對這兩個API有所疑慮,請查看上面提供的鏈接。那么最終效果如下:

寫在最后

這算是我在編寫 Element-Helper 插件時的一些總結和過程吧,實現方式不一定完善或優雅,表述不清楚或錯誤的地方請留言指正。題外話,如果你想開發 VSCode 插件,以 Atom 插件開發為入門也是不錯的選擇,它們都是基于 Electron 框架,很多概念都互通,但 Atom 更易入手和靈活(個人見解)。最后,希望本文能為大家提供些許幫助。Enjoy it!

 

來自:https://zhuanlan.zhihu.com/p/27913291

 

 本文由用戶 quzx 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!