大家好,我是韦恩,今天是铁人赛的二十六天,让我们来设计extension中的MVVM架构吧!
在软件设计中,MVVM(Model–view–viewmodel)是一种通用且流行的软件架构模式,在各个主流前端框架,如vuejs、react、angular中,都可以见到MVVM的影子。
[图片来源:维基百科]
MVVM架构将应用程序分为三大部分:
export interface SnippetJsonObject {
[title: string]: Omit<SnippetSetting, 'title'>;
}
export interface SnippetSetting {
title: string;
prefix: string;
body: string[];
description: string;
}
export interface SnippetStorageItem {
category: string;
settings: SnippetSetting[];
};
export type SnippetStorage = SnippetStorageItem[];
同时,我们也会在Workspace中储存转换後的资料。
export class Workspace {
...
public readonly path: string | undefined;
private _storage : SnippetStorage | undefined;
public get storage() {
return this._storage;
}
...
}
当资料载入时,我们会将资料显示在介面上,也即是接下来我们要提的View的部分。
在我们的extension中,View的部分主要是TreeView与WebView。
TreeView的DataProvider经过调整後会是底下这样,因为我们的Extension只会有一个TreeView,我们也随之在TreeView的DataProvider中使用singleton的模式。
export class TreeDataProvider implements vscode.TreeDataProvider<TreeViewItem> {
private static instance: TreeDataProvider | undefined;
public static getInsance(workspace: Workspace) {
if(!TreeDataProvider.instance) {
TreeDataProvider.instance = new TreeDataProvider(workspace);
}
return TreeDataProvider.instance;
}
private eventEmitter = new vscode.EventEmitter<TreeViewItem | undefined | void>();
public get onDidChangeTreeData(): vscode.Event<TreeViewItem | undefined | void> {
return this.eventEmitter.event;
}
private get treeItems(): TreeViewItem[] | undefined {
return this.workspace.storage?.map(storageItem => new TreeViewItem(
storageItem.category,
storageItem.settings.map((setting) => this.snippetToItem(setting.title, setting, storageItem.category)))
);
}
private constructor(
private workspace: Workspace
) { }
private snippetToItem(title: string, snippet: SnippetSetting, category: string) {
return new TreeViewItem(title).setSnippet(snippet).setCategory(category);
}
public getTreeItem(element: TreeViewItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
return element;
}
public getChildren(element: TreeViewItem): vscode.ProviderResult<TreeViewItem[]> {
if(!element) {
return this.treeItems || [];
}
return element.children;
}
public updateView() {
this.eventEmitter.fire();
}
}
读者们可以看到,上面我们并未处理UI的逻辑,仅是将ViewModel的资料转换为TreeView所需的TreeItem。
然後,我们会这样让View与ViewModel结合
export const initWorkspaceViewModel = (context: vscode.ExtensionContext) => {
const workspace = new Workspace(context);
workspace.onDidChangeData(EventType.LOAD, () => {
registerTreeview(context, workspace);
});
workspace.onDidChangeData(EventType.CHANGE, () => {
TreeDataProvider.getInsance(workspace).updateView();
});
loadWorkspaceData(workspace: Workspace)
return workspace;
};
上面我们让TreeView的DataProvider在实例化时,就直接拿到workspace,并使用workspace的storage中的snippet资料。然後,我们会透过监听的事件,在ViewModel资料变化时通知TreeView时刷新UI的元件,完成View与ViewModel的Data Binding。
当使用者想要编辑或删除TreeView的时候,我们让使用者可以直接在树状元件上点击对应Icon触发command。
为此,我们在Contribution Point中先注册好UI与对应设定
{
...
"contributes": {
...
"commands": [
...
{
"command": "ithome30-code-manager.editSnippet",
"title": "Code Manager: Edit Exist Item",
"icon": {
"light": "assets/edit.svg",
"dark": "assets/edit.svg"
}
},
{
"command": "ithome30-code-manager.deleteSnippet",
"title": "Code Manager: Delete Item",
"icon": {
"light": "assets/trash.svg",
"dark": "assets/trash.svg"
}
}
],
"menus": {
...
"view/title": [
{
"command": "ithome30-code-manager.addSnippet",
"when": "view == cmtreeview"
}
],
"view/item/context": [
{
"command": "ithome30-code-manager.editSnippet",
"when": "view == cmtreeview && viewItem == snippetItem",
"group": "inline"
},
{
"command": "ithome30-code-manager.deleteSnippet",
"when": "view == cmtreeview && viewItem == snippetItem",
"group": "inline"
}
]
}
},
...
}
在点击编辑或删除的icon时,vscode会触发命令,这时候我们可以拿到对应的TreeViewItem并对ViewModel(Workspace)做出资料改动的操作。
底下是我们的命令处理实作:
export function registerTreeItemCommand(context: vscode.ExtensionContext, workspace: Workspace) {
const editCommand = vscode.commands.registerCommand('ithome30-code-manager.editSnippet', async (item: TreeViewItem) => {
if(item.children) {
const newCategoryName = await input('Enter new ategory name', item.label || '');
if(newCategoryName === '') return;
workspace.editCategory(item.label!, newCategoryName);
}
});
const deleteCommand = vscode.commands.registerCommand('ithome30-code-manager.deleteSnippet', async (item: TreeViewItem) => {
const isCategory = !!item.children;
const prompt = isCategory ? `Make sure to delete category: ${item.label}` : `Make sure to delete snippet: ${item.snippet?.title}`;
const isConfirm = await confirm(prompt);
if(!isConfirm) return;
if(isCategory) {
workspace.deleteCategory(item.label);
} else {
workspace.deleteSnippet({ category: item.category!, setting: item.snippet!});
}
});
context.subscriptions.push(editCommand, deleteCommand);
}
上面我们仅在使用者对介面操作时执行workspace.editCategory
与workspace.editCategory
等方法,改变ViewModel的资料,ViewModel在改变资料後会通知TreeView刷新介面展示改动後的元件。
在上面,我们简单介绍了MVVM,并以TreeView跟Workspace的相关实作做范例。现在我们再提一些Model与ViewModel沟通的部分。
前段有提到,Model层是代表实际储存的数据模型,在我们的专案里,我们是用vscode的snippet格式储存相关设定在json档案里。现在我们要将model的资料转换并提供给ViewModel,也就是Workspace这个类别。
我们在一个snippetfile.ts定义读取snippet档案,并将其转换为ViewModel中储存的资料格式,如底下所示。
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import * as jsonfile from 'jsonfile';
...
export const loadSnippets = (workspacePath: string): SnippetStorage => {
const files = fs.readdirSync(workspacePath);
return files?.map((foldername) => {
if (fs.statSync(path.join(workspacePath, foldername)).isDirectory()) {
return {
category: foldername,
settings: snippetSettings(
jsonfile.readFileSync(path.join(workspacePath, foldername, `${foldername}.code-snippets`))
)
};
}
return {
category: foldername,
settings: snippetSettings(
jsonfile.readFileSync(path.join(workspacePath, `global.code-snippets`))
)
};
});
}
接着,我们提供loadWorkspace这个方法,让Workspace可以使用转换後的资料
export function loadWorkspaceData(workspace: Workspace) {
if(!workspace.path) {
vscode.window.showInformationMessage('Should create or choose a code-manager workspace first'!);
return;
}
const storage = loadSnippets(workspace.path);
workspace.loadSnippets(storage);
}
}
在初始化Worksapce时,我们即让Workspace载入资料
export const initWorkspaceViewModel = (context: vscode.ExtensionContext) => {
const workspace = new Workspace(context);
workspace.onDidChangeData(EventType.LOAD, () => {
registerTreeview(context, workspace);
});
...
loadWorkspacedata(workspace: Workspace)
return workspace;
};
在Workspace资料载入後,Workspace会发射load事件,让TreeView初始化并展示Workspace的资料。
如此,我们即完成了读取snippet资料时Model->ViewModel->View
的资料流。
反过来,当使用者对View进行相关操作後,ViewModel会改变,改变後即须通知Model层做对应的修改。
因此,我们在snippetfile.ts提供writeSnippetToFile
与相关的删除方法,接着我们会观察监听workspace的事件,在ViewModel发生变化时,将改动时将ViewModel的数据格式转回Model层的数据模型,写入持久化储存的档案之中。
export function watchWorkspaceEvent(workspace: Workspace) {
const workspace = new Workspace(context);
workspace.onDidChangeData(EventType.ADD_CATEGORY, (item: SnippetStorageItem) => {
if(!workspace.path) return;
writeSnippet(workspace.path, item.category, item.settings);
});
workspace.onDidChangeData(EventType.EDIT_CATEGORY, (oldCategory: string, newCategory: string) => {
if(!workspace.path) return;
renameCategory(workspace.path, oldCategory, newCategory);
});
workspace.onDidChangeData(EventType.DELETE_CATEGORY, (category: string) => {
if(!workspace.path) return;
deleteCategory(workspace.path, category);
});
...
}
接着,我们和上面绑定View一样在initWorkspace这个方法里进行ViewModel与Model绑定的动作。
...
import * as snippetModel from '../snippets/snippetfile';
export const initWorkspaceViewModel = (context: vscode.ExtensionContext) => {
const workspace = new Workspace(context);
...
snippetModel.watchWorkspaceEvent(workspace);
...
return workspace;
};
在extension处於active状态时,我们即在extension的进入点active函式中执行initWorkspace方法,绑定我们的CodeManager里的Model-ViewModel-View各元件。
export function activate(context: vscode.ExtensionContext) {
const workspace = initWorkspaceViewModel(context);
registerTreeItemCommand(context, workspace);
...
}
好啦,今天,我们介绍了MVVM架构,并了解在extenision中可以怎麽实现绑定Model-View-Model的逻辑。
明天我们将介绍CodeManager里是怎麽实作WebViewPanel的部分,了解如何将React的View层整合进我们Extension的MVVM架构里。
我们明天见,谢谢大家。
因为专案的程序码繁多,此处仅就重点介绍extension的架构与重要逻辑的程序码实现。全部的专案程序码笔者会放置於github的repo上,并於之後提供连结给读者参考。
前言 工作了好一段时间後,直到那次处理了OOM(Out Of Memory)问题,才发现JDK内有很...
前言 今天要继续昨天做过的部分,因此一开始需要昨天的程序码 import pandas as pd ...
“It's only because of their stupidity that they'r...
在开始写测试的时候因为许多 action 进入前都必须要先登入使用者才能有权限做其他事情,但在测试该...
在这一篇主要讲了Node 在终机端和脚本文件this不同的指向,那麽今天要来简单介绍Nodejs作用...