大家好,我是韋恩,今天是鐵人賽的二十五天,讓我們來設計extension的事件與資料流處理吧!
在昨天的專案開發裡,我們發現到了如果在樹狀元件裡進行json檔案的修改與dataStorage的處理。會發生資料與單一元件耦合的情況。先前的樹狀元件的dataStorage的型態是一個儲存treeItem的陣列,這樣的資料型態在我們實作Webview的snippet查詢與相關功能時,會十分的不方便。
我們可以預期至少有三個以上的元件或模組(Jsonfile、WebviewPanel、TreeView),將依賴於workspace中的snippet的資料。為此,我們需要把snippet的資料抽離出來,在一個獨立的model處理,並在資料發生變化時,將改動與資料傳給相關模組與元件,讓這些模組與元件使用個別的方式處理資料,並進行各元件模組對應的操作。
有一個設計模式在處理這種情況非常管用,即是我們先前在介紹treeview(二)時提到的觀察者模式
。而在nodejs與vscode的api裡,已經內建了實作了觀察者模式的物件-事件發射器(EventEmitter)。前面我們提到,vscode內建的EventEmitter是個簡化版的實作。在處理各種情境的事件與訂閱上,nodejs的eventEmitter功能豐富且更為強大,因此在這裡我們會在資料的事件處理上使用nodejs的實作處理。
讓我們創建一個workspace.ts檔案,並在其中引用nodejs的事件模組。
import * as event from 'events';
接著,我們引用相關所需的api,並宣告一個Workspace的class,與相關屬性。
export class Workspace {
...
private _storage : SnippetStorage | undefined;
public get storage() {
return this._storage;
}
public get isExist() {
return !!this.path;
}
constructor(context: vscode.ExtensionContext) {
this.path = context.globalState.get('workspace');
}
...
}
我們會在workpspace實例化的時候拿到使用者的code-manager的工作區路徑,並且用其判斷workspace是否存在。
同時我們也指定了storage的資料屬性。
接著,讓我們在Workspace裡面創建一個事件發射器:
export class Workspace {
...
private readonly subject = new event.EventEmitter();
...
}
上面我們並不使用繼承的方式,而是在Workspace中直接創建eventEmitter,透過組合
的方式在Workspace中使用事件的功能。同時,我們按照觀察者模式的傳統將eventEmitter命名為subject,subject意思為主題。
主題(subject)會讓使用者觀察該主題的消息並訂閱感興趣的事件,只要Workspace一有資料變化,subject便會將主題的資料發射給所有想知道資料變化的觀察者們,讓訂閱主題的觀察者們各自進行對應的資料處理。
接著,在我們的snippet應用程式中,允許使用者對snippet做新增修改與刪除等動作,因此我們會有這些跟資料改動相關的事件。
const enum EventType {
'LOAD' = 'LOAD',
'CHANGE' = 'CHANGE',
'READ' = 'READ',
'ADD' = 'ADD',
'EDIT' = 'EDIT',
'DELETE' = 'DELETE',
'ADD_CATEGORY' = 'ADD_CATEGORY',
'EDIT_CATEGORY' = 'EDIT_CATEGORY',
'DELETE_CATEGORY' = 'REMOVE_CATEGORY',
}
上面除了增刪改查外,我們還定義了LOAD事件與CHANGE事件,用於不同情境。
第一個LOAD事件,用於處理載入全部snippet的狀況。
第二個CHANGE事件,在任何方式修改資料時,將修改後的snippet資料,全數通知給訂閱這個事件的人。
為了讓外部可以監聽事件,我們照vscode元件的命名風格設計了onDidChangeData方法,讓使用者可以夠過訂閱不同類型事件(EventType),並設定事件發生時的對應處理方法eventHandler。
export class Workspace {
...
public onDidChangeData<T extends []>(event: EventType, eventHandler: (...args: T) => void) {
this.subject.on(event, eventHandler as any);
}
...
}
接著,我們提供新增、刪除與修改snippet等方法,讓我們也開始對應的實作吧!
export class Workspace {
...
public loadSnippets(storage: SnippetStorage) {
this._storage = storage;
this.subject.emit(EventType.LOAD, this.storage);
}
...
}
export class Workspace {
...
public addSnippet(changes: { category: string; setting: SnippetSetting}) {
const categoryItem = this.storage?.find(i => i.category === changes.category);
if(categoryItem) {
categoryItem.settings = categoryItem.settings
.filter(s=> s.title !== changes.setting.title)
.concat([changes.setting]);
}
this.subject.emit(EventType.ADD, categoryItem || {
category: changes.category,
settings: [changes.setting]
});
this.subject.emit(EventType.CHANGE, this.storage);
}
public editSnippet(changes: { category: string; setting: SnippetSetting}) {
const categoryItem = this.storage?.find(i => i.category === changes.category);
if(categoryItem) {
categoryItem.settings = categoryItem.settings
.filter(s=> s.title !== changes.setting.title)
.concat([changes.setting]);
}
this.subject.emit(EventType.ADD, categoryItem || {
category: changes.category,
settings: [changes.setting]
});
this.subject.emit(EventType.CHANGE, this.storage);
}
public deleteSnippet(category: string, title: string) {
const categoryItem = this.storage?.find(i => i.category === category);
if(categoryItem) {
categoryItem.settings = categoryItem.settings.filter(s=> s.title !== title)
}
this.subject.emit(EventType.DELETE, categoryItem || {
category,
settings: []
});
this.subject.emit(EventType.CHANGE, this.storage);
}
...
}
在上面的資料新增、編輯與刪除等方法中,我們會修改snippet資料,並將變化使用對應事件發射通知。
為什麼我們不只用一個change事件,並直接給出所有snippet的資料就好了呢?
因為在jsonfile的模組裡,我們並不會一次將所有snippet檔案全部重寫一次,僅需根據需求對不同category的檔案做讀寫與刪除等動作。
為此,我們使用不同的事件,發射個事件所需獲取的資料。
在我們開發的extension裡,還有ADD_CATEGORY等新增、刪除與修改category的事件,因為此處的邏輯與上面相關事件重複,這裡我們並不條列出來,有興趣的讀者可以至extension完成後的repo查看相關實作。
好的,今天我們實作了Workspace資料處理與相關事件的處理,明天我們會繼續專案開發,祝各位收穫滿滿。
我們明天見,掰掰!